mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-17 08:05:19 +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/cache-do": "workspace:*",
|
||||||
"@gitbook/emoji-codepoints": "workspace:*",
|
"@gitbook/emoji-codepoints": "workspace:*",
|
||||||
"@gitbook/icons": "workspace:*",
|
"@gitbook/icons": "workspace:*",
|
||||||
|
"@gitbook/openapi-parser": "workspace:*",
|
||||||
"@gitbook/react-contentkit": "workspace:*",
|
"@gitbook/react-contentkit": "workspace:*",
|
||||||
"@gitbook/react-math": "workspace:*",
|
"@gitbook/react-math": "workspace:*",
|
||||||
"@gitbook/react-openapi": "workspace:*",
|
"@gitbook/react-openapi": "workspace:*",
|
||||||
@@ -148,6 +149,23 @@
|
|||||||
"react": "*",
|
"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": {
|
"packages/proxy": {
|
||||||
"name": "@gitbook/proxy",
|
"name": "@gitbook/proxy",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@@ -191,20 +209,16 @@
|
|||||||
"name": "@gitbook/react-openapi",
|
"name": "@gitbook/react-openapi",
|
||||||
"version": "0.7.1",
|
"version": "0.7.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gitbook/openapi-parser": "workspace:*",
|
||||||
"@scalar/api-client-react": "1.0.87",
|
"@scalar/api-client-react": "1.0.87",
|
||||||
"@scalar/oas-utils": "^0.2.101",
|
"@scalar/oas-utils": "^0.2.101",
|
||||||
"@scalar/openapi-parser": "^0.10.4",
|
"clsx": "^2.1.1",
|
||||||
"@scalar/openapi-types": "^0.1.6",
|
|
||||||
"classnames": "^2.5.1",
|
|
||||||
"flatted": "^3.2.9",
|
"flatted": "^3.2.9",
|
||||||
"react-aria": "^3.37.0",
|
"react-aria": "^3.37.0",
|
||||||
"react-aria-components": "^1.6.0",
|
"react-aria-components": "^1.6.0",
|
||||||
"swagger2openapi": "^7.0.8",
|
|
||||||
"usehooks-ts": "^3.1.0",
|
"usehooks-ts": "^3.1.0",
|
||||||
"yaml": "1.10.2",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/swagger2openapi": "^7.0.4",
|
|
||||||
"bun-types": "^1.1.20",
|
"bun-types": "^1.1.20",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
},
|
},
|
||||||
@@ -589,6 +603,8 @@
|
|||||||
|
|
||||||
"@gitbook/icons": ["@gitbook/icons@workspace:packages/icons"],
|
"@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/proxy": ["@gitbook/proxy@workspace:packages/proxy"],
|
||||||
|
|
||||||
"@gitbook/react-contentkit": ["@gitbook/react-contentkit@workspace:packages/react-contentkit"],
|
"@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/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/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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"deep-equal/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
|
|||||||
+2
-2
@@ -15,8 +15,8 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "turbo run dev --ui=stream",
|
"dev": "turbo run dev",
|
||||||
"dev:v2": "turbo run dev:v2 --ui=stream",
|
"dev:v2": "turbo run dev:v2",
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:v2": "turbo run build:v2",
|
"build:v2": "turbo run build:v2",
|
||||||
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@gitbook/cache-do": "workspace:*",
|
"@gitbook/cache-do": "workspace:*",
|
||||||
"@gitbook/emoji-codepoints": "workspace:*",
|
"@gitbook/emoji-codepoints": "workspace:*",
|
||||||
"@gitbook/icons": "workspace:*",
|
"@gitbook/icons": "workspace:*",
|
||||||
|
"@gitbook/openapi-parser": "workspace:*",
|
||||||
"@gitbook/react-contentkit": "workspace:*",
|
"@gitbook/react-contentkit": "workspace:*",
|
||||||
"@gitbook/react-math": "workspace:*",
|
"@gitbook/react-math": "workspace:*",
|
||||||
"@gitbook/react-openapi": "workspace:*",
|
"@gitbook/react-openapi": "workspace:*",
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { ContentRef, DocumentBlockOpenAPI } from '@gitbook/api';
|
import { ContentRef, DocumentBlockOpenAPI } from '@gitbook/api';
|
||||||
|
import { parseOpenAPI, OpenAPIParseError, traverse } from '@gitbook/openapi-parser';
|
||||||
import {
|
import {
|
||||||
OpenAPIOperationData,
|
OpenAPIOperationData,
|
||||||
fetchOpenAPIOperation,
|
fetchOpenAPIOperation,
|
||||||
OpenAPIFetcher,
|
OpenAPIFetcher,
|
||||||
parseOpenAPI,
|
|
||||||
OpenAPIParseError,
|
|
||||||
} from '@gitbook/react-openapi';
|
} from '@gitbook/react-openapi';
|
||||||
|
|
||||||
import { cache, noCacheFetchOptions, CacheFunctionOptions } from '@/lib/cache';
|
import { cache, noCacheFetchOptions, CacheFunctionOptions } from '@/lib/cache';
|
||||||
@@ -49,7 +48,7 @@ export async function fetchOpenAPIBlock(
|
|||||||
|
|
||||||
const fetcher: OpenAPIFetcher = {
|
const fetcher: OpenAPIFetcher = {
|
||||||
fetch: cache({
|
fetch: cache({
|
||||||
name: 'openapi.fetch.v4',
|
name: 'openapi.fetch.v5',
|
||||||
get: async (url: string, options: CacheFunctionOptions) => {
|
get: async (url: string, options: CacheFunctionOptions) => {
|
||||||
// Wrap the raw string to prevent invalid URLs from being passed to fetch.
|
// 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:
|
// 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 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 {
|
return {
|
||||||
// Cache for 4 hours
|
// Cache for 4 hours
|
||||||
ttl: 24 * 60 * 60,
|
ttl: 24 * 60 * 60,
|
||||||
// Revalidate every 2 hours
|
// Revalidate every 2 hours
|
||||||
revalidateBefore: 22 * 60 * 60,
|
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"],
|
"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 {
|
export class OpenAPIParseError extends Error {
|
||||||
public name = 'OpenAPIParseError';
|
public override name = 'OpenAPIParseError';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
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 will also convert Swagger 2.0 to OpenAPI 3.0.
|
||||||
* It can throw an `OpenAPIParseError` if the document is invalid.
|
* It can throw an `OpenAPIParseError` if the document is invalid.
|
||||||
*/
|
*/
|
||||||
export async function parseOpenAPI(input: {
|
export async function parseOpenAPI(input: { value: string; url: string }) {
|
||||||
value: string;
|
|
||||||
url: string;
|
|
||||||
parseMarkdown: (input: string) => Promise<string>;
|
|
||||||
}) {
|
|
||||||
try {
|
try {
|
||||||
return await parseOpenAPIV3(input);
|
return await parseOpenAPIV3(input);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -21,6 +17,3 @@ export async function parseOpenAPI(input: {
|
|||||||
throw error;
|
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.
|
* 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.
|
* This option can be used to hide code samples for the entire spec.
|
||||||
*/
|
*/
|
||||||
'x-hideTryItPanel'?: boolean;
|
'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/
|
* https://redocly.com/docs/api-reference-docs/specification-extensions/x-hidetryitpanel/
|
||||||
*/
|
*/
|
||||||
'x-hideTryItPanel'?: boolean;
|
'x-hideTryItPanel'?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description in HTML format.
|
||||||
|
*/
|
||||||
|
'x-description-html'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,3 +54,23 @@ export interface OpenAPICustomCodeSample {
|
|||||||
label: string;
|
label: string;
|
||||||
source: 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 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 { OpenAPIParseError } from './error';
|
||||||
import { parseOpenAPIV3 } from './v3';
|
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.
|
* 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: {
|
export async function convertOpenAPIV2ToOpenAPIV3(input: {
|
||||||
value: AnyApiDefinitionFormat;
|
value: AnyApiDefinitionFormat;
|
||||||
url: string;
|
url: string;
|
||||||
parseMarkdown: (input: string) => Promise<string>;
|
}): Promise<Filesystem<OpenAPIV3xDocument>> {
|
||||||
}): Promise<
|
const { value, url } = input;
|
||||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
|
||||||
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
|
||||||
> {
|
|
||||||
const { value, url, parseMarkdown } = input;
|
|
||||||
// In this case we want the raw value to be able to convert it.
|
// In this case we want the raw value to be able to convert it.
|
||||||
const schema = typeof value === 'string' ? rawParseOpenAPI({ value, url }) : value;
|
const schema = typeof value === 'string' ? rawParseOpenAPI({ value, url }) : value;
|
||||||
try {
|
try {
|
||||||
@@ -34,7 +29,7 @@ export async function convertOpenAPIV2ToOpenAPIV3(input: {
|
|||||||
patch: true,
|
patch: true,
|
||||||
})) as ConvertOutputOptions;
|
})) as ConvertOutputOptions;
|
||||||
|
|
||||||
return parseOpenAPIV3({ url, value: convertResult.openapi, parseMarkdown });
|
return parseOpenAPIV3({ url, value: convertResult.openapi });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === 'S2OError') {
|
if (error instanceof Error && error.name === 'S2OError') {
|
||||||
throw new OpenAPIParseError(
|
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",
|
"version": "0.7.1",
|
||||||
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gitbook/openapi-parser": "workspace:*",
|
||||||
"@scalar/api-client-react": "1.0.87",
|
"@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",
|
"@scalar/oas-utils": "^0.2.101",
|
||||||
"classnames": "^2.5.1",
|
"clsx": "^2.1.1",
|
||||||
"flatted": "^3.2.9",
|
"flatted": "^3.2.9",
|
||||||
"swagger2openapi": "^7.0.8",
|
|
||||||
"react-aria-components": "^1.6.0",
|
"react-aria-components": "^1.6.0",
|
||||||
"react-aria": "^3.37.0",
|
"react-aria": "^3.37.0",
|
||||||
"usehooks-ts": "^3.1.0",
|
"usehooks-ts": "^3.1.0"
|
||||||
"yaml": "1.10.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/swagger2openapi": "^7.0.4",
|
|
||||||
"bun-types": "^1.1.20",
|
"bun-types": "^1.1.20",
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.3"
|
||||||
},
|
},
|
||||||
@@ -31,10 +28,10 @@
|
|||||||
"react": "*"
|
"react": "*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc --project tsconfig.build.json",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"unit": "bun test",
|
"unit": "bun test",
|
||||||
"dev": "tsc -w",
|
"dev": "bun run build -- --watch",
|
||||||
"clean": "rm -rf ./dist"
|
"clean": "rm -rf ./dist"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import React, { useCallback } from 'react';
|
import { useCallback, useRef, useState, useSyncExternalStore } from 'react';
|
||||||
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
||||||
import { useDisclosureState } from 'react-stately';
|
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;
|
return [tabs, setSyncedTabs] as const;
|
||||||
}
|
}
|
||||||
@@ -80,15 +80,15 @@ export function InteractiveSection(props: {
|
|||||||
stateKey && stateKey in syncedTabs
|
stateKey && stateKey in syncedTabs
|
||||||
? tabs.find((tab) => tab.key === syncedTabs[stateKey])
|
? tabs.find((tab) => tab.key === syncedTabs[stateKey])
|
||||||
: undefined;
|
: undefined;
|
||||||
const [selectedTabKey, setSelectedTab] = React.useState(tabFromState?.key ?? defaultTab);
|
const [selectedTabKey, setSelectedTab] = useState(tabFromState?.key ?? defaultTab);
|
||||||
const selectedTab: InteractiveSectionTab | undefined =
|
const selectedTab: InteractiveSectionTab | undefined =
|
||||||
tabFromState ?? tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
|
tabFromState ?? tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
|
||||||
|
|
||||||
const state = useDisclosureState({
|
const state = useDisclosureState({
|
||||||
defaultExpanded: defaultOpened,
|
defaultExpanded: defaultOpened,
|
||||||
});
|
});
|
||||||
const panelRef = React.useRef<HTMLDivElement | null>(null);
|
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||||
const triggerRef = React.useRef<HTMLButtonElement | null>(null);
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
|
const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
|
||||||
const { buttonProps } = useButton(triggerProps, triggerRef);
|
const { buttonProps } = useButton(triggerProps, triggerRef);
|
||||||
const { isFocusVisible, focusProps } = useFocusRing();
|
const { isFocusVisible, focusProps } = useFocusRing();
|
||||||
@@ -96,7 +96,7 @@ export function InteractiveSection(props: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={id}
|
id={id}
|
||||||
className={classNames(
|
className={clsx(
|
||||||
'openapi-section',
|
'openapi-section',
|
||||||
toggeable ? 'openapi-section-toggeable' : null,
|
toggeable ? 'openapi-section-toggeable' : null,
|
||||||
className,
|
className,
|
||||||
@@ -110,10 +110,10 @@ export function InteractiveSection(props: {
|
|||||||
state.toggle();
|
state.toggle();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={classNames('openapi-section-header', `${className}-header`)}
|
className={clsx('openapi-section-header', `${className}-header`)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={clsx(
|
||||||
'openapi-section-header-content',
|
'openapi-section-header-content',
|
||||||
`${className}-header-content`,
|
`${className}-header-content`,
|
||||||
)}
|
)}
|
||||||
@@ -122,10 +122,7 @@ export function InteractiveSection(props: {
|
|||||||
<button
|
<button
|
||||||
{...mergeProps(buttonProps, focusProps)}
|
{...mergeProps(buttonProps, focusProps)}
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
className={classNames(
|
className={clsx('openapi-section-toggle', `${className}-toggle`)}
|
||||||
'openapi-section-toggle',
|
|
||||||
`${className}-toggle`,
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
outline: isFocusVisible
|
outline: isFocusVisible
|
||||||
? '2px solid rgb(var(--primary-color-500) / 0.4)'
|
? '2px solid rgb(var(--primary-color-500) / 0.4)'
|
||||||
@@ -138,7 +135,7 @@ export function InteractiveSection(props: {
|
|||||||
{header}
|
{header}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={clsx(
|
||||||
'openapi-section-header-controls',
|
'openapi-section-header-controls',
|
||||||
`${className}-header-controls`,
|
`${className}-header-controls`,
|
||||||
)}
|
)}
|
||||||
@@ -148,12 +145,12 @@ export function InteractiveSection(props: {
|
|||||||
>
|
>
|
||||||
{tabs.length > 1 ? (
|
{tabs.length > 1 ? (
|
||||||
<select
|
<select
|
||||||
className={classNames(
|
className={clsx(
|
||||||
'openapi-section-select',
|
'openapi-section-select',
|
||||||
'openapi-select',
|
'openapi-select',
|
||||||
`${className}-tabs-select`,
|
`${className}-tabs-select`,
|
||||||
)}
|
)}
|
||||||
value={selectedTab.key}
|
value={selectedTab?.key ?? ''}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setSelectedTab(event.target.value);
|
setSelectedTab(event.target.value);
|
||||||
if (stateKey) {
|
if (stateKey) {
|
||||||
@@ -179,14 +176,14 @@ export function InteractiveSection(props: {
|
|||||||
<div
|
<div
|
||||||
ref={panelRef}
|
ref={panelRef}
|
||||||
{...panelProps}
|
{...panelProps}
|
||||||
className={classNames('openapi-section-body', `${className}-body`)}
|
className={clsx('openapi-section-body', `${className}-body`)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{selectedTab?.body}
|
{selectedTab?.body}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{overlay ? (
|
{overlay ? (
|
||||||
<div className={classNames('openapi-section-overlay', `${className}-overlay`)}>
|
<div className={clsx('openapi-section-overlay', `${className}-overlay`)}>
|
||||||
{overlay}
|
{overlay}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
export function Markdown(props: { source: string; className?: string }) {
|
export function Markdown(props: { source: string; className?: string }) {
|
||||||
const { source, className } = props;
|
const { source, className } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames('openapi-markdown', className)}
|
className={clsx('openapi-markdown', className)}
|
||||||
dangerouslySetInnerHTML={{ __html: source }}
|
dangerouslySetInnerHTML={{ __html: source }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { CodeSampleInput, codeSampleGenerators } from './code-samples';
|
import { CodeSampleInput, codeSampleGenerators } from './code-samples';
|
||||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||||
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import { useRef } from 'react';
|
||||||
import { OpenAPIClientContext } from './types';
|
import type { OpenAPIClientContext } from './types';
|
||||||
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
||||||
import { useDisclosureState } from 'react-stately';
|
import { useDisclosureState } from 'react-stately';
|
||||||
|
|
||||||
@@ -15,8 +15,8 @@ interface Props {
|
|||||||
*/
|
*/
|
||||||
export function OpenAPIDisclosure({ context, children, label }: Props): JSX.Element {
|
export function OpenAPIDisclosure({ context, children, label }: Props): JSX.Element {
|
||||||
const state = useDisclosureState({});
|
const state = useDisclosureState({});
|
||||||
const panelRef = React.useRef<HTMLDivElement | null>(null);
|
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||||
const triggerRef = React.useRef<HTMLButtonElement | null>(null);
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
|
const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
|
||||||
const { buttonProps } = useButton(triggerProps, triggerRef);
|
const { buttonProps } = useButton(triggerProps, triggerRef);
|
||||||
const { isFocusVisible, focusProps } = useFocusRing();
|
const { isFocusVisible, focusProps } = useFocusRing();
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
groups: TDisclosureGroup[];
|
groups: TDisclosureGroup[];
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
@@ -22,9 +20,9 @@ import {
|
|||||||
useDisclosureGroupState,
|
useDisclosureGroupState,
|
||||||
useDisclosureState,
|
useDisclosureState,
|
||||||
} from 'react-stately';
|
} 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.
|
* Display an interactive OpenAPI disclosure group.
|
||||||
@@ -48,7 +46,7 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
|||||||
|
|
||||||
const defaultId = useId();
|
const defaultId = useId();
|
||||||
const id = group.id || defaultId;
|
const id = group.id || defaultId;
|
||||||
const groupState = React.useContext(DisclosureGroupStateContext);
|
const groupState = useContext(DisclosureGroupStateContext);
|
||||||
const isExpanded = groupState?.expandedKeys.has(id) || false;
|
const isExpanded = groupState?.expandedKeys.has(id) || false;
|
||||||
const state = useDisclosureState({
|
const state = useDisclosureState({
|
||||||
isExpanded,
|
isExpanded,
|
||||||
@@ -59,8 +57,8 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const panelRef = React.useRef<HTMLDivElement | null>(null);
|
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||||
const triggerRef = React.useRef<HTMLButtonElement | null>(null);
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const isDisabled = groupState?.isDisabled || !group.tabs?.length || false;
|
const isDisabled = groupState?.isDisabled || !group.tabs?.length || false;
|
||||||
const { buttonProps: triggerProps, panelProps } = useDisclosure(
|
const { buttonProps: triggerProps, panelProps } = useDisclosure(
|
||||||
{
|
{
|
||||||
@@ -75,7 +73,7 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
|||||||
const { isFocusVisible, focusProps } = useFocusRing();
|
const { isFocusVisible, focusProps } = useFocusRing();
|
||||||
|
|
||||||
const defaultTab = group.tabs?.[0]?.id || '';
|
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);
|
const selectedTab = group.tabs?.find((tab) => tab.id === selectedTabKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -107,11 +105,7 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
|||||||
<div className="openapi-disclosure-group-mediatype">
|
<div className="openapi-disclosure-group-mediatype">
|
||||||
{group.tabs?.length > 1 ? (
|
{group.tabs?.length > 1 ? (
|
||||||
<select
|
<select
|
||||||
className={classNames(
|
className="openapi-section-select openapi-select openapi-disclosure-group-tabs-select"
|
||||||
'openapi-section-select',
|
|
||||||
'openapi-select',
|
|
||||||
`openapi-disclosure-group-tabs-select`,
|
|
||||||
)}
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
value={selectedTab?.id}
|
value={selectedTab?.id}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import * as React from 'react';
|
import clsx from 'clsx';
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
import type { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
import { OpenAPICodeSample } from './OpenAPICodeSample';
|
import { OpenAPICodeSample } from './OpenAPICodeSample';
|
||||||
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
|
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
|
||||||
import { OpenAPISpec } from './OpenAPISpec';
|
import { OpenAPISpec } from './OpenAPISpec';
|
||||||
import { OpenAPIClientContext, OpenAPIContextProps } from './types';
|
import { OpenAPIClientContext, type OpenAPIContextProps } from './types';
|
||||||
import { OpenAPIPath } from './OpenAPIPath';
|
import { OpenAPIPath } from './OpenAPIPath';
|
||||||
|
import { resolveDescription } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display an interactive OpenAPI operation.
|
* Display an interactive OpenAPI operation.
|
||||||
@@ -26,18 +26,18 @@ export function OpenAPIOperation(props: {
|
|||||||
blockKey: context.blockKey,
|
blockKey: context.blockKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const trimmedDescription = operation.description?.trim();
|
const description = resolveDescription(operation)?.trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('openapi-operation', className)}>
|
<div className={clsx('openapi-operation', className)}>
|
||||||
<div className="openapi-summary" id={context.id}>
|
<div className="openapi-summary" id={context.id}>
|
||||||
<h2 className="openapi-summary-title" data-deprecated={operation.deprecated}>
|
<h2 className="openapi-summary-title" data-deprecated={operation.deprecated}>
|
||||||
{operation.summary}
|
{operation.summary}
|
||||||
</h2>
|
</h2>
|
||||||
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames('openapi-columns')}>
|
<div className="openapi-columns">
|
||||||
<div className={classNames('openapi-column-spec')}>
|
<div className="openapi-column-spec">
|
||||||
{operation['x-deprecated-sunset'] ? (
|
{operation['x-deprecated-sunset'] ? (
|
||||||
<div className="openapi-deprecated-sunset openapi-description openapi-markdown">
|
<div className="openapi-deprecated-sunset openapi-description openapi-markdown">
|
||||||
This operation is deprecated and will be sunset on{' '}
|
This operation is deprecated and will be sunset on{' '}
|
||||||
@@ -47,16 +47,16 @@ export function OpenAPIOperation(props: {
|
|||||||
{`.`}
|
{`.`}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{trimmedDescription ? (
|
{description ? (
|
||||||
<div className="openapi-intro">
|
<div className="openapi-intro">
|
||||||
<Markdown className="openapi-description" source={trimmedDescription} />
|
<Markdown className="openapi-description" source={description} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<OpenAPIPath data={data} context={context} />
|
<OpenAPIPath data={data} context={context} />
|
||||||
<OpenAPISpec data={data} context={clientContext} />
|
<OpenAPISpec data={data} context={clientContext} />
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames('openapi-column-preview')}>
|
<div className="openapi-column-preview">
|
||||||
<div className={classNames('openapi-column-preview-body')}>
|
<div className="openapi-column-preview-body">
|
||||||
<OpenAPICodeSample {...props} />
|
<OpenAPICodeSample {...props} />
|
||||||
<OpenAPIResponseExample {...props} />
|
<OpenAPIResponseExample {...props} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import * as React from 'react';
|
|
||||||
|
import { createContext, useContext, useMemo } from 'react';
|
||||||
import { useEventCallback } from 'usehooks-ts';
|
import { useEventCallback } from 'usehooks-ts';
|
||||||
|
|
||||||
interface OpenAPIOperationPointer {
|
interface OpenAPIOperationPointer {
|
||||||
@@ -11,7 +12,7 @@ interface OpenAPIOperationContextValue {
|
|||||||
onOpenClient: (pointer: OpenAPIOperationPointer) => void;
|
onOpenClient: (pointer: OpenAPIOperationPointer) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OpenAPIOperationContext = React.createContext<OpenAPIOperationContextValue>({
|
const OpenAPIOperationContext = createContext<OpenAPIOperationContextValue>({
|
||||||
onOpenClient: () => {},
|
onOpenClient: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ export function OpenAPIOperationContextProvider(
|
|||||||
props.onOpenClient?.(pointer);
|
props.onOpenClient?.(pointer);
|
||||||
});
|
});
|
||||||
|
|
||||||
const value = React.useMemo(() => ({ onOpenClient }), [onOpenClient]);
|
const value = useMemo(() => ({ onOpenClient }), [onOpenClient]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OpenAPIOperationContext.Provider value={value}>
|
<OpenAPIOperationContext.Provider value={value}>
|
||||||
@@ -40,5 +41,5 @@ export function OpenAPIOperationContextProvider(
|
|||||||
* Hook to access the OpenAPIOperationContext.
|
* Hook to access the OpenAPIOperationContext.
|
||||||
*/
|
*/
|
||||||
export function useOpenAPIOperationContext() {
|
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 { ScalarApiButton } from './ScalarApiButton';
|
||||||
import { OpenAPIContextProps } from './types';
|
import type { OpenAPIContextProps } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the path of an operation.
|
* Display the path of an operation.
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import * as React from 'react';
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||||
|
|
||||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
|
||||||
import { OpenAPIRootSchema } from './OpenAPISchema';
|
import { OpenAPIRootSchema } from './OpenAPISchema';
|
||||||
import { noReference } from './utils';
|
import { noReference } from './utils';
|
||||||
import { OpenAPIClientContext } from './types';
|
import type { OpenAPIClientContext } from './types';
|
||||||
import { InteractiveSection } from './InteractiveSection';
|
import { InteractiveSection } from './InteractiveSection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import classNames from 'classnames';
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
|
||||||
import { OpenAPISchemaProperties } from './OpenAPISchema';
|
import { OpenAPISchemaProperties } from './OpenAPISchema';
|
||||||
import { checkIsReference, noReference } from './utils';
|
import { checkIsReference, noReference, resolveDescription } from './utils';
|
||||||
import { OpenAPIClientContext } from './types';
|
import type { OpenAPIClientContext } from './types';
|
||||||
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,7 +18,9 @@ export function OpenAPIResponse(props: {
|
|||||||
);
|
);
|
||||||
const content = Object.entries(mediaType.schema ?? {});
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ export function OpenAPIResponse(props: {
|
|||||||
/>
|
/>
|
||||||
</OpenAPIDisclosure>
|
</OpenAPIDisclosure>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={classNames('openapi-responsebody')}>
|
<div className="openapi-responsebody">
|
||||||
<OpenAPISchemaProperties
|
<OpenAPISchemaProperties
|
||||||
id={`response-${context.blockKey}`}
|
id={`response-${context.blockKey}`}
|
||||||
properties={[
|
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 { generateSchemaExample } from './generateSchemaExample';
|
||||||
import { OpenAPIContextProps } from './types';
|
import type { OpenAPIContextProps } from './types';
|
||||||
import { checkIsReference, noReference } from './utils';
|
import { checkIsReference, noReference, resolveDescription } from './utils';
|
||||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
|
||||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||||
import { InteractiveSection } from './InteractiveSection';
|
import { InteractiveSection } from './InteractiveSection';
|
||||||
|
|
||||||
@@ -46,9 +46,10 @@ export function OpenAPIResponseExample(props: {
|
|||||||
if (!responseObject.content) {
|
if (!responseObject.content) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const key = Object.keys(responseObject.content)[0];
|
||||||
return (
|
return (
|
||||||
responseObject.content['application/json'] ??
|
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 {
|
return {
|
||||||
key: key,
|
key: key,
|
||||||
label: key,
|
label: key,
|
||||||
description: responseObject.description,
|
description: resolveDescription(responseObject),
|
||||||
body: <OpenAPIEmptyResponseExample />,
|
body: <OpenAPIEmptyResponseExample />,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -65,11 +66,13 @@ export function OpenAPIResponseExample(props: {
|
|||||||
(() => {
|
(() => {
|
||||||
const { examples, example } = mediaTypeObject;
|
const { examples, example } = mediaTypeObject;
|
||||||
if (examples) {
|
if (examples) {
|
||||||
const firstKey = Object.keys(examples)[0];
|
const key = Object.keys(examples)[0];
|
||||||
// @TODO handle multiple examples
|
if (key) {
|
||||||
const firstExample = noReference(examples[firstKey]);
|
// @TODO handle multiple examples
|
||||||
if (firstExample) {
|
const firstExample = noReference(examples[key]);
|
||||||
return firstExample;
|
if (firstExample) {
|
||||||
|
return firstExample;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +92,7 @@ export function OpenAPIResponseExample(props: {
|
|||||||
return {
|
return {
|
||||||
key: key,
|
key: key,
|
||||||
label: key,
|
label: key,
|
||||||
description: responseObject.description,
|
description: resolveDescription(responseObject),
|
||||||
body: example?.value ? (
|
body: example?.value ? (
|
||||||
<context.CodeBlock
|
<context.CodeBlock
|
||||||
code={
|
code={
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import * as React from 'react';
|
import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
||||||
import classNames from 'classnames';
|
import { createStateKey, resolveDescription } from './utils';
|
||||||
import { createStateKey, noReference } from './utils';
|
|
||||||
import { OpenAPIResponse } from './OpenAPIResponse';
|
import { OpenAPIResponse } from './OpenAPIResponse';
|
||||||
import { OpenAPIClientContext } from './types';
|
import { OpenAPIClientContext } from './types';
|
||||||
import { InteractiveSection } from './InteractiveSection';
|
import { InteractiveSection } from './InteractiveSection';
|
||||||
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
|
||||||
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
|
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
import { OpenAPIRootSchema, OpenAPISchemaProperties, OpenAPISchemaProperty } from './OpenAPISchema';
|
|
||||||
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display an interactive response body.
|
* Display an interactive response body.
|
||||||
@@ -23,7 +19,7 @@ export function OpenAPIResponses(props: {
|
|||||||
<InteractiveSection
|
<InteractiveSection
|
||||||
stateKey={createStateKey('response', context.blockKey)}
|
stateKey={createStateKey('response', context.blockKey)}
|
||||||
header="Responses"
|
header="Responses"
|
||||||
className={classNames('openapi-responses')}
|
className="openapi-responses"
|
||||||
>
|
>
|
||||||
<OpenAPIDisclosureGroup
|
<OpenAPIDisclosureGroup
|
||||||
allowsMultipleExpanded
|
allowsMultipleExpanded
|
||||||
@@ -31,6 +27,7 @@ export function OpenAPIResponses(props: {
|
|||||||
groups={Object.entries(responses).map(
|
groups={Object.entries(responses).map(
|
||||||
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
|
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
|
||||||
const content = Object.entries(response.content ?? {});
|
const content = Object.entries(response.content ?? {});
|
||||||
|
const description = resolveDescription(response);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: statusCode,
|
id: statusCode,
|
||||||
@@ -42,9 +39,9 @@ export function OpenAPIResponses(props: {
|
|||||||
<span className="openapi-response-statuscode">
|
<span className="openapi-response-statuscode">
|
||||||
{statusCode}
|
{statusCode}
|
||||||
</span>
|
</span>
|
||||||
{response.description ? (
|
{description ? (
|
||||||
<Markdown
|
<Markdown
|
||||||
source={response.description}
|
source={description}
|
||||||
className="openapi-response-description"
|
className="openapi-response-description"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { it, describe, expect } from 'bun:test';
|
import { it, describe, expect } from 'bun:test';
|
||||||
import { getSchemaAlternatives } from './OpenAPISchema';
|
import { getSchemaAlternatives } from './OpenAPISchema';
|
||||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||||
|
|
||||||
describe('getSchemaAlternatives', () => {
|
describe('getSchemaAlternatives', () => {
|
||||||
it('should flatten oneOf', () => {
|
it('should flatten oneOf', () => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import classNames from 'classnames';
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
import clsx from 'clsx';
|
||||||
import React, { useId } from 'react';
|
import { useId } from 'react';
|
||||||
|
|
||||||
import { InteractiveSection } from './InteractiveSection';
|
import { InteractiveSection } from './InteractiveSection';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
import { OpenAPIClientContext } from './types';
|
import type { OpenAPIClientContext } from './types';
|
||||||
import { checkIsReference, noReference } from './utils';
|
import { checkIsReference, noReference, resolveDescription } from './utils';
|
||||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||||
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
||||||
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
||||||
@@ -49,7 +49,7 @@ export function OpenAPISchemaProperty(
|
|||||||
|
|
||||||
if ((properties && !!properties.length) || schema.type === 'object') {
|
if ((properties && !!properties.length) || schema.type === 'object') {
|
||||||
return (
|
return (
|
||||||
<InteractiveSection id={id} className={classNames('openapi-schema', className)}>
|
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
|
||||||
<OpenAPISchemaPresentation {...props} />
|
<OpenAPISchemaPresentation {...props} />
|
||||||
<OpenAPIDisclosure context={context}>
|
<OpenAPIDisclosure context={context}>
|
||||||
{properties && properties.length > 0 ? (
|
{properties && properties.length > 0 ? (
|
||||||
@@ -66,10 +66,11 @@ export function OpenAPISchemaProperty(
|
|||||||
|
|
||||||
if (alternatives?.[0]?.length) {
|
if (alternatives?.[0]?.length) {
|
||||||
return (
|
return (
|
||||||
<InteractiveSection id={id} className={classNames('openapi-schema', className)}>
|
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
|
||||||
<OpenAPISchemaPresentation {...props} />
|
<OpenAPISchemaPresentation {...props} />
|
||||||
{alternatives[0].map((alternative, index) => (
|
{alternatives[0].map((alternative, index) => (
|
||||||
<OpenAPISchemaAlternative
|
<OpenAPISchemaAlternative
|
||||||
|
key={index}
|
||||||
schema={alternative}
|
schema={alternative}
|
||||||
circularRefs={circularRefs}
|
circularRefs={circularRefs}
|
||||||
context={context}
|
context={context}
|
||||||
@@ -80,7 +81,7 @@ export function OpenAPISchemaProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InteractiveSection id={id} className={classNames('openapi-schema', className)}>
|
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
|
||||||
<OpenAPISchemaPresentation {...props} />
|
<OpenAPISchemaPresentation {...props} />
|
||||||
{(properties && properties.length > 0) ||
|
{(properties && properties.length > 0) ||
|
||||||
(schema.enum && schema.enum.length > 0) ||
|
(schema.enum && schema.enum.length > 0) ||
|
||||||
@@ -118,7 +119,7 @@ export function OpenAPISchemaProperties(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={id} className={classNames('openapi-schema-properties')}>
|
<div id={id} className="openapi-schema-properties">
|
||||||
{properties.map((property) => (
|
{properties.map((property) => (
|
||||||
<OpenAPISchemaProperty
|
<OpenAPISchemaProperty
|
||||||
key={property.propertyName}
|
key={property.propertyName}
|
||||||
@@ -228,8 +229,10 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const description = resolveDescription(schema);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('openapi-schema-presentation')}>
|
<div className="openapi-schema-presentation">
|
||||||
<OpenAPISchemaName
|
<OpenAPISchemaName
|
||||||
type={getSchemaTitle(schema)}
|
type={getSchemaTitle(schema)}
|
||||||
propertyName={propertyName}
|
propertyName={propertyName}
|
||||||
@@ -244,8 +247,8 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{schema.description ? (
|
{description ? (
|
||||||
<Markdown source={schema.description} className="openapi-schema-description" />
|
<Markdown source={description} className="openapi-schema-description" />
|
||||||
) : null}
|
) : null}
|
||||||
{shouldDisplayExample(schema) ? (
|
{shouldDisplayExample(schema) ? (
|
||||||
<div className="openapi-schema-example">
|
<div className="openapi-schema-example">
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
interface OpenAPISchemaNameProps {
|
interface OpenAPISchemaNameProps {
|
||||||
propertyName?: string | JSX.Element;
|
propertyName?: string | JSX.Element;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@@ -16,22 +13,15 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps): JSX.Element {
|
|||||||
const { type, propertyName, required, deprecated } = props;
|
const { type, propertyName, required, deprecated } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('openapi-schema-name')}>
|
<div className="openapi-schema-name">
|
||||||
{propertyName ? (
|
{propertyName ? (
|
||||||
<span
|
<span data-deprecated={deprecated} className="openapi-schema-propertyname">
|
||||||
data-deprecated={deprecated}
|
|
||||||
className={classNames('openapi-schema-propertyname')}
|
|
||||||
>
|
|
||||||
{propertyName}
|
{propertyName}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{type ? <span className={classNames('openapi-schema-type')}>{type}</span> : null}
|
{type ? <span className="openapi-schema-type">{type}</span> : null}
|
||||||
{required ? (
|
{required ? <span className="openapi-schema-required">required</span> : null}
|
||||||
<span className={classNames('openapi-schema-required')}>required</span>
|
{deprecated ? <span className="openapi-deprecated">Deprecated</span> : null}
|
||||||
) : null}
|
|
||||||
{deprecated ? (
|
|
||||||
<span className={classNames('openapi-deprecated')}>Deprecated</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as React from 'react';
|
import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
||||||
import { OpenAPIV3_1 } from '@scalar/openapi-types';
|
import type { OpenAPIClientContext } from './types';
|
||||||
import { OpenAPIClientContext } from './types';
|
|
||||||
import { InteractiveSection } from './InteractiveSection';
|
import { InteractiveSection } from './InteractiveSection';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||||
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
||||||
|
import { resolveDescription } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Present securities authorization that can be used for this operation.
|
* Present securities authorization that can be used for this operation.
|
||||||
@@ -27,6 +27,7 @@ export function OpenAPISecurities(props: {
|
|||||||
toggleIcon={context.icons.chevronRight}
|
toggleIcon={context.icons.chevronRight}
|
||||||
className="openapi-securities"
|
className="openapi-securities"
|
||||||
tabs={securities.map(([key, security]) => {
|
tabs={securities.map(([key, security]) => {
|
||||||
|
const description = resolveDescription(security);
|
||||||
return {
|
return {
|
||||||
key: key,
|
key: key,
|
||||||
label: key,
|
label: key,
|
||||||
@@ -35,9 +36,9 @@ export function OpenAPISecurities(props: {
|
|||||||
<div className="openapi-schema-presentation">
|
<div className="openapi-schema-presentation">
|
||||||
{getLabelForType(security)}
|
{getLabelForType(security)}
|
||||||
|
|
||||||
{security.description ? (
|
{description ? (
|
||||||
<Markdown
|
<Markdown
|
||||||
source={security.description}
|
source={description}
|
||||||
className="openapi-securities-description"
|
className="openapi-securities-description"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -66,11 +67,12 @@ function getLabelForType(security: OpenAPIV3_1.SecuritySchemeObject) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (security.scheme == 'bearer') {
|
if (security.scheme == 'bearer') {
|
||||||
|
const description = resolveDescription(security);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OpenAPISchemaName propertyName="Authorization" type="string" required />
|
<OpenAPISchemaName propertyName="Authorization" type="string" required />
|
||||||
{/** Show a default description if none is provided */}
|
{/** Show a default description if none is provided */}
|
||||||
{!security.description ? (
|
{!description ? (
|
||||||
<Markdown
|
<Markdown
|
||||||
source={`Bearer authentication header of the form Bearer ${`<token>`}.`}
|
source={`Bearer authentication header of the form Bearer ${`<token>`}.`}
|
||||||
className="openapi-securities-description"
|
className="openapi-securities-description"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as React from 'react';
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
|
||||||
import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable';
|
import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,6 +8,10 @@ export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) {
|
|||||||
const { servers } = props;
|
const { servers } = props;
|
||||||
const server = servers[0];
|
const server = servers[0];
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const parts = parseServerURL(server?.url ?? '');
|
const parts = parseServerURL(server?.url ?? '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -17,16 +20,13 @@ export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) {
|
|||||||
if (part.kind === 'text') {
|
if (part.kind === 'text') {
|
||||||
return <span key={i}>{part.text}</span>;
|
return <span key={i}>{part.text}</span>;
|
||||||
} else {
|
} else {
|
||||||
if (!server.variables?.[part.name]) {
|
const variable = server.variables?.[part.name];
|
||||||
|
if (!variable) {
|
||||||
return <span key={i}>{`{${part.name}}`}</span>;
|
return <span key={i}>{`{${part.name}}`}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OpenAPIServerURLVariable
|
<OpenAPIServerURLVariable key={i} name={part.name} variable={variable} />
|
||||||
key={i}
|
|
||||||
name={part.name}
|
|
||||||
variable={server.variables[part.name]}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
@@ -39,6 +39,9 @@ export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) {
|
|||||||
*/
|
*/
|
||||||
export function getServersURL(servers: OpenAPIV3.ServerObject[]): string {
|
export function getServersURL(servers: OpenAPIV3.ServerObject[]): string {
|
||||||
const server = servers[0];
|
const server = servers[0];
|
||||||
|
if (!server) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const parts = parseServerURL(server?.url ?? '');
|
const parts = parseServerURL(server?.url ?? '');
|
||||||
|
|
||||||
return parts
|
return parts
|
||||||
@@ -56,10 +59,14 @@ function parseServerURL(url: string) {
|
|||||||
const parts = url.split(/{([^}]+)}/g);
|
const parts = url.split(/{([^}]+)}/g);
|
||||||
const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = [];
|
const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = [];
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
if (!part) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (i % 2 === 0) {
|
if (i % 2 === 0) {
|
||||||
result.push({ kind: 'text', text: parts[i] });
|
result.push({ kind: 'text', text: part });
|
||||||
} else {
|
} else {
|
||||||
result.push({ kind: 'variable', name: parts[i] });
|
result.push({ kind: 'variable', name: part });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { OpenAPIV3 } from '@scalar/openapi-types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interactive component to show the value of a server variable and let the user change it.
|
* 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;
|
variable: OpenAPIV3.ServerVariableObject;
|
||||||
}) {
|
}) {
|
||||||
const { variable } = props;
|
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';
|
'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 { InteractiveSection } from './InteractiveSection';
|
||||||
import { OpenAPIRequestBody } from './OpenAPIRequestBody';
|
import { OpenAPIRequestBody } from './OpenAPIRequestBody';
|
||||||
import { OpenAPIResponses } from './OpenAPIResponses';
|
import { OpenAPIResponses } from './OpenAPIResponses';
|
||||||
import { OpenAPISchemaProperties } from './OpenAPISchema';
|
import { OpenAPISchemaProperties } from './OpenAPISchema';
|
||||||
import { OpenAPISecurities } from './OpenAPISecurities';
|
import { OpenAPISecurities } from './OpenAPISecurities';
|
||||||
import { OpenAPIClientContext } from './types';
|
import { OpenAPIClientContext } from './types';
|
||||||
import { noReference } from './utils';
|
import { noReference, resolveDescription } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client component to render the spec for the request and response.
|
* 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} />
|
<OpenAPISecurities securities={securities} context={context} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{parameterGroups.map((group) => (
|
{parameterGroups.map((group) => {
|
||||||
<InteractiveSection
|
return (
|
||||||
key={group.key}
|
<InteractiveSection
|
||||||
className="openapi-parameters"
|
key={group.key}
|
||||||
header={group.label}
|
className="openapi-parameters"
|
||||||
>
|
header={group.label}
|
||||||
<OpenAPISchemaProperties
|
>
|
||||||
properties={group.parameters.map((parameter) => ({
|
<OpenAPISchemaProperties
|
||||||
propertyName: parameter.name,
|
properties={group.parameters.map((parameter) => {
|
||||||
schema: {
|
const description = resolveDescription(parameter);
|
||||||
// Description of the parameter is defined at the parameter level
|
return {
|
||||||
// we use display it if the schema doesn't override it
|
propertyName: parameter.name,
|
||||||
description: parameter.description,
|
schema: {
|
||||||
example: parameter.example,
|
// Description of the parameter is defined at the parameter level
|
||||||
// Deprecated can be defined at the parameter level
|
// we use display it if the schema doesn't override it
|
||||||
deprecated: parameter.deprecated,
|
description: description,
|
||||||
...(noReference(parameter.schema) ?? {}),
|
example: parameter.example,
|
||||||
},
|
// Deprecated can be defined at the parameter level
|
||||||
required: parameter.required,
|
deprecated: parameter.deprecated,
|
||||||
}))}
|
...(noReference(parameter.schema) ?? {}),
|
||||||
context={context}
|
},
|
||||||
/>
|
required: parameter.required,
|
||||||
</InteractiveSection>
|
};
|
||||||
))}
|
})}
|
||||||
|
context={context}
|
||||||
|
/>
|
||||||
|
</InteractiveSection>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{operation.requestBody ? (
|
{operation.requestBody ? (
|
||||||
<OpenAPIRequestBody
|
<OpenAPIRequestBody
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'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 { Key, Tab, TabList, TabPanel, Tabs, TabsProps } from 'react-aria-components';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
|
|
||||||
@@ -17,10 +17,10 @@ type OpenAPITabsContextData = {
|
|||||||
setSelectedKey: (key: Key) => void;
|
setSelectedKey: (key: Key) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OpenAPITabsContext = React.createContext<OpenAPITabsContextData | null>(null);
|
const OpenAPITabsContext = createContext<OpenAPITabsContextData | null>(null);
|
||||||
|
|
||||||
function useOpenAPITabsContext() {
|
function useOpenAPITabsContext() {
|
||||||
const context = React.useContext(OpenAPITabsContext);
|
const context = useContext(OpenAPITabsContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('OpenAPITabsContext is missing');
|
throw new Error('OpenAPITabsContext is missing');
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,14 @@ function useOpenAPITabsContext() {
|
|||||||
*/
|
*/
|
||||||
export function OpenAPITabs(props: React.PropsWithChildren<TabsProps & { items: Tab[] }>) {
|
export function OpenAPITabs(props: React.PropsWithChildren<TabsProps & { items: Tab[] }>) {
|
||||||
const { children, items } = props;
|
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 };
|
const contextValue = { items, selectedKey, setSelectedKey };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ApiClientModalProvider, useApiClientModal } from '@scalar/api-client-react';
|
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 { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { useOpenAPIOperationContext } from './OpenAPIOperationContext';
|
import { useOpenAPIOperationContext } from './OpenAPIOperationContext';
|
||||||
@@ -19,7 +19,7 @@ export function ScalarApiButton({
|
|||||||
path: string;
|
path: string;
|
||||||
specUrl: string;
|
specUrl: string;
|
||||||
}) {
|
}) {
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const controllerRef = useRef<ScalarModalControllerRef>(null);
|
const controllerRef = useRef<ScalarModalControllerRef>(null);
|
||||||
return (
|
return (
|
||||||
<div className="scalar scalar-activate">
|
<div className="scalar scalar-activate">
|
||||||
@@ -96,7 +96,7 @@ function ScalarModalController(props: {
|
|||||||
const trackOpening = useEventCallback(() => {
|
const trackOpening = useEventCallback(() => {
|
||||||
onOpenClient({ method: props.method, path: props.path });
|
onOpenClient({ method: props.method, path: props.path });
|
||||||
});
|
});
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (openClient) {
|
if (openClient) {
|
||||||
openClient();
|
openClient();
|
||||||
trackOpening();
|
trackOpening();
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ export function parseHostAndPath(url: string) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If the URL was invalid do our best to parse the URL.
|
// 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
|
// 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)
|
// separate paths from the first element (host)
|
||||||
const parts = fullUrl.split('/');
|
const parts = fullUrl.split('/');
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { it, expect } from 'bun:test';
|
import { it, expect } from 'bun:test';
|
||||||
|
|
||||||
import { fetchOpenAPIOperation, OpenAPIFetcher } from './fetchOpenAPIOperation';
|
import { fetchOpenAPIOperation, type OpenAPIFetcher } from './fetchOpenAPIOperation';
|
||||||
import { parseOpenAPI } from './parser';
|
import { parseOpenAPI, traverse } from '@gitbook/openapi-parser';
|
||||||
|
|
||||||
const fetcher: OpenAPIFetcher = {
|
const fetcher: OpenAPIFetcher = {
|
||||||
fetch: async (url) => {
|
fetch: async (url) => {
|
||||||
const response = await fetch(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 { toJSON, fromJSON } from 'flatted';
|
||||||
|
|
||||||
import { OpenAPICustomSpecProperties, OpenAPIParseError } from './parser';
|
import {
|
||||||
import { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
type OpenAPICustomOperationProperties,
|
||||||
|
type OpenAPICustomSpecProperties,
|
||||||
|
type OpenAPIV3xDocument,
|
||||||
|
type Filesystem,
|
||||||
|
type OpenAPIV3,
|
||||||
|
type OpenAPIV3_1,
|
||||||
|
OpenAPIParseError,
|
||||||
|
dereference,
|
||||||
|
} from '@gitbook/openapi-parser';
|
||||||
import { noReference } from './utils';
|
import { noReference } from './utils';
|
||||||
import { dereference } from '@scalar/openapi-parser';
|
|
||||||
|
|
||||||
export interface OpenAPIFetcher {
|
export interface OpenAPIFetcher {
|
||||||
/**
|
/**
|
||||||
* Fetch an OpenAPI file by its URL. It should return a fully parsed OpenAPI v3 document.
|
* Fetch an OpenAPI file by its URL. It should return a fully parsed OpenAPI v3 document.
|
||||||
*/
|
*/
|
||||||
fetch: (
|
fetch: (url: string) => Promise<Filesystem<OpenAPIV3xDocument>>;
|
||||||
url: string,
|
|
||||||
) => Promise<
|
|
||||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
|
||||||
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
||||||
@@ -25,7 +27,7 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
|||||||
servers: OpenAPIV3.ServerObject[];
|
servers: OpenAPIV3.ServerObject[];
|
||||||
|
|
||||||
/** Spec of the operation */
|
/** Spec of the operation */
|
||||||
operation: OpenAPIV3.OperationObject;
|
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
|
||||||
|
|
||||||
/** Securities that should be used for this operation */
|
/** Securities that should be used for this operation */
|
||||||
securities: [string, OpenAPIV3.SecuritySchemeObject][];
|
securities: [string, OpenAPIV3.SecuritySchemeObject][];
|
||||||
@@ -44,9 +46,8 @@ export async function fetchOpenAPIOperation(
|
|||||||
},
|
},
|
||||||
fetcher: OpenAPIFetcher,
|
fetcher: OpenAPIFetcher,
|
||||||
): Promise<OpenAPIOperationData | null> {
|
): Promise<OpenAPIOperationData | null> {
|
||||||
const refSchema = await fetcher.fetch(input.url);
|
const filesystem = await fetcher.fetch(input.url);
|
||||||
const schema = await memoDereferenceSchema(refSchema, input.url);
|
const schema = await memoDereferenceFilesystem(filesystem, input.url);
|
||||||
|
|
||||||
let operation = getOperationByPathAndMethod(schema, input.path, input.method);
|
let operation = getOperationByPathAndMethod(schema, input.path, input.method);
|
||||||
|
|
||||||
if (!operation) {
|
if (!operation) {
|
||||||
@@ -69,9 +70,11 @@ export async function fetchOpenAPIOperation(
|
|||||||
const securities: OpenAPIOperationData['securities'] = [];
|
const securities: OpenAPIOperationData['securities'] = [];
|
||||||
for (const entry of security) {
|
for (const entry of security) {
|
||||||
const securityKey = Object.keys(entry)[0];
|
const securityKey = Object.keys(entry)[0];
|
||||||
const securityScheme = schema.components?.securitySchemes?.[securityKey];
|
if (securityKey) {
|
||||||
if (securityScheme) {
|
const securityScheme = schema.components?.securitySchemes?.[securityKey];
|
||||||
securities.push([securityKey, noReference(securityScheme)]);
|
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`.
|
* Memoized version of `dereferenceSchema`.
|
||||||
*/
|
*/
|
||||||
function memoDereferenceSchema<T extends OpenAPI.Document>(schema: T, url: string): Promise<T> {
|
function memoDereferenceFilesystem(
|
||||||
if (dereferenceSchemaCache.has(schema)) {
|
filesystem: Filesystem,
|
||||||
return dereferenceSchemaCache.get(schema) as Promise<T>;
|
url: string,
|
||||||
|
): Promise<OpenAPIV3xDocument> {
|
||||||
|
if (dereferenceCache.has(filesystem)) {
|
||||||
|
return dereferenceCache.get(filesystem) as Promise<OpenAPIV3xDocument>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = dereferenceSchema(schema, url);
|
const promise = dereferenceFilesystem(filesystem, url);
|
||||||
dereferenceSchemaCache.set(schema, promise);
|
dereferenceCache.set(filesystem, promise);
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dereference an OpenAPI schema.
|
* Dereference an OpenAPI schema.
|
||||||
*/
|
*/
|
||||||
async function dereferenceSchema<T extends OpenAPI.Document>(schema: T, url: string): Promise<T> {
|
async function dereferenceFilesystem(
|
||||||
const derefResult = await dereference(schema);
|
filesystem: Filesystem,
|
||||||
|
url: string,
|
||||||
|
): Promise<OpenAPIV3xDocument> {
|
||||||
|
const result = await dereference(filesystem);
|
||||||
|
|
||||||
if (!derefResult.schema) {
|
if (!result.schema) {
|
||||||
throw new OpenAPIParseError(
|
throw new OpenAPIParseError(
|
||||||
'Failed to dereference OpenAPI document',
|
'Failed to dereference OpenAPI document',
|
||||||
url,
|
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 { noReference } from './utils';
|
||||||
import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters';
|
import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters';
|
||||||
|
|
||||||
@@ -46,9 +46,12 @@ export function generateMediaTypeExample(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mediaType.examples) {
|
if (mediaType.examples) {
|
||||||
const example = mediaType.examples[Object.keys(mediaType.examples)[0]];
|
const key = Object.keys(mediaType.examples)[0];
|
||||||
if (example) {
|
if (key) {
|
||||||
return noReference(example).value;
|
const example = mediaType.examples[key];
|
||||||
|
if (example) {
|
||||||
|
return noReference(example).value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './fetchOpenAPIOperation';
|
export * from './fetchOpenAPIOperation';
|
||||||
export * from './OpenAPIOperation';
|
export * from './OpenAPIOperation';
|
||||||
export * from './OpenAPIOperationContext';
|
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 {
|
export function noReference<T>(input: T | OpenAPIV3.ReferenceObject): T {
|
||||||
if (checkIsReference(input)) {
|
if (checkIsReference(input)) {
|
||||||
@@ -15,3 +15,14 @@ export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceOb
|
|||||||
export function createStateKey(key: string, scope?: string) {
|
export function createStateKey(key: string, scope?: string) {
|
||||||
return scope ? `${scope}_${key}` : key;
|
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": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": false,
|
"allowUnusedLabels": false,
|
||||||
"declaration": true,
|
"allowUnreachableCode": false,
|
||||||
"outDir": "dist",
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"incremental": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "Bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
|
||||||
"types": [
|
"types": [
|
||||||
"bun-types" // add Bun global
|
"bun-types" // add Bun global
|
||||||
]
|
],
|
||||||
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://turbo.build/schema.json",
|
"$schema": "https://turbo.build/schema.json",
|
||||||
"ui": "tui",
|
"ui": "stream",
|
||||||
"globalEnv": ["NODE_ENV", "CI", "ARGOS_*", "GITHUB_*", "GITBOOK_*", "SENTRY_*"],
|
"globalEnv": ["NODE_ENV", "CI", "ARGOS_*", "GITHUB_*", "GITBOOK_*", "SENTRY_*"],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
// Prepare the package for all other tasks
|
// Prepare the package for all other tasks
|
||||||
|
|||||||
Reference in New Issue
Block a user