Less strict OpenAPI parser (#3529)

Co-authored-by: Nolann Biron <biron.nolann@gmail.com>
This commit is contained in:
Greg Bergé
2025-08-05 10:45:45 +02:00
committed by GitHub
parent 44f41510fa
commit f1a6dec951
13 changed files with 2196 additions and 102 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Update OpenAPI parser
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/openapi-parser": major
---
Less strict OpenAPI parser, now returns { errors, filesystem }
@@ -7,7 +7,7 @@ const spec = await Bun.file(new URL('./fixtures/multiline-spec.yaml', import.met
describe('#enrichFilesystem', () => {
it('supports multiline descriptions', async () => {
const filesystem = await parseOpenAPI({
const { filesystem } = await parseOpenAPI({
value: spec,
rootURL: null,
});
+1 -7
View File
@@ -108,13 +108,7 @@ async function fetchFilesystemUncached(
}
const text = await response.text();
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 { filesystem } = await parseOpenAPI({ value: text, rootURL: url });
const richFilesystem = await enrichFilesystem(filesystem);
return richFilesystem;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,830 @@
openapi: 3.0.4
info:
title: Swagger Petstore - OpenAPI 3.0
description: |-
This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about
Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!
You can now help us improve the API whether it's by making changes to the definition itself or to the code.
That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
Some useful links:
- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
termsOfService: https://swagger.io/terms/
contact:
email: apiteam@swagger.io
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
version: 1.0.26
externalDocs:
description: Find out more about Swagger
url: https://swagger.io
servers:
- url: "/api/v3"
tags:
- name: pet
description: Everything about your Pets
externalDocs:
description: Find out more
url: https://swagger.io
- name: store
description: Access to Petstore orders
externalDocs:
description: Find out more about our store
url: https://swagger.io
- name: user
description: Operations about user
paths:
"/pet":
put:
tags:
- pet
summary: Update an existing pet.
description: Update an existing pet by Id.
operationId: updatePet
requestBody:
description: Update an existent pet in the store
content:
application/json:
schema:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
application/x-www-form-urlencoded:
schema:
"$ref": "#/components/schemas/Pet"
required: true
responses:
'200':
description: Successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
'400':
description: Invalid ID supplied
'404':
description: Pet not found
'422':
description: Validation exception
default:
description: Unexpected error
security:
- petstore_auth:
- write:pets
- read:pets
post:
tags:
- pet
summary: Add a new pet to the store.
description: Add a new pet to the store.
operationId: addPet
requestBody:
description: Create a new pet in the store
content:
application/json:
schema:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
application/x-www-form-urlencoded:
schema:
"$ref": "#/components/schemas/Pet"
required: true
responses:
'200':
description: Successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
'400':
description: Invalid input
'422':
description: Validation exception
default:
description: Unexpected error
security:
- petstore_auth:
- write:pets
- read:pets
"/pet/findByStatus":
get:
tags:
- pet
summary: Finds Pets by status.
description: Multiple status values can be provided with comma separated strings.
operationId: findPetsByStatus
parameters:
- name: status
in: query
description: Status values that need to be considered for filter
required: false
explode: true
schema:
type: string
default: available
enum:
- available
- pending
- sold
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
type: array
items:
"$ref": "#/components/schemas/Pet"
'400':
description: Invalid status value
default:
description: Unexpected error
security:
- petstore_auth:
- write:pets
- read:pets
"/pet/findByTags":
get:
tags:
- pet
summary: Finds Pets by tags.
description: Multiple tags can be provided with comma separated strings. Use
tag1, tag2, tag3 for testing.
operationId: findPetsByTags
parameters:
- name: tags
in: query
description: Tags to filter by
required: false
explode: true
schema:
type: array
items:
type: string
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
type: array
items:
"$ref": "#/components/schemas/Pet"
'400':
description: Invalid tag value
default:
description: Unexpected error
security:
- petstore_auth:
- write:pets
- read:pets
"/pet/{petId}":
get:
tags:
- pet
summary: Find pet by ID.
description: Returns a single pet.
operationId: getPetById
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
'400':
description: Invalid ID supplied
'404':
description: Pet not found
default:
description: Unexpected error
security:
- api_key: []
- petstore_auth:
- write:pets
- read:pets
post:
tags:
- pet
summary: Updates a pet in the store with form data.
description: Updates a pet resource based on the form data.
operationId: updatePetWithForm
parameters:
- name: petId
in: path
description: ID of pet that needs to be updated
required: true
schema:
type: integer
format: int64
- name: name
in: query
description: Name of pet that needs to be updated
schema:
type: string
- name: status
in: query
description: Status of pet that needs to be updated
schema:
type: string
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
'400':
description: Invalid input
default:
description: Unexpected error
security:
- petstore_auth:
- write:pets
- read:pets
delete:
tags:
- pet
summary: Deletes a pet.
description: Delete a pet.
operationId: deletePet
parameters:
- name: api_key
in: header
description: ''
required: false
schema:
type: string
- name: petId
in: path
description: Pet id to delete
required: true
schema:
type: integer
format: int64
responses:
'200':
description: Pet deleted
'400':
description: Invalid pet value
default:
description: Unexpected error
security:
- petstore_auth:
- write:pets
- read:pets
"/pet/{petId}/uploadImage":
post:
tags:
- pet
summary: Uploads an image.
description: Upload image of the pet.
operationId: uploadFile
parameters:
- name: petId
in: path
description: ID of pet to update
required: true
schema:
type: integer
format: int64
- name: additionalMetadata
in: query
description: Additional Metadata
required: false
schema:
type: string
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/ApiResponse"
'400':
description: No file uploaded
'404':
description: Pet not found
default:
description: Unexpected error
security:
- petstore_auth:
- write:pets
- read:pets
"/store/inventory":
get:
tags:
- store
summary: Returns pet inventories by status.
description: Returns a map of status codes to quantities.
operationId: getInventory
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: object
additionalProperties:
type: integer
format: int32
default:
description: Unexpected error
security:
- api_key: []
"/store/order":
post:
tags:
- store
summary: Place an order for a pet.
description: Place a new order in the store.
operationId: placeOrder
requestBody:
content:
application/json:
schema:
"$ref": "#/components/schemas/Order"
application/xml:
schema:
"$ref": "#/components/schemas/Order"
application/x-www-form-urlencoded:
schema:
"$ref": "#/components/schemas/Order"
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Order"
'400':
description: Invalid input
'422':
description: Validation exception
default:
description: Unexpected error
"/store/order/{orderId}":
get:
tags:
- store
summary: Find purchase order by ID.
description: For valid response try integer IDs with value <= 5 or > 10. Other
values will generate exceptions.
operationId: getOrderById
parameters:
- name: orderId
in: path
description: ID of order that needs to be fetched
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Order"
application/xml:
schema:
"$ref": "#/components/schemas/Order"
'400':
description: Invalid ID supplied
'404':
description: Order not found
default:
description: Unexpected error
delete:
tags:
- store
summary: Delete purchase order by identifier.
description: For valid response try integer IDs with value < 1000. Anything
above 1000 or non-integers will generate API errors.
operationId: deleteOrder
parameters:
- name: orderId
in: path
description: ID of the order that needs to be deleted
required: true
schema:
type: integer
format: int64
responses:
'200':
description: order deleted
'400':
description: Invalid ID supplied
'404':
description: Order not found
default:
description: Unexpected error
"/user":
post:
tags:
- user
summary: Create user.
description: This can only be done by the logged in user.
operationId: createUser
requestBody:
description: Created user object
content:
application/json:
schema:
"$ref": "#/components/schemas/User"
application/xml:
schema:
"$ref": "#/components/schemas/User"
application/x-www-form-urlencoded:
schema:
"$ref": "#/components/schemas/User"
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/User"
application/xml:
schema:
"$ref": "#/components/schemas/User"
default:
description: Unexpected error
"/user/createWithList":
post:
tags:
- user
summary: Creates list of users with given input array.
description: Creates list of users with given input array.
operationId: createUsersWithListInput
requestBody:
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/User"
responses:
'200':
description: Successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/User"
application/xml:
schema:
"$ref": "#/components/schemas/User"
default:
description: Unexpected error
"/user/login":
get:
tags:
- user
summary: Logs user into the system.
description: Log into the system.
operationId: loginUser
parameters:
- name: username
in: query
description: The user name for login
required: false
schema:
type: string
- name: password
in: query
description: The password for login in clear text
required: false
schema:
type: string
responses:
'200':
description: successful operation
headers:
X-Rate-Limit:
description: calls per hour allowed by the user
schema:
type: integer
format: int32
X-Expires-After:
description: date in UTC when token expires
schema:
type: string
format: date-time
content:
application/xml:
schema:
type: string
application/json:
schema:
type: string
'400':
description: Invalid username/password supplied
default:
description: Unexpected error
"/user/logout":
get:
tags:
- user
summary: Logs out current logged in user session.
description: Log user out of the system.
operationId: logoutUser
parameters: []
responses:
'200':
description: successful operation
default:
description: Unexpected error
"/user/{username}":
get:
tags:
- user
summary: Get user by user name.
description: Get user detail based on username.
operationId: getUserByName
parameters:
- name: username
in: path
description: The name that needs to be fetched. Use user1 for testing
required: true
schema:
type: string
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/User"
application/xml:
schema:
"$ref": "#/components/schemas/User"
'400':
description: Invalid username supplied
'404':
description: User not found
default:
description: Unexpected error
put:
tags:
- user
summary: Update user resource.
description: This can only be done by the logged in user.
operationId: updateUser
parameters:
- name: username
in: path
description: name that need to be deleted
required: true
schema:
type: string
requestBody:
description: Update an existent user in the store
content:
application/json:
schema:
"$ref": "#/components/schemas/User"
application/xml:
schema:
"$ref": "#/components/schemas/User"
application/x-www-form-urlencoded:
schema:
"$ref": "#/components/schemas/User"
responses:
'200':
description: successful operation
'400':
description: bad request
'404':
description: user not found
default:
description: Unexpected error
delete:
tags:
- user
summary: Delete user resource.
description: This can only be done by the logged in user.
operationId: deleteUser
parameters:
- name: username
in: path
description: The name that needs to be deleted
required: true
schema:
type: string
responses:
'200':
description: User deleted
'400':
description: Invalid username supplied
'404':
description: User not found
default:
description: Unexpected error
components:
schemas:
Order:
type: object
properties:
id:
type: integer
format: int64
example: 10
petId:
type: integer
format: int64
example: 198772
quantity:
type: integer
format: int32
example: 7
shipDate:
type: string
format: date-time
status:
type: string
description: Order Status
example: approved
enum:
- placed
- approved
- delivered
complete:
type: boolean
xml:
name: order
Category:
type: object
properties:
id:
type: integer
format: int64
example: 1
name:
type: string
example: Dogs
xml:
name: category
User:
type: object
properties:
id:
type: integer
format: int64
example: 10
username:
type: string
example: theUser
firstName:
type: string
example: John
lastName:
type: string
example: James
email:
type: string
example: john@email.com
password:
type: string
example: '12345'
phone:
type: string
example: '12345'
userStatus:
type: integer
description: User Status
format: int32
example: 1
xml:
name: user
Tag:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
xml:
name: tag
Pet:
required:
- name
- photoUrls
type: object
properties:
id:
type: integer
format: int64
example: 10
name:
type: string
example: doggie
category:
"$ref": "#/components/schemas/Category"
photoUrls:
type: array
xml:
wrapped: true
items:
type: string
xml:
name: photoUrl
tags:
type: array
xml:
wrapped: true
items:
"$ref": "#/components/schemas/Tag"
status:
type: string
description: pet status in the store
enum:
- available
- pending
- sold
xml:
name: pet
ApiResponse:
type: object
properties:
code:
type: integer
format: int32
type:
type: string
message:
type: string
xml:
name: "##default"
requestBodies:
Pet:
description: Pet object that needs to be added to the store
content:
application/json:
schema:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
UserArray:
description: List of user object
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/User"
securitySchemes:
petstore_auth:
type: oauth2
flows:
implicit:
authorizationUrl: https://petstore3.swagger.io/oauth/authorize
scopes:
write:pets: modify pets in your account
read:pets: read your pets
api_key:
type: apiKey
name: api_key
in: header
+27 -4
View File
@@ -4,26 +4,30 @@ import { parseOpenAPI } from './parse';
const spec = await Bun.file(new URL('./fixtures/recursive-spec.json', import.meta.url)).text();
const specV2 = await Bun.file(new URL('./fixtures/spec-v2.json', import.meta.url)).text();
const petstoreyaml = await Bun.file(new URL('./fixtures/petstore.yaml', import.meta.url)).text();
const petstoreInvalid = await Bun.file(
new URL('./fixtures/petstore-invalid.json', import.meta.url)
).text();
const html = `<!DOCTYPE html><html lang=\"en\"><head> <meta charset=\"utf-8\"> <title>API Documentation</title> <base href=\"/api-docs/\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> <link rel=\"icon\" type=\"image/x-icon\" href=\"favicon.ico\"> <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.7.0/css/all.css\" integrity=\"sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ\" crossorigin=\"anonymous\"> <style>@import\"https://fonts.googleapis.com/css2?family=Nunito:wght@200&display=swap\";@charset \"UTF-8\";:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,\"Segoe UI\",Roboto,\"Helvetica Neue\",\"Noto Sans\",\"Liberation Sans\",Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, .15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, .175);--bs-border-radius:.375rem;--bs-border-radius-sm:.25rem;--bs-border-radius-lg:.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,:after,:before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}:root{--primaryColor1:#373e3e;--primaryColor2:#373e3e;--white:#ffffff;--lightGrey:#F8F9F9;--grey:#F8F9F9;--darkGrey:#6c757d;--black:black}*{box-sizing:border-box}body{font-family:Nunito,sans-serif;background-color:var(--grey);margin:5px auto}</style><link rel=\"stylesheet\" href=\"styles.css\" media=\"print\" onload=\"this.media='all'\"><noscript><link rel=\"stylesheet\" href=\"styles.css\"></noscript></head> <body> <app-root></app-root> <script> parent.postMessage(location.hash, location.origin); </script> <script src=\"runtime.js\" type=\"module\"></script><script src=\"polyfills.js\" type=\"module\"></script><script src=\"scripts.js\" defer></script><script src=\"main.js\" type=\"module\"></script> <script type=\"text/javascript\" src=\"/_Incapsula_Resource?SWJIYLWA=719d34d31c8e3a6e6fffd425f7e032f3&ns=2&cb=242625292\" async></script></body></html>`;
describe('#parseOpenAPI', () => {
it('parses a recursive OpenAPI document', async () => {
const schema = await parseOpenAPI({
const result = await parseOpenAPI({
value: spec,
rootURL: null,
});
// Ensure the structure returned is not recursive (not dereferenced).
JSON.stringify(schema);
JSON.stringify(result.filesystem);
});
it('parses a swagger v2', async () => {
const schema = await parseOpenAPI({
const result = await parseOpenAPI({
value: specV2,
rootURL: null,
});
// Ensure the structure returned is not recursive (not dereferenced).
JSON.stringify(schema);
JSON.stringify(result.filesystem);
});
it('throws an error for invalid OpenAPI document', async () => {
@@ -39,4 +43,23 @@ describe('#parseOpenAPI', () => {
}
}
});
it('allows a document yaml', async () => {
const result = await parseOpenAPI({
value: petstoreyaml,
rootURL: null,
});
// Ensure the structure returned is not recursive (not dereferenced).
JSON.stringify(result.filesystem);
});
it('allows a document with errors', async () => {
const result = await parseOpenAPI({
value: petstoreInvalid,
rootURL: null,
});
// Ensure the structure returned is not recursive (not dereferenced).
JSON.stringify(result.filesystem);
expect(result.errors).toHaveLength(1);
});
});
+14 -5
View File
@@ -1,5 +1,6 @@
import type { AnyObject, bundle } from '@scalar/openapi-parser';
import type { AnyObject, ErrorObject, bundle } from '@scalar/openapi-parser';
import { OpenAPIParseError } from './error';
import type { Filesystem, OpenAPIV3xDocument } from './types';
import { convertOpenAPIV2ToOpenAPIV3 } from './v2';
import { parseOpenAPIV3 } from './v3';
@@ -14,10 +15,6 @@ export interface ParseOpenAPIInput {
* The root URL of the specified OpenAPI document.
*/
rootURL: string | null;
/**
* Trust the input. This will skip advanced validation.
*/
trust?: boolean;
/**
* Options for the parser.
*/
@@ -26,6 +23,18 @@ export interface ParseOpenAPIInput {
};
}
export interface ParseOpenAPIResult {
/**
* Informational errors that were found while parsing the OpenAPI document.
*/
errors: ErrorObject[];
/**
* The parsed OpenAPI document.
*/
filesystem: Filesystem<OpenAPIV3xDocument>;
}
/**
* Parse a raw string into an OpenAPI document.
* It will also convert Swagger 2.0 to OpenAPI 3.0.
+3 -3
View File
@@ -5,12 +5,12 @@ const specV2 = await Bun.file(new URL('./fixtures/spec-v2.json', import.meta.url
describe('#convertOpenAPIV2ToOpenAPIV3', () => {
it('converts an OpenAPIV2 in V3', async () => {
const schema = await convertOpenAPIV2ToOpenAPIV3({
const result = await convertOpenAPIV2ToOpenAPIV3({
value: specV2,
rootURL: null,
});
// Ensure the structure returned is not recursive (not dereferenced).
JSON.stringify(schema);
expect(schema[0]?.specification.openapi).toBe('3.1.1');
JSON.stringify(result.filesystem);
expect(result.filesystem[0]?.specification.openapi).toBe('3.1.1');
});
});
+25 -22
View File
@@ -1,7 +1,6 @@
import { upgrade } from '@scalar/openapi-parser';
import { OpenAPIParseError } from './error';
import type { ParseOpenAPIInput } from './parse';
import type { Filesystem, OpenAPIV3xDocument } from './types';
import type { ParseOpenAPIInput, ParseOpenAPIResult } from './parse';
import { parseOpenAPIV3 } from './v3';
/**
@@ -9,24 +8,28 @@ import { parseOpenAPIV3 } from './v3';
*/
export async function convertOpenAPIV2ToOpenAPIV3(
input: ParseOpenAPIInput
): Promise<Filesystem<OpenAPIV3xDocument>> {
const { value, rootURL } = input;
const upgradeResult = (() => {
try {
return upgrade(value);
} catch (error) {
if (error instanceof Error) {
throw new OpenAPIParseError('Failed to convert Swagger 2.0 to OpenAPI 3.1.1', {
code: 'v2-conversion',
rootURL,
cause: error,
});
}
throw error;
}
})();
return parseOpenAPIV3({ ...input, rootURL, value: upgradeResult.specification, trust: true });
): Promise<ParseOpenAPIResult> {
const result = upgradeFromInput(input);
return parseOpenAPIV3({ ...input, rootURL: input.rootURL, value: result.specification });
}
/**
* Upgrade a Swagger 2.0 schema to an OpenAPI 3.0 schema.
* This function will throw an error if the conversion fails.
*/
function upgradeFromInput(input: ParseOpenAPIInput) {
const { value, rootURL } = input;
try {
return upgrade(value);
} catch (error) {
if (error instanceof Error) {
throw new OpenAPIParseError('Failed to convert Swagger 2.0 to OpenAPI 3.1.1', {
code: 'v2-conversion',
rootURL,
cause: error,
});
}
throw error;
}
}
+13 -58
View File
@@ -1,38 +1,15 @@
import { validate } from '@scalar/openapi-parser';
import { OpenAPIParseError } from './error';
import { createFileSystem } from './filesystem';
import type { ParseOpenAPIInput } from './parse';
import type { Filesystem, OpenAPIV3xDocument } from './types';
import type { ParseOpenAPIInput, ParseOpenAPIResult } 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: ParseOpenAPIInput
): Promise<Filesystem<OpenAPIV3xDocument>> {
const { value, rootURL, trust, options = {} } = input;
const specification = trust
? await trustedValidate({ value, rootURL })
: await untrustedValidate({ value, rootURL });
const filesystem = await createFileSystem({
value: specification,
rootURL,
options,
});
return filesystem;
}
type ValidateOpenAPIV3Input = Pick<ParseOpenAPIInput, 'value' | 'rootURL'>;
/**
* Validate an untrusted OpenAPI v3 document.
*/
async function untrustedValidate(input: ValidateOpenAPIV3Input) {
const { value, rootURL } = input;
export async function parseOpenAPIV3(input: ParseOpenAPIInput): Promise<ParseOpenAPIResult> {
const { value, rootURL, options = {} } = input;
const result = await validate(value);
if (result.version === '2.0') {
@@ -42,8 +19,9 @@ async function untrustedValidate(input: ValidateOpenAPIV3Input) {
});
}
// Spec is invalid, we stop here.
if (!result.valid) {
// We don't rely on `result.invalid` because it's too strict.
// If we succeed in parsing a schema, then we consider it valid.
if (!result.specification || !result.version) {
throw new OpenAPIParseError('Invalid OpenAPI document', {
code: 'invalid',
rootURL,
@@ -51,34 +29,11 @@ async function untrustedValidate(input: ValidateOpenAPIV3Input) {
});
}
return result.specification;
}
/**
* Validate a trusted OpenAPI v3 document.
* It assumes the specification is already a valid specification.
* It's faster than `untrustedValidate`.
*/
async function trustedValidate(input: ValidateOpenAPIV3Input) {
const { value, rootURL } = input;
const result = (() => {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch (_error) {
/** In case of an invalid JSON, we fallback to untrusted validation. */
return untrustedValidate(input);
}
}
return value;
})();
if ('swagger' in result && result.swagger) {
throw new OpenAPIParseError('Only OpenAPI v3 is supported', {
code: 'parse-v2-in-v3',
rootURL,
});
}
return result;
const filesystem = await createFileSystem({
value: result.specification,
rootURL,
options,
});
return { filesystem, errors: result.errors ?? [] };
}
@@ -6,7 +6,7 @@ import { resolveOpenAPIOperation } from './resolveOpenAPIOperation';
async function fetchFilesystem(url: string) {
const response = await fetch(url);
const text = await response.text();
const filesystem = await parseOpenAPI({ value: text, rootURL: url });
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-gitbook-description-html'] = node.description;
@@ -6,7 +6,7 @@ import { resolveOpenAPISchemas } from './resolveOpenAPISchemas';
async function fetchFilesystem(url: string) {
const response = await fetch(url);
const text = await response.text();
const filesystem = await parseOpenAPI({ value: text, rootURL: url });
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-gitbook-description-html'] = node.description;