mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-21 16:16:33 +03:00
feat: add basic support openApi 3.1 (#1622)
This commit is contained in:
parent
4b072be8d1
commit
823be24b31
|
@ -5,7 +5,11 @@ import { resolve as urlResolve } from 'url';
|
|||
import { RedocStandalone } from '../src';
|
||||
import ComboBox from './ComboBox';
|
||||
|
||||
const DEFAULT_SPEC = 'openapi.yaml';
|
||||
const NEW_VERSION_SPEC = 'openapi-3-1.yaml';
|
||||
|
||||
const demos = [
|
||||
{ value: NEW_VERSION_SPEC, label: 'Petstore OpenAPI 3.1' },
|
||||
{ value: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml', label: 'Instagram' },
|
||||
{
|
||||
value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/openapi.yaml',
|
||||
|
@ -16,8 +20,6 @@ const demos = [
|
|||
{ value: 'https://docs.graphhopper.com/openapi.json', label: 'GraphHopper' },
|
||||
];
|
||||
|
||||
const DEFAULT_SPEC = 'openapi.yaml';
|
||||
|
||||
class DemoApp extends React.Component<
|
||||
{},
|
||||
{ specUrl: string; dropdownOpen: boolean; cors: boolean }
|
||||
|
@ -45,6 +47,9 @@ class DemoApp extends React.Component<
|
|||
}
|
||||
|
||||
handleChange = (url: string) => {
|
||||
if (url === NEW_VERSION_SPEC) {
|
||||
this.setState({ cors: false })
|
||||
}
|
||||
this.setState({
|
||||
specUrl: url,
|
||||
});
|
||||
|
|
1249
demo/openapi-3-1.yaml
Normal file
1249
demo/openapi-3-1.yaml
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -16,5 +16,6 @@ describe('Standalone bundle test', () => {
|
|||
}
|
||||
|
||||
baseCheck('OAS3 mode', 'e2e/standalone.html');
|
||||
baseCheck('OAS3.1 mode', 'e2e/standalone-3-1.html');
|
||||
baseCheck('OAS2 compatibility mode', 'e2e/standalone-compatibility.html');
|
||||
});
|
||||
|
|
8
e2e/standalone-3-1.html
Normal file
8
e2e/standalone-3-1.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
|
||||
<body>
|
||||
<redoc spec-url="../demo/openapi-3-1.yaml" native-scrollbars></redoc>
|
||||
<script src="../bundles/redoc.standalone.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
24
package-lock.json
generated
24
package-lock.json
generated
|
@ -1222,9 +1222,9 @@
|
|||
}
|
||||
},
|
||||
"@redocly/openapi-core": {
|
||||
"version": "1.0.0-beta.44",
|
||||
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.44.tgz",
|
||||
"integrity": "sha512-9HNnh1MzvMsLK1liuidFBqWiAsZ2Yg3RY58fcEsy0QruSMdDbn7SoeI1qnXe6O+BkBS+vAP4oVzZDMHCMKGsOQ==",
|
||||
"version": "1.0.0-beta.48",
|
||||
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.48.tgz",
|
||||
"integrity": "sha512-rlus9qQC4Pkzz2Ljcv+jQjFdKOYSWnsYXWN6zNik9iiiQtMmGEdszsERCbSAYw/CZ5DRCAHMeKrh8f4LBCpx1A==",
|
||||
"requires": {
|
||||
"@redocly/ajv": "^6.12.3",
|
||||
"@types/node": "^14.11.8",
|
||||
|
@ -1238,9 +1238,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "14.14.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz",
|
||||
"integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw=="
|
||||
"version": "14.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.1.tgz",
|
||||
"integrity": "sha512-/tpUyFD7meeooTRwl3sYlihx2BrJE7q9XF71EguPFIySj9B7qgnRtHsHTho+0AUm4m1SvWGm6uSncrR94q6Vtw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1459,8 +1459,7 @@
|
|||
"@types/json-schema": {
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
||||
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA=="
|
||||
},
|
||||
"@types/json5": {
|
||||
"version": "0.0.29",
|
||||
|
@ -11693,11 +11692,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"openapi-sampler": {
|
||||
"version": "1.0.0-beta.18",
|
||||
"resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.0-beta.18.tgz",
|
||||
"integrity": "sha512-nG/0kvvSY5FbrU5A+Dbp1xTQN++7pKIh87/atryZlxrzDuok5Y6TCbpxO1jYqpUKLycE4ReKGHCywezngG6xtQ==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.0.tgz",
|
||||
"integrity": "sha512-HysKj4ZuLk0RpZkopao5SIupUX8LMOTsEDTw9dSzcRv6BBW6Ep1IdbKwYsCrYM9tnw4VZtebR/N5sJHY6qqRew==",
|
||||
"requires": {
|
||||
"json-pointer": "^0.6.0"
|
||||
"@types/json-schema": "^7.0.7",
|
||||
"json-pointer": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"opn": {
|
||||
|
|
|
@ -134,7 +134,7 @@
|
|||
"styled-components": "^4.1.1 || ^5.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@redocly/openapi-core": "^1.0.0-beta.44",
|
||||
"@redocly/openapi-core": "^1.0.0-beta.48",
|
||||
"@redocly/react-dropdown-aria": "^2.0.11",
|
||||
"@types/node": "^13.11.1",
|
||||
"classnames": "^2.2.6",
|
||||
|
@ -147,7 +147,7 @@
|
|||
"marked": "^0.7.0",
|
||||
"memoize-one": "~5.1.1",
|
||||
"mobx-react": "^7.0.5",
|
||||
"openapi-sampler": "^1.0.0-beta.18",
|
||||
"openapi-sampler": "^1.0.0",
|
||||
"perfect-scrollbar": "^1.4.0",
|
||||
"polished": "^3.6.5",
|
||||
"prismjs": "^1.22.0",
|
||||
|
|
|
@ -61,11 +61,6 @@ export const RecursiveLabel = styled(FieldLabel)`
|
|||
font-size: 13px;
|
||||
`;
|
||||
|
||||
export const NullableLabel = styled(FieldLabel)`
|
||||
color: #0e7c86;
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
export const PatternLabel = styled(FieldLabel)`
|
||||
color: #0e7c86;
|
||||
&::before,
|
||||
|
|
|
@ -38,7 +38,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
|
|||
const license =
|
||||
(info.license && (
|
||||
<InfoSpan>
|
||||
License: <a href={info.license.url}>{info.license.name}</a>
|
||||
License: {info.license.identifier ? info.license.identifier : (<a href={info.license.url}>{info.license.name}</a>)}
|
||||
</InfoSpan>
|
||||
)) ||
|
||||
null;
|
||||
|
@ -100,7 +100,8 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
|
|||
)) ||
|
||||
null}
|
||||
</StyledMarkdownBlock>
|
||||
<Markdown source={store.spec.info.description} data-role="redoc-description" />
|
||||
<Markdown source={store.spec.info.summary} data-role="redoc-summary"/>
|
||||
<Markdown source={store.spec.info.description} data-role="redoc-description"/>
|
||||
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
|
||||
</MiddlePanel>
|
||||
</Row>
|
||||
|
|
|
@ -8,7 +8,7 @@ import { RedocRawOptions } from '../../services/RedocNormalizedOptions';
|
|||
|
||||
export interface EnumValuesProps {
|
||||
values: string[];
|
||||
type: string;
|
||||
type: string | string[];
|
||||
}
|
||||
|
||||
export interface EnumValuesState {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
NullableLabel,
|
||||
PatternLabel,
|
||||
RecursiveLabel,
|
||||
TypeFormat,
|
||||
|
@ -79,7 +78,6 @@ export class FieldDetails extends React.PureComponent<FieldProps, { patternShown
|
|||
)}
|
||||
{schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>}
|
||||
<ConstraintsView constraints={schema.constraints} />
|
||||
{schema.nullable && <NullableLabel> {l('nullable')} </NullableLabel>}
|
||||
{schema.pattern && !hideSchemaPattern && (
|
||||
<>
|
||||
<PatternLabel>
|
||||
|
@ -112,6 +110,7 @@ export class FieldDetails extends React.PureComponent<FieldProps, { patternShown
|
|||
<ExternalDocumentation externalDocs={schema.externalDocs} compact={true} />
|
||||
)}
|
||||
{(renderDiscriminatorSwitch && renderDiscriminatorSwitch(this.props)) || null}
|
||||
{field.const && (<FieldDetail label={l('const') + ':'} value={field.const}/>) || null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ export class Redoc extends React.Component<RedocProps> {
|
|||
const store = this.props.store;
|
||||
return (
|
||||
<ThemeProvider theme={options.theme}>
|
||||
<StoreProvider value={this.props.store}>
|
||||
<StoreProvider value={store}>
|
||||
<OptionsProvider value={options}>
|
||||
<RedocWrap className="redoc-wrap">
|
||||
<StickyResponsiveSidebar menu={menu} className="menu-content">
|
||||
|
|
|
@ -63,6 +63,10 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
|
|||
return <OneOfSchema schema={schema} {...this.props} />;
|
||||
}
|
||||
|
||||
if (type && Array.isArray(type)) {
|
||||
return <ArraySchema {...(this.props as any)} />;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'object':
|
||||
if (schema.fields?.length) {
|
||||
|
|
|
@ -6,6 +6,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
<Field
|
||||
field={
|
||||
FieldModel {
|
||||
"const": "",
|
||||
"deprecated": false,
|
||||
"description": "",
|
||||
"example": undefined,
|
||||
|
@ -17,6 +18,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
"required": false,
|
||||
"schema": SchemaModel {
|
||||
"activeOneOf": 0,
|
||||
"const": "",
|
||||
"constraints": Array [],
|
||||
"default": undefined,
|
||||
"deprecated": false,
|
||||
|
@ -29,7 +31,6 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
"format": undefined,
|
||||
"isCircular": undefined,
|
||||
"isPrimitive": true,
|
||||
"nullable": false,
|
||||
"options": "<<<filtered>>>",
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Dog/properties/packSize",
|
||||
|
@ -56,6 +57,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
<Field
|
||||
field={
|
||||
FieldModel {
|
||||
"const": "",
|
||||
"deprecated": false,
|
||||
"description": "",
|
||||
"example": undefined,
|
||||
|
@ -67,6 +69,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
"required": true,
|
||||
"schema": SchemaModel {
|
||||
"activeOneOf": 0,
|
||||
"const": "",
|
||||
"constraints": Array [],
|
||||
"default": undefined,
|
||||
"deprecated": false,
|
||||
|
@ -79,7 +82,6 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
"format": undefined,
|
||||
"isCircular": undefined,
|
||||
"isPrimitive": true,
|
||||
"nullable": false,
|
||||
"options": "<<<filtered>>>",
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Dog/properties/type",
|
||||
|
|
|
@ -145,7 +145,10 @@ export class AppStore {
|
|||
|
||||
if (idx === -1 && IS_BROWSER) {
|
||||
const $description = document.querySelector('[data-role="redoc-description"]');
|
||||
const $summary = document.querySelector('[data-role="redoc-summary"]');
|
||||
|
||||
if ($description) elements.push($description);
|
||||
if ($summary) elements.push($summary);
|
||||
}
|
||||
|
||||
this.marker.addOnly(elements);
|
||||
|
|
|
@ -6,10 +6,10 @@ export interface LabelsConfig {
|
|||
deprecated: string;
|
||||
example: string;
|
||||
examples: string;
|
||||
nullable: string;
|
||||
recursive: string;
|
||||
arrayOf: string;
|
||||
webhook: string;
|
||||
const: string;
|
||||
}
|
||||
|
||||
export type LabelsConfigRaw = Partial<LabelsConfig>;
|
||||
|
@ -22,10 +22,10 @@ const labels: LabelsConfig = {
|
|||
deprecated: 'Deprecated',
|
||||
example: 'Example',
|
||||
examples: 'Examples',
|
||||
nullable: 'Nullable',
|
||||
recursive: 'Recursive',
|
||||
arrayOf: 'Array of ',
|
||||
webhook: 'Event',
|
||||
const: 'Value',
|
||||
};
|
||||
|
||||
export function setRedocLabels(_labels?: LabelsConfigRaw) {
|
||||
|
|
|
@ -53,7 +53,7 @@ export class MenuBuilder {
|
|||
const spec = parser.spec;
|
||||
|
||||
const items: ContentItemModel[] = [];
|
||||
const tagsMap = MenuBuilder.getTagsWithOperations(spec);
|
||||
const tagsMap = MenuBuilder.getTagsWithOperations(parser, spec);
|
||||
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options));
|
||||
if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
|
||||
items.push(
|
||||
|
@ -215,24 +215,33 @@ export class MenuBuilder {
|
|||
/**
|
||||
* collects tags and maps each tag to list of operations belonging to this tag
|
||||
*/
|
||||
static getTagsWithOperations(spec: OpenAPISpec): TagsInfoMap {
|
||||
static getTagsWithOperations(parser: OpenAPIParser, spec: OpenAPISpec): TagsInfoMap {
|
||||
const tags: TagsInfoMap = {};
|
||||
const webhooks = spec['x-webhooks'] || spec.webhooks;
|
||||
for (const tag of spec.tags || []) {
|
||||
tags[tag.name] = { ...tag, operations: [] };
|
||||
}
|
||||
|
||||
getTags(spec.paths);
|
||||
if (spec['x-webhooks']) {
|
||||
getTags(spec['x-webhooks'], true);
|
||||
if (webhooks) {
|
||||
getTags(parser, webhooks, true);
|
||||
}
|
||||
|
||||
function getTags(paths: OpenAPIPaths, isWebhook?: boolean) {
|
||||
if (spec.paths){
|
||||
getTags(parser, spec.paths);
|
||||
}
|
||||
|
||||
function getTags(parser: OpenAPIParser, paths: OpenAPIPaths, isWebhook?: boolean) {
|
||||
for (const pathName of Object.keys(paths)) {
|
||||
const path = paths[pathName];
|
||||
const operations = Object.keys(path).filter(isOperationName);
|
||||
for (const operationName of operations) {
|
||||
const operationInfo = path[operationName];
|
||||
let operationTags = operationInfo.tags;
|
||||
if (path.$ref) {
|
||||
const resolvedPaths = parser.deref<OpenAPIPaths>(path as OpenAPIPaths);
|
||||
getTags(parser, { [pathName]: resolvedPaths }, isWebhook);
|
||||
continue;
|
||||
}
|
||||
let operationTags = operationInfo?.tags;
|
||||
|
||||
if (!operationTags || !operationTags.length) {
|
||||
// empty tag
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { OpenAPIExternalDocumentation, OpenAPISpec } from '../types';
|
||||
import { OpenAPIExternalDocumentation, OpenAPIPath, OpenAPISpec, Referenced } from '../types';
|
||||
|
||||
import { ContentItemModel, MenuBuilder } from './MenuBuilder';
|
||||
import { ApiInfoModel } from './models/ApiInfo';
|
||||
|
@ -28,6 +28,7 @@ export class SpecStore {
|
|||
this.externalDocs = this.parser.spec.externalDocs;
|
||||
this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
|
||||
this.securitySchemes = new SecuritySchemesModel(this.parser);
|
||||
this.webhooks = new WebhookModel(this.parser, options, this.parser.spec['x-webhooks']);
|
||||
const webhookPath: Referenced<OpenAPIPath> = {...this.parser?.spec?.['x-webhooks'], ...this.parser?.spec.webhooks};
|
||||
this.webhooks = new WebhookModel(this.parser, options, webhookPath);
|
||||
}
|
||||
}
|
||||
|
|
66
src/services/__tests__/fixtures/3.1/pathItems.json
Normal file
66
src/services/__tests__/fixtures/3.1/pathItems.json
Normal file
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "Swagger Petstore"
|
||||
},
|
||||
"webhooks": {
|
||||
"myWebhook": {
|
||||
"$ref": "#/components/pathItems/catsWebhook",
|
||||
"description": "Overriding description",
|
||||
"summary": "Overriding summary"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"pathItems": {
|
||||
"catsWebhook": {
|
||||
"put": {
|
||||
"summary": "Get a cat details after update",
|
||||
"description": "Get a cat details after update",
|
||||
"operationId": "updatedCat",
|
||||
"tags": [
|
||||
"pet"
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Information about cat in the system",
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "update Cat details"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Create new cat",
|
||||
"description": "Info about new cat",
|
||||
"operationId": "createdCat",
|
||||
"tags": [
|
||||
"pet"
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Information about cat in the system",
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "create Cat details"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -34,5 +34,33 @@ describe('Models', () => {
|
|||
const info = new ApiInfoModel(parser);
|
||||
expect(info.description).toEqual('Test description\nsome text\n');
|
||||
});
|
||||
|
||||
test('should correctly populate summary up to the first md heading', () => {
|
||||
parser.spec = {
|
||||
openapi: '3.1.0',
|
||||
info: {
|
||||
summary: 'Test summary\nsome text\n## Heading\n test',
|
||||
},
|
||||
} as any;
|
||||
|
||||
const info = new ApiInfoModel(parser);
|
||||
expect(info.summary).toEqual('Test summary\nsome text\n## Heading\n test');
|
||||
});
|
||||
|
||||
test('should correctly populate license identifier', () => {
|
||||
parser.spec = {
|
||||
openapi: '3.1.0',
|
||||
info: {
|
||||
license: {
|
||||
name: 'MIT',
|
||||
identifier: 'MIT',
|
||||
url: 'https://opensource.org/licenses/MIT'
|
||||
}
|
||||
},
|
||||
} as any;
|
||||
|
||||
const { license = { identifier: null } } = new ApiInfoModel(parser);
|
||||
expect(license.identifier).toEqual('MIT');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
25
src/services/__tests__/models/MenuBuilder.test.ts
Normal file
25
src/services/__tests__/models/MenuBuilder.test.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { MenuBuilder } from '../../MenuBuilder';
|
||||
import { OpenAPIParser } from '../../OpenAPIParser';
|
||||
|
||||
import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
|
||||
|
||||
const opts = new RedocNormalizedOptions({});
|
||||
|
||||
describe('Models', () => {
|
||||
describe('MenuBuilder', () => {
|
||||
let parser;
|
||||
|
||||
test('should resolve pathItems', () => {
|
||||
const spec = require('../fixtures/3.1/pathItems.json');
|
||||
parser = new OpenAPIParser(spec, undefined, opts);
|
||||
const contentItems = MenuBuilder.buildStructure(parser, opts);
|
||||
expect(contentItems).toHaveLength(1);
|
||||
expect(contentItems[0].items).toHaveLength(2);
|
||||
expect(contentItems[0].id).toEqual('tag/pet');
|
||||
expect(contentItems[0].name).toEqual('pet');
|
||||
expect(contentItems[0].type).toEqual('tag');
|
||||
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,6 +7,7 @@ export class ApiInfoModel implements OpenAPIInfo {
|
|||
version: string;
|
||||
|
||||
description: string;
|
||||
summary: string;
|
||||
termsOfService?: string;
|
||||
contact?: OpenAPIContact;
|
||||
license?: OpenAPILicense;
|
||||
|
@ -17,6 +18,7 @@ export class ApiInfoModel implements OpenAPIInfo {
|
|||
constructor(private parser: OpenAPIParser) {
|
||||
Object.assign(this, parser.spec.info);
|
||||
this.description = parser.spec.info.description || '';
|
||||
this.summary = parser.spec.info.summary || '';
|
||||
|
||||
const firstHeadingLinePos = this.description.search(/^##?\s+/m);
|
||||
if (firstHeadingLinePos > -1) {
|
||||
|
|
|
@ -55,6 +55,7 @@ export class FieldModel {
|
|||
extensions?: Record<string, any>;
|
||||
explode: boolean;
|
||||
style?: OpenAPIParameterStyle;
|
||||
const?: any;
|
||||
|
||||
serializationMime?: string;
|
||||
|
||||
|
@ -111,6 +112,8 @@ export class FieldModel {
|
|||
if (options.showExtensions) {
|
||||
this.extensions = extractExtensions(info, options.showExtensions);
|
||||
}
|
||||
|
||||
this.const = this.schema?.const || info?.const || '';
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
|
@ -25,7 +25,7 @@ import { l } from '../Labels';
|
|||
export class SchemaModel {
|
||||
pointer: string;
|
||||
|
||||
type: string;
|
||||
type: string | string[];
|
||||
displayType: string;
|
||||
typePrefix: string = '';
|
||||
title: string;
|
||||
|
@ -60,6 +60,7 @@ export class SchemaModel {
|
|||
rawSchema: OpenAPISchema;
|
||||
schema: MergedOpenAPISchema;
|
||||
extensions?: Record<string, any>;
|
||||
const: any;
|
||||
|
||||
/**
|
||||
* @param isChild if schema discriminator Child
|
||||
|
@ -106,7 +107,6 @@ export class SchemaModel {
|
|||
this.description = schema.description || '';
|
||||
this.type = schema.type || detectType(schema);
|
||||
this.format = schema.format;
|
||||
this.nullable = !!schema.nullable;
|
||||
this.enum = schema.enum || [];
|
||||
this.example = schema.example;
|
||||
this.deprecated = !!schema.deprecated;
|
||||
|
@ -114,12 +114,22 @@ export class SchemaModel {
|
|||
this.externalDocs = schema.externalDocs;
|
||||
|
||||
this.constraints = humanizeConstraints(schema);
|
||||
this.displayType = this.type;
|
||||
this.displayFormat = this.format;
|
||||
this.isPrimitive = isPrimitiveType(schema, this.type);
|
||||
this.default = schema.default;
|
||||
this.readOnly = !!schema.readOnly;
|
||||
this.writeOnly = !!schema.writeOnly;
|
||||
this.const = schema.const || '';
|
||||
|
||||
if (!!schema.nullable) {
|
||||
if (Array.isArray(this.type) && !this.type.includes('null')) {
|
||||
this.type = [...this.type, 'null'];
|
||||
}
|
||||
}
|
||||
|
||||
this.displayType = Array.isArray(this.type)
|
||||
? this.type.map(item => item === null ? 'null' : item).join(' or ')
|
||||
: this.type;
|
||||
|
||||
if (this.isCircular) {
|
||||
return;
|
||||
|
@ -156,7 +166,7 @@ export class SchemaModel {
|
|||
|
||||
if (this.type === 'object') {
|
||||
this.fields = buildFields(parser, schema, this.pointer, this.options);
|
||||
} else if (this.type === 'array' && schema.items) {
|
||||
} else if ((this.type === 'array' || Array.isArray(this.type)) && schema.items) {
|
||||
this.items = new SchemaModel(parser, schema.items, this.pointer + '/items', this.options);
|
||||
this.displayType = pluralizeType(this.items.displayType);
|
||||
this.displayFormat = this.items.format;
|
||||
|
@ -169,6 +179,11 @@ export class SchemaModel {
|
|||
if (this.items.isPrimitive) {
|
||||
this.enum = this.items.enum;
|
||||
}
|
||||
if (Array.isArray(this.type)) {
|
||||
const filteredType = this.type.filter(item => item !== 'array');
|
||||
if (filteredType.length)
|
||||
this.displayType += ` or ${filteredType.join(' or ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.enum.length && this.options.sortEnumValuesAlphabetically) {
|
||||
|
@ -186,7 +201,7 @@ export class SchemaModel {
|
|||
const title =
|
||||
isNamedDefinition(variant.$ref) && !merged.title
|
||||
? JsonPointer.baseName(variant.$ref)
|
||||
: merged.title;
|
||||
: `${(merged.title || '')}${(merged.const && JSON.stringify(merged.const)) || ''}`;
|
||||
|
||||
const schema = new SchemaModel(
|
||||
parser,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { OpenAPIPath, Referenced } from '../../types';
|
||||
import { OpenAPIParser } from '../OpenAPIParser';
|
||||
import { OperationModel } from './Operation';
|
||||
import { isOperationName } from '../..';
|
||||
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||
import { isOperationName } from '../..';
|
||||
|
||||
export class WebhookModel {
|
||||
operations: OperationModel[] = [];
|
||||
|
@ -14,12 +14,21 @@ export class WebhookModel {
|
|||
) {
|
||||
const webhooks = parser.deref<OpenAPIPath>(infoOrRef || {});
|
||||
parser.exitRef(infoOrRef);
|
||||
this.initWebhooks(parser, webhooks, options);
|
||||
}
|
||||
|
||||
initWebhooks(parser: OpenAPIParser, webhooks: OpenAPIPath, options: RedocNormalizedOptions) {
|
||||
for (const webhookName of Object.keys(webhooks)) {
|
||||
const webhook = webhooks[webhookName];
|
||||
const operations = Object.keys(webhook).filter(isOperationName);
|
||||
for (const operationName of operations) {
|
||||
const operationInfo = webhook[operationName];
|
||||
if (webhook.$ref) {
|
||||
const resolvedWebhook = parser.deref<OpenAPIPath>(webhook || {});
|
||||
this.initWebhooks(parser, { [operationName]: resolvedWebhook }, options);
|
||||
}
|
||||
|
||||
if (!operationInfo) continue;
|
||||
const operation = new OperationModel(
|
||||
parser,
|
||||
{
|
||||
|
|
12
src/types/open-api.d.ts
vendored
12
src/types/open-api.d.ts
vendored
|
@ -10,6 +10,7 @@ export interface OpenAPISpec {
|
|||
tags?: OpenAPITag[];
|
||||
externalDocs?: OpenAPIExternalDocumentation;
|
||||
'x-webhooks'?: OpenAPIPaths;
|
||||
webhooks?: OpenAPIPaths;
|
||||
}
|
||||
|
||||
export interface OpenAPIInfo {
|
||||
|
@ -17,6 +18,7 @@ export interface OpenAPIInfo {
|
|||
version: string;
|
||||
|
||||
description?: string;
|
||||
summary?: string;
|
||||
termsOfService?: string;
|
||||
contact?: OpenAPIContact;
|
||||
license?: OpenAPILicense;
|
||||
|
@ -56,6 +58,7 @@ export interface OpenAPIPath {
|
|||
trace?: OpenAPIOperation;
|
||||
servers?: OpenAPIServer[];
|
||||
parameters?: Array<Referenced<OpenAPIParameter>>;
|
||||
$ref?: string;
|
||||
}
|
||||
|
||||
export interface OpenAPIXCodeSample {
|
||||
|
@ -96,6 +99,7 @@ export interface OpenAPIParameter {
|
|||
examples?: { [media: string]: Referenced<OpenAPIExample> };
|
||||
content?: { [media: string]: OpenAPIMediaType };
|
||||
encoding?: Record<string, OpenAPIEncoding>;
|
||||
const?: any;
|
||||
}
|
||||
|
||||
export interface OpenAPIExample {
|
||||
|
@ -107,7 +111,7 @@ export interface OpenAPIExample {
|
|||
|
||||
export interface OpenAPISchema {
|
||||
$ref?: string;
|
||||
type?: string;
|
||||
type?: string | string[];
|
||||
properties?: { [name: string]: OpenAPISchema };
|
||||
additionalProperties?: boolean | OpenAPISchema;
|
||||
description?: string;
|
||||
|
@ -129,9 +133,9 @@ export interface OpenAPISchema {
|
|||
title?: string;
|
||||
multipleOf?: number;
|
||||
maximum?: number;
|
||||
exclusiveMaximum?: boolean;
|
||||
exclusiveMaximum?: boolean | number;
|
||||
minimum?: number;
|
||||
exclusiveMinimum?: boolean;
|
||||
exclusiveMinimum?: boolean | number;
|
||||
maxLength?: number;
|
||||
minLength?: number;
|
||||
pattern?: string;
|
||||
|
@ -142,6 +146,7 @@ export interface OpenAPISchema {
|
|||
minProperties?: number;
|
||||
enum?: any[];
|
||||
example?: any;
|
||||
const?: string;
|
||||
}
|
||||
|
||||
export interface OpenAPIDiscriminator {
|
||||
|
@ -271,4 +276,5 @@ export interface OpenAPIContact {
|
|||
export interface OpenAPILicense {
|
||||
name: string;
|
||||
url?: string;
|
||||
identifier?: string;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -10,6 +10,12 @@ describe('#loadAndBundleSpec', () => {
|
|||
expect(bundledSpec).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should load And Bundle Spec demo/openapi-3-1.yaml', async () => {
|
||||
const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/openapi-3-1.yaml')));
|
||||
const bundledSpec = await loadAndBundleSpec(spec);
|
||||
expect(bundledSpec).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should load And Bundle Spec demo/swagger.yaml', async () => {
|
||||
const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/swagger.yaml')));
|
||||
const bundledSpec = await loadAndBundleSpec(spec);
|
||||
|
|
|
@ -174,6 +174,79 @@ describe('Utils', () => {
|
|||
expect(isPrimitiveType(schema)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true for array contains object and schema hasn\'t properties', () => {
|
||||
const schema = {
|
||||
type: ['object', 'string'],
|
||||
};
|
||||
expect(isPrimitiveType(schema)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false for array contains object and schema has properties', () => {
|
||||
const schema = {
|
||||
type: ['object', 'string'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(isPrimitiveType(schema)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false for array contains array type and schema has items', () => {
|
||||
const schema = {
|
||||
type: ['array'],
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
},
|
||||
};
|
||||
expect(isPrimitiveType(schema)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false for array contains object and array types and schema has items', () => {
|
||||
const schema = {
|
||||
type: ['array', 'object'],
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
},
|
||||
};
|
||||
expect(isPrimitiveType(schema)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false for array contains object and array types and schema has properties', () => {
|
||||
const schema = {
|
||||
type: ['array', 'object'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(isPrimitiveType(schema)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true for array contains array of strings', () => {
|
||||
const schema = {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(isPrimitiveType(schema)).toEqual(true);
|
||||
});
|
||||
|
||||
it('Should return false for array of string which include the null value', () => {
|
||||
const schema = {
|
||||
type: ['object', 'string', 'null'],
|
||||
};
|
||||
expect(isPrimitiveType(schema)).toEqual(true);
|
||||
});
|
||||
|
||||
it('Should return false for array with non-empty objects', () => {
|
||||
const schema = {
|
||||
type: 'array',
|
||||
|
|
|
@ -56,6 +56,7 @@ const operationNames = {
|
|||
patch: true,
|
||||
delete: true,
|
||||
options: true,
|
||||
$ref: true,
|
||||
};
|
||||
|
||||
export function isOperationName(key: string): boolean {
|
||||
|
@ -68,7 +69,7 @@ export function getOperationSummary(operation: ExtendedOpenAPIOperation): string
|
|||
operation.operationId ||
|
||||
(operation.description && operation.description.substring(0, 50)) ||
|
||||
operation.pathName ||
|
||||
'<no summary>'
|
||||
'<no summary>'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -96,7 +97,7 @@ const schemaKeywordTypes = {
|
|||
};
|
||||
|
||||
export function detectType(schema: OpenAPISchema): string {
|
||||
if (schema.type !== undefined) {
|
||||
if (schema.type !== undefined && !Array.isArray(schema.type)) {
|
||||
return schema.type;
|
||||
}
|
||||
const keywords = Object.keys(schemaKeywordTypes);
|
||||
|
@ -110,25 +111,25 @@ export function detectType(schema: OpenAPISchema): string {
|
|||
return 'any';
|
||||
}
|
||||
|
||||
export function isPrimitiveType(schema: OpenAPISchema, type: string | undefined = schema.type) {
|
||||
export function isPrimitiveType(schema: OpenAPISchema, type: string | string[] | undefined = schema.type) {
|
||||
if (schema.oneOf !== undefined || schema.anyOf !== undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type === 'object') {
|
||||
return schema.properties !== undefined
|
||||
let isPrimitive = true;
|
||||
const isArray = Array.isArray(type);
|
||||
|
||||
if (type === 'object' || (isArray && type?.includes('object'))) {
|
||||
isPrimitive = schema.properties !== undefined
|
||||
? Object.keys(schema.properties).length === 0
|
||||
: schema.additionalProperties === undefined;
|
||||
}
|
||||
|
||||
if (type === 'array') {
|
||||
if (schema.items === undefined) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (schema.items !== undefined && (type === 'array' || (isArray && type?.includes('array')))) {
|
||||
isPrimitive = isPrimitiveType(schema.items, schema.items.type);
|
||||
}
|
||||
|
||||
return true;
|
||||
return isPrimitive;
|
||||
}
|
||||
|
||||
export function isJsonLike(contentType: string): boolean {
|
||||
|
@ -367,12 +368,12 @@ export function langFromMime(contentType: string): string {
|
|||
}
|
||||
|
||||
export function isNamedDefinition(pointer?: string): boolean {
|
||||
return /^#\/components\/schemas\/[^\/]+$/.test(pointer || '');
|
||||
return /^#\/components\/(schemas|pathItems)\/[^\/]+$/.test(pointer || '');
|
||||
}
|
||||
|
||||
export function getDefinitionName(pointer?: string): string | undefined {
|
||||
if (!pointer) return undefined;
|
||||
const match = pointer.match(/^#\/components\/schemas\/([^\/]+)$/);
|
||||
const match = pointer.match(/^#\/components\/(schemas|pathItems)\/([^\/]+)$/);
|
||||
return match === null ? undefined : match[1]
|
||||
}
|
||||
|
||||
|
@ -445,6 +446,18 @@ export function humanizeConstraints(schema: OpenAPISchema): string[] {
|
|||
numberRange += schema.minimum;
|
||||
}
|
||||
|
||||
if (typeof schema.exclusiveMinimum === 'number' || typeof schema.exclusiveMaximum === 'number') {
|
||||
let minimum = 0;
|
||||
let maximum = 0;
|
||||
if (schema.minimum) minimum = schema.minimum;
|
||||
if (typeof schema.exclusiveMinimum === 'number') minimum = minimum <= schema.exclusiveMinimum ? minimum : schema.exclusiveMinimum;
|
||||
|
||||
if (schema.maximum) maximum = schema.maximum;
|
||||
if (typeof schema.exclusiveMaximum === 'number') maximum = maximum > schema.exclusiveMaximum ? maximum : schema.exclusiveMaximum;
|
||||
|
||||
numberRange = `[${minimum} .. ${maximum}]`
|
||||
}
|
||||
|
||||
if (numberRange !== undefined) {
|
||||
res.push(numberRange);
|
||||
}
|
||||
|
@ -574,10 +587,10 @@ export function setSecuritySchemePrefix(prefix: string) {
|
|||
}
|
||||
|
||||
export const shortenHTTPVerb = verb =>
|
||||
({
|
||||
delete: 'del',
|
||||
options: 'opts',
|
||||
}[verb] || verb);
|
||||
({
|
||||
delete: 'del',
|
||||
options: 'opts',
|
||||
}[verb] || verb);
|
||||
|
||||
export function isRedocExtension(key: string): boolean {
|
||||
const redocExtensions = {
|
||||
|
|
Loading…
Reference in New Issue
Block a user