mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Refactor and optimize OpenAPI parsing (#2830)
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@gitbook/openapi-parser': major
|
||||
'@gitbook/react-openapi': major
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Improve and split OpenAPI parser into its own package
|
||||
@@ -37,6 +37,7 @@
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@gitbook/react-contentkit": "workspace:*",
|
||||
"@gitbook/react-math": "workspace:*",
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
@@ -148,6 +149,23 @@
|
||||
"react": "*",
|
||||
},
|
||||
},
|
||||
"packages/openapi-parser": {
|
||||
"name": "@gitbook/openapi-parser",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@scalar/openapi-parser": "^0.10.4",
|
||||
"@scalar/openapi-types": "^0.1.6",
|
||||
"swagger2openapi": "^7.0.8",
|
||||
"yaml": "1.10.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"@types/swagger2openapi": "^7.0.4",
|
||||
"bun-types": "^1.1.20",
|
||||
"typescript": "^5.5.3",
|
||||
},
|
||||
},
|
||||
"packages/proxy": {
|
||||
"name": "@gitbook/proxy",
|
||||
"version": "0.1.0",
|
||||
@@ -191,20 +209,16 @@
|
||||
"name": "@gitbook/react-openapi",
|
||||
"version": "0.7.1",
|
||||
"dependencies": {
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@scalar/api-client-react": "1.0.87",
|
||||
"@scalar/oas-utils": "^0.2.101",
|
||||
"@scalar/openapi-parser": "^0.10.4",
|
||||
"@scalar/openapi-types": "^0.1.6",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"flatted": "^3.2.9",
|
||||
"react-aria": "^3.37.0",
|
||||
"react-aria-components": "^1.6.0",
|
||||
"swagger2openapi": "^7.0.8",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"yaml": "1.10.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/swagger2openapi": "^7.0.4",
|
||||
"bun-types": "^1.1.20",
|
||||
"typescript": "^5.5.3",
|
||||
},
|
||||
@@ -589,6 +603,8 @@
|
||||
|
||||
"@gitbook/icons": ["@gitbook/icons@workspace:packages/icons"],
|
||||
|
||||
"@gitbook/openapi-parser": ["@gitbook/openapi-parser@workspace:packages/openapi-parser"],
|
||||
|
||||
"@gitbook/proxy": ["@gitbook/proxy@workspace:packages/proxy"],
|
||||
|
||||
"@gitbook/react-contentkit": ["@gitbook/react-contentkit@workspace:packages/react-contentkit"],
|
||||
@@ -1327,6 +1343,10 @@
|
||||
|
||||
"@tsconfig/node18": ["@tsconfig/node18@1.0.3", "", {}, "sha512-RbwvSJQsuN9TB04AQbGULYfOGE/RnSFk/FLQ5b0NmDf5Kx2q/lABZbHQPKCO1vZ6Fiwkplu+yb9pGdLy1iGseQ=="],
|
||||
|
||||
"@tsconfig/node20": ["@tsconfig/node20@20.1.4", "", {}, "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg=="],
|
||||
|
||||
"@tsconfig/strictest": ["@tsconfig/strictest@2.0.5", "", {}, "sha512-ec4tjL2Rr0pkZ5hww65c+EEPYwxOi4Ryv+0MtjeaSQRJyq322Q27eOQiFbuNgw2hpL4hB1/W/HBGk3VKS43osg=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.36", "", { "dependencies": { "@types/node": "*" } }, "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
@@ -1701,7 +1721,7 @@
|
||||
|
||||
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
|
||||
|
||||
"clsx": ["clsx@2.0.0", "", {}, "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="],
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
|
||||
|
||||
@@ -4219,6 +4239,10 @@
|
||||
|
||||
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="],
|
||||
|
||||
"@react-aria/focus/clsx": ["clsx@2.0.0", "", {}, "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="],
|
||||
|
||||
"@react-aria/utils/clsx": ["clsx@2.0.0", "", {}, "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="],
|
||||
|
||||
"@rollup/plugin-commonjs/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
|
||||
"@scalar/api-client/@scalar/oas-utils": ["@scalar/oas-utils@0.2.75", "", { "dependencies": { "@hyperjump/json-schema": "^1.9.6", "@scalar/object-utils": "1.1.12", "@scalar/openapi-types": "0.1.5", "@scalar/themes": "0.9.48", "@scalar/types": "0.0.19", "flatted": "^3.3.1", "microdiff": "^1.4.0", "nanoid": "^5.0.7", "yaml": "^2.4.5", "zod": "^3.23.8" } }, "sha512-deBH359aA9hO+QzdcdJkd0KpZFlSf9xYf+58nl+sVOQL+uEay/LAn1kO+iiE0JAkL78wlW+nqje9sVBKEAdVdw=="],
|
||||
@@ -4551,6 +4575,8 @@
|
||||
|
||||
"convict/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
||||
|
||||
"cva/clsx": ["clsx@2.0.0", "", {}, "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="],
|
||||
|
||||
"decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="],
|
||||
|
||||
"deep-equal/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run dev --ui=stream",
|
||||
"dev:v2": "turbo run dev:v2 --ui=stream",
|
||||
"dev": "turbo run dev",
|
||||
"dev:v2": "turbo run dev:v2",
|
||||
"build": "turbo run build",
|
||||
"build:v2": "turbo run build:v2",
|
||||
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@gitbook/react-contentkit": "workspace:*",
|
||||
"@gitbook/react-math": "workspace:*",
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { ContentRef, DocumentBlockOpenAPI } from '@gitbook/api';
|
||||
import { parseOpenAPI, OpenAPIParseError, traverse } from '@gitbook/openapi-parser';
|
||||
import {
|
||||
OpenAPIOperationData,
|
||||
fetchOpenAPIOperation,
|
||||
OpenAPIFetcher,
|
||||
parseOpenAPI,
|
||||
OpenAPIParseError,
|
||||
} from '@gitbook/react-openapi';
|
||||
|
||||
import { cache, noCacheFetchOptions, CacheFunctionOptions } from '@/lib/cache';
|
||||
@@ -49,7 +48,7 @@ export async function fetchOpenAPIBlock(
|
||||
|
||||
const fetcher: OpenAPIFetcher = {
|
||||
fetch: cache({
|
||||
name: 'openapi.fetch.v4',
|
||||
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:
|
||||
@@ -66,13 +65,30 @@ const fetcher: OpenAPIFetcher = {
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
const data = await parseOpenAPI({ url, value: text, parseMarkdown });
|
||||
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;
|
||||
}
|
||||
}
|
||||
return node;
|
||||
});
|
||||
return {
|
||||
// Cache for 4 hours
|
||||
ttl: 24 * 60 * 60,
|
||||
// Revalidate every 2 hours
|
||||
revalidateBefore: 22 * 60 * 60,
|
||||
data,
|
||||
data: transformedFs,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -26,5 +26,10 @@
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "cf-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "packages/react-openapi", "packages/react-math"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"packages/openapi-parser",
|
||||
"packages/react-openapi",
|
||||
"packages/react-math"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
dist
|
||||
@@ -0,0 +1,3 @@
|
||||
# `@gitbook/openapi-parser`
|
||||
|
||||
Modern OpenAPI parser written in TypeScript with support for OpenAPI 3.1, OpenAPI 3.0 and Swagger 2.0.
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@gitbook/openapi-parser",
|
||||
"description": "Modern OpenAPI parser written in TypeScript with support for OpenAPI 3.1, OpenAPI 3.0 and Swagger 2.0.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.0.0",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@scalar/openapi-parser": "^0.10.4",
|
||||
"@scalar/openapi-types": "^0.1.6",
|
||||
"swagger2openapi": "^7.0.8",
|
||||
"yaml": "1.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/swagger2openapi": "^7.0.4",
|
||||
"bun-types": "^1.1.20",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test",
|
||||
"dev": "bun run build -- --watch",
|
||||
"clean": "rm -rf ./dist"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
/**
|
||||
* Error thrown when the OpenAPI document is invalid.
|
||||
*/
|
||||
export class OpenAPIParseError extends Error {
|
||||
public name = 'OpenAPIParseError';
|
||||
public override name = 'OpenAPIParseError';
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
@@ -0,0 +1,46 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { createFileSystem } from './filesystem';
|
||||
import { Server } from 'bun';
|
||||
|
||||
async function serveFixture(fixture: string) {
|
||||
return new Response(await Bun.file(new URL(`./fixtures/${fixture}`, import.meta.url)).bytes(), {
|
||||
headers: { 'Content-Type': 'application/yaml' },
|
||||
});
|
||||
}
|
||||
|
||||
describe('#createFileSystem', () => {
|
||||
let server: Server;
|
||||
|
||||
beforeEach(async () => {
|
||||
server = Bun.serve({
|
||||
static: {
|
||||
'/root/spec.yaml': await serveFixture('/remote-ref/root/spec.yaml'),
|
||||
'/root/user.yaml': await serveFixture('/remote-ref/root/user.yaml'),
|
||||
'/root/pet.yaml': await serveFixture('/remote-ref/root/pet.yaml'),
|
||||
'/tag.yaml': await serveFixture('/remote-ref/tag.yaml'),
|
||||
},
|
||||
fetch() {
|
||||
return new Response('404!');
|
||||
},
|
||||
port: 3020,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
it('creates a filesystem by resolving URLs', async () => {
|
||||
const url = new URL('/root/spec.yaml', server.url).href;
|
||||
const filesystem = await createFileSystem({
|
||||
value: url,
|
||||
baseUrl: url,
|
||||
});
|
||||
expect(filesystem).toHaveLength(4);
|
||||
expect(filesystem[0]!.isEntrypoint).toBe(true);
|
||||
expect(filesystem[1]!.isEntrypoint).toBe(false);
|
||||
expect(filesystem[1]!.filename).toBe('user.yaml');
|
||||
expect(filesystem[2]!.filename).toBe('../tag.yaml');
|
||||
expect(filesystem[3]!.filename).toBe('http://localhost:3020/root/pet.yaml');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type AnyApiDefinitionFormat, load } from '@scalar/openapi-parser';
|
||||
import { fetchUrls } from './scalar-plugins/fetchURLs';
|
||||
import type { Filesystem } from './types';
|
||||
|
||||
/**
|
||||
* Create a filesystem from an OpenAPI document.
|
||||
* Fetches all the URLs specified in references and builds a filesystem.
|
||||
*/
|
||||
export async function createFileSystem(input: {
|
||||
value: AnyApiDefinitionFormat;
|
||||
baseUrl: string;
|
||||
}): Promise<Filesystem> {
|
||||
const { filesystem } = await load(input.value, {
|
||||
plugins: [fetchUrls({ baseUrl: input.baseUrl })],
|
||||
});
|
||||
return filesystem;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
components:
|
||||
schemas:
|
||||
x-swagger-router-model: io.swagger.petstore.model.Pet
|
||||
required:
|
||||
- name
|
||||
- photoUrls
|
||||
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'
|
||||
xml:
|
||||
name: tag
|
||||
status:
|
||||
type: string
|
||||
description: pet status in the store
|
||||
enum:
|
||||
- available
|
||||
- pending
|
||||
- sold
|
||||
xml:
|
||||
name: pet
|
||||
type: object
|
||||
@@ -0,0 +1,740 @@
|
||||
openapi: 3.0.2
|
||||
servers:
|
||||
- url: /v3
|
||||
info:
|
||||
description: |-
|
||||
This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about
|
||||
Swagger at [http://swagger.io](http://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)
|
||||
version: 1.0.20-SNAPSHOT
|
||||
title: Swagger Petstore - OpenAPI 3.0
|
||||
termsOfService: 'http://swagger.io/terms/'
|
||||
contact:
|
||||
email: apiteam@swagger.io
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
|
||||
tags:
|
||||
- name: pet
|
||||
description: Everything about your Pets
|
||||
externalDocs:
|
||||
description: Find out more
|
||||
url: 'http://swagger.io'
|
||||
- name: store
|
||||
description: Access to Petstore orders
|
||||
externalDocs:
|
||||
description: Find out more about our store
|
||||
url: 'http://swagger.io'
|
||||
- name: user
|
||||
description: Operations about user
|
||||
paths:
|
||||
/pet:
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: Add a new pet to the store
|
||||
description: Add a new pet to the store
|
||||
operationId: addPet
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'405':
|
||||
description: Invalid input
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
requestBody:
|
||||
description: Create a new pet in the store
|
||||
required: true
|
||||
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'
|
||||
put:
|
||||
tags:
|
||||
- pet
|
||||
summary: Update an existing pet
|
||||
description: Update an existing pet by Id
|
||||
operationId: updatePet
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Pet not found
|
||||
'405':
|
||||
description: Validation exception
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
requestBody:
|
||||
description: Update an existent pet in the store
|
||||
required: true
|
||||
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'
|
||||
/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
|
||||
enum:
|
||||
- available
|
||||
- pending
|
||||
- sold
|
||||
default: available
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
description: Invalid status value
|
||||
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/xml:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
description: Invalid tag value
|
||||
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/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Pet not found
|
||||
security:
|
||||
- api_key: []
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: Updates a pet in the store with form data
|
||||
description: ''
|
||||
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:
|
||||
'405':
|
||||
description: Invalid input
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
delete:
|
||||
tags:
|
||||
- pet
|
||||
summary: Deletes a pet
|
||||
description: ''
|
||||
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:
|
||||
'400':
|
||||
description: Invalid pet value
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
'/pet/{petId}/uploadImage':
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: uploads an image
|
||||
description: ''
|
||||
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
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
requestBody:
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
/store/inventory:
|
||||
get:
|
||||
tags:
|
||||
- store
|
||||
summary: Returns pet inventories by status
|
||||
description: Returns a map of status codes to quantities
|
||||
operationId: getInventory
|
||||
x-swagger-router-controller: OrderController
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
format: int32
|
||||
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
|
||||
x-swagger-router-controller: OrderController
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'405':
|
||||
description: Invalid input
|
||||
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'
|
||||
'/store/order/{orderId}':
|
||||
get:
|
||||
tags:
|
||||
- store
|
||||
summary: Find purchase order by ID
|
||||
x-swagger-router-controller: OrderController
|
||||
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/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Order not found
|
||||
delete:
|
||||
tags:
|
||||
- store
|
||||
summary: Delete purchase order by ID
|
||||
x-swagger-router-controller: OrderController
|
||||
description: >-
|
||||
For valid response try integer IDs with value < 1000. Anything above
|
||||
1000 or nonintegers 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:
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Order not found
|
||||
/user:
|
||||
post:
|
||||
tags:
|
||||
- user
|
||||
summary: Create user
|
||||
description: This can only be done by the logged in user.
|
||||
operationId: createUser
|
||||
responses:
|
||||
default:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
requestBody:
|
||||
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'
|
||||
description: Created user object
|
||||
/user/createWithList:
|
||||
post:
|
||||
tags:
|
||||
- user
|
||||
summary: Creates list of users with given input array
|
||||
description: 'Creates list of users with given input array'
|
||||
x-swagger-router-controller: UserController
|
||||
operationId: createUsersWithListInput
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
default:
|
||||
description: successful operation
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
/user/login:
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Logs user into the system
|
||||
description: ''
|
||||
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
|
||||
/user/logout:
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Logs out current logged in user session
|
||||
description: ''
|
||||
operationId: logoutUser
|
||||
parameters: []
|
||||
responses:
|
||||
default:
|
||||
description: successful operation
|
||||
'/user/{username}':
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Get user by user name
|
||||
description: ''
|
||||
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/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
'400':
|
||||
description: Invalid username supplied
|
||||
'404':
|
||||
description: User not found
|
||||
put:
|
||||
tags:
|
||||
- user
|
||||
summary: Update user
|
||||
x-swagger-router-controller: UserController
|
||||
description: This can only be done by the logged in user.
|
||||
operationId: updateUser
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: name that needs to be updated
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
default:
|
||||
description: successful operation
|
||||
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'
|
||||
delete:
|
||||
tags:
|
||||
- user
|
||||
summary: Delete user
|
||||
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:
|
||||
'400':
|
||||
description: Invalid username supplied
|
||||
'404':
|
||||
description: User not found
|
||||
externalDocs:
|
||||
description: Find out more about Swagger
|
||||
url: 'http://swagger.io'
|
||||
components:
|
||||
schemas:
|
||||
Order:
|
||||
x-swagger-router-model: io.swagger.petstore.model.Order
|
||||
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
|
||||
enum:
|
||||
- placed
|
||||
- approved
|
||||
- delivered
|
||||
example: approved
|
||||
complete:
|
||||
type: boolean
|
||||
xml:
|
||||
name: order
|
||||
type: object
|
||||
Customer:
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 100000
|
||||
username:
|
||||
type: string
|
||||
example: fehguy
|
||||
address:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Address'
|
||||
xml:
|
||||
wrapped: true
|
||||
name: addresses
|
||||
xml:
|
||||
name: customer
|
||||
type: object
|
||||
Address:
|
||||
properties:
|
||||
street:
|
||||
type: string
|
||||
example: 437 Lytton
|
||||
city:
|
||||
type: string
|
||||
example: Palo Alto
|
||||
state:
|
||||
type: string
|
||||
example: CA
|
||||
zip:
|
||||
type: string
|
||||
example: 94301
|
||||
xml:
|
||||
name: address
|
||||
type: object
|
||||
Category:
|
||||
x-swagger-router-model: io.swagger.petstore.model.Category
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1
|
||||
name:
|
||||
type: string
|
||||
example: Dogs
|
||||
xml:
|
||||
name: category
|
||||
type: object
|
||||
User:
|
||||
$ref: 'user.yaml#/components/schemas/User'
|
||||
Tag:
|
||||
$ref: '../tag.yaml#/components/schemas/Tag'
|
||||
Pet:
|
||||
$ref: 'http://localhost:3020/root/pet.yaml#/components/schemas/Pet'
|
||||
ApiResponse:
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
format: int32
|
||||
type:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
xml:
|
||||
name: '##default'
|
||||
type: object
|
||||
requestBodies:
|
||||
Pet:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
description: Pet object that needs to be added to the store
|
||||
UserArray:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
description: List of user object
|
||||
securitySchemes:
|
||||
petstore_auth:
|
||||
type: oauth2
|
||||
flows:
|
||||
implicit:
|
||||
authorizationUrl: 'https://petstore.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
|
||||
@@ -0,0 +1,35 @@
|
||||
components:
|
||||
schemas:
|
||||
User:
|
||||
x-swagger-router-model: io.swagger.petstore.model.User
|
||||
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
|
||||
format: int32
|
||||
example: 1
|
||||
description: User Status
|
||||
xml:
|
||||
name: user
|
||||
type: object
|
||||
@@ -0,0 +1,13 @@
|
||||
components:
|
||||
schemas:
|
||||
Tag:
|
||||
x-swagger-router-model: io.swagger.petstore.model.Tag
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
xml:
|
||||
name: tag
|
||||
type: object
|
||||
@@ -0,0 +1,643 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.",
|
||||
"version": "1.0.7",
|
||||
"title": "Swagger Petstore",
|
||||
"termsOfService": "http://swagger.io/terms/",
|
||||
"contact": { "email": "apiteam@swagger.io" },
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"host": "petstore.swagger.io",
|
||||
"basePath": "/v2",
|
||||
"tags": [
|
||||
{
|
||||
"name": "pet",
|
||||
"description": "Everything about your Pets",
|
||||
"externalDocs": { "description": "Find out more", "url": "http://swagger.io" }
|
||||
},
|
||||
{ "name": "store", "description": "Access to Petstore orders" },
|
||||
{
|
||||
"name": "user",
|
||||
"description": "Operations about user",
|
||||
"externalDocs": {
|
||||
"description": "Find out more about our store",
|
||||
"url": "http://swagger.io"
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemes": ["https", "http"],
|
||||
"paths": {
|
||||
"/pet/{petId}/uploadImage": {
|
||||
"post": {
|
||||
"tags": ["pet"],
|
||||
"summary": "uploads an image",
|
||||
"description": "",
|
||||
"operationId": "uploadFile",
|
||||
"consumes": ["multipart/form-data"],
|
||||
"produces": ["application/json"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "petId",
|
||||
"in": "path",
|
||||
"description": "ID of pet to update",
|
||||
"required": true,
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"name": "additionalMetadata",
|
||||
"in": "formData",
|
||||
"description": "Additional data to pass to server",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "file",
|
||||
"in": "formData",
|
||||
"description": "file to upload",
|
||||
"required": false,
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"schema": { "$ref": "#/definitions/ApiResponse" }
|
||||
}
|
||||
},
|
||||
"security": [{ "petstore_auth": ["write:pets", "read:pets"] }]
|
||||
}
|
||||
},
|
||||
"/pet": {
|
||||
"post": {
|
||||
"tags": ["pet"],
|
||||
"summary": "Add a new pet to the store",
|
||||
"description": "",
|
||||
"operationId": "addPet",
|
||||
"consumes": ["application/json", "application/xml"],
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "Pet object that needs to be added to the store",
|
||||
"required": true,
|
||||
"schema": { "$ref": "#/definitions/Pet" }
|
||||
}
|
||||
],
|
||||
"responses": { "405": { "description": "Invalid input" } },
|
||||
"security": [{ "petstore_auth": ["write:pets", "read:pets"] }]
|
||||
},
|
||||
"put": {
|
||||
"tags": ["pet"],
|
||||
"summary": "Update an existing pet",
|
||||
"description": "",
|
||||
"operationId": "updatePet",
|
||||
"consumes": ["application/json", "application/xml"],
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "Pet object that needs to be added to the store",
|
||||
"required": true,
|
||||
"schema": { "$ref": "#/definitions/Pet" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"400": { "description": "Invalid ID supplied" },
|
||||
"404": { "description": "Pet not found" },
|
||||
"405": { "description": "Validation exception" }
|
||||
},
|
||||
"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",
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "status",
|
||||
"in": "query",
|
||||
"description": "Status values that need to be considered for filter",
|
||||
"required": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["available", "pending", "sold"],
|
||||
"default": "available"
|
||||
},
|
||||
"collectionFormat": "multi"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"schema": { "type": "array", "items": { "$ref": "#/definitions/Pet" } }
|
||||
},
|
||||
"400": { "description": "Invalid status value" }
|
||||
},
|
||||
"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",
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tags",
|
||||
"in": "query",
|
||||
"description": "Tags to filter by",
|
||||
"required": true,
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"collectionFormat": "multi"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"schema": { "type": "array", "items": { "$ref": "#/definitions/Pet" } }
|
||||
},
|
||||
"400": { "description": "Invalid tag value" }
|
||||
},
|
||||
"security": [{ "petstore_auth": ["write:pets", "read:pets"] }],
|
||||
"deprecated": true
|
||||
}
|
||||
},
|
||||
"/pet/{petId}": {
|
||||
"get": {
|
||||
"tags": ["pet"],
|
||||
"summary": "Find pet by ID",
|
||||
"description": "Returns a single pet",
|
||||
"operationId": "getPetById",
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "petId",
|
||||
"in": "path",
|
||||
"description": "ID of pet to return",
|
||||
"required": true,
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"schema": { "$ref": "#/definitions/Pet" }
|
||||
},
|
||||
"400": { "description": "Invalid ID supplied" },
|
||||
"404": { "description": "Pet not found" }
|
||||
},
|
||||
"security": [{ "api_key": [] }]
|
||||
},
|
||||
"post": {
|
||||
"tags": ["pet"],
|
||||
"summary": "Updates a pet in the store with form data",
|
||||
"description": "",
|
||||
"operationId": "updatePetWithForm",
|
||||
"consumes": ["application/x-www-form-urlencoded"],
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "petId",
|
||||
"in": "path",
|
||||
"description": "ID of pet that needs to be updated",
|
||||
"required": true,
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "formData",
|
||||
"description": "Updated name of the pet",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"in": "formData",
|
||||
"description": "Updated status of the pet",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": { "405": { "description": "Invalid input" } },
|
||||
"security": [{ "petstore_auth": ["write:pets", "read:pets"] }]
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["pet"],
|
||||
"summary": "Deletes a pet",
|
||||
"description": "",
|
||||
"operationId": "deletePet",
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{ "name": "api_key", "in": "header", "required": false, "type": "string" },
|
||||
{
|
||||
"name": "petId",
|
||||
"in": "path",
|
||||
"description": "Pet id to delete",
|
||||
"required": true,
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"400": { "description": "Invalid ID supplied" },
|
||||
"404": { "description": "Pet not found" }
|
||||
},
|
||||
"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",
|
||||
"produces": ["application/json"],
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "integer", "format": "int32" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [{ "api_key": [] }]
|
||||
}
|
||||
},
|
||||
"/store/order": {
|
||||
"post": {
|
||||
"tags": ["store"],
|
||||
"summary": "Place an order for a pet",
|
||||
"description": "",
|
||||
"operationId": "placeOrder",
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "order placed for purchasing the pet",
|
||||
"required": true,
|
||||
"schema": { "$ref": "#/definitions/Order" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"schema": { "$ref": "#/definitions/Order" }
|
||||
},
|
||||
"400": { "description": "Invalid Order" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/store/order/{orderId}": {
|
||||
"get": {
|
||||
"tags": ["store"],
|
||||
"summary": "Find purchase order by ID",
|
||||
"description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions",
|
||||
"operationId": "getOrderById",
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "orderId",
|
||||
"in": "path",
|
||||
"description": "ID of pet that needs to be fetched",
|
||||
"required": true,
|
||||
"type": "integer",
|
||||
"maximum": 10,
|
||||
"minimum": 1,
|
||||
"format": "int64"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"schema": { "$ref": "#/definitions/Order" }
|
||||
},
|
||||
"400": { "description": "Invalid ID supplied" },
|
||||
"404": { "description": "Order not found" }
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["store"],
|
||||
"summary": "Delete purchase order by ID",
|
||||
"description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors",
|
||||
"operationId": "deleteOrder",
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "orderId",
|
||||
"in": "path",
|
||||
"description": "ID of the order that needs to be deleted",
|
||||
"required": true,
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"format": "int64"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"400": { "description": "Invalid ID supplied" },
|
||||
"404": { "description": "Order not found" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/createWithList": {
|
||||
"post": {
|
||||
"tags": ["user"],
|
||||
"summary": "Creates list of users with given input array",
|
||||
"description": "",
|
||||
"operationId": "createUsersWithListInput",
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "List of user object",
|
||||
"required": true,
|
||||
"schema": { "type": "array", "items": { "$ref": "#/definitions/User" } }
|
||||
}
|
||||
],
|
||||
"responses": { "default": { "description": "successful operation" } }
|
||||
}
|
||||
},
|
||||
"/user/{username}": {
|
||||
"get": {
|
||||
"tags": ["user"],
|
||||
"summary": "Get user by user name",
|
||||
"description": "",
|
||||
"operationId": "getUserByName",
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"description": "The name that needs to be fetched. Use user1 for testing. ",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"schema": { "$ref": "#/definitions/User" }
|
||||
},
|
||||
"400": { "description": "Invalid username supplied" },
|
||||
"404": { "description": "User not found" }
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": ["user"],
|
||||
"summary": "Updated user",
|
||||
"description": "This can only be done by the logged in user.",
|
||||
"operationId": "updateUser",
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"description": "name that need to be updated",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "Updated user object",
|
||||
"required": true,
|
||||
"schema": { "$ref": "#/definitions/User" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"400": { "description": "Invalid user supplied" },
|
||||
"404": { "description": "User not found" }
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["user"],
|
||||
"summary": "Delete user",
|
||||
"description": "This can only be done by the logged in user.",
|
||||
"operationId": "deleteUser",
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"description": "The name that needs to be deleted",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"400": { "description": "Invalid username supplied" },
|
||||
"404": { "description": "User not found" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/login": {
|
||||
"get": {
|
||||
"tags": ["user"],
|
||||
"summary": "Logs user into the system",
|
||||
"description": "",
|
||||
"operationId": "loginUser",
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"in": "query",
|
||||
"description": "The user name for login",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"in": "query",
|
||||
"description": "The password for login in clear text",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"headers": {
|
||||
"X-Expires-After": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "date in UTC when token expires"
|
||||
},
|
||||
"X-Rate-Limit": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "calls per hour allowed by the user"
|
||||
}
|
||||
},
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
"400": { "description": "Invalid username/password supplied" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/logout": {
|
||||
"get": {
|
||||
"tags": ["user"],
|
||||
"summary": "Logs out current logged in user session",
|
||||
"description": "",
|
||||
"operationId": "logoutUser",
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [],
|
||||
"responses": { "default": { "description": "successful operation" } }
|
||||
}
|
||||
},
|
||||
"/user/createWithArray": {
|
||||
"post": {
|
||||
"tags": ["user"],
|
||||
"summary": "Creates list of users with given input array",
|
||||
"description": "",
|
||||
"operationId": "createUsersWithArrayInput",
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "List of user object",
|
||||
"required": true,
|
||||
"schema": { "type": "array", "items": { "$ref": "#/definitions/User" } }
|
||||
}
|
||||
],
|
||||
"responses": { "default": { "description": "successful operation" } }
|
||||
}
|
||||
},
|
||||
"/user": {
|
||||
"post": {
|
||||
"tags": ["user"],
|
||||
"summary": "Create user",
|
||||
"description": "This can only be done by the logged in user.",
|
||||
"operationId": "createUser",
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json", "application/xml"],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "Created user object",
|
||||
"required": true,
|
||||
"schema": { "$ref": "#/definitions/User" }
|
||||
}
|
||||
],
|
||||
"responses": { "default": { "description": "successful operation" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"api_key": { "type": "apiKey", "name": "api_key", "in": "header" },
|
||||
"petstore_auth": {
|
||||
"type": "oauth2",
|
||||
"authorizationUrl": "https://petstore.swagger.io/oauth/authorize",
|
||||
"flow": "implicit",
|
||||
"scopes": { "read:pets": "read your pets", "write:pets": "modify pets in your account" }
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"ApiResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "integer", "format": "int32" },
|
||||
"type": { "type": "string" },
|
||||
"message": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"Category": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer", "format": "int64" },
|
||||
"name": { "type": "string" }
|
||||
},
|
||||
"xml": { "name": "Category" }
|
||||
},
|
||||
"Pet": {
|
||||
"type": "object",
|
||||
"required": ["name", "photoUrls"],
|
||||
"properties": {
|
||||
"id": { "type": "integer", "format": "int64" },
|
||||
"category": { "$ref": "#/definitions/Category" },
|
||||
"name": { "type": "string", "example": "doggie" },
|
||||
"photoUrls": {
|
||||
"type": "array",
|
||||
"xml": { "wrapped": true },
|
||||
"items": { "type": "string", "xml": { "name": "photoUrl" } }
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"xml": { "wrapped": true },
|
||||
"items": { "xml": { "name": "tag" }, "$ref": "#/definitions/Tag" }
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "pet status in the store",
|
||||
"enum": ["available", "pending", "sold"]
|
||||
}
|
||||
},
|
||||
"xml": { "name": "Pet" }
|
||||
},
|
||||
"Tag": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer", "format": "int64" },
|
||||
"name": { "type": "string" }
|
||||
},
|
||||
"xml": { "name": "Tag" }
|
||||
},
|
||||
"Order": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer", "format": "int64" },
|
||||
"petId": { "type": "integer", "format": "int64" },
|
||||
"quantity": { "type": "integer", "format": "int32" },
|
||||
"shipDate": { "type": "string", "format": "date-time" },
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Order Status",
|
||||
"enum": ["placed", "approved", "delivered"]
|
||||
},
|
||||
"complete": { "type": "boolean" }
|
||||
},
|
||||
"xml": { "name": "Order" }
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer", "format": "int64" },
|
||||
"username": { "type": "string" },
|
||||
"firstName": { "type": "string" },
|
||||
"lastName": { "type": "string" },
|
||||
"email": { "type": "string" },
|
||||
"password": { "type": "string" },
|
||||
"phone": { "type": "string" },
|
||||
"userStatus": { "type": "integer", "format": "int32", "description": "User Status" }
|
||||
},
|
||||
"xml": { "name": "User" }
|
||||
}
|
||||
},
|
||||
"externalDocs": { "description": "Find out more about Swagger", "url": "http://swagger.io" }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { parseOpenAPI } from './parse';
|
||||
|
||||
export type * from '@scalar/openapi-types';
|
||||
export type * from './types';
|
||||
export * from './error';
|
||||
export * from './traverse';
|
||||
export { dereference } from '@scalar/openapi-parser';
|
||||
export type { AnyObject } from '@scalar/openapi-parser';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, it } from 'bun:test';
|
||||
import { parseOpenAPI } from './parse';
|
||||
|
||||
const spec = await Bun.file(new URL('./fixtures/recursive-spec.json', import.meta.url)).text();
|
||||
|
||||
describe('#parseOpenAPI', () => {
|
||||
it('parses an OpenAPI document', async () => {
|
||||
const schema = await parseOpenAPI({
|
||||
value: spec,
|
||||
url: 'https://example.com',
|
||||
});
|
||||
// Ensure the structure returned is not recursive (not dereferenced).
|
||||
JSON.stringify(schema);
|
||||
});
|
||||
});
|
||||
@@ -7,11 +7,7 @@ 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;
|
||||
parseMarkdown: (input: string) => Promise<string>;
|
||||
}) {
|
||||
export async function parseOpenAPI(input: { value: string; url: string }) {
|
||||
try {
|
||||
return await parseOpenAPIV3(input);
|
||||
} catch (error) {
|
||||
@@ -21,6 +17,3 @@ export async function parseOpenAPI(input: {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export type * from './types';
|
||||
export * from './error';
|
||||
@@ -0,0 +1,85 @@
|
||||
import { LoadPlugin } from '@scalar/openapi-parser';
|
||||
|
||||
export const fetchUrlsDefaultConfiguration = {
|
||||
limit: 40,
|
||||
};
|
||||
|
||||
export const fetchUrls: (customConfiguration: {
|
||||
/**
|
||||
* Base URL to use for relative paths.
|
||||
*/
|
||||
baseUrl: string;
|
||||
|
||||
/**
|
||||
* Limit the number of requests. Set to `false` to disable the limit.
|
||||
*/
|
||||
limit?: number | false;
|
||||
}) => LoadPlugin = (customConfiguration) => {
|
||||
// State
|
||||
let numberOfRequests = 0;
|
||||
|
||||
// Configuration
|
||||
const configuration = {
|
||||
...fetchUrlsDefaultConfiguration,
|
||||
...customConfiguration,
|
||||
};
|
||||
|
||||
return {
|
||||
check(value?: any) {
|
||||
// Not a string
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Not http/https or relative path
|
||||
if (
|
||||
!value.startsWith('http://') &&
|
||||
!value.startsWith('https://') &&
|
||||
!isRelativePath(value)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async get(value?: any) {
|
||||
// Limit ht enumber of requests
|
||||
if (configuration?.limit !== false && numberOfRequests >= configuration?.limit) {
|
||||
console.warn(
|
||||
`[fetchUrls] Maximum number of requests reeached (${configuration?.limit}), skipping request`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
numberOfRequests++;
|
||||
const url = getReferenceUrl(value, configuration.baseUrl);
|
||||
const response = await fetch(url);
|
||||
return await response.text();
|
||||
} catch (error: any) {
|
||||
console.error('[fetchUrls]', error.message, `(${value})`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a path is relative.
|
||||
*/
|
||||
function isRelativePath(path: string): boolean {
|
||||
// Exclude external URLs
|
||||
const externalUrlPattern = /^(https?:\/\/|www\.|data:|#\/)/;
|
||||
return !externalUrlPattern.test(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reference URL.
|
||||
*/
|
||||
function getReferenceUrl(value: string, baseUrl: string) {
|
||||
if (isRelativePath(value)) {
|
||||
return new URL(value, baseUrl).href;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { dereference } from '@scalar/openapi-parser';
|
||||
import { traverse } from './traverse';
|
||||
import { createFileSystem } from './filesystem';
|
||||
|
||||
const recursiveSpec = await Bun.file(
|
||||
new URL('./fixtures/recursive-spec.json', import.meta.url),
|
||||
).text();
|
||||
|
||||
describe('#traverse', () => {
|
||||
it('traverses a recursive structure asynchronously', async () => {
|
||||
expect.assertions(2);
|
||||
const result = await dereference(recursiveSpec);
|
||||
|
||||
// Confirm that it is a recursive structure
|
||||
try {
|
||||
JSON.stringify(result);
|
||||
} catch (error) {
|
||||
expect((error as Error).message).toBe(
|
||||
'JSON.stringify cannot serialize cyclic structures.',
|
||||
);
|
||||
}
|
||||
|
||||
const specification = await traverse(result.specification!, async (node) => {
|
||||
if ('description' in node && node['description']) {
|
||||
node['description'] = 'Hello, world!';
|
||||
return node;
|
||||
}
|
||||
return node;
|
||||
});
|
||||
|
||||
expect(specification.info!.description).toBe('Hello, world!');
|
||||
});
|
||||
|
||||
it('traverses a complete filesystem', async () => {
|
||||
const filesystem = await createFileSystem({
|
||||
value: JSON.parse(recursiveSpec),
|
||||
baseUrl: 'https://example.com',
|
||||
});
|
||||
|
||||
const transformedFilesystem = await traverse(filesystem, async (node) => {
|
||||
if ('description' in node && node['description']) {
|
||||
node['description'] = 'Hello, world!';
|
||||
return node;
|
||||
}
|
||||
return node;
|
||||
});
|
||||
|
||||
expect(transformedFilesystem[0]!.specification['info']!.description).toBe('Hello, world!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { AnyObject } from '@scalar/openapi-parser';
|
||||
|
||||
/**
|
||||
* Recursively traverses the specification and applies the transform function to each node.
|
||||
* The one from @scalar/openapi-parser does not support recursion.
|
||||
*/
|
||||
export async function traverse<T extends AnyObject | AnyObject[]>(
|
||||
specification: T,
|
||||
transform: (specification: AnyObject, path?: string[]) => AnyObject | Promise<AnyObject>,
|
||||
path: string[] = [],
|
||||
seen = new WeakSet(),
|
||||
): Promise<T> {
|
||||
const result: AnyObject = {};
|
||||
|
||||
if (typeof specification !== 'object' || specification === null) {
|
||||
return specification as Promise<T>;
|
||||
}
|
||||
|
||||
if (seen.has(specification)) {
|
||||
return specification as Promise<T>;
|
||||
}
|
||||
|
||||
if (Array.isArray(specification)) {
|
||||
return Promise.all(
|
||||
specification.map((item, index) =>
|
||||
traverse(item, transform, [...path, index.toString()], seen),
|
||||
),
|
||||
) as Promise<T>;
|
||||
}
|
||||
|
||||
const keys = Object.keys(specification);
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
const value = specification[key];
|
||||
result[key] = await traverse(value, transform, [...path, key], seen);
|
||||
}),
|
||||
);
|
||||
|
||||
return transform(result, path) as Promise<T>;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export type IconComponent = React.ComponentType<{ className?: string }>;
|
||||
import type { AnyObject } from '@scalar/openapi-parser';
|
||||
import type { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
|
||||
/**
|
||||
* Custom properties that can be defined at the entire spec level.
|
||||
@@ -15,6 +16,11 @@ export interface OpenAPICustomSpecProperties {
|
||||
* This option can be used to hide code samples for the entire spec.
|
||||
*/
|
||||
'x-hideTryItPanel'?: boolean;
|
||||
|
||||
/**
|
||||
* Description in HTML format.
|
||||
*/
|
||||
'x-description-html'?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +37,11 @@ export interface OpenAPICustomOperationProperties {
|
||||
* https://redocly.com/docs/api-reference-docs/specification-extensions/x-hidetryitpanel/
|
||||
*/
|
||||
'x-hideTryItPanel'?: boolean;
|
||||
|
||||
/**
|
||||
* Description in HTML format.
|
||||
*/
|
||||
'x-description-html'?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,3 +54,23 @@ export interface OpenAPICustomCodeSample {
|
||||
label: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export type OpenAPIV3xDocument =
|
||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
||||
| OpenAPIV3.Document<OpenAPICustomSpecProperties>;
|
||||
|
||||
/**
|
||||
* Not literally a filesystem, but a list of files with their content.
|
||||
* This is an abstraction layer to handle multiple files in the browser (without access to the hard disk).
|
||||
*/
|
||||
export type Filesystem<T extends AnyObject = AnyObject> = FilesystemEntry<T>[];
|
||||
/**
|
||||
* Holds all information about a single file (doesn’t have to be a literal file, see Filesystem).
|
||||
*/
|
||||
export type FilesystemEntry<T extends AnyObject> = {
|
||||
dir: string;
|
||||
isEntrypoint: boolean;
|
||||
references: string[];
|
||||
filename: string;
|
||||
specification: T;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { convertOpenAPIV2ToOpenAPIV3 } from './v2';
|
||||
|
||||
const specV2 = await Bun.file(new URL('./fixtures/spec-v2.json', import.meta.url)).text();
|
||||
|
||||
describe('#convertOpenAPIV2ToOpenAPIV3', () => {
|
||||
it('converts an OpenAPIV2 in V3', async () => {
|
||||
const schema = await convertOpenAPIV2ToOpenAPIV3({
|
||||
value: specV2,
|
||||
url: 'https://example.com',
|
||||
});
|
||||
// Ensure the structure returned is not recursive (not dereferenced).
|
||||
JSON.stringify(schema);
|
||||
expect(schema[0]!.specification.openapi).toBe('3.0.0');
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
import YAML from 'yaml';
|
||||
import swagger2openapi, { ConvertOutputOptions } from 'swagger2openapi';
|
||||
import swagger2openapi, { type ConvertOutputOptions } from 'swagger2openapi';
|
||||
|
||||
import { OpenAPICustomSpecProperties } from './types';
|
||||
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
import { OpenAPIParseError } from './error';
|
||||
import { parseOpenAPIV3 } from './v3';
|
||||
import { AnyApiDefinitionFormat } from '@scalar/openapi-parser';
|
||||
import type { AnyApiDefinitionFormat } from '@scalar/openapi-parser';
|
||||
import type { Filesystem, OpenAPIV3xDocument } from './types';
|
||||
|
||||
/**
|
||||
* Convert a Swagger 2.0 schema to an OpenAPI 3.0 schema.
|
||||
@@ -13,12 +12,8 @@ import { AnyApiDefinitionFormat } from '@scalar/openapi-parser';
|
||||
export async function convertOpenAPIV2ToOpenAPIV3(input: {
|
||||
value: AnyApiDefinitionFormat;
|
||||
url: string;
|
||||
parseMarkdown: (input: string) => Promise<string>;
|
||||
}): Promise<
|
||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
||||
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
||||
> {
|
||||
const { value, url, parseMarkdown } = input;
|
||||
}): Promise<Filesystem<OpenAPIV3xDocument>> {
|
||||
const { value, url } = input;
|
||||
// In this case we want the raw value to be able to convert it.
|
||||
const schema = typeof value === 'string' ? rawParseOpenAPI({ value, url }) : value;
|
||||
try {
|
||||
@@ -34,7 +29,7 @@ export async function convertOpenAPIV2ToOpenAPIV3(input: {
|
||||
patch: true,
|
||||
})) as ConvertOutputOptions;
|
||||
|
||||
return parseOpenAPIV3({ url, value: convertResult.openapi, parseMarkdown });
|
||||
return parseOpenAPIV3({ url, value: convertResult.openapi });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'S2OError') {
|
||||
throw new OpenAPIParseError(
|
||||
@@ -0,0 +1,30 @@
|
||||
import { type AnyApiDefinitionFormat, validate } from '@scalar/openapi-parser';
|
||||
import { OpenAPIParseError } from './error';
|
||||
import { createFileSystem } from './filesystem';
|
||||
import type { Filesystem, OpenAPIV3xDocument } from './types';
|
||||
|
||||
/**
|
||||
* 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: {
|
||||
value: AnyApiDefinitionFormat;
|
||||
url: string;
|
||||
}): Promise<Filesystem<OpenAPIV3xDocument>> {
|
||||
const { value, url } = input;
|
||||
const result = await validate(value);
|
||||
|
||||
// Spec is invalid, we stop here.
|
||||
if (!result.specification) {
|
||||
throw new OpenAPIParseError('Invalid OpenAPI document', url, 'invalid-spec');
|
||||
}
|
||||
|
||||
if (result.version === '2.0') {
|
||||
throw new OpenAPIParseError('Only OpenAPI v3 is supported', url, 'v2-spec');
|
||||
}
|
||||
|
||||
const filesystem = await createFileSystem({ value: result.specification, baseUrl: url });
|
||||
|
||||
return filesystem;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": ["./tsconfig.json"],
|
||||
"exclude": ["**/*.test.ts"],
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"noEmit": false,
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": ["@tsconfig/strictest/tsconfig.json", "@tsconfig/node20/tsconfig.json"],
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"noEmit": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -9,21 +9,18 @@
|
||||
}
|
||||
},
|
||||
"version": "0.7.1",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@scalar/api-client-react": "1.0.87",
|
||||
"@scalar/openapi-parser": "^0.10.4",
|
||||
"@scalar/openapi-types": "^0.1.6",
|
||||
"@scalar/oas-utils": "^0.2.101",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"flatted": "^3.2.9",
|
||||
"swagger2openapi": "^7.0.8",
|
||||
"react-aria-components": "^1.6.0",
|
||||
"react-aria": "^3.37.0",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"yaml": "1.10.2"
|
||||
"usehooks-ts": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/swagger2openapi": "^7.0.4",
|
||||
"bun-types": "^1.1.20",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
@@ -31,10 +28,10 @@
|
||||
"react": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test",
|
||||
"dev": "tsc -w",
|
||||
"dev": "bun run build -- --watch",
|
||||
"clean": "rm -rf ./dist"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useRef, useState, useSyncExternalStore } from 'react';
|
||||
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
||||
import { useDisclosureState } from 'react-stately';
|
||||
|
||||
@@ -30,7 +30,7 @@ function useSyncedTabsGlobalState() {
|
||||
[],
|
||||
);
|
||||
|
||||
const tabs = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||
const tabs = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||
|
||||
return [tabs, setSyncedTabs] as const;
|
||||
}
|
||||
@@ -80,15 +80,15 @@ export function InteractiveSection(props: {
|
||||
stateKey && stateKey in syncedTabs
|
||||
? tabs.find((tab) => tab.key === syncedTabs[stateKey])
|
||||
: undefined;
|
||||
const [selectedTabKey, setSelectedTab] = React.useState(tabFromState?.key ?? defaultTab);
|
||||
const [selectedTabKey, setSelectedTab] = useState(tabFromState?.key ?? defaultTab);
|
||||
const selectedTab: InteractiveSectionTab | undefined =
|
||||
tabFromState ?? tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
|
||||
|
||||
const state = useDisclosureState({
|
||||
defaultExpanded: defaultOpened,
|
||||
});
|
||||
const panelRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
|
||||
const { buttonProps } = useButton(triggerProps, triggerRef);
|
||||
const { isFocusVisible, focusProps } = useFocusRing();
|
||||
@@ -96,7 +96,7 @@ export function InteractiveSection(props: {
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
className={classNames(
|
||||
className={clsx(
|
||||
'openapi-section',
|
||||
toggeable ? 'openapi-section-toggeable' : null,
|
||||
className,
|
||||
@@ -110,10 +110,10 @@ export function InteractiveSection(props: {
|
||||
state.toggle();
|
||||
}
|
||||
}}
|
||||
className={classNames('openapi-section-header', `${className}-header`)}
|
||||
className={clsx('openapi-section-header', `${className}-header`)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
className={clsx(
|
||||
'openapi-section-header-content',
|
||||
`${className}-header-content`,
|
||||
)}
|
||||
@@ -122,10 +122,7 @@ export function InteractiveSection(props: {
|
||||
<button
|
||||
{...mergeProps(buttonProps, focusProps)}
|
||||
ref={triggerRef}
|
||||
className={classNames(
|
||||
'openapi-section-toggle',
|
||||
`${className}-toggle`,
|
||||
)}
|
||||
className={clsx('openapi-section-toggle', `${className}-toggle`)}
|
||||
style={{
|
||||
outline: isFocusVisible
|
||||
? '2px solid rgb(var(--primary-color-500) / 0.4)'
|
||||
@@ -138,7 +135,7 @@ export function InteractiveSection(props: {
|
||||
{header}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
className={clsx(
|
||||
'openapi-section-header-controls',
|
||||
`${className}-header-controls`,
|
||||
)}
|
||||
@@ -148,12 +145,12 @@ export function InteractiveSection(props: {
|
||||
>
|
||||
{tabs.length > 1 ? (
|
||||
<select
|
||||
className={classNames(
|
||||
className={clsx(
|
||||
'openapi-section-select',
|
||||
'openapi-select',
|
||||
`${className}-tabs-select`,
|
||||
)}
|
||||
value={selectedTab.key}
|
||||
value={selectedTab?.key ?? ''}
|
||||
onChange={(event) => {
|
||||
setSelectedTab(event.target.value);
|
||||
if (stateKey) {
|
||||
@@ -179,14 +176,14 @@ export function InteractiveSection(props: {
|
||||
<div
|
||||
ref={panelRef}
|
||||
{...panelProps}
|
||||
className={classNames('openapi-section-body', `${className}-body`)}
|
||||
className={clsx('openapi-section-body', `${className}-body`)}
|
||||
>
|
||||
{children}
|
||||
{selectedTab?.body}
|
||||
</div>
|
||||
) : null}
|
||||
{overlay ? (
|
||||
<div className={classNames('openapi-section-overlay', `${className}-overlay`)}>
|
||||
<div className={clsx('openapi-section-overlay', `${className}-overlay`)}>
|
||||
{overlay}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import classNames from 'classnames';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export function Markdown(props: { source: string; className?: string }) {
|
||||
const { source, className } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('openapi-markdown', className)}
|
||||
className={clsx('openapi-markdown', className)}
|
||||
dangerouslySetInnerHTML={{ __html: source }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
import { CodeSampleInput, codeSampleGenerators } from './code-samples';
|
||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { OpenAPIClientContext } from './types';
|
||||
import { useRef } from 'react';
|
||||
import type { OpenAPIClientContext } from './types';
|
||||
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
||||
import { useDisclosureState } from 'react-stately';
|
||||
|
||||
@@ -15,8 +15,8 @@ interface Props {
|
||||
*/
|
||||
export function OpenAPIDisclosure({ context, children, label }: Props): JSX.Element {
|
||||
const state = useDisclosureState({});
|
||||
const panelRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
|
||||
const { buttonProps } = useButton(triggerProps, triggerRef);
|
||||
const { isFocusVisible, focusProps } = useFocusRing();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
groups: TDisclosureGroup[];
|
||||
icon?: React.ReactNode;
|
||||
@@ -22,9 +20,9 @@ import {
|
||||
useDisclosureGroupState,
|
||||
useDisclosureState,
|
||||
} from 'react-stately';
|
||||
import classNames from 'classnames';
|
||||
import { createContext, useContext, useRef, useState } from 'react';
|
||||
|
||||
const DisclosureGroupStateContext = React.createContext<DisclosureGroupState | null>(null);
|
||||
const DisclosureGroupStateContext = createContext<DisclosureGroupState | null>(null);
|
||||
|
||||
/**
|
||||
* Display an interactive OpenAPI disclosure group.
|
||||
@@ -48,7 +46,7 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
||||
|
||||
const defaultId = useId();
|
||||
const id = group.id || defaultId;
|
||||
const groupState = React.useContext(DisclosureGroupStateContext);
|
||||
const groupState = useContext(DisclosureGroupStateContext);
|
||||
const isExpanded = groupState?.expandedKeys.has(id) || false;
|
||||
const state = useDisclosureState({
|
||||
isExpanded,
|
||||
@@ -59,8 +57,8 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
||||
},
|
||||
});
|
||||
|
||||
const panelRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const isDisabled = groupState?.isDisabled || !group.tabs?.length || false;
|
||||
const { buttonProps: triggerProps, panelProps } = useDisclosure(
|
||||
{
|
||||
@@ -75,7 +73,7 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
||||
const { isFocusVisible, focusProps } = useFocusRing();
|
||||
|
||||
const defaultTab = group.tabs?.[0]?.id || '';
|
||||
const [selectedTabKey, setSelectedTabKey] = React.useState(defaultTab);
|
||||
const [selectedTabKey, setSelectedTabKey] = useState(defaultTab);
|
||||
const selectedTab = group.tabs?.find((tab) => tab.id === selectedTabKey);
|
||||
|
||||
return (
|
||||
@@ -107,11 +105,7 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
||||
<div className="openapi-disclosure-group-mediatype">
|
||||
{group.tabs?.length > 1 ? (
|
||||
<select
|
||||
className={classNames(
|
||||
'openapi-section-select',
|
||||
'openapi-select',
|
||||
`openapi-disclosure-group-tabs-select`,
|
||||
)}
|
||||
className="openapi-section-select openapi-select openapi-disclosure-group-tabs-select"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
value={selectedTab?.id}
|
||||
onChange={(event) => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||
import type { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPICodeSample } from './OpenAPICodeSample';
|
||||
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
|
||||
import { OpenAPISpec } from './OpenAPISpec';
|
||||
import { OpenAPIClientContext, OpenAPIContextProps } from './types';
|
||||
import { OpenAPIClientContext, type OpenAPIContextProps } from './types';
|
||||
import { OpenAPIPath } from './OpenAPIPath';
|
||||
import { resolveDescription } from './utils';
|
||||
|
||||
/**
|
||||
* Display an interactive OpenAPI operation.
|
||||
@@ -26,18 +26,18 @@ export function OpenAPIOperation(props: {
|
||||
blockKey: context.blockKey,
|
||||
};
|
||||
|
||||
const trimmedDescription = operation.description?.trim();
|
||||
const description = resolveDescription(operation)?.trim();
|
||||
|
||||
return (
|
||||
<div className={classNames('openapi-operation', className)}>
|
||||
<div className={clsx('openapi-operation', className)}>
|
||||
<div className="openapi-summary" id={context.id}>
|
||||
<h2 className="openapi-summary-title" data-deprecated={operation.deprecated}>
|
||||
{operation.summary}
|
||||
</h2>
|
||||
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
||||
</div>
|
||||
<div className={classNames('openapi-columns')}>
|
||||
<div className={classNames('openapi-column-spec')}>
|
||||
<div className="openapi-columns">
|
||||
<div className="openapi-column-spec">
|
||||
{operation['x-deprecated-sunset'] ? (
|
||||
<div className="openapi-deprecated-sunset openapi-description openapi-markdown">
|
||||
This operation is deprecated and will be sunset on{' '}
|
||||
@@ -47,16 +47,16 @@ export function OpenAPIOperation(props: {
|
||||
{`.`}
|
||||
</div>
|
||||
) : null}
|
||||
{trimmedDescription ? (
|
||||
{description ? (
|
||||
<div className="openapi-intro">
|
||||
<Markdown className="openapi-description" source={trimmedDescription} />
|
||||
<Markdown className="openapi-description" source={description} />
|
||||
</div>
|
||||
) : null}
|
||||
<OpenAPIPath data={data} context={context} />
|
||||
<OpenAPISpec data={data} context={clientContext} />
|
||||
</div>
|
||||
<div className={classNames('openapi-column-preview')}>
|
||||
<div className={classNames('openapi-column-preview-body')}>
|
||||
<div className="openapi-column-preview">
|
||||
<div className="openapi-column-preview-body">
|
||||
<OpenAPICodeSample {...props} />
|
||||
<OpenAPIResponseExample {...props} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
|
||||
import { createContext, useContext, useMemo } from 'react';
|
||||
import { useEventCallback } from 'usehooks-ts';
|
||||
|
||||
interface OpenAPIOperationPointer {
|
||||
@@ -11,7 +12,7 @@ interface OpenAPIOperationContextValue {
|
||||
onOpenClient: (pointer: OpenAPIOperationPointer) => void;
|
||||
}
|
||||
|
||||
const OpenAPIOperationContext = React.createContext<OpenAPIOperationContextValue>({
|
||||
const OpenAPIOperationContext = createContext<OpenAPIOperationContextValue>({
|
||||
onOpenClient: () => {},
|
||||
});
|
||||
|
||||
@@ -27,7 +28,7 @@ export function OpenAPIOperationContextProvider(
|
||||
props.onOpenClient?.(pointer);
|
||||
});
|
||||
|
||||
const value = React.useMemo(() => ({ onOpenClient }), [onOpenClient]);
|
||||
const value = useMemo(() => ({ onOpenClient }), [onOpenClient]);
|
||||
|
||||
return (
|
||||
<OpenAPIOperationContext.Provider value={value}>
|
||||
@@ -40,5 +41,5 @@ export function OpenAPIOperationContextProvider(
|
||||
* Hook to access the OpenAPIOperationContext.
|
||||
*/
|
||||
export function useOpenAPIOperationContext() {
|
||||
return React.useContext(OpenAPIOperationContext);
|
||||
return useContext(OpenAPIOperationContext);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||
import type { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||
import { ScalarApiButton } from './ScalarApiButton';
|
||||
import { OpenAPIContextProps } from './types';
|
||||
import type { OpenAPIContextProps } from './types';
|
||||
|
||||
/**
|
||||
* Display the path of an operation.
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import { OpenAPIRootSchema } from './OpenAPISchema';
|
||||
import { noReference } from './utils';
|
||||
import { OpenAPIClientContext } from './types';
|
||||
import type { OpenAPIClientContext } from './types';
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import { OpenAPISchemaProperties } from './OpenAPISchema';
|
||||
import { checkIsReference, noReference } from './utils';
|
||||
import { OpenAPIClientContext } from './types';
|
||||
import { checkIsReference, noReference, resolveDescription } from './utils';
|
||||
import type { OpenAPIClientContext } from './types';
|
||||
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
||||
|
||||
/**
|
||||
@@ -19,7 +18,9 @@ export function OpenAPIResponse(props: {
|
||||
);
|
||||
const content = Object.entries(mediaType.schema ?? {});
|
||||
|
||||
if (content.length === 0 && !response.description && headers.length === 0) {
|
||||
const description = resolveDescription(response);
|
||||
|
||||
if (content.length === 0 && !description && headers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -37,7 +38,7 @@ export function OpenAPIResponse(props: {
|
||||
/>
|
||||
</OpenAPIDisclosure>
|
||||
) : null}
|
||||
<div className={classNames('openapi-responsebody')}>
|
||||
<div className="openapi-responsebody">
|
||||
<OpenAPISchemaProperties
|
||||
id={`response-${context.blockKey}`}
|
||||
properties={[
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import type { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||
import { generateSchemaExample } from './generateSchemaExample';
|
||||
import { OpenAPIContextProps } from './types';
|
||||
import { checkIsReference, noReference } from './utils';
|
||||
import type { OpenAPIContextProps } from './types';
|
||||
import { checkIsReference, noReference, resolveDescription } from './utils';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
|
||||
@@ -46,9 +46,10 @@ export function OpenAPIResponseExample(props: {
|
||||
if (!responseObject.content) {
|
||||
return null;
|
||||
}
|
||||
const key = Object.keys(responseObject.content)[0];
|
||||
return (
|
||||
responseObject.content['application/json'] ??
|
||||
responseObject.content[Object.keys(responseObject.content)[0]]
|
||||
(key ? responseObject.content[key] : null)
|
||||
);
|
||||
})();
|
||||
|
||||
@@ -56,7 +57,7 @@ export function OpenAPIResponseExample(props: {
|
||||
return {
|
||||
key: key,
|
||||
label: key,
|
||||
description: responseObject.description,
|
||||
description: resolveDescription(responseObject),
|
||||
body: <OpenAPIEmptyResponseExample />,
|
||||
};
|
||||
}
|
||||
@@ -65,11 +66,13 @@ export function OpenAPIResponseExample(props: {
|
||||
(() => {
|
||||
const { examples, example } = mediaTypeObject;
|
||||
if (examples) {
|
||||
const firstKey = Object.keys(examples)[0];
|
||||
// @TODO handle multiple examples
|
||||
const firstExample = noReference(examples[firstKey]);
|
||||
if (firstExample) {
|
||||
return firstExample;
|
||||
const key = Object.keys(examples)[0];
|
||||
if (key) {
|
||||
// @TODO handle multiple examples
|
||||
const firstExample = noReference(examples[key]);
|
||||
if (firstExample) {
|
||||
return firstExample;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +92,7 @@ export function OpenAPIResponseExample(props: {
|
||||
return {
|
||||
key: key,
|
||||
label: key,
|
||||
description: responseObject.description,
|
||||
description: resolveDescription(responseObject),
|
||||
body: example?.value ? (
|
||||
<context.CodeBlock
|
||||
code={
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { createStateKey, noReference } from './utils';
|
||||
import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
||||
import { createStateKey, resolveDescription } from './utils';
|
||||
import { OpenAPIResponse } from './OpenAPIResponse';
|
||||
import { OpenAPIClientContext } from './types';
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPIRootSchema, OpenAPISchemaProperties, OpenAPISchemaProperty } from './OpenAPISchema';
|
||||
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
||||
|
||||
/**
|
||||
* Display an interactive response body.
|
||||
@@ -23,7 +19,7 @@ export function OpenAPIResponses(props: {
|
||||
<InteractiveSection
|
||||
stateKey={createStateKey('response', context.blockKey)}
|
||||
header="Responses"
|
||||
className={classNames('openapi-responses')}
|
||||
className="openapi-responses"
|
||||
>
|
||||
<OpenAPIDisclosureGroup
|
||||
allowsMultipleExpanded
|
||||
@@ -31,6 +27,7 @@ export function OpenAPIResponses(props: {
|
||||
groups={Object.entries(responses).map(
|
||||
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
|
||||
const content = Object.entries(response.content ?? {});
|
||||
const description = resolveDescription(response);
|
||||
|
||||
return {
|
||||
id: statusCode,
|
||||
@@ -42,9 +39,9 @@ export function OpenAPIResponses(props: {
|
||||
<span className="openapi-response-statuscode">
|
||||
{statusCode}
|
||||
</span>
|
||||
{response.description ? (
|
||||
{description ? (
|
||||
<Markdown
|
||||
source={response.description}
|
||||
source={description}
|
||||
className="openapi-response-description"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { it, describe, expect } from 'bun:test';
|
||||
import { getSchemaAlternatives } from './OpenAPISchema';
|
||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
|
||||
describe('getSchemaAlternatives', () => {
|
||||
it('should flatten oneOf', () => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import classNames from 'classnames';
|
||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
||||
import React, { useId } from 'react';
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import clsx from 'clsx';
|
||||
import { useId } from 'react';
|
||||
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPIClientContext } from './types';
|
||||
import { checkIsReference, noReference } from './utils';
|
||||
import type { OpenAPIClientContext } from './types';
|
||||
import { checkIsReference, noReference, resolveDescription } from './utils';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
||||
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
||||
@@ -49,7 +49,7 @@ export function OpenAPISchemaProperty(
|
||||
|
||||
if ((properties && !!properties.length) || schema.type === 'object') {
|
||||
return (
|
||||
<InteractiveSection id={id} className={classNames('openapi-schema', className)}>
|
||||
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
|
||||
<OpenAPISchemaPresentation {...props} />
|
||||
<OpenAPIDisclosure context={context}>
|
||||
{properties && properties.length > 0 ? (
|
||||
@@ -66,10 +66,11 @@ export function OpenAPISchemaProperty(
|
||||
|
||||
if (alternatives?.[0]?.length) {
|
||||
return (
|
||||
<InteractiveSection id={id} className={classNames('openapi-schema', className)}>
|
||||
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
|
||||
<OpenAPISchemaPresentation {...props} />
|
||||
{alternatives[0].map((alternative, index) => (
|
||||
<OpenAPISchemaAlternative
|
||||
key={index}
|
||||
schema={alternative}
|
||||
circularRefs={circularRefs}
|
||||
context={context}
|
||||
@@ -80,7 +81,7 @@ export function OpenAPISchemaProperty(
|
||||
}
|
||||
|
||||
return (
|
||||
<InteractiveSection id={id} className={classNames('openapi-schema', className)}>
|
||||
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
|
||||
<OpenAPISchemaPresentation {...props} />
|
||||
{(properties && properties.length > 0) ||
|
||||
(schema.enum && schema.enum.length > 0) ||
|
||||
@@ -118,7 +119,7 @@ export function OpenAPISchemaProperties(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<div id={id} className={classNames('openapi-schema-properties')}>
|
||||
<div id={id} className="openapi-schema-properties">
|
||||
{properties.map((property) => (
|
||||
<OpenAPISchemaProperty
|
||||
key={property.propertyName}
|
||||
@@ -228,8 +229,10 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
|
||||
);
|
||||
};
|
||||
|
||||
const description = resolveDescription(schema);
|
||||
|
||||
return (
|
||||
<div className={classNames('openapi-schema-presentation')}>
|
||||
<div className="openapi-schema-presentation">
|
||||
<OpenAPISchemaName
|
||||
type={getSchemaTitle(schema)}
|
||||
propertyName={propertyName}
|
||||
@@ -244,8 +247,8 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{schema.description ? (
|
||||
<Markdown source={schema.description} className="openapi-schema-description" />
|
||||
{description ? (
|
||||
<Markdown source={description} className="openapi-schema-description" />
|
||||
) : null}
|
||||
{shouldDisplayExample(schema) ? (
|
||||
<div className="openapi-schema-example">
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface OpenAPISchemaNameProps {
|
||||
propertyName?: string | JSX.Element;
|
||||
required?: boolean;
|
||||
@@ -16,22 +13,15 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps): JSX.Element {
|
||||
const { type, propertyName, required, deprecated } = props;
|
||||
|
||||
return (
|
||||
<div className={classNames('openapi-schema-name')}>
|
||||
<div className="openapi-schema-name">
|
||||
{propertyName ? (
|
||||
<span
|
||||
data-deprecated={deprecated}
|
||||
className={classNames('openapi-schema-propertyname')}
|
||||
>
|
||||
<span data-deprecated={deprecated} className="openapi-schema-propertyname">
|
||||
{propertyName}
|
||||
</span>
|
||||
) : null}
|
||||
{type ? <span className={classNames('openapi-schema-type')}>{type}</span> : null}
|
||||
{required ? (
|
||||
<span className={classNames('openapi-schema-required')}>required</span>
|
||||
) : null}
|
||||
{deprecated ? (
|
||||
<span className={classNames('openapi-deprecated')}>Deprecated</span>
|
||||
) : null}
|
||||
{type ? <span className="openapi-schema-type">{type}</span> : null}
|
||||
{required ? <span className="openapi-schema-required">required</span> : null}
|
||||
{deprecated ? <span className="openapi-deprecated">Deprecated</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
import { OpenAPIClientContext } from './types';
|
||||
import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
||||
import type { OpenAPIClientContext } from './types';
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
||||
import { resolveDescription } from './utils';
|
||||
|
||||
/**
|
||||
* Present securities authorization that can be used for this operation.
|
||||
@@ -27,6 +27,7 @@ export function OpenAPISecurities(props: {
|
||||
toggleIcon={context.icons.chevronRight}
|
||||
className="openapi-securities"
|
||||
tabs={securities.map(([key, security]) => {
|
||||
const description = resolveDescription(security);
|
||||
return {
|
||||
key: key,
|
||||
label: key,
|
||||
@@ -35,9 +36,9 @@ export function OpenAPISecurities(props: {
|
||||
<div className="openapi-schema-presentation">
|
||||
{getLabelForType(security)}
|
||||
|
||||
{security.description ? (
|
||||
{description ? (
|
||||
<Markdown
|
||||
source={security.description}
|
||||
source={description}
|
||||
className="openapi-securities-description"
|
||||
/>
|
||||
) : null}
|
||||
@@ -66,11 +67,12 @@ function getLabelForType(security: OpenAPIV3_1.SecuritySchemeObject) {
|
||||
}
|
||||
|
||||
if (security.scheme == 'bearer') {
|
||||
const description = resolveDescription(security);
|
||||
return (
|
||||
<>
|
||||
<OpenAPISchemaName propertyName="Authorization" type="string" required />
|
||||
{/** Show a default description if none is provided */}
|
||||
{!security.description ? (
|
||||
{!description ? (
|
||||
<Markdown
|
||||
source={`Bearer authentication header of the form Bearer ${`<token>`}.`}
|
||||
className="openapi-securities-description"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable';
|
||||
|
||||
/**
|
||||
@@ -9,6 +8,10 @@ export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) {
|
||||
const { servers } = props;
|
||||
const server = servers[0];
|
||||
|
||||
if (!server) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = parseServerURL(server?.url ?? '');
|
||||
|
||||
return (
|
||||
@@ -17,16 +20,13 @@ export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) {
|
||||
if (part.kind === 'text') {
|
||||
return <span key={i}>{part.text}</span>;
|
||||
} else {
|
||||
if (!server.variables?.[part.name]) {
|
||||
const variable = server.variables?.[part.name];
|
||||
if (!variable) {
|
||||
return <span key={i}>{`{${part.name}}`}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<OpenAPIServerURLVariable
|
||||
key={i}
|
||||
name={part.name}
|
||||
variable={server.variables[part.name]}
|
||||
/>
|
||||
<OpenAPIServerURLVariable key={i} name={part.name} variable={variable} />
|
||||
);
|
||||
}
|
||||
})}
|
||||
@@ -39,6 +39,9 @@ export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) {
|
||||
*/
|
||||
export function getServersURL(servers: OpenAPIV3.ServerObject[]): string {
|
||||
const server = servers[0];
|
||||
if (!server) {
|
||||
return '';
|
||||
}
|
||||
const parts = parseServerURL(server?.url ?? '');
|
||||
|
||||
return parts
|
||||
@@ -56,10 +59,14 @@ function parseServerURL(url: string) {
|
||||
const parts = url.split(/{([^}]+)}/g);
|
||||
const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = [];
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (!part) {
|
||||
continue;
|
||||
}
|
||||
if (i % 2 === 0) {
|
||||
result.push({ kind: 'text', text: parts[i] });
|
||||
result.push({ kind: 'text', text: part });
|
||||
} else {
|
||||
result.push({ kind: 'variable', name: parts[i] });
|
||||
result.push({ kind: 'variable', name: part });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
|
||||
/**
|
||||
* Interactive component to show the value of a server variable and let the user change it.
|
||||
@@ -12,5 +10,5 @@ export function OpenAPIServerURLVariable(props: {
|
||||
variable: OpenAPIV3.ServerVariableObject;
|
||||
}) {
|
||||
const { variable } = props;
|
||||
return <span className={classNames('openapi-url-var')}>{variable.default}</span>;
|
||||
return <span className="openapi-url-var">{variable.default}</span>;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { OpenAPI } from '@scalar/openapi-types';
|
||||
import type { OpenAPI } from '@gitbook/openapi-parser';
|
||||
|
||||
import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation';
|
||||
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 { noReference } from './utils';
|
||||
import { noReference, resolveDescription } from './utils';
|
||||
|
||||
/**
|
||||
* Client component to render the spec for the request and response.
|
||||
@@ -31,30 +31,35 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
|
||||
<OpenAPISecurities securities={securities} context={context} />
|
||||
) : null}
|
||||
|
||||
{parameterGroups.map((group) => (
|
||||
<InteractiveSection
|
||||
key={group.key}
|
||||
className="openapi-parameters"
|
||||
header={group.label}
|
||||
>
|
||||
<OpenAPISchemaProperties
|
||||
properties={group.parameters.map((parameter) => ({
|
||||
propertyName: parameter.name,
|
||||
schema: {
|
||||
// Description of the parameter is defined at the parameter level
|
||||
// we use display it if the schema doesn't override it
|
||||
description: parameter.description,
|
||||
example: parameter.example,
|
||||
// Deprecated can be defined at the parameter level
|
||||
deprecated: parameter.deprecated,
|
||||
...(noReference(parameter.schema) ?? {}),
|
||||
},
|
||||
required: parameter.required,
|
||||
}))}
|
||||
context={context}
|
||||
/>
|
||||
</InteractiveSection>
|
||||
))}
|
||||
{parameterGroups.map((group) => {
|
||||
return (
|
||||
<InteractiveSection
|
||||
key={group.key}
|
||||
className="openapi-parameters"
|
||||
header={group.label}
|
||||
>
|
||||
<OpenAPISchemaProperties
|
||||
properties={group.parameters.map((parameter) => {
|
||||
const description = resolveDescription(parameter);
|
||||
return {
|
||||
propertyName: parameter.name,
|
||||
schema: {
|
||||
// Description of the parameter is defined at the parameter level
|
||||
// we use display it if the schema doesn't override it
|
||||
description: description,
|
||||
example: parameter.example,
|
||||
// Deprecated can be defined at the parameter level
|
||||
deprecated: parameter.deprecated,
|
||||
...(noReference(parameter.schema) ?? {}),
|
||||
},
|
||||
required: parameter.required,
|
||||
};
|
||||
})}
|
||||
context={context}
|
||||
/>
|
||||
</InteractiveSection>
|
||||
);
|
||||
})}
|
||||
|
||||
{operation.requestBody ? (
|
||||
<OpenAPIRequestBody
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { createContext, useContext, useMemo, useState } from 'react';
|
||||
import { Key, Tab, TabList, TabPanel, Tabs, TabsProps } from 'react-aria-components';
|
||||
import { Markdown } from './Markdown';
|
||||
|
||||
@@ -17,10 +17,10 @@ type OpenAPITabsContextData = {
|
||||
setSelectedKey: (key: Key) => void;
|
||||
};
|
||||
|
||||
const OpenAPITabsContext = React.createContext<OpenAPITabsContextData | null>(null);
|
||||
const OpenAPITabsContext = createContext<OpenAPITabsContextData | null>(null);
|
||||
|
||||
function useOpenAPITabsContext() {
|
||||
const context = React.useContext(OpenAPITabsContext);
|
||||
const context = useContext(OpenAPITabsContext);
|
||||
if (!context) {
|
||||
throw new Error('OpenAPITabsContext is missing');
|
||||
}
|
||||
@@ -32,7 +32,14 @@ function useOpenAPITabsContext() {
|
||||
*/
|
||||
export function OpenAPITabs(props: React.PropsWithChildren<TabsProps & { items: Tab[] }>) {
|
||||
const { children, items } = props;
|
||||
const [selectedKey, setSelectedKey] = React.useState(items[0].key);
|
||||
|
||||
const [selectedKey, setSelectedKey] = useState(() => {
|
||||
const firstItem = items[0];
|
||||
if (!firstItem) {
|
||||
throw new Error('OpenAPITabs: at least one tab is required');
|
||||
}
|
||||
return firstItem.key;
|
||||
});
|
||||
|
||||
const contextValue = { items, selectedKey, setSelectedKey };
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { ApiClientModalProvider, useApiClientModal } from '@scalar/api-client-react';
|
||||
import React, { useImperativeHandle, useRef } from 'react';
|
||||
import { useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { useOpenAPIOperationContext } from './OpenAPIOperationContext';
|
||||
@@ -19,7 +19,7 @@ export function ScalarApiButton({
|
||||
path: string;
|
||||
specUrl: string;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const controllerRef = useRef<ScalarModalControllerRef>(null);
|
||||
return (
|
||||
<div className="scalar scalar-activate">
|
||||
@@ -96,7 +96,7 @@ function ScalarModalController(props: {
|
||||
const trackOpening = useEventCallback(() => {
|
||||
onOpenClient({ method: props.method, path: props.path });
|
||||
});
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (openClient) {
|
||||
openClient();
|
||||
trackOpening();
|
||||
|
||||
@@ -144,7 +144,8 @@ export function parseHostAndPath(url: string) {
|
||||
} catch (e) {
|
||||
// If the URL was invalid do our best to parse the URL.
|
||||
// Check for the protocol part and pull it off to grab the host
|
||||
const fullUrl = url.match(/\/\//) ? url.split('//')[1] : url;
|
||||
const splitted = url.split('//');
|
||||
const fullUrl = splitted[1] ? splitted[1] : url;
|
||||
|
||||
// separate paths from the first element (host)
|
||||
const parts = fullUrl.split('/');
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { it, expect } from 'bun:test';
|
||||
|
||||
import { fetchOpenAPIOperation, OpenAPIFetcher } from './fetchOpenAPIOperation';
|
||||
import { parseOpenAPI } from './parser';
|
||||
import { fetchOpenAPIOperation, type OpenAPIFetcher } from './fetchOpenAPIOperation';
|
||||
import { parseOpenAPI, traverse } from '@gitbook/openapi-parser';
|
||||
|
||||
const fetcher: OpenAPIFetcher = {
|
||||
fetch: async (url) => {
|
||||
const response = await fetch(url);
|
||||
return parseOpenAPI({ value: await response.text(), url, parseMarkdown: async (v) => v });
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { toJSON, fromJSON } from 'flatted';
|
||||
|
||||
import { OpenAPICustomSpecProperties, OpenAPIParseError } from './parser';
|
||||
import { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
import {
|
||||
type OpenAPICustomOperationProperties,
|
||||
type OpenAPICustomSpecProperties,
|
||||
type OpenAPIV3xDocument,
|
||||
type Filesystem,
|
||||
type OpenAPIV3,
|
||||
type OpenAPIV3_1,
|
||||
OpenAPIParseError,
|
||||
dereference,
|
||||
} from '@gitbook/openapi-parser';
|
||||
import { noReference } from './utils';
|
||||
import { dereference } from '@scalar/openapi-parser';
|
||||
|
||||
export interface OpenAPIFetcher {
|
||||
/**
|
||||
* Fetch an OpenAPI file by its URL. It should return a fully parsed OpenAPI v3 document.
|
||||
*/
|
||||
fetch: (
|
||||
url: string,
|
||||
) => Promise<
|
||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
||||
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
||||
>;
|
||||
fetch: (url: string) => Promise<Filesystem<OpenAPIV3xDocument>>;
|
||||
}
|
||||
|
||||
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
||||
@@ -25,7 +27,7 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
||||
servers: OpenAPIV3.ServerObject[];
|
||||
|
||||
/** Spec of the operation */
|
||||
operation: OpenAPIV3.OperationObject;
|
||||
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
|
||||
|
||||
/** Securities that should be used for this operation */
|
||||
securities: [string, OpenAPIV3.SecuritySchemeObject][];
|
||||
@@ -44,9 +46,8 @@ export async function fetchOpenAPIOperation(
|
||||
},
|
||||
fetcher: OpenAPIFetcher,
|
||||
): Promise<OpenAPIOperationData | null> {
|
||||
const refSchema = await fetcher.fetch(input.url);
|
||||
const schema = await memoDereferenceSchema(refSchema, input.url);
|
||||
|
||||
const filesystem = await fetcher.fetch(input.url);
|
||||
const schema = await memoDereferenceFilesystem(filesystem, input.url);
|
||||
let operation = getOperationByPathAndMethod(schema, input.path, input.method);
|
||||
|
||||
if (!operation) {
|
||||
@@ -69,9 +70,11 @@ export async function fetchOpenAPIOperation(
|
||||
const securities: OpenAPIOperationData['securities'] = [];
|
||||
for (const entry of security) {
|
||||
const securityKey = Object.keys(entry)[0];
|
||||
const securityScheme = schema.components?.securitySchemes?.[securityKey];
|
||||
if (securityScheme) {
|
||||
securities.push([securityKey, noReference(securityScheme)]);
|
||||
if (securityKey) {
|
||||
const securityScheme = schema.components?.securitySchemes?.[securityKey];
|
||||
if (securityScheme) {
|
||||
securities.push([securityKey, noReference(securityScheme)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,28 +93,34 @@ export async function fetchOpenAPIOperation(
|
||||
};
|
||||
}
|
||||
|
||||
const dereferenceSchemaCache = new WeakMap<OpenAPI.Document, Promise<OpenAPI.Document>>();
|
||||
const dereferenceCache = new WeakMap<Filesystem, Promise<OpenAPIV3xDocument>>();
|
||||
|
||||
/**
|
||||
* Memoized version of `dereferenceSchema`.
|
||||
*/
|
||||
function memoDereferenceSchema<T extends OpenAPI.Document>(schema: T, url: string): Promise<T> {
|
||||
if (dereferenceSchemaCache.has(schema)) {
|
||||
return dereferenceSchemaCache.get(schema) as Promise<T>;
|
||||
function memoDereferenceFilesystem(
|
||||
filesystem: Filesystem,
|
||||
url: string,
|
||||
): Promise<OpenAPIV3xDocument> {
|
||||
if (dereferenceCache.has(filesystem)) {
|
||||
return dereferenceCache.get(filesystem) as Promise<OpenAPIV3xDocument>;
|
||||
}
|
||||
|
||||
const promise = dereferenceSchema(schema, url);
|
||||
dereferenceSchemaCache.set(schema, promise);
|
||||
const promise = dereferenceFilesystem(filesystem, url);
|
||||
dereferenceCache.set(filesystem, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dereference an OpenAPI schema.
|
||||
*/
|
||||
async function dereferenceSchema<T extends OpenAPI.Document>(schema: T, url: string): Promise<T> {
|
||||
const derefResult = await dereference(schema);
|
||||
async function dereferenceFilesystem(
|
||||
filesystem: Filesystem,
|
||||
url: string,
|
||||
): Promise<OpenAPIV3xDocument> {
|
||||
const result = await dereference(filesystem);
|
||||
|
||||
if (!derefResult.schema) {
|
||||
if (!result.schema) {
|
||||
throw new OpenAPIParseError(
|
||||
'Failed to dereference OpenAPI document',
|
||||
url,
|
||||
@@ -119,7 +128,7 @@ async function dereferenceSchema<T extends OpenAPI.Document>(schema: T, url: str
|
||||
);
|
||||
}
|
||||
|
||||
return derefResult.schema as T;
|
||||
return result.schema as OpenAPIV3xDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import { noReference } from './utils';
|
||||
import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters';
|
||||
|
||||
@@ -46,9 +46,12 @@ export function generateMediaTypeExample(
|
||||
}
|
||||
|
||||
if (mediaType.examples) {
|
||||
const example = mediaType.examples[Object.keys(mediaType.examples)[0]];
|
||||
if (example) {
|
||||
return noReference(example).value;
|
||||
const key = Object.keys(mediaType.examples)[0];
|
||||
if (key) {
|
||||
const example = mediaType.examples[key];
|
||||
if (example) {
|
||||
return noReference(example).value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './fetchOpenAPIOperation';
|
||||
export * from './OpenAPIOperation';
|
||||
export * from './OpenAPIOperationContext';
|
||||
export * from './parser';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { expect, it } from 'bun:test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { parseOpenAPI } from '.';
|
||||
|
||||
it('should parse and not give a recursive structure', async () => {
|
||||
const schema = await parseOpenAPI({
|
||||
value: await readFile(new URL('./fixtures/spec-example.json', import.meta.url), 'utf-8'),
|
||||
url: 'https://example.com',
|
||||
parseMarkdown: async (input) => input,
|
||||
});
|
||||
|
||||
JSON.stringify(schema);
|
||||
expect(schema.openapi).toBe('3.0.0');
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
import { AnyObject } from '@scalar/openapi-parser';
|
||||
import { traverse } from './traverse';
|
||||
|
||||
/**
|
||||
* Parse descriptions in the spec to markdown.
|
||||
*/
|
||||
export async function parseDescriptions<T extends AnyObject>(input: {
|
||||
specification: T;
|
||||
parseMarkdown: (input: string) => Promise<string>;
|
||||
}): Promise<T> {
|
||||
const { specification, parseMarkdown } = input;
|
||||
const promises: Record<string, Promise<string>> = {};
|
||||
const results: Record<string, string> = {};
|
||||
traverse(specification, (obj, path) => {
|
||||
if (checkHasDescription(obj) && path) {
|
||||
promises[hashPath(path)] = parseMarkdown(obj.description);
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
await Promise.all(
|
||||
Object.entries(promises).map(async ([key, promise]) => {
|
||||
results[key] = await promise;
|
||||
}),
|
||||
);
|
||||
return traverse(specification, (obj, path) => {
|
||||
if (
|
||||
checkHasDescription(obj) &&
|
||||
typeof obj.description === 'string' &&
|
||||
path &&
|
||||
results[hashPath(path)]
|
||||
) {
|
||||
obj.description = results[hashPath(path)];
|
||||
}
|
||||
return obj;
|
||||
}) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the object contains a description.
|
||||
*/
|
||||
function checkHasDescription(obj: AnyObject): obj is { description: string } {
|
||||
return 'description' in obj && typeof obj.description === 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a path.
|
||||
*/
|
||||
function hashPath(path: string[]): string {
|
||||
return path.join('/');
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { AnyObject } from '@scalar/openapi-parser';
|
||||
|
||||
/**
|
||||
* Recursively traverses the specification and applies the transform function to each node.
|
||||
* The one from @scalar/openapi-parser does not support recursion.
|
||||
*/
|
||||
export function traverse(
|
||||
specification: AnyObject,
|
||||
transform: (specification: AnyObject, path?: string[]) => AnyObject,
|
||||
path: string[] = [],
|
||||
seen = new WeakSet(),
|
||||
) {
|
||||
const result: AnyObject = {};
|
||||
if (seen.has(specification)) {
|
||||
return specification;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(specification)) {
|
||||
const currentPath = [...path, key];
|
||||
if (Array.isArray(value)) {
|
||||
result[key] = value.map((item, index) => {
|
||||
if (typeof item === 'object' && !Array.isArray(item) && item !== null) {
|
||||
return traverse(item, transform, [...currentPath, index.toString()], seen);
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
} else if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
|
||||
result[key] = traverse(value, transform, currentPath, seen);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return transform(result, path);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { OpenAPICustomSpecProperties } from './types';
|
||||
import { AnyApiDefinitionFormat, validate } from '@scalar/openapi-parser';
|
||||
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
import { OpenAPIParseError } from './error';
|
||||
import { parseDescriptions } from './markdown';
|
||||
|
||||
/**
|
||||
* 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: {
|
||||
value: AnyApiDefinitionFormat;
|
||||
url: string;
|
||||
parseMarkdown: (input: string) => Promise<string>;
|
||||
}): Promise<
|
||||
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
||||
> {
|
||||
const { value, url } = input;
|
||||
const result = await validate(value);
|
||||
|
||||
// Spec is invalid, we stop here.
|
||||
if (!result.specification) {
|
||||
throw new OpenAPIParseError('Invalid OpenAPI document', url, 'invalid-spec');
|
||||
}
|
||||
|
||||
if (result.version === '2.0') {
|
||||
throw new OpenAPIParseError('Only OpenAPI v3 is supported', url, 'v2-spec');
|
||||
}
|
||||
|
||||
const specification = await parseDescriptions({
|
||||
specification: result.specification,
|
||||
parseMarkdown: input.parseMarkdown,
|
||||
});
|
||||
|
||||
switch (result.version) {
|
||||
case '3.0':
|
||||
return specification as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
|
||||
case '3.1':
|
||||
default:
|
||||
return specification as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
||||
import type { AnyObject, OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
|
||||
export function noReference<T>(input: T | OpenAPIV3.ReferenceObject): T {
|
||||
if (checkIsReference(input)) {
|
||||
@@ -15,3 +15,14 @@ export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceOb
|
||||
export function createStateKey(key: string, scope?: string) {
|
||||
return scope ? `${scope}_${key}` : key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the description of an object.
|
||||
*/
|
||||
export function resolveDescription(object: AnyObject) {
|
||||
return 'x-description-html' in object && typeof object['x-description-html'] === 'string'
|
||||
? object['x-description-html']
|
||||
: typeof object.description === 'string'
|
||||
? object.description
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": ["./tsconfig.json"],
|
||||
"exclude": ["**/*.test.ts"],
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"noEmit": false,
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,33 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"checkJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"jsx": "preserve",
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
],
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"ui": "tui",
|
||||
"ui": "stream",
|
||||
"globalEnv": ["NODE_ENV", "CI", "ARGOS_*", "GITHUB_*", "GITBOOK_*", "SENTRY_*"],
|
||||
"tasks": {
|
||||
// Prepare the package for all other tasks
|
||||
|
||||
Reference in New Issue
Block a user