Improve OpenAPI abstraction to be more flexible on usage (#2832)

This commit is contained in:
Greg Bergé
2025-02-14 10:41:09 +01:00
committed by GitHub
parent dda0cc635e
commit 46edde9da6
25 changed files with 364 additions and 346 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@gitbook/openapi-parser': patch
'@gitbook/react-openapi': patch
'gitbook': patch
---
Improve the OpenAPI package API
@@ -33,7 +33,7 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
return (
<div className={tcls('hidden')}>
<p>
Error with {error.url}: {error.message}
Error with {error.rootURL}: {error.message}
</p>
</div>
);
+44 -57
View File
@@ -1,10 +1,6 @@
import { ContentRef, DocumentBlockOpenAPI } from '@gitbook/api';
import { parseOpenAPI, OpenAPIParseError, traverse } from '@gitbook/openapi-parser';
import {
OpenAPIOperationData,
fetchOpenAPIOperation,
OpenAPIFetcher,
} from '@gitbook/react-openapi';
import { type OpenAPIOperationData, resolveOpenAPIOperation } from '@gitbook/react-openapi';
import { cache, noCacheFetchOptions, CacheFunctionOptions } from '@/lib/cache';
@@ -27,14 +23,11 @@ export async function fetchOpenAPIBlock(
}
try {
const data = await fetchOpenAPIOperation(
{
url: resolved.href,
path: block.data.path,
method: block.data.method,
},
fetcher,
);
const filesystem = await fetchFilesystem(resolved.href);
const data = await resolveOpenAPIOperation(filesystem, {
path: block.data.path,
method: block.data.method,
});
return { data, specUrl: resolved.href };
} catch (error) {
@@ -46,50 +39,44 @@ export async function fetchOpenAPIBlock(
}
}
const fetcher: OpenAPIFetcher = {
fetch: cache({
name: 'openapi.fetch.v5',
get: async (url: string, options: CacheFunctionOptions) => {
// Wrap the raw string to prevent invalid URLs from being passed to fetch.
// This can happen if the URL has whitespace, which is currently handled differently by Cloudflare's implementation of fetch:
// https://github.com/cloudflare/workerd/issues/1957
const response = await fetch(new URL(url), {
...noCacheFetchOptions,
signal: options.signal,
});
const fetchFilesystem = cache({
name: 'openapi.fetch.v5',
get: async (url: string, options: CacheFunctionOptions) => {
// Wrap the raw string to prevent invalid URLs from being passed to fetch.
// This can happen if the URL has whitespace, which is currently handled differently by Cloudflare's implementation of fetch:
// https://github.com/cloudflare/workerd/issues/1957
const response = await fetch(new URL(url), {
...noCacheFetchOptions,
signal: options.signal,
});
if (!response.ok) {
throw new Error(
`Failed to fetch OpenAPI file: ${response.status} ${response.statusText}`,
);
}
if (!response.ok) {
throw new Error(
`Failed to fetch OpenAPI file: ${response.status} ${response.statusText}`,
);
}
const text = await response.text();
const filesystem = await parseOpenAPI({ url, value: text });
const cache: Map<string, Promise<string>> = new Map();
const transformedFs = await traverse(filesystem, async (node) => {
if (
'description' in node &&
typeof node.description === 'string' &&
node.description
) {
if (cache.has(node.description)) {
node['x-description-html'] = await cache.get(node.description);
} else {
const promise = parseMarkdown(node.description);
cache.set(node.description, promise);
node['x-description-html'] = await promise;
}
const text = await response.text();
const filesystem = await parseOpenAPI({ value: text, rootURL: url });
const cache: Map<string, Promise<string>> = new Map();
const transformedFs = await traverse(filesystem, async (node) => {
if ('description' in node && typeof node.description === 'string' && node.description) {
if (cache.has(node.description)) {
node['x-description-html'] = await cache.get(node.description);
} else {
const promise = parseMarkdown(node.description);
cache.set(node.description, promise);
node['x-description-html'] = await promise;
}
return node;
});
return {
// Cache for 4 hours
ttl: 24 * 60 * 60,
// Revalidate every 2 hours
revalidateBefore: 22 * 60 * 60,
data: transformedFs,
};
},
}),
};
}
return node;
});
return {
// Cache for 4 hours
ttl: 24 * 60 * 60,
// Revalidate every 2 hours
revalidateBefore: 22 * 60 * 60,
data: transformedFs,
};
},
});
+17 -3
View File
@@ -1,14 +1,28 @@
type OpenAPIParseErrorCode =
| 'invalid'
| 'parse-v2-in-v3'
| 'v2-conversion'
| 'dereference'
| 'yaml-parse';
/**
* Error thrown when the OpenAPI document is invalid.
*/
export class OpenAPIParseError extends Error {
public override name = 'OpenAPIParseError';
public code: OpenAPIParseErrorCode;
public rootURL: string | null;
constructor(
message: string,
public readonly url: string,
public readonly code?: 'invalid-spec' | 'v2-spec' | 'failed-dereference',
options: {
code: OpenAPIParseErrorCode;
rootURL?: string | null;
cause?: Error;
},
) {
super(message);
super(message, { cause: options.cause });
this.code = options.code;
this.rootURL = options.rootURL ?? null;
}
}
@@ -34,7 +34,7 @@ describe('#createFileSystem', () => {
const url = new URL('/root/spec.yaml', server.url).href;
const filesystem = await createFileSystem({
value: url,
baseUrl: url,
rootURL: url,
});
expect(filesystem).toHaveLength(4);
expect(filesystem[0]!.isEntrypoint).toBe(true);
+10 -3
View File
@@ -1,5 +1,5 @@
import { type AnyApiDefinitionFormat, load } from '@scalar/openapi-parser';
import { fetchUrls } from './scalar-plugins/fetchURLs';
import { fetchURLs } from './scalar-plugins/fetchURLs';
import type { Filesystem } from './types';
/**
@@ -7,11 +7,18 @@ import type { Filesystem } from './types';
* Fetches all the URLs specified in references and builds a filesystem.
*/
export async function createFileSystem(input: {
/**
* The OpenAPI document to create the filesystem from.
*/
value: AnyApiDefinitionFormat;
baseUrl: string;
/**
* The root URL of the specified OpenAPI document.
* Used to resolve relative URLs.
*/
rootURL: string | null;
}): Promise<Filesystem> {
const { filesystem } = await load(input.value, {
plugins: [fetchUrls({ baseUrl: input.baseUrl })],
plugins: [fetchURLs({ rootURL: input.rootURL })],
});
return filesystem;
}
+1 -1
View File
@@ -7,7 +7,7 @@ describe('#parseOpenAPI', () => {
it('parses an OpenAPI document', async () => {
const schema = await parseOpenAPI({
value: spec,
url: 'https://example.com',
rootURL: null,
});
// Ensure the structure returned is not recursive (not dereferenced).
JSON.stringify(schema);
+12 -2
View File
@@ -1,3 +1,4 @@
import { AnyApiDefinitionFormat } from '@scalar/openapi-parser';
import { OpenAPIParseError } from './error';
import { convertOpenAPIV2ToOpenAPIV3 } from './v2';
import { parseOpenAPIV3 } from './v3';
@@ -7,11 +8,20 @@ import { parseOpenAPIV3 } from './v3';
* 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: { value: string; url: string }) {
export async function parseOpenAPI(input: {
/**
* The API definition to parse.
*/
value: AnyApiDefinitionFormat;
/**
* The root URL of the specified OpenAPI document.
*/
rootURL: string | null;
}) {
try {
return await parseOpenAPIV3(input);
} catch (error) {
if (error instanceof OpenAPIParseError && error.code === 'v2-spec') {
if (error instanceof OpenAPIParseError && error.code === 'parse-v2-in-v3') {
return convertOpenAPIV2ToOpenAPIV3(input);
}
throw error;
@@ -4,11 +4,11 @@ export const fetchUrlsDefaultConfiguration = {
limit: 40,
};
export const fetchUrls: (customConfiguration: {
export const fetchURLs: (customConfiguration: {
/**
* Base URL to use for relative paths.
* Root URL to resolve relative URLs.
*/
baseUrl: string;
rootURL: string | null;
/**
* Limit the number of requests. Set to `false` to disable the limit.
@@ -53,7 +53,7 @@ export const fetchUrls: (customConfiguration: {
try {
numberOfRequests++;
const url = getReferenceUrl(value, configuration.baseUrl);
const url = getReferenceUrl({ value, rootURL: configuration.rootURL });
const response = await fetch(url);
return await response.text();
} catch (error: any) {
@@ -66,6 +66,7 @@ export const fetchUrls: (customConfiguration: {
/**
* Check if a path is relative.
* Meaning it does not start with http://, https://, www., data:, or #/.
*/
function isRelativePath(path: string): boolean {
// Exclude external URLs
@@ -76,9 +77,13 @@ function isRelativePath(path: string): boolean {
/**
* Get the reference URL.
*/
function getReferenceUrl(value: string, baseUrl: string) {
function getReferenceUrl(input: { value: string; rootURL: string | null }) {
const { value, rootURL } = input;
if (isRelativePath(value)) {
return new URL(value, baseUrl).href;
if (!rootURL) {
throw new Error(`[fetchUrls] Cannot resolve relative path without rootURL (${value})`);
}
return new URL(value, rootURL).href;
}
return value;
+1 -1
View File
@@ -35,7 +35,7 @@ describe('#traverse', () => {
it('traverses a complete filesystem', async () => {
const filesystem = await createFileSystem({
value: JSON.parse(recursiveSpec),
baseUrl: 'https://example.com',
rootURL: 'https://example.com',
});
const transformedFilesystem = await traverse(filesystem, async (node) => {
+1 -1
View File
@@ -7,7 +7,7 @@ describe('#convertOpenAPIV2ToOpenAPIV3', () => {
it('converts an OpenAPIV2 in V3', async () => {
const schema = await convertOpenAPIV2ToOpenAPIV3({
value: specV2,
url: 'https://example.com',
rootURL: null,
});
// Ensure the structure returned is not recursive (not dereferenced).
JSON.stringify(schema);
+21 -11
View File
@@ -10,12 +10,18 @@ import type { Filesystem, OpenAPIV3xDocument } from './types';
* Convert a Swagger 2.0 schema to an OpenAPI 3.0 schema.
*/
export async function convertOpenAPIV2ToOpenAPIV3(input: {
/**
* The API definition to parse.
*/
value: AnyApiDefinitionFormat;
url: string;
/**
* The root URL of the specified OpenAPI document.
*/
rootURL: string | null;
}): Promise<Filesystem<OpenAPIV3xDocument>> {
const { value, url } = input;
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, url }) : value;
const schema = typeof value === 'string' ? rawParseOpenAPI({ value, rootURL }) : value;
try {
// @ts-expect-error Types are incompatible between the two libraries
const convertResult = (await swagger2openapi.convertObj(schema, {
@@ -29,13 +35,14 @@ export async function convertOpenAPIV2ToOpenAPIV3(input: {
patch: true,
})) as ConvertOutputOptions;
return parseOpenAPIV3({ url, value: convertResult.openapi });
return parseOpenAPIV3({ 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: ' + (error as Error).message,
url,
);
throw new OpenAPIParseError('Failed to convert Swagger 2.0 to OpenAPI 3.0', {
code: 'v2-conversion',
rootURL,
cause: error,
});
} else {
throw error;
}
@@ -46,8 +53,8 @@ export async function convertOpenAPIV2ToOpenAPIV3(input: {
* Parse the config file from a raw string.
* Useful to get the raw object from a file.
*/
function rawParseOpenAPI(input: { value: string; url: string }): unknown {
const { value, url } = input;
function rawParseOpenAPI(input: { value: string; rootURL: string | null }): unknown {
const { value, rootURL } = input;
// Try with JSON
try {
@@ -58,7 +65,10 @@ function rawParseOpenAPI(input: { value: string; url: string }): unknown {
return YAML.parse(value);
} catch (yamlError) {
if (yamlError instanceof Error && yamlError.name.startsWith('YAML')) {
throw new OpenAPIParseError('Failed to parse YAML: ' + yamlError.message, url);
throw new OpenAPIParseError('Failed to parse YAML: ' + yamlError.message, {
code: 'yaml-parse',
rootURL,
});
}
throw yamlError;
}
+17 -5
View File
@@ -9,22 +9,34 @@ import type { Filesystem, OpenAPIV3xDocument } from './types';
* It can throw an `OpenAPIFetchError` if the document is invalid.
*/
export async function parseOpenAPIV3(input: {
/**
* The API definition to parse.
*/
value: AnyApiDefinitionFormat;
url: string;
/**
* The root URL of the specified OpenAPI document.
*/
rootURL: string | null;
}): Promise<Filesystem<OpenAPIV3xDocument>> {
const { value, url } = input;
const { value, rootURL } = input;
const result = await validate(value);
// Spec is invalid, we stop here.
if (!result.specification) {
throw new OpenAPIParseError('Invalid OpenAPI document', url, 'invalid-spec');
throw new OpenAPIParseError('Invalid OpenAPI document', {
code: 'invalid',
rootURL,
});
}
if (result.version === '2.0') {
throw new OpenAPIParseError('Only OpenAPI v3 is supported', url, 'v2-spec');
throw new OpenAPIParseError('Only OpenAPI v3 is supported', {
code: 'parse-v2-in-v3',
rootURL,
});
}
const filesystem = await createFileSystem({ value: result.specification, baseUrl: url });
const filesystem = await createFileSystem({ value: result.specification, rootURL });
return filesystem;
}
File diff suppressed because one or more lines are too long
@@ -1,9 +1,8 @@
import { CodeSampleInput, codeSampleGenerators } from './code-samples';
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
import { InteractiveSection } from './InteractiveSection';
import { getServersURL } from './OpenAPIServerURL';
import { OpenAPIContextProps } from './types';
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
import { noReference } from './utils';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
@@ -1,11 +1,10 @@
import clsx from 'clsx';
import type { OpenAPIOperationData } from './fetchOpenAPIOperation';
import { Markdown } from './Markdown';
import { OpenAPICodeSample } from './OpenAPICodeSample';
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
import { OpenAPISpec } from './OpenAPISpec';
import { OpenAPIClientContext, type OpenAPIContextProps } from './types';
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIOperationData } from './types';
import { OpenAPIPath } from './OpenAPIPath';
import { resolveDescription } from './utils';
+1 -2
View File
@@ -1,6 +1,5 @@
import type { OpenAPIOperationData } from './fetchOpenAPIOperation';
import { ScalarApiButton } from './ScalarApiButton';
import type { OpenAPIContextProps } from './types';
import type { OpenAPIOperationData, OpenAPIContextProps } from './types';
/**
* Display the path of an operation.
@@ -1,7 +1,6 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import type { OpenAPIOperationData } from './fetchOpenAPIOperation';
import { generateSchemaExample } from './generateSchemaExample';
import type { OpenAPIContextProps } from './types';
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
import { checkIsReference, noReference, resolveDescription } from './utils';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
@@ -1,8 +1,7 @@
import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
import type { OpenAPIClientContext } from './types';
import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
import { InteractiveSection } from './InteractiveSection';
import { Markdown } from './Markdown';
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
import { OpenAPISchemaName } from './OpenAPISchemaName';
import { resolveDescription } from './utils';
+1 -2
View File
@@ -2,13 +2,12 @@
import type { OpenAPI } from '@gitbook/openapi-parser';
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
import { InteractiveSection } from './InteractiveSection';
import { OpenAPIRequestBody } from './OpenAPIRequestBody';
import { OpenAPIResponses } from './OpenAPIResponses';
import { OpenAPISchemaProperties } from './OpenAPISchema';
import { OpenAPISecurities } from './OpenAPISecurities';
import { OpenAPIClientContext } from './types';
import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
import { noReference, resolveDescription } from './utils';
/**
@@ -1,193 +0,0 @@
import { it, expect } from 'bun:test';
import { fetchOpenAPIOperation, type OpenAPIFetcher } from './fetchOpenAPIOperation';
import { parseOpenAPI, traverse } from '@gitbook/openapi-parser';
const fetcher: OpenAPIFetcher = {
fetch: async (url) => {
const response = await fetch(url);
const text = await response.text();
const filesystem = await parseOpenAPI({ value: text, url });
const transformedFs = await traverse(filesystem, async (node) => {
if ('description' in node && typeof node.description === 'string' && node.description) {
node['x-description-html'] = node.description;
}
return node;
});
return transformedFs;
},
};
it('should resolve refs', async () => {
const resolved = await fetchOpenAPIOperation(
{
url: 'https://petstore3.swagger.io/api/v3/openapi.json',
method: 'put',
path: '/pet',
},
fetcher,
);
expect(resolved).toMatchObject({
servers: [
{
url: '/api/v3',
},
],
operation: {
tags: ['pet'],
summary: 'Update an existing pet',
description: 'Update an existing pet by Id',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'photoUrls'],
},
},
},
},
},
});
});
it('should support yaml', async () => {
const resolved = await fetchOpenAPIOperation(
{
url: 'https://petstore3.swagger.io/api/v3/openapi.yaml',
method: 'put',
path: '/pet',
},
fetcher,
);
expect(resolved).toMatchObject({
servers: [
{
url: '/api/v3',
},
],
operation: {
tags: ['pet'],
summary: 'Update an existing pet',
description: 'Update an existing pet by Id',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'photoUrls'],
},
},
},
},
},
});
});
it('should resolve circular refs', async () => {
const resolved = await fetchOpenAPIOperation(
{
url: 'https://api.gitbook.com/openapi.json',
method: 'post',
path: '/search/ask',
},
fetcher,
);
expect(resolved).toMatchObject({
servers: [
{
url: '{host}/v1',
},
],
operation: {
operationId: 'askQuery',
},
});
});
it('should resolve to null if the method is not supported', async () => {
const resolved = await fetchOpenAPIOperation(
{
url: 'https://petstore3.swagger.io/api/v3/openapi.json',
method: 'dontexist',
path: '/pet',
},
fetcher,
);
expect(resolved).toBe(null);
});
it('should parse Swagger 2.0', async () => {
const resolved = await fetchOpenAPIOperation(
{
url: 'https://petstore.swagger.io/v2/swagger.json',
method: 'put',
path: '/pet',
},
fetcher,
);
expect(resolved).toMatchObject({
servers: [
{
url: 'https://petstore.swagger.io/v2',
},
{
url: 'http://petstore.swagger.io/v2',
},
],
operation: {
tags: ['pet'],
summary: 'Update an existing pet',
description: '',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'photoUrls'],
},
},
},
},
},
});
});
it('should resolve a ref with whitespace', async () => {
const resolved = await fetchOpenAPIOperation(
{
url: ' https://petstore3.swagger.io/api/v3/openapi.json',
method: 'put',
path: '/pet',
},
fetcher,
);
expect(resolved).toMatchObject({
servers: [
{
url: '/api/v3',
},
],
operation: {
tags: ['pet'],
summary: 'Update an existing pet',
description: 'Update an existing pet by Id',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'photoUrls'],
},
},
},
},
},
});
});
+2 -1
View File
@@ -1,3 +1,4 @@
export * from './fetchOpenAPIOperation';
export * from './resolveOpenAPIOperation';
export * from './OpenAPIOperation';
export * from './OpenAPIOperationContext';
export type { OpenAPIOperationData } from './types';
@@ -0,0 +1,177 @@
import { it, expect, describe } from 'bun:test';
import { resolveOpenAPIOperation } from './resolveOpenAPIOperation';
import { parseOpenAPI, traverse } from '@gitbook/openapi-parser';
async function fetchFilesystem(url: string) {
const response = await fetch(url);
const text = await response.text();
const filesystem = await parseOpenAPI({ value: text, rootURL: url });
const transformedFs = await traverse(filesystem, async (node) => {
if ('description' in node && typeof node.description === 'string' && node.description) {
node['x-description-html'] = node.description;
}
return node;
});
return transformedFs;
}
describe('#resolveOpenAPIOperation', () => {
it('should resolve refs', async () => {
const filesystem = await fetchFilesystem(
'https://petstore3.swagger.io/api/v3/openapi.json',
);
const resolved = await resolveOpenAPIOperation(filesystem, { method: 'put', path: '/pet' });
expect(resolved).toMatchObject({
servers: [
{
url: '/api/v3',
},
],
operation: {
tags: ['pet'],
summary: 'Update an existing pet',
description: 'Update an existing pet by Id',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'photoUrls'],
},
},
},
},
},
});
});
it('should support yaml', async () => {
const filesystem = await fetchFilesystem(
'https://petstore3.swagger.io/api/v3/openapi.yaml',
);
const resolved = await resolveOpenAPIOperation(filesystem, { method: 'put', path: '/pet' });
expect(resolved).toMatchObject({
servers: [
{
url: '/api/v3',
},
],
operation: {
tags: ['pet'],
summary: 'Update an existing pet',
description: 'Update an existing pet by Id',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'photoUrls'],
},
},
},
},
},
});
});
it('should resolve circular refs', async () => {
const filesystem = await fetchFilesystem('https://api.gitbook.com/openapi.json');
const resolved = await resolveOpenAPIOperation(filesystem, {
method: 'post',
path: '/search/ask',
});
expect(resolved).toMatchObject({
servers: [
{
url: '{host}/v1',
},
],
operation: {
operationId: 'askQuery',
},
});
});
it('should resolve to null if the method is not supported', async () => {
const filesystem = await fetchFilesystem(
'https://petstore3.swagger.io/api/v3/openapi.json',
);
const resolved = await resolveOpenAPIOperation(filesystem, {
method: 'dontexist',
path: '/pet',
});
expect(resolved).toBe(null);
});
it('should parse Swagger 2.0', async () => {
const filesystem = await fetchFilesystem('https://petstore.swagger.io/v2/swagger.json');
const resolved = await resolveOpenAPIOperation(filesystem, {
method: 'put',
path: '/pet',
});
expect(resolved).toMatchObject({
servers: [
{
url: 'https://petstore.swagger.io/v2',
},
{
url: 'http://petstore.swagger.io/v2',
},
],
operation: {
tags: ['pet'],
summary: 'Update an existing pet',
description: '',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'photoUrls'],
},
},
},
},
},
});
});
it('should resolve a ref with whitespace', async () => {
const filesystem = await fetchFilesystem(
' https://petstore3.swagger.io/api/v3/openapi.json',
);
const resolved = await resolveOpenAPIOperation(filesystem, {
method: 'put',
path: '/pet',
});
expect(resolved).toMatchObject({
servers: [
{
url: '/api/v3',
},
],
operation: {
tags: ['pet'],
summary: 'Update an existing pet',
description: 'Update an existing pet by Id',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'photoUrls'],
},
},
},
},
},
});
});
});
@@ -1,61 +1,37 @@
import { toJSON, fromJSON } from 'flatted';
import {
type OpenAPICustomOperationProperties,
type OpenAPICustomSpecProperties,
type OpenAPIV3xDocument,
type Filesystem,
type OpenAPIV3,
type OpenAPIV3_1,
OpenAPIParseError,
dereference,
} from '@gitbook/openapi-parser';
import { noReference } from './utils';
export interface OpenAPIFetcher {
/**
* Fetch an OpenAPI file by its URL. It should return a fully parsed OpenAPI v3 document.
*/
fetch: (url: string) => Promise<Filesystem<OpenAPIV3xDocument>>;
}
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
path: string;
method: string;
/** Servers to be used for this operation */
servers: OpenAPIV3.ServerObject[];
/** Spec of the operation */
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
/** Securities that should be used for this operation */
securities: [string, OpenAPIV3.SecuritySchemeObject][];
}
import { OpenAPIOperationData } from './types';
export { toJSON, fromJSON };
/**
* Resolve an OpenAPI operation in a file and compile it to a more usable format.
*/
export async function fetchOpenAPIOperation(
input: {
url: string;
export async function resolveOpenAPIOperation(
filesystem: Filesystem<OpenAPIV3xDocument>,
operationDescriptor: {
path: string;
method: string;
},
fetcher: OpenAPIFetcher,
): Promise<OpenAPIOperationData | null> {
const filesystem = await fetcher.fetch(input.url);
const schema = await memoDereferenceFilesystem(filesystem, input.url);
let operation = getOperationByPathAndMethod(schema, input.path, input.method);
const { path, method } = operationDescriptor;
const schema = await memoDereferenceFilesystem(filesystem);
let operation = getOperationByPathAndMethod(schema, path, method);
if (!operation) {
return null;
}
// Resolve common parameters
const commonParameters = getPathObjectParameter(schema, input.path);
const commonParameters = getPathObjectParameter(schema, path);
if (commonParameters) {
operation = {
...operation,
@@ -81,8 +57,8 @@ export async function fetchOpenAPIOperation(
return {
servers,
operation,
method: input.method,
path: input.path,
method,
path,
securities,
'x-codeSamples':
typeof schema['x-codeSamples'] === 'boolean' ? schema['x-codeSamples'] : undefined,
@@ -98,15 +74,12 @@ const dereferenceCache = new WeakMap<Filesystem, Promise<OpenAPIV3xDocument>>();
/**
* Memoized version of `dereferenceSchema`.
*/
function memoDereferenceFilesystem(
filesystem: Filesystem,
url: string,
): Promise<OpenAPIV3xDocument> {
function memoDereferenceFilesystem(filesystem: Filesystem): Promise<OpenAPIV3xDocument> {
if (dereferenceCache.has(filesystem)) {
return dereferenceCache.get(filesystem) as Promise<OpenAPIV3xDocument>;
}
const promise = dereferenceFilesystem(filesystem, url);
const promise = dereferenceFilesystem(filesystem);
dereferenceCache.set(filesystem, promise);
return promise;
}
@@ -114,18 +87,11 @@ function memoDereferenceFilesystem(
/**
* Dereference an OpenAPI schema.
*/
async function dereferenceFilesystem(
filesystem: Filesystem,
url: string,
): Promise<OpenAPIV3xDocument> {
async function dereferenceFilesystem(filesystem: Filesystem): Promise<OpenAPIV3xDocument> {
const result = await dereference(filesystem);
if (!result.schema) {
throw new OpenAPIParseError(
'Failed to dereference OpenAPI document',
url,
'failed-dereference',
);
throw new Error('Failed to dereference OpenAPI document');
}
return result.schema as OpenAPIV3xDocument;
+20
View File
@@ -1,3 +1,9 @@
import type {
OpenAPICustomOperationProperties,
OpenAPICustomSpecProperties,
OpenAPIV3,
} from '@gitbook/openapi-parser';
export interface OpenAPIContextProps extends OpenAPIClientContext {
CodeBlock: React.ComponentType<{ code: string; syntax: string }>;
@@ -24,3 +30,17 @@ export interface OpenAPIClientContext {
/** Optional id attached to the OpenAPI Operation heading and used as an anchor */
id?: string;
}
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
path: string;
method: string;
/** Servers to be used for this operation */
servers: OpenAPIV3.ServerObject[];
/** Spec of the operation */
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
/** Securities that should be used for this operation */
securities: [string, OpenAPIV3.SecuritySchemeObject][];
}