fix: rewrite recursive checks (#2072)

Co-authored-by: Roman Hotsiy <gotsijroman@gmail.com>
This commit is contained in:
Alex Varchuk 2022-07-18 17:47:06 +03:00 committed by GitHub
parent 9920991080
commit 2970f959cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1305 additions and 505 deletions

13
package-lock.json generated
View File

@ -81,6 +81,7 @@
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"lodash.noop": "^3.0.1", "lodash.noop": "^3.0.1",
"mobx": "^6.3.2", "mobx": "^6.3.2",
"outdent": "^0.8.0",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"pretty-quick": "^3.0.0", "pretty-quick": "^3.0.0",
"raf": "^3.4.1", "raf": "^3.4.1",
@ -14282,6 +14283,12 @@
"integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=", "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=",
"dev": true "dev": true
}, },
"node_modules/outdent": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz",
"integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==",
"dev": true
},
"node_modules/p-each-series": { "node_modules/p-each-series": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz",
@ -29936,6 +29943,12 @@
"integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=", "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=",
"dev": true "dev": true
}, },
"outdent": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz",
"integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==",
"dev": true
},
"p-each-series": { "p-each-series": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz",

View File

@ -110,6 +110,7 @@
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"lodash.noop": "^3.0.1", "lodash.noop": "^3.0.1",
"mobx": "^6.3.2", "mobx": "^6.3.2",
"outdent": "^0.8.0",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"pretty-quick": "^3.0.0", "pretty-quick": "^3.0.0",
"raf": "^3.4.1", "raf": "^3.4.1",
@ -193,10 +194,12 @@
"coveragePathIgnorePatterns": [ "coveragePathIgnorePatterns": [
"\\.d\\.ts$", "\\.d\\.ts$",
"/benchmark/", "/benchmark/",
"/node_modules/" "/node_modules/",
"src/services/__tests__/models/helpers.ts"
], ],
"modulePathIgnorePatterns": [ "modulePathIgnorePatterns": [
"/benchmark/" "/benchmark/",
"src/services/__tests__/models/helpers.ts"
], ],
"snapshotSerializers": [ "snapshotSerializers": [
"enzyme-to-json/serializer" "enzyme-to-json/serializer"

View File

@ -4,8 +4,8 @@ import * as React from 'react';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown'; import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown';
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
import { ContentItemModel } from '../../services/MenuBuilder'; import type { ContentItemModel } from '../../services';
import { GroupModel, OperationModel } from '../../services/models'; import type { GroupModel, OperationModel } from '../../services/models';
import { Operation } from '../Operation/Operation'; import { Operation } from '../Operation/Operation';
@observer @observer

View File

@ -0,0 +1,16 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { RecursiveLabel, TypeName, TypeTitle } from '../../common-elements/fields';
import { l } from '../../services/Labels';
import type { SchemaProps } from '.';
export const RecursiveSchema = observer(({ schema }: SchemaProps) => {
return (
<div>
<TypeName>{schema.displayType}</TypeName>
{schema.title && <TypeTitle> {schema.title} </TypeTitle>}
<RecursiveLabel> {l('recursive')} </RecursiveLabel>
</div>
);
});

View File

@ -1,7 +1,6 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { RecursiveLabel, TypeName, TypeTitle } from '../../common-elements/fields';
import { FieldDetails } from '../Fields/FieldDetails'; import { FieldDetails } from '../Fields/FieldDetails';
import { FieldModel, SchemaModel } from '../../services/models'; import { FieldModel, SchemaModel } from '../../services/models';
@ -9,8 +8,8 @@ import { FieldModel, SchemaModel } from '../../services/models';
import { ArraySchema } from './ArraySchema'; import { ArraySchema } from './ArraySchema';
import { ObjectSchema } from './ObjectSchema'; import { ObjectSchema } from './ObjectSchema';
import { OneOfSchema } from './OneOfSchema'; import { OneOfSchema } from './OneOfSchema';
import { RecursiveSchema } from './RecursiveSchema';
import { l } from '../../services/Labels';
import { isArray } from '../../utils/helpers'; import { isArray } from '../../utils/helpers';
export interface SchemaOptions { export interface SchemaOptions {
@ -36,13 +35,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
const { type, oneOf, discriminatorProp, isCircular } = schema; const { type, oneOf, discriminatorProp, isCircular } = schema;
if (isCircular) { if (isCircular) {
return ( return <RecursiveSchema schema={schema} />;
<div>
<TypeName>{schema.displayType}</TypeName>
{schema.title && <TypeTitle> {schema.title} </TypeTitle>}
<RecursiveLabel> {l('recursive')} </RecursiveLabel>
</div>
);
} }
if (discriminatorProp !== undefined) { if (discriminatorProp !== undefined) {
@ -52,11 +45,14 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
); );
return null; return null;
} }
return ( const activeSchema = oneOf[schema.activeOneOf];
return activeSchema.isCircular ? (
<RecursiveSchema schema={activeSchema} />
) : (
<ObjectSchema <ObjectSchema
{...rest} {...rest}
level={level} level={level}
schema={oneOf![schema.activeOneOf]} schema={activeSchema}
discriminator={{ discriminator={{
fieldName: discriminatorProp, fieldName: discriminatorProp,
parentSchema: schema, parentSchema: schema,

View File

@ -1,12 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { IMenuItem } from '../../services/MenuStore'; import type { IMenuItem, SearchResult } from '../../services/types';
import { SearchStore } from '../../services/SearchStore'; import type { SearchStore } from '../../services/SearchStore';
import type { MarkerService } from '../../services/MarkerService';
import { MenuItem } from '../SideMenu/MenuItem'; import { MenuItem } from '../SideMenu/MenuItem';
import { MarkerService } from '../../services/MarkerService';
import { SearchResult } from '../../services/SearchWorker.worker';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
import { bind, debounce } from 'decko'; import { bind, debounce } from 'decko';
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar'; import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';

View File

@ -2,13 +2,14 @@ import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { ShelfIcon } from '../../common-elements/shelfs'; import { ShelfIcon } from '../../common-elements/shelfs';
import { IMenuItem, OperationModel } from '../../services'; import { OperationModel } from '../../services';
import { shortenHTTPVerb } from '../../utils/openapi'; import { shortenHTTPVerb } from '../../utils/openapi';
import { MenuItems } from './MenuItems'; import { MenuItems } from './MenuItems';
import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements'; import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { scrollIntoViewIfNeeded } from '../../utils'; import { scrollIntoViewIfNeeded } from '../../utils';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
import type { IMenuItem } from '../../services';
export interface MenuItemProps { export interface MenuItemProps {
item: IMenuItem; item: IMenuItem;

View File

@ -1,7 +1,7 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { IMenuItem } from '../../services'; import type { IMenuItem } from '../../services';
import { MenuItem } from './MenuItem'; import { MenuItem } from './MenuItem';
import { MenuItemUl } from './styled.elements'; import { MenuItemUl } from './styled.elements';

View File

@ -1,7 +1,8 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { IMenuItem, MenuStore } from '../../services/MenuStore'; import { MenuStore } from '../../services';
import type { IMenuItem } from '../../services';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
import { MenuItems } from './MenuItems'; import { MenuItems } from './MenuItems';

View File

@ -22,7 +22,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined, "examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": false, "isPrimitive": false,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -70,7 +70,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined, "examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": true, "isPrimitive": true,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -300,6 +300,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "number", "type": "number",
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Dog/properties/packSize",
],
"schema": Object { "schema": Object {
"default": undefined, "default": undefined,
"type": "number", "type": "number",
@ -337,7 +341,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined, "examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": true, "isPrimitive": true,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -565,11 +569,27 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"rawSchema": Object { "rawSchema": Object {
"default": undefined, "default": undefined,
"type": "string", "type": "string",
"x-refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Pet",
],
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Dog",
"#/components/schemas/Pet",
"#/components/schemas/Dog",
"#/components/schemas/Pet",
"#/components/schemas/Dog/properties/type",
],
"schema": Object { "schema": Object {
"default": undefined, "default": undefined,
"type": "string", "type": "string",
"x-refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Pet",
],
}, },
"title": "", "title": "",
"type": "string", "type": "string",
@ -579,7 +599,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
], ],
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": false, "isPrimitive": false,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -818,21 +838,24 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "object", "type": "object",
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Dog",
],
"schema": Object { "schema": Object {
"allOf": undefined, "allOf": undefined,
"discriminator": Object { "discriminator": Object {
"propertyName": "type", "propertyName": "type",
}, },
"format": undefined,
"parentRefs": Array [
"#/components/schemas/Pet",
],
"properties": Object { "properties": Object {
"packSize": Object { "packSize": Object {
"type": "number", "type": "number",
}, },
"type": Object { "type": Object {
"type": "string", "type": "string",
"x-refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Pet",
],
}, },
}, },
"required": Array [ "required": Array [
@ -840,6 +863,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
], ],
"title": "Dog", "title": "Dog",
"type": "object", "type": "object",
"x-circular-ref": undefined,
"x-parentRefs": Array [
"#/components/schemas/Pet",
],
}, },
"title": "Dog", "title": "Dog",
"type": "object", "type": "object",
@ -889,7 +916,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined, "examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": true, "isPrimitive": true,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -1117,11 +1144,27 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"rawSchema": Object { "rawSchema": Object {
"default": undefined, "default": undefined,
"type": "string", "type": "string",
"x-refsStack": Array [
"#/components/schemas/Cat",
"#/components/schemas/Pet",
],
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Cat",
"#/components/schemas/Cat",
"#/components/schemas/Pet",
"#/components/schemas/Cat",
"#/components/schemas/Pet",
"#/components/schemas/Cat/properties/type",
],
"schema": Object { "schema": Object {
"default": undefined, "default": undefined,
"type": "string", "type": "string",
"x-refsStack": Array [
"#/components/schemas/Cat",
"#/components/schemas/Pet",
],
}, },
"title": "", "title": "",
"type": "string", "type": "string",
@ -1156,7 +1199,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined, "examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": true, "isPrimitive": true,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -1384,11 +1427,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"rawSchema": Object { "rawSchema": Object {
"default": undefined, "default": undefined,
"type": "number", "type": "number",
"x-refsStack": Array [
"#/components/schemas/Cat",
],
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Cat",
"#/components/schemas/Cat",
"#/components/schemas/Cat",
"#/components/schemas/Cat/properties/packSize",
],
"schema": Object { "schema": Object {
"default": undefined, "default": undefined,
"type": "number", "type": "number",
"x-refsStack": Array [
"#/components/schemas/Cat",
],
}, },
"title": "", "title": "",
"type": "number", "type": "number",
@ -1398,7 +1453,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
], ],
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": false, "isPrimitive": false,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -1639,21 +1694,27 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "object", "type": "object",
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Cat",
],
"schema": Object { "schema": Object {
"allOf": undefined, "allOf": undefined,
"discriminator": Object { "discriminator": Object {
"propertyName": "type", "propertyName": "type",
}, },
"format": undefined,
"parentRefs": Array [
"#/components/schemas/Pet",
],
"properties": Object { "properties": Object {
"packSize": Object { "packSize": Object {
"type": "number", "type": "number",
"x-refsStack": Array [
"#/components/schemas/Cat",
],
}, },
"type": Object { "type": Object {
"type": "string", "type": "string",
"x-refsStack": Array [
"#/components/schemas/Cat",
"#/components/schemas/Pet",
],
}, },
}, },
"required": Array [ "required": Array [
@ -1661,6 +1722,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
], ],
"title": "Cat", "title": "Cat",
"type": "object", "type": "object",
"x-circular-ref": undefined,
"x-parentRefs": Array [
"#/components/schemas/Pet",
],
}, },
"title": "Cat", "title": "Cat",
"type": "object", "type": "object",
@ -1904,6 +1969,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "object", "type": "object",
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Pet",
],
"schema": Object { "schema": Object {
"discriminator": Object { "discriminator": Object {
"propertyName": "type", "propertyName": "type",
@ -1970,7 +2038,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined, "examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": true, "isPrimitive": true,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -2200,6 +2268,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "number", "type": "number",
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Dog/properties/packSize",
],
"schema": Object { "schema": Object {
"default": undefined, "default": undefined,
"type": "number", "type": "number",
@ -2237,7 +2309,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined, "examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": true, "isPrimitive": true,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -2465,11 +2537,27 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"rawSchema": Object { "rawSchema": Object {
"default": undefined, "default": undefined,
"type": "string", "type": "string",
"x-refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Pet",
],
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Dog",
"#/components/schemas/Pet",
"#/components/schemas/Dog",
"#/components/schemas/Pet",
"#/components/schemas/Dog/properties/type",
],
"schema": Object { "schema": Object {
"default": undefined, "default": undefined,
"type": "string", "type": "string",
"x-refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Pet",
],
}, },
"title": "", "title": "",
"type": "string", "type": "string",
@ -2479,7 +2567,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
}, },
], ],
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": false, "isPrimitive": false,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -2718,21 +2806,24 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "object", "type": "object",
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Dog",
],
"schema": Object { "schema": Object {
"allOf": undefined, "allOf": undefined,
"discriminator": Object { "discriminator": Object {
"propertyName": "type", "propertyName": "type",
}, },
"format": undefined,
"parentRefs": Array [
"#/components/schemas/Pet",
],
"properties": Object { "properties": Object {
"packSize": Object { "packSize": Object {
"type": "number", "type": "number",
}, },
"type": Object { "type": Object {
"type": "string", "type": "string",
"x-refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Pet",
],
}, },
}, },
"required": Array [ "required": Array [
@ -2740,6 +2831,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
], ],
"title": "Dog", "title": "Dog",
"type": "object", "type": "object",
"x-circular-ref": undefined,
"x-parentRefs": Array [
"#/components/schemas/Pet",
],
}, },
"title": "Dog", "title": "Dog",
"type": "object", "type": "object",
@ -2783,7 +2878,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"examples": undefined, "examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": true, "isPrimitive": true,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -2795,6 +2890,10 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"type": "number", "type": "number",
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Dog/properties/packSize",
],
"schema": Object { "schema": Object {
"default": undefined, "default": undefined,
"type": "number", "type": "number",
@ -2840,7 +2939,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"examples": undefined, "examples": undefined,
"externalDocs": undefined, "externalDocs": undefined,
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": false,
"isPrimitive": true, "isPrimitive": true,
"maxItems": undefined, "maxItems": undefined,
"minItems": undefined, "minItems": undefined,
@ -2850,11 +2949,27 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"rawSchema": Object { "rawSchema": Object {
"default": undefined, "default": undefined,
"type": "string", "type": "string",
"x-refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Pet",
],
}, },
"readOnly": false, "readOnly": false,
"refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Dog",
"#/components/schemas/Pet",
"#/components/schemas/Dog",
"#/components/schemas/Pet",
"#/components/schemas/Dog/properties/type",
],
"schema": Object { "schema": Object {
"default": undefined, "default": undefined,
"type": "string", "type": "string",
"x-refsStack": Array [
"#/components/schemas/Dog",
"#/components/schemas/Pet",
],
}, },
"title": "", "title": "",
"type": "string", "type": "string",

View File

@ -1,12 +1,13 @@
import { Lambda, observe } from 'mobx'; import { Lambda, observe } from 'mobx';
import { OpenAPISpec } from '../types'; import type { OpenAPISpec } from '../types';
import { loadAndBundleSpec } from '../utils/loadAndBundleSpec'; import { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
import { history } from './HistoryService'; import { history } from './HistoryService';
import { MarkerService } from './MarkerService'; import { MarkerService } from './MarkerService';
import { MenuStore } from './MenuStore'; import { MenuStore } from './MenuStore';
import { SpecStore } from './models'; import { SpecStore } from './models';
import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions } from './RedocNormalizedOptions';
import type { RedocRawOptions } from './RedocNormalizedOptions';
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
import { SearchStore } from './SearchStore'; import { SearchStore } from './SearchStore';
@ -15,18 +16,7 @@ import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes';
import { SCHEMA_DEFINITION_JSX_NAME, SECURITY_DEFINITIONS_JSX_NAME } from '../utils/openapi'; import { SCHEMA_DEFINITION_JSX_NAME, SECURITY_DEFINITIONS_JSX_NAME } from '../utils/openapi';
import { IS_BROWSER } from '../utils'; import { IS_BROWSER } from '../utils';
import type { StoreState } from './types';
export interface StoreState {
menu: {
activeItemIdx: number;
};
spec: {
url?: string;
data: any;
};
searchIndex: any;
options: RedocRawOptions;
}
export async function createStore( export async function createStore(
spec: object, spec: object,

View File

@ -1,25 +1,4 @@
export interface LabelsConfig { import type { LabelsConfig, LabelsConfigRaw } from './types';
enum: string;
enumSingleValue: string;
enumArray: string;
default: string;
deprecated: string;
example: string;
examples: string;
recursive: string;
arrayOf: string;
webhook: string;
const: string;
noResultsFound: string;
download: string;
downloadSpecification: string;
responses: string;
callbackResponses: string;
requestSamples: string;
responseSamples: string;
}
export type LabelsConfigRaw = Partial<LabelsConfig>;
const labels: LabelsConfig = { const labels: LabelsConfig = {
enum: 'Enum', enum: 'Enum',

View File

@ -1,9 +1,8 @@
import * as React from 'react';
import { marked } from 'marked'; import { marked } from 'marked';
import { highlight, safeSlugify, unescapeHTMLChars } from '../utils'; import { highlight, safeSlugify, unescapeHTMLChars } from '../utils';
import { AppStore } from './AppStore';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions } from './RedocNormalizedOptions';
import type { MarkdownHeading, MDXComponentMeta } from './types';
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
@ -22,20 +21,6 @@ export const MDX_COMPONENT_REGEXP = '(?:^ {0,3}<({component})([\\s\\S]*?)>([\\s\
export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')'; export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')';
export interface MDXComponentMeta {
component: React.ComponentType;
propsSelector: (store?: AppStore) => any;
props?: object;
}
export interface MarkdownHeading {
id: string;
name: string;
level: number;
items?: MarkdownHeading[];
description?: string;
}
export function buildComponentComment(name: string) { export function buildComponentComment(name: string) {
return `<!-- ReDoc-Inject: <${name}> -->`; return `<!-- ReDoc-Inject: <${name}> -->`;
} }

View File

@ -1,41 +1,12 @@
import { import type { OpenAPISpec, OpenAPIPaths } from '../types';
OpenAPIOperation,
OpenAPIParameter,
OpenAPISpec,
OpenAPITag,
Referenced,
OpenAPIServer,
OpenAPIPaths,
} from '../types';
import { isOperationName, JsonPointer, alphabeticallyByProp } from '../utils'; import { isOperationName, JsonPointer, alphabeticallyByProp } from '../utils';
import { MarkdownRenderer } from './MarkdownRenderer'; import { MarkdownRenderer } from './MarkdownRenderer';
import { GroupModel, OperationModel } from './models'; import { GroupModel, OperationModel } from './models';
import { OpenAPIParser } from './OpenAPIParser'; import type { OpenAPIParser } from './OpenAPIParser';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import type { RedocNormalizedOptions } from './RedocNormalizedOptions';
import type { ContentItemModel, TagGroup, TagInfo, TagsInfoMap } from './types';
export type TagInfo = OpenAPITag & {
operations: ExtendedOpenAPIOperation[];
used?: boolean;
};
export type ExtendedOpenAPIOperation = {
pointer: string;
pathName: string;
httpVerb: string;
pathParameters: Array<Referenced<OpenAPIParameter>>;
pathServers: Array<OpenAPIServer> | undefined;
isWebhook: boolean;
} & OpenAPIOperation;
export type TagsInfoMap = Record<string, TagInfo>;
export interface TagGroup {
name: string;
tags: string[];
}
export const GROUP_DEPTH = 0; export const GROUP_DEPTH = 0;
export type ContentItemModel = GroupModel | OperationModel;
export class MenuBuilder { export class MenuBuilder {
/** /**
@ -239,7 +210,7 @@ export class MenuBuilder {
for (const operationName of operations) { for (const operationName of operations) {
const operationInfo = path[operationName]; const operationInfo = path[operationName];
if (path.$ref) { if (path.$ref) {
const resolvedPaths = parser.deref<OpenAPIPaths>(path as OpenAPIPaths); const { resolved: resolvedPaths } = parser.deref<OpenAPIPaths>(path as OpenAPIPaths);
getTags(parser, { [pathName]: resolvedPaths }, isWebhook); getTags(parser, { [pathName]: resolvedPaths }, isWebhook);
continue; continue;
} }

View File

@ -1,37 +1,15 @@
import { action, observable, makeObservable } from 'mobx'; import { action, observable, makeObservable } from 'mobx';
import { querySelector } from '../utils/dom'; import { querySelector } from '../utils/dom';
import { SpecStore } from './models'; import { escapeHTMLAttrChars, flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
import { history as historyInst, HistoryService } from './HistoryService'; import { history as historyInst, HistoryService } from './HistoryService';
import { ScrollService } from './ScrollService';
import { escapeHTMLAttrChars, flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
import { GROUP_DEPTH } from './MenuBuilder'; import { GROUP_DEPTH } from './MenuBuilder';
export type MenuItemGroupType = 'group' | 'tag' | 'section'; import type { SpecStore } from './models';
export type MenuItemType = MenuItemGroupType | 'operation'; import type { ScrollService } from './ScrollService';
import type { IMenuItem } from './types';
/** Generic interface for MenuItems */ /** Generic interface for MenuItems */
export interface IMenuItem {
id: string;
absoluteIdx?: number;
name: string;
sidebarLabel: string;
description?: string;
depth: number;
active: boolean;
expanded: boolean;
items: IMenuItem[];
parent?: IMenuItem;
deprecated?: boolean;
type: MenuItemType;
deactivate(): void;
activate(): void;
collapse(): void;
expand(): void;
}
export const SECTION_ATTR = 'data-section-id'; export const SECTION_ATTR = 'data-section-id';

View File

@ -1,45 +1,27 @@
import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types'; import type { OpenAPIRef, OpenAPISchema, OpenAPISpec } from '../types';
import { IS_BROWSER, getDefinitionName } from '../utils/';
import { isArray, isBoolean, IS_BROWSER } from '../utils';
import { JsonPointer } from '../utils/JsonPointer'; import { JsonPointer } from '../utils/JsonPointer';
import { getDefinitionName, isNamedDefinition } from '../utils/openapi';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions } from './RedocNormalizedOptions';
import type { MergedOpenAPISchema } from './types';
export type MergedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] };
/**
* Helper class to keep track of visited references to avoid
* endless recursion because of circular refs
*/
class RefCounter {
_counter = {};
reset(): void {
this._counter = {};
}
visit(ref: string): void {
this._counter[ref] = this._counter[ref] ? this._counter[ref] + 1 : 1;
}
exit(ref: string): void {
this._counter[ref] = this._counter[ref] && this._counter[ref] - 1;
}
visited(ref: string): boolean {
return !!this._counter[ref];
}
}
/** /**
* Loads and keeps spec. Provides raw spec operations * Loads and keeps spec. Provides raw spec operations
*/ */
export function pushRef(stack: string[], ref?: string): string[] {
return ref && stack[stack.length - 1] !== ref ? [...stack, ref] : stack;
}
export function concatRefStacks(base: string[], stack?: string[]): string[] {
return stack ? base.concat(stack) : base;
}
export class OpenAPIParser { export class OpenAPIParser {
specUrl?: string; specUrl?: string;
spec: OpenAPISpec; spec: OpenAPISpec;
private _refCounter: RefCounter = new RefCounter(); private readonly allowMergeRefs: boolean = false;
private allowMergeRefs: boolean = false;
constructor( constructor(
spec: OpenAPISpec, spec: OpenAPISpec,
@ -51,13 +33,13 @@ export class OpenAPIParser {
this.spec = spec; this.spec = spec;
this.allowMergeRefs = spec.openapi.startsWith('3.1'); this.allowMergeRefs = spec.openapi.startsWith('3.1');
const href = IS_BROWSER ? window.location.href : undefined; const href = IS_BROWSER ? window.location.href : '';
if (typeof specUrl === 'string') { if (typeof specUrl === 'string') {
this.specUrl = new URL(specUrl, href).href; this.specUrl = new URL(specUrl, href).href;
} }
} }
validate(spec: any) { validate(spec: GenericObject): void {
if (spec.openapi === undefined) { if (spec.openapi === undefined) {
throw new Error('Document must be valid OpenAPI 3.0.0 definition'); throw new Error('Document must be valid OpenAPI 3.0.0 definition');
} }
@ -86,101 +68,82 @@ export class OpenAPIParser {
/** /**
* checks if the object is OpenAPI reference (contains $ref property) * checks if the object is OpenAPI reference (contains $ref property)
*/ */
isRef(obj: any): obj is OpenAPIRef { isRef<T extends unknown>(obj: OpenAPIRef | T): obj is OpenAPIRef {
if (!obj) { if (!obj) {
return false; return false;
} }
obj = <OpenAPIRef>obj;
return obj.$ref !== undefined && obj.$ref !== null; return obj.$ref !== undefined && obj.$ref !== null;
} }
/**
* resets visited endpoints. should be run after
*/
resetVisited() {
if (process.env.NODE_ENV !== 'production') {
// check in dev mode
for (const k in this._refCounter._counter) {
if (this._refCounter._counter[k] > 0) {
console.warn('Not exited reference: ' + k);
}
}
}
this._refCounter = new RefCounter();
}
exitRef<T>(ref: Referenced<T>) {
if (!this.isRef(ref)) {
return;
}
this._refCounter.exit(ref.$ref);
}
/** /**
* Resolve given reference object or return as is if it is not a reference * Resolve given reference object or return as is if it is not a reference
* @param obj object to dereference * @param obj object to dereference
* @param forceCircular whether to dereference even if it is circular ref * @param forceCircular whether to dereference even if it is circular ref
* @param mergeAsAllOf
*/ */
deref<T extends object>(obj: OpenAPIRef | T, forceCircular = false, mergeAsAllOf = false): T { deref<T extends unknown>(
obj: OpenAPIRef | T,
baseRefsStack: string[] = [],
mergeAsAllOf = false,
): { resolved: T; refsStack: string[] } {
// this can be set by all of when it mergers props from different sources
const objRefsStack = obj?.['x-refsStack'];
baseRefsStack = concatRefStacks(baseRefsStack, objRefsStack);
if (this.isRef(obj)) { if (this.isRef(obj)) {
const schemaName = getDefinitionName(obj.$ref); const schemaName = getDefinitionName(obj.$ref);
if (schemaName && this.options.ignoreNamedSchemas.has(schemaName)) { if (schemaName && this.options.ignoreNamedSchemas.has(schemaName)) {
return { type: 'object', title: schemaName } as T; return { resolved: { type: 'object', title: schemaName } as T, refsStack: baseRefsStack };
} }
const resolved = this.byRef<T>(obj.$ref)!; let resolved = this.byRef<T>(obj.$ref);
const visited = this._refCounter.visited(obj.$ref); if (!resolved) {
this._refCounter.visit(obj.$ref); throw new Error(`Failed to resolve $ref "${obj.$ref}"`);
if (visited && !forceCircular) {
// circular reference detected
// tslint:disable-next-line
return Object.assign({}, resolved, { 'x-circular-ref': true });
}
// deref again in case one more $ref is here
let result = resolved;
if (this.isRef(resolved)) {
result = this.deref(resolved, false, mergeAsAllOf);
this.exitRef(resolved);
}
return this.allowMergeRefs ? this.mergeRefs(obj, resolved, mergeAsAllOf) : result;
}
return obj;
} }
shallowDeref<T extends unknown>(obj: OpenAPIRef | T): T { let refsStack = baseRefsStack;
if (this.isRef(obj)) { if (baseRefsStack.includes(obj.$ref)) {
const schemaName = getDefinitionName(obj.$ref); resolved = Object.assign({}, resolved, { 'x-circular-ref': true });
if (schemaName && this.options.ignoreNamedSchemas.has(schemaName)) { } else if (this.isRef(resolved)) {
return { type: 'object', title: schemaName } as T; const res = this.deref(resolved, baseRefsStack, mergeAsAllOf);
} refsStack = res.refsStack;
const resolved = this.byRef<T>(obj.$ref); resolved = res.resolved;
return this.allowMergeRefs ? this.mergeRefs(obj, resolved, false) : (resolved as T);
}
return obj;
} }
mergeRefs(ref, resolved, mergeAsAllOf: boolean) { refsStack = pushRef(baseRefsStack, obj.$ref);
resolved = this.allowMergeRefs ? this.mergeRefs(obj, resolved, mergeAsAllOf) : resolved;
return { resolved, refsStack };
}
return {
resolved: obj,
refsStack: concatRefStacks(baseRefsStack, objRefsStack),
};
}
mergeRefs<T extends unknown>(ref: OpenAPIRef, resolved: T, mergeAsAllOf: boolean): T {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { $ref, ...rest } = ref; const { $ref, ...rest } = ref;
const keys = Object.keys(rest); const keys = Object.keys(rest);
if (keys.length === 0) { if (keys.length === 0) {
if (this.isRef(resolved)) {
return this.shallowDeref(resolved);
}
return resolved; return resolved;
} }
if ( if (
mergeAsAllOf && mergeAsAllOf &&
keys.some(k => k !== 'description' && k !== 'title' && k !== 'externalDocs') keys.some(
k => !['description', 'title', 'externalDocs', 'x-refsStack', 'x-parentRefs'].includes(k),
)
) { ) {
return { return {
allOf: [resolved, rest], allOf: [resolved, rest],
}; } as T;
} else { } else {
// small optimization // small optimization
return { return {
...resolved, ...(resolved as GenericObject),
...rest, ...rest,
}; } as T;
} }
} }
@ -189,15 +152,15 @@ export class OpenAPIParser {
* @param schema schema with allOF * @param schema schema with allOF
* @param $ref pointer of the schema * @param $ref pointer of the schema
* @param forceCircular whether to dereference children even if it is a circular ref * @param forceCircular whether to dereference children even if it is a circular ref
* @param used$Refs
*/ */
mergeAllOf( mergeAllOf(
schema: OpenAPISchema, schema: MergedOpenAPISchema,
$ref?: string, $ref: string | undefined,
forceCircular: boolean = false, refsStack: string[],
used$Refs = new Set<string>(),
): MergedOpenAPISchema { ): MergedOpenAPISchema {
if ($ref) { if (schema['x-circular-ref']) {
used$Refs.add($ref); return schema;
} }
schema = this.hoistOneOfs(schema); schema = this.hoistOneOfs(schema);
@ -208,8 +171,8 @@ export class OpenAPIParser {
let receiver: MergedOpenAPISchema = { let receiver: MergedOpenAPISchema = {
...schema, ...schema,
'x-parentRefs': [],
allOf: undefined, allOf: undefined,
parentRefs: [],
title: schema.title || getDefinitionName($ref), title: schema.title || getDefinitionName($ref),
}; };
@ -222,36 +185,41 @@ export class OpenAPIParser {
} }
const allOfSchemas = schema.allOf const allOfSchemas = schema.allOf
.map(subSchema => { .map((subSchema: OpenAPISchema) => {
if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) { const { resolved, refsStack: subRefsStack } = this.deref(subSchema, refsStack, true);
const subRef = subSchema.$ref || undefined;
const subMerged = this.mergeAllOf(resolved, subRef, subRefsStack);
if (subMerged['x-circular-ref'] && subMerged.allOf) {
// if mergeAllOf is circular and still contains allOf, we should ignore it
return undefined; return undefined;
} }
if (subRef) {
const resolved = this.deref(subSchema, forceCircular, true); // collect information for implicit descriminator lookup
const subRef = subSchema.$ref || undefined; receiver['x-parentRefs']?.push(...(subMerged['x-parentRefs'] || []), subRef);
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs); }
receiver.parentRefs!.push(...(subMerged.parentRefs || []));
return { return {
$ref: subRef, $ref: subRef,
refsStack: pushRef(subRefsStack, subRef),
schema: subMerged, schema: subMerged,
}; };
}) })
.filter(child => child !== undefined) as Array<{ .filter(child => child !== undefined) as Array<{
$ref: string | undefined;
schema: MergedOpenAPISchema; schema: MergedOpenAPISchema;
refsStack: string[];
}>; }>;
for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) { for (const { schema: subSchema, refsStack: subRefsStack } of allOfSchemas) {
const { const {
type, type,
format,
enum: enumProperty, enum: enumProperty,
properties, properties,
items, items,
required, required,
title,
oneOf, oneOf,
anyOf, anyOf,
title, 'x-circular-ref': isCircular,
...otherConstraints ...otherConstraints
} = subSchema; } = subSchema;
@ -264,7 +232,6 @@ export class OpenAPIParser {
receiver.type = [...type, ...receiver.type]; receiver.type = [...type, ...receiver.type];
} else { } else {
receiver.type = type; receiver.type = type;
receiver.format = format;
} }
} }
@ -276,43 +243,51 @@ export class OpenAPIParser {
} }
} }
if (properties !== undefined) { if (properties !== undefined && typeof properties === 'object') {
receiver.properties = receiver.properties || {}; receiver.properties = receiver.properties || {};
for (const prop in properties) { for (const prop in properties) {
const propRefsStack = concatRefStacks(subRefsStack, properties[prop]?.['x-refsStack']);
if (!receiver.properties[prop]) { if (!receiver.properties[prop]) {
receiver.properties[prop] = properties[prop]; receiver.properties[prop] = {
} else { ...properties[prop],
'x-refsStack': propRefsStack,
} as MergedOpenAPISchema;
} else if (!isCircular) {
// merge inner properties // merge inner properties
const mergedProp = this.mergeAllOf( const mergedProp = this.mergeAllOf(
{ allOf: [receiver.properties[prop], properties[prop]] }, {
allOf: [receiver.properties[prop], properties[prop]],
'x-refsStack': propRefsStack,
},
$ref + '/properties/' + prop, $ref + '/properties/' + prop,
propRefsStack,
); );
receiver.properties[prop] = mergedProp; receiver.properties[prop] = mergedProp;
this.exitParents(mergedProp); // every prop resolution should have separate recursive stack
} }
} }
} }
if (items !== undefined) { if (items !== undefined && !isCircular) {
const receiverItems = isBoolean(receiver.items) // FIXME: this is invalid here, we need to fix it in separate PR
const receiverItems =
typeof receiver.items === 'boolean'
? { items: receiver.items } ? { items: receiver.items }
: receiver.items : receiver.items
? (Object.assign({}, receiver.items) as OpenAPISchema) ? (Object.assign({}, receiver.items) as OpenAPISchema)
: {}; : {};
const subSchemaItems = isBoolean(items) const subSchemaItems =
? { items } typeof subSchema.items === 'boolean'
: (Object.assign({}, items) as OpenAPISchema); ? { items: subSchema.items }
: (Object.assign({}, subSchema.items) as OpenAPISchema);
// merge inner properties // merge inner properties
receiver.items = this.mergeAllOf( receiver.items = this.mergeAllOf(
{ allOf: [receiverItems, subSchemaItems] }, {
allOf: [receiverItems, subSchemaItems],
},
$ref + '/items', $ref + '/items',
subRefsStack,
); );
} }
if (required !== undefined) {
receiver.required = (receiver.required || []).concat(required);
}
if (oneOf !== undefined) { if (oneOf !== undefined) {
receiver.oneOf = oneOf; receiver.oneOf = oneOf;
} }
@ -321,18 +296,18 @@ export class OpenAPIParser {
receiver.anyOf = anyOf; receiver.anyOf = anyOf;
} }
if (required !== undefined) {
receiver.required = [...(receiver.required || []), ...required];
}
// merge rest of constraints // merge rest of constraints
// TODO: do more intelligent merge // TODO: do more intelligent merge
receiver = { ...receiver, title: receiver.title || title, ...otherConstraints }; receiver = {
...receiver,
if (subSchemaRef) { title: receiver.title || title,
receiver.parentRefs!.push(subSchemaRef); 'x-circular-ref': receiver['x-circular-ref'] || isCircular,
if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) { ...otherConstraints,
// this is not so correct behaviour. commented out for now };
// ref: https://github.com/Redocly/redoc/issues/601
// receiver.title = JsonPointer.baseName(subSchemaRef);
}
}
} }
return receiver; return receiver;
@ -347,10 +322,12 @@ export class OpenAPIParser {
const res: Record<string, string[]> = {}; const res: Record<string, string[]> = {};
const schemas = (this.spec.components && this.spec.components.schemas) || {}; const schemas = (this.spec.components && this.spec.components.schemas) || {};
for (const defName in schemas) { for (const defName in schemas) {
const def = this.deref(schemas[defName]); const { resolved: def } = this.deref(schemas[defName]);
if ( if (
def.allOf !== undefined && def.allOf !== undefined &&
def.allOf.find(obj => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1) def.allOf.find(
(obj: OpenAPISchema) => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1,
)
) { ) {
res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName]; res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName];
} }
@ -358,12 +335,6 @@ export class OpenAPIParser {
return res; return res;
} }
exitParents(shema: MergedOpenAPISchema) {
for (const parent$ref of shema.parentRefs || []) {
this.exitRef({ $ref: parent$ref });
}
}
private hoistOneOfs(schema: OpenAPISchema) { private hoistOneOfs(schema: OpenAPISchema) {
if (schema.allOf === undefined) { if (schema.allOf === undefined) {
return schema; return schema;
@ -372,19 +343,14 @@ export class OpenAPIParser {
const allOf = schema.allOf; const allOf = schema.allOf;
for (let i = 0; i < allOf.length; i++) { for (let i = 0; i < allOf.length; i++) {
const sub = allOf[i]; const sub = allOf[i];
if (isArray(sub.oneOf)) { if (Array.isArray(sub.oneOf)) {
const beforeAllOf = allOf.slice(0, i); const beforeAllOf = allOf.slice(0, i);
const afterAllOf = allOf.slice(i + 1); const afterAllOf = allOf.slice(i + 1);
return { return {
oneOf: sub.oneOf.map(part => { oneOf: sub.oneOf.map((part: OpenAPISchema) => {
const merged = this.mergeAllOf({ return {
allOf: [...beforeAllOf, part, ...afterAllOf], allOf: [...beforeAllOf, part, ...afterAllOf],
}); };
// each oneOf should be independent so exiting all the parent refs
// otherwise it will cause false-positive recursive detection
this.exitParents(merged);
return merged;
}), }),
}; };
} }

View File

@ -2,14 +2,9 @@ import defaultTheme, { ResolvedThemeInterface, resolveTheme, ThemeInterface } fr
import { querySelector } from '../utils/dom'; import { querySelector } from '../utils/dom';
import { isArray, isNumeric, mergeObjects } from '../utils/helpers'; import { isArray, isNumeric, mergeObjects } from '../utils/helpers';
import { LabelsConfigRaw, setRedocLabels } from './Labels'; import { setRedocLabels } from './Labels';
import { MDXComponentMeta } from './MarkdownRenderer'; import { SideNavStyleEnum } from './types';
import type { LabelsConfigRaw, MDXComponentMeta } from './types';
export enum SideNavStyleEnum {
SummaryOnly = 'summary-only',
PathOnly = 'path-only',
IdOnly = 'id-only',
}
export interface RedocRawOptions { export interface RedocRawOptions {
theme?: ThemeInterface; theme?: ThemeInterface;

View File

@ -2,7 +2,7 @@ import { bind } from 'decko';
import * as EventEmitter from 'eventemitter3'; import * as EventEmitter from 'eventemitter3';
import { IS_BROWSER, querySelector, Throttle } from '../utils'; import { IS_BROWSER, querySelector, Throttle } from '../utils';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import type { RedocNormalizedOptions } from './RedocNormalizedOptions';
const EVENT = 'scroll'; const EVENT = 'scroll';

View File

@ -1,6 +1,6 @@
import { IS_BROWSER } from '../utils/'; import { IS_BROWSER } from '../utils/';
import { IMenuItem } from './MenuStore'; import type { IMenuItem } from './types';
import { OperationModel } from './models'; import type { OperationModel } from './models';
import Worker from './SearchWorker.worker'; import Worker from './SearchWorker.worker';

View File

@ -1,4 +1,5 @@
import * as lunr from 'lunr'; import * as lunr from 'lunr';
import type { SearchResult } from './types';
/* just for better typings */ /* just for better typings */
export default class Worker { export default class Worker {
@ -11,17 +12,6 @@ export default class Worker {
fromExternalJS = fromExternalJS; fromExternalJS = fromExternalJS;
} }
export interface SearchDocument {
title: string;
description: string;
id: string;
}
export interface SearchResult<T = string> {
meta: T;
score: number;
}
let store: any[] = []; let store: any[] = [];
lunr.tokenizer.separator = /\s+/; lunr.tokenizer.separator = /\s+/;

View File

@ -1,11 +1,12 @@
import { OpenAPIExternalDocumentation, OpenAPIPath, OpenAPISpec, Referenced } from '../types'; import type { OpenAPIExternalDocumentation, OpenAPIPath, OpenAPISpec, Referenced } from '../types';
import { ContentItemModel, MenuBuilder } from './MenuBuilder'; import { MenuBuilder } from './MenuBuilder';
import { ApiInfoModel } from './models/ApiInfo'; import { ApiInfoModel } from './models/ApiInfo';
import { WebhookModel } from './models/Webhook'; import { WebhookModel } from './models/Webhook';
import { SecuritySchemesModel } from './models/SecuritySchemes'; import { SecuritySchemesModel } from './models/SecuritySchemes';
import { OpenAPIParser } from './OpenAPIParser'; import { OpenAPIParser } from './OpenAPIParser';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import type { RedocNormalizedOptions } from './RedocNormalizedOptions';
import type { ContentItemModel } from './types';
/** /**
* Store that contains all the specification related information in the form of tree * Store that contains all the specification related information in the form of tree
*/ */

View File

@ -1,4 +1,5 @@
import { MarkdownRenderer, MDXComponentMeta } from '../MarkdownRenderer'; import type { MDXComponentMeta } from '../types';
import { MarkdownRenderer } from '../MarkdownRenderer';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
const TestComponent = () => null; const TestComponent = () => null;

View File

@ -41,14 +41,14 @@ describe('Models', () => {
expect(schema.title).toEqual('Foo'); expect(schema.title).toEqual('Foo');
}); });
test('should merge oneOff to inside allOff', () => { test('should merge oneOf to inside allOff', () => {
// TODO: should hoist // TODO: should hoist
const spec = require('./fixtures/mergeAllOf.json'); const spec = require('./fixtures/mergeAllOf.json');
parser = new OpenAPIParser(spec, undefined, opts); parser = new OpenAPIParser(spec, undefined, opts);
const schema = parser.mergeAllOf(spec.components.schemas.Case4); const schema = parser.mergeAllOf(spec.components.schemas.Case4);
expect(schema.title).toEqual('Foo'); expect(schema.title).toEqual('Foo');
expect(schema.parentRefs).toHaveLength(1); expect(schema['x-parentRefs']).toHaveLength(1);
expect(schema.parentRefs[0]).toEqual('#/components/schemas/Ref'); expect(schema['x-parentRefs'][0]).toEqual('#/components/schemas/Ref');
expect(schema.oneOf).toEqual([{ title: 'Bar' }, { title: 'Baz' }]); expect(schema.oneOf).toEqual([{ title: 'Bar' }, { title: 'Baz' }]);
}); });
@ -60,7 +60,7 @@ describe('Models', () => {
description: 'Overriden description', description: 'Overriden description',
}; };
expect(parser.shallowDeref(schemaOrRef)).toMatchSnapshot(); expect(parser.deref(schemaOrRef)).toMatchSnapshot();
}); });
test('should correct resolve double $ref if no need sibling', () => { test('should correct resolve double $ref if no need sibling', () => {
@ -70,7 +70,7 @@ describe('Models', () => {
$ref: '#/components/schemas/Parent', $ref: '#/components/schemas/Parent',
}; };
expect(parser.deref(schemaOrRef, false, true)).toMatchSnapshot(); expect(parser.deref(schemaOrRef, [], true)).toMatchSnapshot();
}); });
}); });
}); });

