Skip to content

Commit a054554

Browse files
authored
Faster OpenAPI spec validation (#2871)
1 parent 1f11650 commit a054554

File tree

6 files changed

+84
-34
lines changed

6 files changed

+84
-34
lines changed

.changeset/early-cameras-battle.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@gitbook/openapi-parser': patch
3+
'gitbook': patch
4+
---
5+
6+
Implement a trusted mode to speed up OpenAPI spec validation

packages/gitbook/src/lib/openapi/fetch.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,13 @@ const fetchFilesystem = cache({
8282
}
8383

8484
const text = await response.text();
85-
const filesystem = await parseOpenAPI({ value: text, rootURL: url });
85+
const filesystem = await parseOpenAPI({
86+
value: text,
87+
rootURL: url,
88+
// If we fetch the OpenAPI specification
89+
// it's the legacy system, it means the spec can be trusted here.
90+
trust: true,
91+
});
8692
const richFilesystem = await enrichFilesystem(filesystem);
8793
return {
8894
// Cache for 4 hours

packages/openapi-parser/src/parse.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@ import { OpenAPIParseError } from './error';
33
import { convertOpenAPIV2ToOpenAPIV3 } from './v2';
44
import { parseOpenAPIV3 } from './v3';
55

6-
/**
7-
* Parse a raw string into an OpenAPI document.
8-
* It will also convert Swagger 2.0 to OpenAPI 3.0.
9-
* It can throw an `OpenAPIParseError` if the document is invalid.
10-
*/
11-
export async function parseOpenAPI(input: {
6+
export interface ParseOpenAPIInput {
127
/**
138
* The API definition to parse.
149
*/
@@ -17,7 +12,18 @@ export async function parseOpenAPI(input: {
1712
* The root URL of the specified OpenAPI document.
1813
*/
1914
rootURL: string | null;
20-
}) {
15+
/**
16+
* Trust the input. This will skip advanced validation.
17+
*/
18+
trust?: boolean;
19+
}
20+
21+
/**
22+
* Parse a raw string into an OpenAPI document.
23+
* It will also convert Swagger 2.0 to OpenAPI 3.0.
24+
* It can throw an `OpenAPIParseError` if the document is invalid.
25+
*/
26+
export async function parseOpenAPI(input: ParseOpenAPIInput) {
2127
try {
2228
return await parseOpenAPIV3(input);
2329
} catch (error) {

packages/openapi-parser/src/scalar-plugins/fetchURLs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const fetchURLs: (customConfiguration: {
4343
return true;
4444
},
4545
async get(value?: any) {
46-
// Limit ht enumber of requests
46+
// Limit the number of requests
4747
if (configuration?.limit !== false && numberOfRequests >= configuration?.limit) {
4848
console.warn(
4949
`[fetchUrls] Maximum number of requests reeached (${configuration?.limit}), skipping request`,

packages/openapi-parser/src/v2.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,15 @@ import swagger2openapi, { type ConvertOutputOptions } from 'swagger2openapi';
33

44
import { OpenAPIParseError } from './error';
55
import { parseOpenAPIV3 } from './v3';
6-
import type { AnyApiDefinitionFormat } from '@scalar/openapi-parser';
76
import type { Filesystem, OpenAPIV3xDocument } from './types';
7+
import type { ParseOpenAPIInput } from './parse';
88

99
/**
1010
* Convert a Swagger 2.0 schema to an OpenAPI 3.0 schema.
1111
*/
12-
export async function convertOpenAPIV2ToOpenAPIV3(input: {
13-
/**
14-
* The API definition to parse.
15-
*/
16-
value: AnyApiDefinitionFormat;
17-
/**
18-
* The root URL of the specified OpenAPI document.
19-
*/
20-
rootURL: string | null;
21-
}): Promise<Filesystem<OpenAPIV3xDocument>> {
12+
export async function convertOpenAPIV2ToOpenAPIV3(
13+
input: ParseOpenAPIInput,
14+
): Promise<Filesystem<OpenAPIV3xDocument>> {
2215
const { value, rootURL } = input;
2316
// In this case we want the raw value to be able to convert it.
2417
const schema = typeof value === 'string' ? rawParseOpenAPI({ value, rootURL }) : value;
@@ -35,7 +28,7 @@ export async function convertOpenAPIV2ToOpenAPIV3(input: {
3528
patch: true,
3629
})) as ConvertOutputOptions;
3730

38-
return parseOpenAPIV3({ rootURL, value: convertResult.openapi });
31+
return parseOpenAPIV3({ ...input, rootURL, value: convertResult.openapi });
3932
} catch (error) {
4033
if (error instanceof Error && error.name === 'S2OError') {
4134
throw new OpenAPIParseError('Failed to convert Swagger 2.0 to OpenAPI 3.0', {

packages/openapi-parser/src/v3.ts

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
1-
import { type AnyApiDefinitionFormat, validate } from '@scalar/openapi-parser';
1+
import { validate } from '@scalar/openapi-parser';
22
import { OpenAPIParseError } from './error';
33
import { createFileSystem } from './filesystem';
44
import type { Filesystem, OpenAPIV3xDocument } from './types';
5+
import type { ParseOpenAPIInput } from './parse';
56

67
/**
78
* Parse a raw string into an OpenAPI document.
89
* It will also convert Swagger 2.0 to OpenAPI 3.0.
910
* It can throw an `OpenAPIFetchError` if the document is invalid.
1011
*/
11-
export async function parseOpenAPIV3(input: {
12-
/**
13-
* The API definition to parse.
14-
*/
15-
value: AnyApiDefinitionFormat;
16-
/**
17-
* The root URL of the specified OpenAPI document.
18-
*/
19-
rootURL: string | null;
20-
}): Promise<Filesystem<OpenAPIV3xDocument>> {
12+
export async function parseOpenAPIV3(
13+
input: ParseOpenAPIInput,
14+
): Promise<Filesystem<OpenAPIV3xDocument>> {
15+
const { value, rootURL, trust } = input;
16+
const specification = trust
17+
? trustedValidate({ value, rootURL })
18+
: await untrustedValidate({ value, rootURL });
19+
20+
const filesystem = await createFileSystem({ value: specification, rootURL });
21+
22+
return filesystem;
23+
}
24+
25+
type ValidateOpenAPIV3Input = Pick<ParseOpenAPIInput, 'value' | 'rootURL'>;
26+
27+
/**
28+
* Validate an untrusted OpenAPI v3 document.
29+
*/
30+
async function untrustedValidate(input: ValidateOpenAPIV3Input) {
2131
const { value, rootURL } = input;
2232
const result = await validate(value);
2333

@@ -36,7 +46,36 @@ export async function parseOpenAPIV3(input: {
3646
});
3747
}
3848

39-
const filesystem = await createFileSystem({ value: result.specification, rootURL });
49+
return result.specification;
50+
}
4051

41-
return filesystem;
52+
/**
53+
* Validate a trusted OpenAPI v3 document.
54+
* It assumes the specification is already a valid specification.
55+
* It's faster than `untrustedValidate`.
56+
*/
57+
function trustedValidate(input: ValidateOpenAPIV3Input) {
58+
const { value, rootURL } = input;
59+
const result = (() => {
60+
if (typeof value === 'string') {
61+
try {
62+
return JSON.parse(value);
63+
} catch (error) {
64+
throw new OpenAPIParseError('Invalid JSON', {
65+
code: 'invalid',
66+
rootURL,
67+
});
68+
}
69+
}
70+
return value;
71+
})();
72+
73+
if ('swagger' in result && result.swagger) {
74+
throw new OpenAPIParseError('Only OpenAPI v3 is supported', {
75+
code: 'parse-v2-in-v3',
76+
rootURL,
77+
});
78+
}
79+
80+
return result;
4281
}

0 commit comments

Comments
 (0)