Faster OpenAPI spec validation (#2871)

This commit is contained in:
Greg Bergé
2025-02-24 21:21:27 +01:00
committed by GitHub
parent 1f11650ca1
commit a0545545d7
6 changed files with 85 additions and 35 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/openapi-parser': patch
'gitbook': patch
---
Implement a trusted mode to speed up OpenAPI spec validation
+7 -1
View File
@@ -82,7 +82,13 @@ const fetchFilesystem = cache({
} }
const text = await response.text(); 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); const richFilesystem = await enrichFilesystem(filesystem);
return { return {
// Cache for 4 hours // Cache for 4 hours
+13 -7
View File
@@ -3,12 +3,7 @@ import { OpenAPIParseError } from './error';
import { convertOpenAPIV2ToOpenAPIV3 } from './v2'; import { convertOpenAPIV2ToOpenAPIV3 } from './v2';
import { parseOpenAPIV3 } from './v3'; import { parseOpenAPIV3 } from './v3';
/** export interface ParseOpenAPIInput {
* 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: {
/** /**
* The API definition to parse. * The API definition to parse.
*/ */
@@ -17,7 +12,18 @@ export async function parseOpenAPI(input: {
* The root URL of the specified OpenAPI document. * The root URL of the specified OpenAPI document.
*/ */
rootURL: string | null; 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 { try {
return await parseOpenAPIV3(input); return await parseOpenAPIV3(input);
} catch (error) { } catch (error) {
@@ -43,7 +43,7 @@ export const fetchURLs: (customConfiguration: {
return true; return true;
}, },
async get(value?: any) { async get(value?: any) {
// Limit ht enumber of requests // Limit the number of requests
if (configuration?.limit !== false && numberOfRequests >= configuration?.limit) { if (configuration?.limit !== false && numberOfRequests >= configuration?.limit) {
console.warn( console.warn(
`[fetchUrls] Maximum number of requests reeached (${configuration?.limit}), skipping request`, `[fetchUrls] Maximum number of requests reeached (${configuration?.limit}), skipping request`,
+5 -12
View File
@@ -3,22 +3,15 @@ import swagger2openapi, { type ConvertOutputOptions } from 'swagger2openapi';
import { OpenAPIParseError } from './error'; import { OpenAPIParseError } from './error';
import { parseOpenAPIV3 } from './v3'; import { parseOpenAPIV3 } from './v3';
import type { AnyApiDefinitionFormat } from '@scalar/openapi-parser';
import type { Filesystem, OpenAPIV3xDocument } from './types'; import type { Filesystem, OpenAPIV3xDocument } from './types';
import type { ParseOpenAPIInput } from './parse';
/** /**
* Convert a Swagger 2.0 schema to an OpenAPI 3.0 schema. * Convert a Swagger 2.0 schema to an OpenAPI 3.0 schema.
*/ */
export async function convertOpenAPIV2ToOpenAPIV3(input: { export async function convertOpenAPIV2ToOpenAPIV3(
/** input: ParseOpenAPIInput,
* The API definition to parse. ): Promise<Filesystem<OpenAPIV3xDocument>> {
*/
value: AnyApiDefinitionFormat;
/**
* The root URL of the specified OpenAPI document.
*/
rootURL: string | null;
}): Promise<Filesystem<OpenAPIV3xDocument>> {
const { value, rootURL } = input; const { value, rootURL } = input;
// In this case we want the raw value to be able to convert it. // In this case we want the raw value to be able to convert it.
const schema = typeof value === 'string' ? rawParseOpenAPI({ value, rootURL }) : value; const schema = typeof value === 'string' ? rawParseOpenAPI({ value, rootURL }) : value;
@@ -35,7 +28,7 @@ export async function convertOpenAPIV2ToOpenAPIV3(input: {
patch: true, patch: true,
})) as ConvertOutputOptions; })) as ConvertOutputOptions;
return parseOpenAPIV3({ rootURL, value: convertResult.openapi }); return parseOpenAPIV3({ ...input, rootURL, value: convertResult.openapi });
} catch (error) { } catch (error) {
if (error instanceof Error && error.name === 'S2OError') { if (error instanceof Error && error.name === 'S2OError') {
throw new OpenAPIParseError('Failed to convert Swagger 2.0 to OpenAPI 3.0', { throw new OpenAPIParseError('Failed to convert Swagger 2.0 to OpenAPI 3.0', {
+53 -14
View File
@@ -1,23 +1,33 @@
import { type AnyApiDefinitionFormat, validate } from '@scalar/openapi-parser'; import { validate } from '@scalar/openapi-parser';
import { OpenAPIParseError } from './error'; import { OpenAPIParseError } from './error';
import { createFileSystem } from './filesystem'; import { createFileSystem } from './filesystem';
import type { Filesystem, OpenAPIV3xDocument } from './types'; import type { Filesystem, OpenAPIV3xDocument } from './types';
import type { ParseOpenAPIInput } from './parse';
/** /**
* Parse a raw string into an OpenAPI document. * Parse a raw string into an OpenAPI document.
* It will also convert Swagger 2.0 to OpenAPI 3.0. * It will also convert Swagger 2.0 to OpenAPI 3.0.
* It can throw an `OpenAPIFetchError` if the document is invalid. * It can throw an `OpenAPIFetchError` if the document is invalid.
*/ */
export async function parseOpenAPIV3(input: { export async function parseOpenAPIV3(
/** input: ParseOpenAPIInput,
* The API definition to parse. ): Promise<Filesystem<OpenAPIV3xDocument>> {
*/ const { value, rootURL, trust } = input;
value: AnyApiDefinitionFormat; const specification = trust
/** ? trustedValidate({ value, rootURL })
* The root URL of the specified OpenAPI document. : await untrustedValidate({ value, rootURL });
*/
rootURL: string | null; const filesystem = await createFileSystem({ value: specification, rootURL });
}): Promise<Filesystem<OpenAPIV3xDocument>> {
return filesystem;
}
type ValidateOpenAPIV3Input = Pick<ParseOpenAPIInput, 'value' | 'rootURL'>;
/**
* Validate an untrusted OpenAPI v3 document.
*/
async function untrustedValidate(input: ValidateOpenAPIV3Input) {
const { value, rootURL } = input; const { value, rootURL } = input;
const result = await validate(value); const result = await validate(value);
@@ -36,7 +46,36 @@ export async function parseOpenAPIV3(input: {
}); });
} }
const filesystem = await createFileSystem({ value: result.specification, rootURL }); return result.specification;
}
return filesystem;
/**
* 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;
} }