diff --git a/.changeset/early-cameras-battle.md b/.changeset/early-cameras-battle.md new file mode 100644 index 000000000..e0b8241eb --- /dev/null +++ b/.changeset/early-cameras-battle.md @@ -0,0 +1,6 @@ +--- +'@gitbook/openapi-parser': patch +'gitbook': patch +--- + +Implement a trusted mode to speed up OpenAPI spec validation diff --git a/packages/gitbook/src/lib/openapi/fetch.ts b/packages/gitbook/src/lib/openapi/fetch.ts index af6443c87..c7ad5d08e 100644 --- a/packages/gitbook/src/lib/openapi/fetch.ts +++ b/packages/gitbook/src/lib/openapi/fetch.ts @@ -82,7 +82,13 @@ const fetchFilesystem = cache({ } const text = await response.text(); - const filesystem = await parseOpenAPI({ value: text, rootURL: url }); + const filesystem = await parseOpenAPI({ + value: text, + rootURL: url, + // If we fetch the OpenAPI specification + // it's the legacy system, it means the spec can be trusted here. + trust: true, + }); const richFilesystem = await enrichFilesystem(filesystem); return { // Cache for 4 hours diff --git a/packages/openapi-parser/src/parse.ts b/packages/openapi-parser/src/parse.ts index 9c102d457..4efbbe402 100644 --- a/packages/openapi-parser/src/parse.ts +++ b/packages/openapi-parser/src/parse.ts @@ -3,12 +3,7 @@ import { OpenAPIParseError } from './error'; import { convertOpenAPIV2ToOpenAPIV3 } from './v2'; import { parseOpenAPIV3 } from './v3'; -/** - * Parse a raw string into an OpenAPI document. - * It will also convert Swagger 2.0 to OpenAPI 3.0. - * It can throw an `OpenAPIParseError` if the document is invalid. - */ -export async function parseOpenAPI(input: { +export interface ParseOpenAPIInput { /** * The API definition to parse. */ @@ -17,7 +12,18 @@ export async function parseOpenAPI(input: { * The root URL of the specified OpenAPI document. */ rootURL: string | null; -}) { + /** + * Trust the input. This will skip advanced validation. + */ + trust?: boolean; +} + +/** + * Parse a raw string into an OpenAPI document. + * It will also convert Swagger 2.0 to OpenAPI 3.0. + * It can throw an `OpenAPIParseError` if the document is invalid. + */ +export async function parseOpenAPI(input: ParseOpenAPIInput) { try { return await parseOpenAPIV3(input); } catch (error) { diff --git a/packages/openapi-parser/src/scalar-plugins/fetchURLs.ts b/packages/openapi-parser/src/scalar-plugins/fetchURLs.ts index df820196e..9076c4477 100644 --- a/packages/openapi-parser/src/scalar-plugins/fetchURLs.ts +++ b/packages/openapi-parser/src/scalar-plugins/fetchURLs.ts @@ -43,7 +43,7 @@ export const fetchURLs: (customConfiguration: { return true; }, async get(value?: any) { - // Limit ht enumber of requests + // Limit the number of requests if (configuration?.limit !== false && numberOfRequests >= configuration?.limit) { console.warn( `[fetchUrls] Maximum number of requests reeached (${configuration?.limit}), skipping request`, diff --git a/packages/openapi-parser/src/v2.ts b/packages/openapi-parser/src/v2.ts index aa0227624..eb811750d 100644 --- a/packages/openapi-parser/src/v2.ts +++ b/packages/openapi-parser/src/v2.ts @@ -3,22 +3,15 @@ import swagger2openapi, { type ConvertOutputOptions } from 'swagger2openapi'; import { OpenAPIParseError } from './error'; import { parseOpenAPIV3 } from './v3'; -import type { AnyApiDefinitionFormat } from '@scalar/openapi-parser'; import type { Filesystem, OpenAPIV3xDocument } from './types'; +import type { ParseOpenAPIInput } from './parse'; /** * Convert a Swagger 2.0 schema to an OpenAPI 3.0 schema. */ -export async function convertOpenAPIV2ToOpenAPIV3(input: { - /** - * The API definition to parse. - */ - value: AnyApiDefinitionFormat; - /** - * The root URL of the specified OpenAPI document. - */ - rootURL: string | null; -}): Promise> { +export async function convertOpenAPIV2ToOpenAPIV3( + input: ParseOpenAPIInput, +): Promise> { const { value, rootURL } = input; // In this case we want the raw value to be able to convert it. const schema = typeof value === 'string' ? rawParseOpenAPI({ value, rootURL }) : value; @@ -35,7 +28,7 @@ export async function convertOpenAPIV2ToOpenAPIV3(input: { patch: true, })) as ConvertOutputOptions; - return parseOpenAPIV3({ rootURL, value: convertResult.openapi }); + return parseOpenAPIV3({ ...input, rootURL, value: convertResult.openapi }); } catch (error) { if (error instanceof Error && error.name === 'S2OError') { throw new OpenAPIParseError('Failed to convert Swagger 2.0 to OpenAPI 3.0', { diff --git a/packages/openapi-parser/src/v3.ts b/packages/openapi-parser/src/v3.ts index 557c2350c..b084a81c0 100644 --- a/packages/openapi-parser/src/v3.ts +++ b/packages/openapi-parser/src/v3.ts @@ -1,23 +1,33 @@ -import { type AnyApiDefinitionFormat, validate } from '@scalar/openapi-parser'; +import { validate } from '@scalar/openapi-parser'; import { OpenAPIParseError } from './error'; import { createFileSystem } from './filesystem'; import type { Filesystem, OpenAPIV3xDocument } from './types'; +import type { ParseOpenAPIInput } from './parse'; /** * Parse a raw string into an OpenAPI document. * It will also convert Swagger 2.0 to OpenAPI 3.0. * It can throw an `OpenAPIFetchError` if the document is invalid. */ -export async function parseOpenAPIV3(input: { - /** - * The API definition to parse. - */ - value: AnyApiDefinitionFormat; - /** - * The root URL of the specified OpenAPI document. - */ - rootURL: string | null; -}): Promise> { +export async function parseOpenAPIV3( + input: ParseOpenAPIInput, +): Promise> { + const { value, rootURL, trust } = input; + const specification = trust + ? trustedValidate({ value, rootURL }) + : await untrustedValidate({ value, rootURL }); + + const filesystem = await createFileSystem({ value: specification, rootURL }); + + return filesystem; +} + +type ValidateOpenAPIV3Input = Pick; + +/** + * Validate an untrusted OpenAPI v3 document. + */ +async function untrustedValidate(input: ValidateOpenAPIV3Input) { const { value, rootURL } = input; const result = await validate(value); @@ -36,7 +46,36 @@ export async function parseOpenAPIV3(input: { }); } - const filesystem = await createFileSystem({ value: result.specification, rootURL }); - - return filesystem; + return result.specification; +} + +/** + * Validate a trusted OpenAPI v3 document. + * It assumes the specification is already a valid specification. + * It's faster than `untrustedValidate`. + */ +function trustedValidate(input: ValidateOpenAPIV3Input) { + const { value, rootURL } = input; + const result = (() => { + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch (error) { + throw new OpenAPIParseError('Invalid JSON', { + code: 'invalid', + rootURL, + }); + } + } + return value; + })(); + + if ('swagger' in result && result.swagger) { + throw new OpenAPIParseError('Only OpenAPI v3 is supported', { + code: 'parse-v2-in-v3', + rootURL, + }); + } + + return result; }