View File

@ -2,12 +2,17 @@
exports[`Models Schema should correct resolve double $ref if no need sibling 1`] = ` exports[`Models Schema should correct resolve double $ref if no need sibling 1`] = `
Object { Object {
"refsStack": Array [
"#/components/schemas/Parent",
],
"resolved": Object {
"properties": Object { "properties": Object {
"test": Object { "test": Object {
"type": "string", "type": "string",
}, },
}, },
"type": "object", "type": "object",
},
} }
`; `;
@ -15,82 +20,80 @@ exports[`Models Schema should hoist oneOfs when mergin allOf 1`] = `
Object { Object {
"oneOf": Array [ "oneOf": Array [
Object { Object {
"oneOf": Array [ "allOf": Array [
Object {
"properties": Object {
"username": Object {
"description": "The user's name",
"type": "string",
},
},
},
Object { Object {
"allOf": undefined,
"parentRefs": Array [],
"properties": Object { "properties": Object {
"extra": Object { "extra": Object {
"type": "string", "type": "string",
}, },
},
},
Object {
"oneOf": Array [
Object {
"properties": Object {
"password": Object { "password": Object {
"description": "The user's password", "description": "The user's password",
"type": "string", "type": "string",
}, },
"username": Object {
"description": "The user's name",
"type": "string",
}, },
}, },
"title": undefined,
},
Object { Object {
"allOf": undefined,
"parentRefs": Array [],
"properties": Object { "properties": Object {
"extra": Object {
"type": "string",
},
"mobile": Object { "mobile": Object {
"description": "The user's mobile", "description": "The user's mobile",
"type": "string", "type": "string",
}, },
"username": Object {
"description": "The user's name",
"type": "string",
}, },
}, },
"title": undefined, ],
}, },
], ],
}, },
Object { Object {
"oneOf": Array [ "allOf": Array [
Object { Object {
"allOf": undefined,
"parentRefs": Array [],
"properties": Object { "properties": Object {
"email": Object { "email": Object {
"description": "The user's email", "description": "The user's email",
"type": "string", "type": "string",
}, },
},
},
Object {
"properties": Object {
"extra": Object { "extra": Object {
"type": "string", "type": "string",
}, },
},
},
Object {
"oneOf": Array [
Object {
"properties": Object {
"password": Object { "password": Object {
"description": "The user's password", "description": "The user's password",
"type": "string", "type": "string",
}, },
}, },
"title": undefined,
}, },
Object { Object {
"allOf": undefined,
"parentRefs": Array [],
"properties": Object { "properties": Object {
"email": Object {
"description": "The user's email",
"type": "string",
},
"extra": Object {
"type": "string",
},
"mobile": Object { "mobile": Object {
"description": "The user's mobile", "description": "The user's mobile",
"type": "string", "type": "string",
}, },
}, },
"title": undefined, },
],
}, },
], ],
}, },
@ -100,7 +103,12 @@ Object {
exports[`Models Schema should override description from $ref of the referenced component, when sibling description exists 1`] = ` exports[`Models Schema should override description from $ref of the referenced component, when sibling description exists 1`] = `
Object { Object {
"refsStack": Array [
"#/components/schemas/Test",
],
"resolved": Object {
"description": "Overriden description", "description": "Overriden description",
"type": "object", "type": "object",
},
} }
`; `;

View File

@ -0,0 +1,559 @@
import outdent from 'outdent';
import { parseYaml } from '@redocly/openapi-core';
/* eslint-disable @typescript-eslint/no-var-requires */
import { SchemaModel } from '../../models';
import { OpenAPIParser } from '../../OpenAPIParser';
import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
import { circularDetailsPrinter, printSchema } from './helpers';
const opts = new RedocNormalizedOptions({}) as RedocNormalizedOptions;
describe('Models', () => {
describe.only('Schema Circular tracking', () => {
let parser;
expect.addSnapshotSerializer({
test: val => typeof val === 'string',
print: v => v as string,
});
test('should detect circular for array nested in allOf', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
Schema:
type: object
properties:
a: { $ref: '#/components/schemas/Schema' }
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(
parser,
spec.components.schemas.Schema,
'#/components/schemas/Schema',
opts,
);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(
`a: <object> !circular`,
);
});
test('should not detect circular refs when ref used multiple times across allOf', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
Foo:
type: object
properties:
foo: { type: string }
Schema:
allOf:
- $ref: '#/components/schemas/Foo'
- type: object
properties:
foobar: { $ref: '#/components/schemas/Foo' }
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.Schema, '', opts);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(`
foo: <string>
foobar:
foo: <string>
`);
});
test('should detect circular for array with self-reference', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
Array:
type: "array"
items: { "$ref": "#/components/schemas/Array" }
Schema:
allOf: [{ "$ref": "#/components/schemas/Array" }]
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(
parser,
spec.components.schemas.Schema,
'#/components/schemas/Schema',
opts,
);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(
`[<array> !circular]`,
);
});
test('should detect circular for object nested in allOf', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
Object:
allOf:
- $ref: '#/components/schemas/Object'
- type: "object"
properties: { "a": { "$ref": "#/components/schemas/Object" } }
Schema:
allOf: [{ "$ref": "#/components/schemas/Object" }]
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(
parser,
spec.components.schemas.Schema,
'#/components/schemas/Schema',
opts,
);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(
`a: <any> !circular`,
);
});
test('should not detect circular for base DTO case', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
BaseDTO:
type: object
properties:
id: {type: string}
BaseB:
type: object
allOf:
- $ref: '#/components/schemas/BaseDTO'
- type: object
properties:
fieldB: { type: string }
BaseA:
type: object
allOf:
- $ref: '#/components/schemas/BaseDTO'
- type: object
properties:
b: { $ref: '#/components/schemas/BaseB' }
fieldA: { type: string }
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.BaseA, '', opts);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(`
id: <string>
b:
id: <string>
fieldB: <string>
fieldA: <string>
`);
});
test('should detect circular ref for self referencing discriminator', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
SelfComponentDto:
type: object
properties:
self:
type: object
discriminator:
propertyName: schemaId
mapping:
title: '#/components/schemas/SelfComponentDto'
oneOf:
- $ref: '#/components/schemas/SelfComponentDto'
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.SelfComponentDto, '', opts);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(`
self: oneOf
title ->
self: oneOf
title -> <object> !circular
`);
});
test('should detect circular with nested oneOf hoisting', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
Node:
type: 'object'
allOf:
- oneOf:
- type: object
properties:
parent:
$ref: '#/components/schemas/Node'
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.Node, '', opts);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(`
oneOf
object ->
parent: oneOf
object ->
parent: <object> !circular
`);
});
test('should detect simple props recursion', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
PropRecursion:
properties:
children:
type: object
properties:
a:
$ref: '#/components/schemas/PropRecursion'
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.PropRecursion, '', opts);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(`
children:
a:
children:
a: <object> !circular
`);
});
test('should detect recursion for props with type array', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
PropsRecursion:
properties:
children:
type: array
items:
$ref: '#/components/schemas/PropsRecursion'
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.PropsRecursion, '', opts);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(`
children: [
children: [<object> !circular]
]
`);
});
test('should detect and ignore allOf recursion', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
Parent:
$ref: '#/components/schemas/Child'
Child:
allOf:
- $ref: '#/components/schemas/Parent'
- type: object
properties:
a:
type: string
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(
parser,
spec.components.schemas.Child,
'#/components/schemas/Child',
opts,
);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(`a: <string>`);
});
test('should detect and ignore allOf recursion in nested prop', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
ExternalReference:
type: object
allOf:
- $ref: '#/components/schemas/CompanyReference'
- type: object
properties:
externalId: { type: string }
CompanyReference:
type: object
required: [ guid, externalId ]
properties:
guid: { type: string }
nestedRecursive: { $ref: '#/components/schemas/ExternalReference' }
Entity:
type: object
allOf:
- $ref: '#/components/schemas/ExternalReference'
- type: object
properties:
directRecursive: { $ref: '#/components/schemas/ExternalReference' }
selfRecursive: { $ref: '#/components/schemas/Entity' }
anotherField: { $ref: '#/components/schemas/AnotherEntity' }
AnotherEntity:
type: object
allOf:
- $ref: '#/components/schemas/CompanyReference'
- type: object
properties:
someField: { type: number }
anotherSelfRecursive: { $ref: '#/components/schemas/AnotherEntity' }
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(
parser,
spec.components.schemas.Entity,
'#/components/schemas/Entity',
opts,
);
// TODO: this has a little issue with too early detection in anotherField -> nestedRecursive
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(`
guid*: <string>
nestedRecursive: <object> !circular
externalId*: <string>
directRecursive:
guid*: <string>
nestedRecursive: <object> !circular
externalId*: <string>
selfRecursive: <object> !circular
anotherField:
guid*: <string>
nestedRecursive: <object> !circular
someField: <number>
anotherSelfRecursive: <object> !circular
`);
});
test('should detect and ignore allOf with discriminator recursion', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
Pet:
type: object
required: [ petType ]
discriminator:
propertyName: petType
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
properties:
category: { $ref: '#/components/schemas/Category' }
status: { type: string }
friend:
allOf: [{ $ref: '#/components/schemas/Pet' }]
petType: { type: string }
Cat:
description: A representation of a cat
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
huntingSkill: { type: string }
Dog:
description: A representation of a dog
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
packSize: { type: integer }
Category:
type: object
properties:
name: { type: string }
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(
parser,
spec.components.schemas.Pet,
'#/components/schemas/Pet',
opts,
);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(`
oneOf
cat ->
category:
name: <string>
status: <string>
friend: <object> !circular
petType*: <string>
huntingSkill: <string>
dog ->
category:
name: <string>
status: <string>
friend: <object> !circular
petType*: <string>
packSize: <integer>
`);
});
test('should detect and recursion on the right level with array of discriminators', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
Pet:
type: object
required: [ petType ]
discriminator:
propertyName: petType
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
properties:
category: { $ref: '#/components/schemas/Category' }
status: { type: string }
friend:
allOf: [{ $ref: '#/components/schemas/Pet' }]
petType: { type: string }
Cat:
description: A representation of a cat
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
huntingSkill: { type: string }
Dog:
description: A representation of a dog
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
packSize: { type: integer }
Category:
type: object
properties:
name: { type: string }
Response:
type: array
items:
$ref: '#/components/schemas/Pet'
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(
parser,
spec.components.schemas.Response,
'#/components/schemas/Response',
opts,
);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(`
[
oneOf
cat ->
category:
name: <string>
status: <string>
friend: <object> !circular
petType*: <string>
huntingSkill: <string>
dog ->
category:
name: <string>
status: <string>
friend: <object> !circular
petType*: <string>
packSize: <integer>
]
`);
});
test('should detect and recursion with discriminator and oneOf', () => {
const spec = parseYaml(outdent`
openapi: 3.0.0
components:
schemas:
User:
type: object
properties:
pet:
oneOf:
- $ref: '#/components/schemas/Pet'
Pet:
type: object
required: [ petType ]
discriminator:
propertyName: petType
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
properties:
category: { $ref: '#/components/schemas/Category' }
status: { type: string }
friend:
allOf: [{ $ref: '#/components/schemas/Pet' }]
petType: { type: string }
Cat:
description: A representation of a cat
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
huntingSkill: { type: string }
Dog:
description: A representation of a dog
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
packSize: { type: integer }
Category:
type: object
properties:
name: { type: string }
`) as any;
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(
parser,
spec.components.schemas.User,
'#/components/schemas/User',
opts,
);
expect(printSchema(schema, circularDetailsPrinter)).toMatchInlineSnapshot(`
pet: oneOf
Pet -> oneOf
cat ->
category:
name: <string>
status: <string>
friend: <object> !circular
petType*: <string>
huntingSkill: <string>
dog ->
category:
name: <string>
status: <string>
friend: <object> !circular
petType*: <string>
packSize: <integer>
`);
});
});
});

View File

@ -4,20 +4,21 @@ exports[`Models Schema schemaDefinition should resolve field with conditional op
Object { Object {
"allOf": undefined, "allOf": undefined,
"default": undefined, "default": undefined,
"format": undefined,
"items": Object { "items": Object {
"allOf": undefined, "allOf": undefined,
"format": "url", "format": "url",
"parentRefs": Array [],
"title": undefined, "title": undefined,
"type": "string", "type": "string",
"x-circular-ref": undefined,
"x-parentRefs": Array [],
}, },
"maxItems": 20, "maxItems": 20,
"minItems": 1, "minItems": 1,
"parentRefs": Array [],
"title": "isString", "title": "isString",
"type": "string", "type": "string",
"x-circular-ref": undefined,
"x-displayName": "isString", "x-displayName": "isString",
"x-parentRefs": Array [],
} }
`; `;
@ -25,17 +26,16 @@ exports[`Models Schema schemaDefinition should resolve field with conditional op
Object { Object {
"allOf": undefined, "allOf": undefined,
"default": undefined, "default": undefined,
"format": undefined,
"items": Object { "items": Object {
"allOf": undefined, "allOf": undefined,
"format": "url", "format": "url",
"parentRefs": Array [],
"title": undefined, "title": undefined,
"type": "string", "type": "string",
"x-circular-ref": undefined,
"x-parentRefs": Array [],
}, },
"maxItems": 10, "maxItems": 10,
"minItems": 1, "minItems": 1,
"parentRefs": Array [],
"pattern": "\\\\d+", "pattern": "\\\\d+",
"title": "notString", "title": "notString",
"type": Array [ "type": Array [
@ -43,16 +43,16 @@ Object {
"integer", "integer",
"null", "null",
], ],
"x-circular-ref": undefined,
"x-displayName": "notString", "x-displayName": "notString",
"x-parentRefs": Array [],
} }
`; `;
exports[`Models Schema schemaDefinition should resolve schema with conditional operators 1`] = ` exports[`Models Schema schemaDefinition should resolve schema with conditional operators 1`] = `
Object { Object {
"allOf": undefined, "allOf": undefined,
"format": undefined,
"maxItems": 2, "maxItems": 2,
"parentRefs": Array [],
"properties": Object { "properties": Object {
"test": Object { "test": Object {
"allOf": undefined, "allOf": undefined,
@ -60,36 +60,40 @@ Object {
"enum": Array [ "enum": Array [
10, 10,
], ],
"format": undefined,
"items": Object { "items": Object {
"allOf": undefined, "allOf": undefined,
"format": "url", "format": "url",
"parentRefs": Array [],
"title": undefined, "title": undefined,
"type": "string", "type": "string",
"x-circular-ref": undefined,
"x-parentRefs": Array [],
}, },
"maxItems": 20, "maxItems": 20,
"minItems": 1, "minItems": 1,
"parentRefs": Array [],
"title": undefined, "title": undefined,
"type": Array [ "type": Array [
"string", "string",
"integer", "integer",
"null", "null",
], ],
"x-circular-ref": undefined,
"x-parentRefs": Array [],
"x-refsStack": Array [
"/oneOf/0",
],
}, },
}, },
"title": "=== 10", "title": "=== 10",
"type": "object", "type": "object",
"x-circular-ref": undefined,
"x-parentRefs": Array [],
} }
`; `;
exports[`Models Schema schemaDefinition should resolve schema with conditional operators 2`] = ` exports[`Models Schema schemaDefinition should resolve schema with conditional operators 2`] = `
Object { Object {
"allOf": undefined, "allOf": undefined,
"format": undefined,
"maxItems": 20, "maxItems": 20,
"parentRefs": Array [],
"properties": Object { "properties": Object {
"test": Object { "test": Object {
"description": "The list of URL to a cute photos featuring pet", "description": "The list of URL to a cute photos featuring pet",
@ -104,9 +108,14 @@ Object {
"integer", "integer",
"null", "null",
], ],
"x-refsStack": Array [
"/oneOf/1",
],
}, },
}, },
"title": "case 2", "title": "case 2",
"type": "object", "type": "object",
"x-circular-ref": undefined,
"x-parentRefs": Array [],
} }
`; `;

View File

@ -0,0 +1,76 @@
import type { SchemaModel } from '../../models';
function printType(type: string | string[]): string {
return `<${type}>`;
}
function printDescription(description: string | string[]): string {
return description ? ` (${description})` : '';
}
export function circularDetailsPrinter(schema: SchemaModel): string {
return schema.isCircular ? ' !circular' : '';
}
export function printSchema(
schema: SchemaModel,
detailsPrinter: (schema: SchemaModel) => string = () => '',
identLevel = 0,
inline = false,
): string {
if (!schema) return '';
const ident = ' '.repeat(identLevel);
if (schema.isPrimitive || schema.isCircular) {
if (schema.type === 'array' && schema.items) {
return `${inline ? ' ' : ident}[${printType(schema.items.type)}${detailsPrinter(
schema.items,
)}]${printDescription(schema.items.description)}`;
} else {
return `${inline ? ' ' : ident}${printType(schema.displayType)}${detailsPrinter(
schema,
)}${printDescription(schema.description)}`;
}
}
if (schema.oneOf) {
return (
`${inline ? ' ' : ident}oneOf\n` +
schema.oneOf
.map(sub => {
return (
`${ident} ${sub.title || sub.displayType} ->` +
printSchema(sub, detailsPrinter, identLevel + 2, true)
);
})
.join('\n')
);
}
if (schema.fields) {
const prefix = inline ? '\n' : '';
return (
prefix +
schema.fields
.map(f => {
return `${ident}${f.name}${f.required ? '*' : ''}:${printSchema(
f.schema,
detailsPrinter,
identLevel + 1,
true,
)}`;
})
.join('\n')
);
}
if (schema.items) {
return (
`${inline ? ' ' : ident}[\n` +
printSchema(schema.items, detailsPrinter, identLevel) +
`\n${inline ? ident.slice(0, -2) : ident}]`
);
}
return ' error';
}

View File

@ -11,3 +11,4 @@ export * from './RedocNormalizedOptions';
export * from './MenuBuilder'; export * from './MenuBuilder';
export * from './SearchStore'; export * from './SearchStore';
export * from './MarkerService'; export * from './MarkerService';
export * from './types';

View File

@ -1,6 +1,6 @@
import { OpenAPIContact, OpenAPIInfo, OpenAPILicense } from '../../types'; import type { OpenAPIContact, OpenAPIInfo, OpenAPILicense } from '../../types';
import { IS_BROWSER } from '../../utils/'; import { IS_BROWSER } from '../../utils/';
import { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
export class ApiInfoModel implements OpenAPIInfo { export class ApiInfoModel implements OpenAPIInfo {

View File

@ -1,10 +1,10 @@
import { action, observable, makeObservable } from 'mobx'; import { action, observable, makeObservable } from 'mobx';
import { OpenAPICallback, Referenced } from '../../types';
import { isOperationName, JsonPointer } from '../../utils'; import { isOperationName, JsonPointer } from '../../utils';
import { OpenAPIParser } from '../OpenAPIParser';
import { OperationModel } from './Operation'; import { OperationModel } from './Operation';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import type { OpenAPIParser } from '../OpenAPIParser';
import type { OpenAPICallback, Referenced } from '../../types';
import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
export class CallbackModel { export class CallbackModel {
@observable @observable
@ -23,8 +23,7 @@ export class CallbackModel {
makeObservable(this); makeObservable(this);
this.name = name; this.name = name;
const paths = parser.deref<OpenAPICallback>(infoOrRef); const { resolved: paths } = parser.deref<OpenAPICallback>(infoOrRef);
parser.exitRef(infoOrRef);
for (const pathName of Object.keys(paths)) { for (const pathName of Object.keys(paths)) {
const path = paths[pathName]; const path = paths[pathName];

View File

@ -1,6 +1,6 @@
import { OpenAPIEncoding, OpenAPIExample, Referenced } from '../../types'; import type { OpenAPIEncoding, OpenAPIExample, Referenced } from '../../types';
import { isFormUrlEncoded, isJsonLike, urlFormEncodePayload } from '../../utils/openapi'; import { isFormUrlEncoded, isJsonLike, urlFormEncodePayload } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
const externalExamplesCache: { [url: string]: Promise<any> } = {}; const externalExamplesCache: { [url: string]: Promise<any> } = {};
@ -16,14 +16,13 @@ export class ExampleModel {
public mime: string, public mime: string,
encoding?: { [field: string]: OpenAPIEncoding }, encoding?: { [field: string]: OpenAPIEncoding },
) { ) {
const example = parser.deref(infoOrRef); const { resolved: example } = parser.deref(infoOrRef);
this.value = example.value; this.value = example.value;
this.summary = example.summary; this.summary = example.summary;
this.description = example.description; this.description = example.description;
if (example.externalValue) { if (example.externalValue) {
this.externalValueUrl = new URL(example.externalValue, parser.specUrl).href; this.externalValueUrl = new URL(example.externalValue, parser.specUrl).href;
} }
parser.exitRef(infoOrRef);
if (isFormUrlEncoded(mime) && this.value && typeof this.value === 'object') { if (isFormUrlEncoded(mime) && this.value && typeof this.value === 'object') {
this.value = urlFormEncodePayload(this.value, encoding); this.value = urlFormEncodePayload(this.value, encoding);
@ -35,7 +34,7 @@ export class ExampleModel {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
if (externalExamplesCache[this.externalValueUrl]) { if (this.externalValueUrl in externalExamplesCache) {
return externalExamplesCache[this.externalValueUrl]; return externalExamplesCache[this.externalValueUrl];
} }

View File

@ -1,15 +1,15 @@
import { action, observable, makeObservable } from 'mobx'; import { action, observable, makeObservable } from 'mobx';
import { import type {
OpenAPIParameter, OpenAPIParameter,
OpenAPIParameterLocation, OpenAPIParameterLocation,
OpenAPIParameterStyle, OpenAPIParameterStyle,
Referenced, Referenced,
} from '../../types'; } from '../../types';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { extractExtensions } from '../../utils/openapi'; import { extractExtensions } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
import { SchemaModel } from './Schema'; import { SchemaModel } from './Schema';
import { ExampleModel } from './Example'; import { ExampleModel } from './Example';
import { isArray, mapValues } from '../../utils/helpers'; import { isArray, mapValues } from '../../utils/helpers';
@ -64,10 +64,11 @@ export class FieldModel {
infoOrRef: Referenced<OpenAPIParameter> & { name?: string; kind?: string }, infoOrRef: Referenced<OpenAPIParameter> & { name?: string; kind?: string },
pointer: string, pointer: string,
options: RedocNormalizedOptions, options: RedocNormalizedOptions,
refsStack?: string[],
) { ) {
makeObservable(this); makeObservable(this);
const info = parser.deref<OpenAPIParameter>(infoOrRef); const { resolved: info } = parser.deref<OpenAPIParameter>(infoOrRef);
this.kind = infoOrRef.kind || 'field'; this.kind = infoOrRef.kind || 'field';
this.name = infoOrRef.name || info.name; this.name = infoOrRef.name || info.name;
this.in = info.in; this.in = info.in;
@ -80,7 +81,7 @@ export class FieldModel {
fieldSchema = info.content[serializationMime] && info.content[serializationMime].schema; fieldSchema = info.content[serializationMime] && info.content[serializationMime].schema;
} }
this.schema = new SchemaModel(parser, fieldSchema || {}, pointer, options); this.schema = new SchemaModel(parser, fieldSchema || {}, pointer, options, false, refsStack);
this.description = this.description =
info.description === undefined ? this.schema.description || '' : info.description; info.description === undefined ? this.schema.description || '' : info.description;
this.example = info.example || this.schema.example; this.example = info.example || this.schema.example;
@ -110,7 +111,6 @@ export class FieldModel {
} }
this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated; this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated;
parser.exitRef(infoOrRef);
if (options.showExtensions) { if (options.showExtensions) {
this.extensions = extractExtensions(info, options.showExtensions); this.extensions = extractExtensions(info, options.showExtensions);

View File

@ -1,10 +1,9 @@
import { action, observable, makeObservable } from 'mobx'; import { action, observable, makeObservable } from 'mobx';
import { OpenAPIExternalDocumentation, OpenAPITag } from '../../types'; import type { OpenAPIExternalDocumentation, OpenAPITag } from '../../types';
import { safeSlugify } from '../../utils'; import { safeSlugify } from '../../utils';
import { MarkdownHeading, MarkdownRenderer } from '../MarkdownRenderer'; import { MarkdownRenderer } from '../MarkdownRenderer';
import { ContentItemModel } from '../MenuBuilder'; import type { ContentItemModel, IMenuItem, MarkdownHeading, MenuItemGroupType } from '../types';
import { IMenuItem, MenuItemGroupType } from '../MenuStore';
/** /**
* Operations Group model ready to be used by components * Operations Group model ready to be used by components

View File

@ -1,11 +1,11 @@
import { action, computed, observable, makeObservable } from 'mobx'; import { action, computed, observable, makeObservable } from 'mobx';
import { OpenAPIMediaType } from '../../types'; import type { OpenAPIMediaType } from '../../types';
import { MediaTypeModel } from './MediaType'; import { MediaTypeModel } from './MediaType';
import { mergeSimilarMediaTypes } from '../../utils'; import { mergeSimilarMediaTypes } from '../../utils';
import { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
/** /**
* MediaContent model ready to be sued by React components * MediaContent model ready to be sued by React components
@ -34,7 +34,6 @@ export class MediaContentModel {
this.mediaTypes = Object.keys(info).map(name => { this.mediaTypes = Object.keys(info).map(name => {
const mime = info[name]; const mime = info[name];
// reset deref cache just in case something is left there // reset deref cache just in case something is left there
parser.resetVisited();
return new MediaTypeModel(parser, name, isRequestType, mime, options); return new MediaTypeModel(parser, name, isRequestType, mime, options);
}); });
} }

View File

@ -1,11 +1,11 @@
import * as Sampler from 'openapi-sampler'; import * as Sampler from 'openapi-sampler';
import { OpenAPIMediaType } from '../../types'; import type { OpenAPIMediaType } from '../../types';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { SchemaModel } from './Schema'; import { SchemaModel } from './Schema';
import { isJsonLike, mapValues } from '../../utils'; import { isJsonLike, mapValues } from '../../utils';
import { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
import { ExampleModel } from './Example'; import { ExampleModel } from './Example';
export class MediaTypeModel { export class MediaTypeModel {
@ -40,7 +40,7 @@ export class MediaTypeModel {
this.examples = { this.examples = {
default: new ExampleModel( default: new ExampleModel(
parser, parser,
{ value: parser.shallowDeref(info.example) }, { value: parser.deref(info.example).resolved },
name, name,
info.encoding, info.encoding,
), ),

View File

@ -1,11 +1,5 @@
import { action, observable, makeObservable } from 'mobx'; import { action, observable, makeObservable } from 'mobx';
import { IMenuItem } from '../MenuStore';
import { GroupModel } from './Group.model';
import { SecurityRequirementModel } from './SecurityRequirement';
import { OpenAPIExternalDocumentation, OpenAPIServer, OpenAPIXCodeSample } from '../../types';
import { import {
extractExtensions, extractExtensions,
getOperationSummary, getOperationSummary,
@ -17,15 +11,20 @@ import {
sortByField, sortByField,
sortByRequired, sortByRequired,
} from '../../utils'; } from '../../utils';
import { ContentItemModel, ExtendedOpenAPIOperation } from '../MenuBuilder';
import { OpenAPIParser } from '../OpenAPIParser'; import { GroupModel } from './Group.model';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { SecurityRequirementModel } from './SecurityRequirement';
import { CallbackModel } from './Callback'; import { CallbackModel } from './Callback';
import { FieldModel } from './Field'; import { FieldModel } from './Field';
import { MediaContentModel } from './MediaContent';
import { RequestBodyModel } from './RequestBody'; import { RequestBodyModel } from './RequestBody';
import { ResponseModel } from './Response'; import { ResponseModel } from './Response';
import { SideNavStyleEnum } from '../RedocNormalizedOptions'; import { SideNavStyleEnum } from '../types';
import type { OpenAPIExternalDocumentation, OpenAPIServer, OpenAPIXCodeSample } from '../../types';
import type { OpenAPIParser } from '../OpenAPIParser';
import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import type { MediaContentModel } from './MediaContent';
import type { ContentItemModel, ExtendedOpenAPIOperation, IMenuItem } from '../types';
export interface XPayloadSample { export interface XPayloadSample {
lang: 'payload'; lang: 'payload';

View File

@ -1,7 +1,7 @@
import { OpenAPIRequestBody, Referenced } from '../../types'; import type { OpenAPIRequestBody, Referenced } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { MediaContentModel } from './MediaContent'; import { MediaContentModel } from './MediaContent';
import { getContentWithLegacyExamples } from '../../utils'; import { getContentWithLegacyExamples } from '../../utils';
@ -19,10 +19,9 @@ export class RequestBodyModel {
constructor({ parser, infoOrRef, options, isEvent }: RequestBodyProps) { constructor({ parser, infoOrRef, options, isEvent }: RequestBodyProps) {
const isRequest = !isEvent; const isRequest = !isEvent;
const info = parser.deref(infoOrRef); const { resolved: info } = parser.deref(infoOrRef);
this.description = info.description || ''; this.description = info.description || '';
this.required = !!info.required; this.required = !!info.required;
parser.exitRef(infoOrRef);
const mediaContent = getContentWithLegacyExamples(info); const mediaContent = getContentWithLegacyExamples(info);
if (mediaContent !== undefined) { if (mediaContent !== undefined) {

View File

@ -1,10 +1,10 @@
import { action, observable, makeObservable } from 'mobx'; import { action, observable, makeObservable } from 'mobx';
import { OpenAPIResponse, Referenced } from '../../types'; import type { OpenAPIResponse, Referenced } from '../../types';
import { getStatusCodeType, extractExtensions } from '../../utils'; import { getStatusCodeType, extractExtensions } from '../../utils';
import { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { FieldModel } from './Field'; import { FieldModel } from './Field';
import { MediaContentModel } from './MediaContent'; import { MediaContentModel } from './MediaContent';
@ -41,8 +41,7 @@ export class ResponseModel {
this.expanded = options.expandResponses === 'all' || options.expandResponses[code]; this.expanded = options.expandResponses === 'all' || options.expandResponses[code];
const info = parser.deref(infoOrRef); const { resolved: info } = parser.deref(infoOrRef);
parser.exitRef(infoOrRef);
this.code = code; this.code = code;
if (info.content !== undefined) { if (info.content !== undefined) {
this.content = new MediaContentModel(parser, info.content, isRequest, options); this.content = new MediaContentModel(parser, info.content, isRequest, options);

View File

@ -1,12 +1,13 @@
import { action, observable, makeObservable } from 'mobx'; import { action, observable, makeObservable } from 'mobx';
import { OpenAPIExternalDocumentation, OpenAPISchema, Referenced } from '../../types'; import type { OpenAPIExternalDocumentation, OpenAPISchema, Referenced } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { pushRef } from '../OpenAPIParser';
import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { FieldModel } from './Field'; import { FieldModel } from './Field';
import { MergedOpenAPISchema } from '../'; import { MergedOpenAPISchema } from '../types';
import { import {
detectType, detectType,
extractExtensions, extractExtensions,
@ -80,18 +81,19 @@ export class SchemaModel {
pointer: string, pointer: string,
private options: RedocNormalizedOptions, private options: RedocNormalizedOptions,
isChild: boolean = false, isChild: boolean = false,
private refsStack: string[] = [],
) { ) {
makeObservable(this); makeObservable(this);
this.pointer = schemaOrRef.$ref || pointer || ''; this.pointer = schemaOrRef.$ref || pointer || '';
this.rawSchema = parser.deref(schemaOrRef, false, true);
this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild);
const { resolved, refsStack: newRefsStack } = parser.deref(schemaOrRef, refsStack, true);
this.refsStack = pushRef(newRefsStack, this.pointer);
this.rawSchema = resolved;
this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, this.refsStack);
this.init(parser, isChild); this.init(parser, isChild);
parser.exitRef(schemaOrRef);
parser.exitParents(this.schema);
if (options.showExtensions) { if (options.showExtensions) {
this.extensions = extractExtensions(this.schema, options.showExtensions); this.extensions = extractExtensions(this.schema, options.showExtensions);
} }
@ -112,7 +114,7 @@ export class SchemaModel {
init(parser: OpenAPIParser, isChild: boolean) { init(parser: OpenAPIParser, isChild: boolean) {
const schema = this.schema; const schema = this.schema;
this.isCircular = schema['x-circular-ref']; this.isCircular = !!schema['x-circular-ref'];
this.title = this.title =
schema.title || (isNamedDefinition(this.pointer) && JsonPointer.baseName(this.pointer)) || ''; schema.title || (isNamedDefinition(this.pointer) && JsonPointer.baseName(this.pointer)) || '';
@ -189,16 +191,18 @@ export class SchemaModel {
} }
if (this.hasType('object')) { if (this.hasType('object')) {
this.fields = buildFields(parser, schema, this.pointer, this.options); this.fields = buildFields(parser, schema, this.pointer, this.options, this.refsStack);
} else if (this.hasType('array')) { } else if (this.hasType('array')) {
if (isArray(schema.items) || isArray(schema.prefixItems)) { if (isArray(schema.items) || isArray(schema.prefixItems)) {
this.fields = buildFields(parser, schema, this.pointer, this.options); this.fields = buildFields(parser, schema, this.pointer, this.options, this.refsStack);
} else if (isObject(schema.items)) { } else if (isObject(schema.items)) {
this.items = new SchemaModel( this.items = new SchemaModel(
parser, parser,
schema.items as OpenAPISchema, schema.items as OpenAPISchema,
this.pointer + '/items', this.pointer + '/items',
this.options, this.options,
false,
this.refsStack,
); );
} }
@ -231,9 +235,9 @@ export class SchemaModel {
private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) { private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) {
this.oneOf = oneOf!.map((variant, idx) => { this.oneOf = oneOf!.map((variant, idx) => {
const derefVariant = parser.deref(variant, false, true); const { resolved: derefVariant, refsStack } = parser.deref(variant, this.refsStack, true);
const merged = parser.mergeAllOf(derefVariant, this.pointer + '/oneOf/' + idx); const merged = parser.mergeAllOf(derefVariant, this.pointer + '/oneOf/' + idx, refsStack);
// try to infer title // try to infer title
const title = const title =
@ -252,13 +256,10 @@ export class SchemaModel {
} as OpenAPISchema, } as OpenAPISchema,
this.pointer + '/oneOf/' + idx, this.pointer + '/oneOf/' + idx,
this.options, this.options,
false,
this.refsStack,
); );
parser.exitRef(variant);
// each oneOf should be independent so exiting all the parent refs
// otherwise it will cause false-positive recursive detection
parser.exitParents(merged);
return schema; return schema;
}); });
@ -280,16 +281,11 @@ export class SchemaModel {
} }
} }
private initDiscriminator( private initDiscriminator(schema: OpenAPISchema, parser: OpenAPIParser) {
schema: OpenAPISchema & {
parentRefs?: string[];
},
parser: OpenAPIParser,
) {
const discriminator = getDiscriminator(schema)!; const discriminator = getDiscriminator(schema)!;
this.discriminatorProp = discriminator.propertyName; this.discriminatorProp = discriminator.propertyName;
const implicitInversedMapping = parser.findDerived([ const implicitInversedMapping = parser.findDerived([
...(schema.parentRefs || []), ...(this.schema['x-parentRefs'] || []),
this.pointer, this.pointer,
]); ]);
@ -372,7 +368,14 @@ export class SchemaModel {
} }
this.oneOf = refs.map(({ $ref, name }) => { this.oneOf = refs.map(({ $ref, name }) => {
const innerSchema = new SchemaModel(parser, parser.byRef($ref)!, $ref, this.options, true); const innerSchema = new SchemaModel(
parser,
{ $ref },
$ref,
this.options,
true,
this.refsStack.slice(0, -1),
);
innerSchema.title = name; innerSchema.title = name;
return innerSchema; return innerSchema;
}); });
@ -405,6 +408,8 @@ export class SchemaModel {
} as OpenAPISchema, } as OpenAPISchema,
this.pointer + '/oneOf/' + idx, this.pointer + '/oneOf/' + idx,
this.options, this.options,
false,
this.refsStack,
), ),
); );
this.oneOfType = 'One of'; this.oneOfType = 'One of';
@ -416,6 +421,7 @@ function buildFields(
schema: OpenAPISchema, schema: OpenAPISchema,
$ref: string, $ref: string,
options: RedocNormalizedOptions, options: RedocNormalizedOptions,
refsStack: string[],
): FieldModel[] { ): FieldModel[] {
const props = schema.properties || schema.prefixItems || schema.items || {}; const props = schema.properties || schema.prefixItems || schema.items || {};
const patternProps = schema.patternProperties || {}; const patternProps = schema.patternProperties || {};
@ -447,6 +453,7 @@ function buildFields(
}, },
$ref + '/properties/' + fieldName, $ref + '/properties/' + fieldName,
options, options,
refsStack,
); );
}); });
@ -479,6 +486,7 @@ function buildFields(
}, },
`${$ref}/patternProperties/${fieldName}`, `${$ref}/patternProperties/${fieldName}`,
options, options,
refsStack,
); );
}), }),
); );
@ -498,6 +506,7 @@ function buildFields(
}, },
$ref + '/additionalProperties', $ref + '/additionalProperties',
options, options,
refsStack,
), ),
); );
} }
@ -509,6 +518,7 @@ function buildFields(
fieldsCount: fields.length, fieldsCount: fields.length,
$ref, $ref,
options, options,
refsStack,
}), }),
); );
@ -521,12 +531,14 @@ function buildAdditionalItems({
fieldsCount, fieldsCount,
$ref, $ref,
options, options,
refsStack,
}: { }: {
parser: OpenAPIParser; parser: OpenAPIParser;
schema?: OpenAPISchema | OpenAPISchema[] | boolean; schema?: OpenAPISchema | OpenAPISchema[] | boolean;
fieldsCount: number; fieldsCount: number;
$ref: string; $ref: string;
options: RedocNormalizedOptions; options: RedocNormalizedOptions;
refsStack: string[];
}) { }) {
if (isBoolean(schema)) { if (isBoolean(schema)) {
return schema return schema
@ -539,6 +551,7 @@ function buildAdditionalItems({
}, },
`${$ref}/additionalItems`, `${$ref}/additionalItems`,
options, options,
refsStack,
), ),
] ]
: []; : [];
@ -556,6 +569,7 @@ function buildAdditionalItems({
}, },
`${$ref}/additionalItems`, `${$ref}/additionalItems`,
options, options,
refsStack,
), ),
), ),
]; ];
@ -571,6 +585,7 @@ function buildAdditionalItems({
}, },
`${$ref}/additionalItems`, `${$ref}/additionalItems`,
options, options,
refsStack,
), ),
]; ];
} }

View File

@ -1,5 +1,5 @@
import { OpenAPISecurityRequirement, OpenAPISecurityScheme } from '../../types'; import type { OpenAPISecurityRequirement, OpenAPISecurityScheme } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
export interface SecurityScheme extends OpenAPISecurityScheme { export interface SecurityScheme extends OpenAPISecurityScheme {
id: string; id: string;
@ -16,7 +16,7 @@ export class SecurityRequirementModel {
this.schemes = Object.keys(requirement || {}) this.schemes = Object.keys(requirement || {})
.map(id => { .map(id => {
const scheme = parser.deref(schemes[id]); const { resolved: scheme } = parser.deref(schemes[id]);
const scopes = requirement[id] || []; const scopes = requirement[id] || [];
if (!scheme) { if (!scheme) {

View File

@ -1,6 +1,6 @@
import { OpenAPISecurityScheme, Referenced } from '../../types'; import type { OpenAPISecurityScheme, Referenced } from '../../types';
import { SECURITY_SCHEMES_SECTION_PREFIX } from '../../utils'; import { SECURITY_SCHEMES_SECTION_PREFIX } from '../../utils';
import { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
export class SecuritySchemeModel { export class SecuritySchemeModel {
id: string; id: string;
@ -24,7 +24,7 @@ export class SecuritySchemeModel {
}; };
constructor(parser: OpenAPIParser, id: string, scheme: Referenced<OpenAPISecurityScheme>) { constructor(parser: OpenAPIParser, id: string, scheme: Referenced<OpenAPISecurityScheme>) {
const info = parser.deref(scheme); const { resolved: info } = parser.deref(scheme);
this.id = id; this.id = id;
this.sectionId = SECURITY_SCHEMES_SECTION_PREFIX + id; this.sectionId = SECURITY_SCHEMES_SECTION_PREFIX + id;
this.type = info.type; this.type = info.type;

View File

@ -1,7 +1,7 @@
import { OpenAPIPath, Referenced } from '../../types'; import type { OpenAPIPath, Referenced } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
import { OperationModel } from './Operation'; import { OperationModel } from './Operation';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { isOperationName } from '../..'; import { isOperationName } from '../..';
export class WebhookModel { export class WebhookModel {
@ -12,8 +12,7 @@ export class WebhookModel {
options: RedocNormalizedOptions, options: RedocNormalizedOptions,
infoOrRef?: Referenced<OpenAPIPath>, infoOrRef?: Referenced<OpenAPIPath>,
) { ) {
const webhooks = parser.deref<OpenAPIPath>(infoOrRef || {}); const { resolved: webhooks } = parser.deref<OpenAPIPath>(infoOrRef || {});
parser.exitRef(infoOrRef);
this.initWebhooks(parser, webhooks, options); this.initWebhooks(parser, webhooks, options);
} }

131
src/services/types.ts Normal file
View File

@ -0,0 +1,131 @@
import {
OpenAPIOperation,
OpenAPIParameter,
OpenAPISchema,
OpenAPIServer,
OpenAPITag,
Referenced,
} from '../types';
import { AppStore } from './AppStore';
import { GroupModel } from './models';
import { OperationModel } from './models/Operation';
import { RedocRawOptions } from './RedocNormalizedOptions';
export interface StoreState {
menu: {
activeItemIdx: number;
};
spec: {
url?: string;
data: any;
};
searchIndex: any;
options: RedocRawOptions;
}
export interface LabelsConfig {
enum: string;
enumSingleValue: string;
enumArray: string;
default: string;
deprecated: string;
example: string;
examples: string;
recursive: string;
arrayOf: string;
webhook: string;
const: string;
noResultsFound: string;
download: string;
downloadSpecification: string;
responses: string;
callbackResponses: string;
requestSamples: string;
responseSamples: string;
}
export type LabelsConfigRaw = Partial<LabelsConfig>;
export interface MDXComponentMeta {
component: React.ComponentType;
propsSelector: (store?: AppStore) => any;
props?: object;
}
export interface MarkdownHeading {
id: string;
name: string;
level: number;
items?: MarkdownHeading[];
description?: string;
}
export type ContentItemModel = GroupModel | OperationModel;
export type TagInfo = OpenAPITag & {
operations: ExtendedOpenAPIOperation[];
used?: boolean;
};
export type ExtendedOpenAPIOperation = {
pointer: string;
pathName: string;
httpVerb: string;
pathParameters: Array<Referenced<OpenAPIParameter>>;
pathServers: Array<OpenAPIServer> | undefined;
isWebhook: boolean;
} & OpenAPIOperation;
export type TagsInfoMap = Record<string, TagInfo>;
export interface TagGroup {
name: string;
tags: string[];
}
export type MenuItemGroupType = 'group' | 'tag' | 'section';
export type MenuItemType = MenuItemGroupType | 'operation';
export interface IMenuItem {
id: string;
absoluteIdx?: number;
name: string;
sidebarLabel: string;
description?: string;
depth: number;
active: boolean;
expanded: boolean;
items: IMenuItem[];
parent?: IMenuItem;
deprecated?: boolean;
type: MenuItemType;
deactivate(): void;
activate(): void;
collapse(): void;
expand(): void;
}
export interface SearchDocument {
title: string;
description: string;
id: string;
}
export interface SearchResult<T = string> {
meta: T;
score: number;
}
export enum SideNavStyleEnum {
SummaryOnly = 'summary-only',
PathOnly = 'path-only',
IdOnly = 'id-only',
}
export type MergedOpenAPISchema = OpenAPISchema & {
'x-refsStack'?: string[];
'x-parentRefs'?: string[];
'x-circular-ref'?: boolean;
};

View File

@ -3,9 +3,10 @@ import { hydrate as hydrateComponent, render } from 'react-dom';
import { configure } from 'mobx'; import { configure } from 'mobx';
import { Redoc, RedocStandalone } from './components/'; import { Redoc, RedocStandalone } from './components/';
import { AppStore, StoreState } from './services/AppStore'; import { AppStore } from './services/AppStore';
import { debugTime, debugTimeEnd } from './utils/debug'; import { debugTime, debugTimeEnd } from './utils/debug';
import { querySelector } from './utils/dom'; import { querySelector } from './utils/dom';
import type { StoreState } from './services';
configure({ configure({
useProxies: 'ifavailable', useProxies: 'ifavailable',

View File

@ -40,7 +40,10 @@ export interface OpenAPIPaths {
[path: string]: OpenAPIPath; [path: string]: OpenAPIPath;
} }
export interface OpenAPIRef { export interface OpenAPIRef {
'x-refsStack'?: string[];
$ref: string; $ref: string;
summary?: string;
description?: string;
} }
export type Referenced<T> = OpenAPIRef | T; export type Referenced<T> = OpenAPIRef | T;

View File

@ -121,6 +121,10 @@ export function isPrimitiveType(
schema: OpenAPISchema, schema: OpenAPISchema,
type: string | string[] | undefined = schema.type, type: string | string[] | undefined = schema.type,
) { ) {
if (schema['x-circular-ref']) {
return true;
}
if (schema.oneOf !== undefined || schema.anyOf !== undefined) { if (schema.oneOf !== undefined || schema.anyOf !== undefined) {
return false; return false;
} }
@ -552,13 +556,13 @@ export function mergeParams(
): Array<Referenced<OpenAPIParameter>> { ): Array<Referenced<OpenAPIParameter>> {
const operationParamNames = {}; const operationParamNames = {};
operationParams.forEach(param => { operationParams.forEach(param => {
param = parser.shallowDeref(param); ({ resolved: param } = parser.deref(param));
operationParamNames[param.name + '_' + param.in] = true; operationParamNames[param.name + '_' + param.in] = true;
}); });
// filter out path params overridden by operation ones with the same name // filter out path params overridden by operation ones with the same name
pathParams = pathParams.filter(param => { pathParams = pathParams.filter(param => {
param = parser.shallowDeref(param); ({ resolved: param } = parser.deref(param));
return !operationParamNames[param.name + '_' + param.in]; return !operationParamNames[param.name + '_' + param.in];
}); });
@ -643,6 +647,8 @@ export const shortenHTTPVerb = verb =>
export function isRedocExtension(key: string): boolean { export function isRedocExtension(key: string): boolean {
const redocExtensions = { const redocExtensions = {
'x-circular-ref': true, 'x-circular-ref': true,
'x-parentRefs': true,
'x-refsStack': true,
'x-code-samples': true, // deprecated 'x-code-samples': true, // deprecated
'x-codeSamples': true, 'x-codeSamples': true,
'x-displayName': true, 'x-displayName': true,