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