{
{ key: idx },
);
}
- return ;
+ const PartComponent = part.component as React.FunctionComponent;
+ return ;
});
}
}
diff --git a/src/components/Schema/RecursiveSchema.tsx b/src/components/Schema/RecursiveSchema.tsx
new file mode 100644
index 00000000..ad730d5f
--- /dev/null
+++ b/src/components/Schema/RecursiveSchema.tsx
@@ -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 (
+
+ {schema.displayType}
+ {schema.title && {schema.title} }
+ {l('recursive')}
+
+ );
+});
diff --git a/src/components/Schema/Schema.tsx b/src/components/Schema/Schema.tsx
index badd2abe..c0d38b1e 100644
--- a/src/components/Schema/Schema.tsx
+++ b/src/components/Schema/Schema.tsx
@@ -1,7 +1,6 @@
import { observer } from 'mobx-react';
import * as React from 'react';
-import { RecursiveLabel, TypeName, TypeTitle } from '../../common-elements/fields';
import { FieldDetails } from '../Fields/FieldDetails';
import { FieldModel, SchemaModel } from '../../services/models';
@@ -9,8 +8,8 @@ import { FieldModel, SchemaModel } from '../../services/models';
import { ArraySchema } from './ArraySchema';
import { ObjectSchema } from './ObjectSchema';
import { OneOfSchema } from './OneOfSchema';
+import { RecursiveSchema } from './RecursiveSchema';
-import { l } from '../../services/Labels';
import { isArray } from '../../utils/helpers';
export interface SchemaOptions {
@@ -36,13 +35,7 @@ export class Schema extends React.Component> {
const { type, oneOf, discriminatorProp, isCircular } = schema;
if (isCircular) {
- return (
-
- {schema.displayType}
- {schema.title && {schema.title} }
- {l('recursive')}
-
- );
+ return ;
}
if (discriminatorProp !== undefined) {
@@ -52,11 +45,14 @@ export class Schema extends React.Component> {
);
return null;
}
- return (
+ const activeSchema = oneOf[schema.activeOneOf];
+ return activeSchema.isCircular ? (
+
+ ) : (
({
- item: this.props.getItemById(res.meta)!,
- score: res.score,
- }));
-
- results.sort((a, b) => b.score - a.score);
+ const results = this.state.results
+ .filter(res => this.props.getItemById(res.meta))
+ .map(res => ({
+ item: this.props.getItemById(res.meta)!,
+ score: res.score,
+ }))
+ .sort((a, b) => b.score - a.score);
return (
diff --git a/src/components/SecurityRequirement/SecurityHeader.tsx b/src/components/SecurityRequirement/SecurityHeader.tsx
index c542b51c..12047db1 100644
--- a/src/components/SecurityRequirement/SecurityHeader.tsx
+++ b/src/components/SecurityRequirement/SecurityHeader.tsx
@@ -17,6 +17,8 @@ export function SecurityHeader(props: SecurityRequirementProps) {
const { security, showSecuritySchemeType, expanded } = props;
const grouping = security.schemes.length > 1;
+ if (security.schemes.length === 0)
+ return None;
return (
{grouping && '('}
diff --git a/src/components/SecurityRequirement/SecurityRequirement.tsx b/src/components/SecurityRequirement/SecurityRequirement.tsx
index 5ae43782..ca8e27a7 100644
--- a/src/components/SecurityRequirement/SecurityRequirement.tsx
+++ b/src/components/SecurityRequirement/SecurityRequirement.tsx
@@ -92,7 +92,7 @@ function getRequiredScopes(id: string, securities: SecurityRequirementModel[]):
let schemesLength = security.schemes.length;
while (schemesLength--) {
const scheme = security.schemes[schemesLength];
- if (scheme.id === id) {
+ if (scheme.id === id && Array.isArray(scheme.scopes)) {
allScopes.push(...scheme.scopes);
}
}
diff --git a/src/components/SideMenu/Logo.tsx b/src/components/SideMenu/Logo.tsx
new file mode 100644
index 00000000..c3e0bfe3
--- /dev/null
+++ b/src/components/SideMenu/Logo.tsx
@@ -0,0 +1,18 @@
+import { useEffect, useState } from 'react';
+import * as React from 'react';
+
+export default function RedoclyLogo(): JSX.Element | null {
+ const [isDisplay, setDisplay] = useState(false);
+
+ useEffect(() => {
+ setDisplay(true);
+ }, []);
+
+ return isDisplay ? (
+
setDisplay(false)}
+ src={'https://cdn.redoc.ly/redoc/logo-mini.svg'}
+ />
+ ) : null;
+}
diff --git a/src/components/SideMenu/MenuItem.tsx b/src/components/SideMenu/MenuItem.tsx
index 5e79943b..163b7d52 100644
--- a/src/components/SideMenu/MenuItem.tsx
+++ b/src/components/SideMenu/MenuItem.tsx
@@ -2,13 +2,14 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { ShelfIcon } from '../../common-elements/shelfs';
-import { IMenuItem, OperationModel } from '../../services';
+import { OperationModel } from '../../services';
import { shortenHTTPVerb } from '../../utils/openapi';
import { MenuItems } from './MenuItems';
import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements';
import { l } from '../../services/Labels';
import { scrollIntoViewIfNeeded } from '../../utils';
import { OptionsContext } from '../OptionsProvider';
+import type { IMenuItem } from '../../services';
export interface MenuItemProps {
item: IMenuItem;
diff --git a/src/components/SideMenu/MenuItems.tsx b/src/components/SideMenu/MenuItems.tsx
index e2e97f64..ec9eef8e 100644
--- a/src/components/SideMenu/MenuItems.tsx
+++ b/src/components/SideMenu/MenuItems.tsx
@@ -1,7 +1,7 @@
import { observer } from 'mobx-react';
import * as React from 'react';
-import { IMenuItem } from '../../services';
+import type { IMenuItem } from '../../services';
import { MenuItem } from './MenuItem';
import { MenuItemUl } from './styled.elements';
diff --git a/src/components/SideMenu/SideMenu.tsx b/src/components/SideMenu/SideMenu.tsx
index 96747a94..58dd34c5 100644
--- a/src/components/SideMenu/SideMenu.tsx
+++ b/src/components/SideMenu/SideMenu.tsx
@@ -1,12 +1,14 @@
import { observer } from 'mobx-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 { MenuItems } from './MenuItems';
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
import { RedocAttribution } from './styled.elements';
+import RedoclyLogo from './Logo';
@observer
export class SideMenu extends React.Component<{ menu: MenuStore; className?: string }> {
@@ -26,7 +28,8 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
- Documentation Powered by Redocly
+
+ API docs by Redocly
diff --git a/src/components/SideMenu/styled.elements.ts b/src/components/SideMenu/styled.elements.ts
index abc9f15e..8ca79a1a 100644
--- a/src/components/SideMenu/styled.elements.ts
+++ b/src/components/SideMenu/styled.elements.ts
@@ -2,7 +2,7 @@ import { default as classnames } from 'classnames';
import { darken } from 'polished';
import { deprecatedCss, ShelfIcon } from '../../common-elements';
-import styled, { css, ResolvedThemeInterface } from '../../styled-components';
+import styled, { css, media, ResolvedThemeInterface } from '../../styled-components';
export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
className: `operation-type ${props.type}`,
@@ -84,6 +84,10 @@ export const MenuItemUl = styled.ul<{ expanded: boolean }>`
margin: 0;
padding: 0;
+ &:first-child {
+ padding-bottom: 32px;
+ }
+
& & {
font-size: 0.929em;
}
@@ -169,21 +173,33 @@ export const MenuItemTitle = styled.span<{ width?: string }>`
`;
export const RedocAttribution = styled.div`
- ${({ theme }) => `
- font-size: 0.8em;
- margin-top: ${theme.spacing.unit * 2}px;
- padding: 0 ${theme.spacing.unit * 4}px;
- text-align: left;
+ ${({ theme }) => css`
+ font-size: 0.8em;
+ margin-top: ${theme.spacing.unit * 2}px;
+ text-align: center;
+ position: fixed;
+ width: ${theme.sidebar.width};
+ bottom: 0;
+ background: ${theme.sidebar.backgroundColor};
- opacity: 0.7;
-
- a,
- a:visited,
- a:hover {
- color: ${theme.sidebar.textColor} !important;
- border-top: 1px solid ${darken(0.1, theme.sidebar.backgroundColor)};
- padding: ${theme.spacing.unit}px 0;
- display: block;
+ a,
+ a:visited,
+ a:hover {
+ color: ${theme.sidebar.textColor} !important;
+ padding: ${theme.spacing.unit}px 0;
+ border-top: 1px solid ${darken(0.1, theme.sidebar.backgroundColor)};
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ `};
+ img {
+ width: 15px;
+ margin-right: 5px;
}
-`};
+
+ ${media.lessThan('small')`
+ width: 100%;
+ `};
`;
diff --git a/src/components/SourceCode/SourceCode.tsx b/src/components/SourceCode/SourceCode.tsx
index 12a580db..9a6e352f 100644
--- a/src/components/SourceCode/SourceCode.tsx
+++ b/src/components/SourceCode/SourceCode.tsx
@@ -9,24 +9,21 @@ export interface SourceCodeProps {
lang: string;
}
-export class SourceCode extends React.PureComponent {
- render() {
- const { source, lang } = this.props;
- return ;
- }
-}
+export const SourceCode = (props: SourceCodeProps) => {
+ const { source, lang } = props;
+ return ;
+};
-export class SourceCodeWithCopy extends React.Component {
- render() {
- return (
-
- {({ renderCopyButton }) => (
-
- {renderCopyButton()}
-
-
- )}
-
- );
- }
-}
+export const SourceCodeWithCopy = (props: SourceCodeProps) => {
+ const { source, lang } = props;
+ return (
+
+ {({ renderCopyButton }) => (
+
+ {renderCopyButton()}
+
+
+ )}
+
+ );
+};
diff --git a/src/components/__tests__/SecurityRequirement.test.tsx b/src/components/__tests__/SecurityRequirement.test.tsx
index 8d5e5761..ad17b81b 100644
--- a/src/components/__tests__/SecurityRequirement.test.tsx
+++ b/src/components/__tests__/SecurityRequirement.test.tsx
@@ -9,6 +9,7 @@ import {
SecuritySchemesModel,
} from '../../services';
import { StoreProvider } from '../StoreBuilder';
+import { SecurityRequirementModel } from '../../services/models/SecurityRequirement';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
import { withTheme } from '../testProviders';
import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes';
@@ -50,6 +51,20 @@ describe('SecurityRequirement', () => {
expect(component.html()).toMatchSnapshot();
});
+ it("should render 'None' when empty object in security open api", () => {
+ const options = new RedocNormalizedOptions({});
+ const parser = new OpenAPIParser(
+ { openapi: '3.0', info: { title: 'test', version: '0' }, paths: {} },
+ undefined,
+ options,
+ );
+ const securityRequirement = [new SecurityRequirementModel({}, parser)];
+ const component = mount(
+ withTheme(),
+ );
+ expect(component.find('span').at(0).text()).toEqual('None');
+ });
+
it('should hide authDefinition', async () => {
const store = await createStore(simpleSecurityFixture, undefined, {
hideSecuritySection: true,
diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap
index 53cb5829..5f865af5 100644
--- a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap
@@ -22,7 +22,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined,
"externalDocs": undefined,
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": false,
"maxItems": undefined,
"minItems": undefined,
@@ -70,7 +70,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined,
"externalDocs": undefined,
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": true,
"maxItems": undefined,
"minItems": undefined,
@@ -300,6 +300,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "number",
},
"readOnly": false,
+ "refsStack": Array [
+ "#/components/schemas/Dog",
+ "#/components/schemas/Dog/properties/packSize",
+ ],
"schema": Object {
"default": undefined,
"type": "number",
@@ -337,7 +341,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined,
"externalDocs": undefined,
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": true,
"maxItems": undefined,
"minItems": undefined,
@@ -565,11 +569,27 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"rawSchema": Object {
"default": undefined,
"type": "string",
+ "x-refsStack": Array [
+ "#/components/schemas/Dog",
+ "#/components/schemas/Pet",
+ ],
},
"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 {
"default": undefined,
"type": "string",
+ "x-refsStack": Array [
+ "#/components/schemas/Dog",
+ "#/components/schemas/Pet",
+ ],
},
"title": "",
"type": "string",
@@ -579,7 +599,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
},
],
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": false,
"maxItems": undefined,
"minItems": undefined,
@@ -818,27 +838,38 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "object",
},
"readOnly": false,
+ "refsStack": Array [
+ "#/components/schemas/Dog",
+ ],
"schema": Object {
"allOf": undefined,
+ "description": undefined,
"discriminator": Object {
"propertyName": "type",
},
- "parentRefs": Array [
- "#/components/schemas/Pet",
- ],
"properties": Object {
"packSize": Object {
"type": "number",
},
"type": Object {
"type": "string",
+ "x-refsStack": Array [
+ "#/components/schemas/Dog",
+ "#/components/schemas/Pet",
+ ],
},
},
+ "readOnly": undefined,
"required": Array [
"type",
],
"title": "Dog",
"type": "object",
+ "writeOnly": undefined,
+ "x-circular-ref": undefined,
+ "x-parentRefs": Array [
+ "#/components/schemas/Pet",
+ ],
},
"title": "Dog",
"type": "object",
@@ -888,7 +919,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined,
"externalDocs": undefined,
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": true,
"maxItems": undefined,
"minItems": undefined,
@@ -1116,11 +1147,27 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"rawSchema": Object {
"default": undefined,
"type": "string",
+ "x-refsStack": Array [
+ "#/components/schemas/Cat",
+ "#/components/schemas/Pet",
+ ],
},
"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 {
"default": undefined,
"type": "string",
+ "x-refsStack": Array [
+ "#/components/schemas/Cat",
+ "#/components/schemas/Pet",
+ ],
},
"title": "",
"type": "string",
@@ -1155,7 +1202,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined,
"externalDocs": undefined,
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": true,
"maxItems": undefined,
"minItems": undefined,
@@ -1383,11 +1430,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"rawSchema": Object {
"default": undefined,
"type": "number",
+ "x-refsStack": Array [
+ "#/components/schemas/Cat",
+ ],
},
"readOnly": false,
+ "refsStack": Array [
+ "#/components/schemas/Cat",
+ "#/components/schemas/Cat",
+ "#/components/schemas/Cat",
+ "#/components/schemas/Cat/properties/packSize",
+ ],
"schema": Object {
"default": undefined,
"type": "number",
+ "x-refsStack": Array [
+ "#/components/schemas/Cat",
+ ],
},
"title": "",
"type": "number",
@@ -1397,7 +1456,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
},
],
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": false,
"maxItems": undefined,
"minItems": undefined,
@@ -1638,27 +1697,41 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "object",
},
"readOnly": false,
+ "refsStack": Array [
+ "#/components/schemas/Cat",
+ ],
"schema": Object {
"allOf": undefined,
+ "description": undefined,
"discriminator": Object {
"propertyName": "type",
},
- "parentRefs": Array [
- "#/components/schemas/Pet",
- ],
"properties": Object {
"packSize": Object {
"type": "number",
+ "x-refsStack": Array [
+ "#/components/schemas/Cat",
+ ],
},
"type": Object {
"type": "string",
+ "x-refsStack": Array [
+ "#/components/schemas/Cat",
+ "#/components/schemas/Pet",
+ ],
},
},
+ "readOnly": undefined,
"required": Array [
"type",
],
"title": "Cat",
"type": "object",
+ "writeOnly": undefined,
+ "x-circular-ref": undefined,
+ "x-parentRefs": Array [
+ "#/components/schemas/Pet",
+ ],
},
"title": "Cat",
"type": "object",
@@ -1902,6 +1975,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "object",
},
"readOnly": false,
+ "refsStack": Array [
+ "#/components/schemas/Pet",
+ ],
"schema": Object {
"discriminator": Object {
"propertyName": "type",
@@ -1968,7 +2044,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined,
"externalDocs": undefined,
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": true,
"maxItems": undefined,
"minItems": undefined,
@@ -2198,6 +2274,10 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "number",
},
"readOnly": false,
+ "refsStack": Array [
+ "#/components/schemas/Dog",
+ "#/components/schemas/Dog/properties/packSize",
+ ],
"schema": Object {
"default": undefined,
"type": "number",
@@ -2235,7 +2315,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"examples": undefined,
"externalDocs": undefined,
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": true,
"maxItems": undefined,
"minItems": undefined,
@@ -2463,11 +2543,27 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"rawSchema": Object {
"default": undefined,
"type": "string",
+ "x-refsStack": Array [
+ "#/components/schemas/Dog",
+ "#/components/schemas/Pet",
+ ],
},
"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 {
"default": undefined,
"type": "string",
+ "x-refsStack": Array [
+ "#/components/schemas/Dog",
+ "#/components/schemas/Pet",
+ ],
},
"title": "",
"type": "string",
@@ -2477,7 +2573,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
},
],
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": false,
"maxItems": undefined,
"minItems": undefined,
@@ -2716,27 +2812,38 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"type": "object",
},
"readOnly": false,
+ "refsStack": Array [
+ "#/components/schemas/Dog",
+ ],
"schema": Object {
"allOf": undefined,
+ "description": undefined,
"discriminator": Object {
"propertyName": "type",
},
- "parentRefs": Array [
- "#/components/schemas/Pet",
- ],
"properties": Object {
"packSize": Object {
"type": "number",
},
"type": Object {
"type": "string",
+ "x-refsStack": Array [
+ "#/components/schemas/Dog",
+ "#/components/schemas/Pet",
+ ],
},
},
+ "readOnly": undefined,
"required": Array [
"type",
],
"title": "Dog",
"type": "object",
+ "writeOnly": undefined,
+ "x-circular-ref": undefined,
+ "x-parentRefs": Array [
+ "#/components/schemas/Pet",
+ ],
},
"title": "Dog",
"type": "object",
@@ -2780,7 +2887,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"examples": undefined,
"externalDocs": undefined,
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": true,
"maxItems": undefined,
"minItems": undefined,
@@ -2792,6 +2899,10 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"type": "number",
},
"readOnly": false,
+ "refsStack": Array [
+ "#/components/schemas/Dog",
+ "#/components/schemas/Dog/properties/packSize",
+ ],
"schema": Object {
"default": undefined,
"type": "number",
@@ -2837,7 +2948,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"examples": undefined,
"externalDocs": undefined,
"format": undefined,
- "isCircular": undefined,
+ "isCircular": false,
"isPrimitive": true,
"maxItems": undefined,
"minItems": undefined,
@@ -2847,11 +2958,27 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"rawSchema": Object {
"default": undefined,
"type": "string",
+ "x-refsStack": Array [
+ "#/components/schemas/Dog",
+ "#/components/schemas/Pet",
+ ],
},
"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 {
"default": undefined,
"type": "string",
+ "x-refsStack": Array [
+ "#/components/schemas/Dog",
+ "#/components/schemas/Pet",
+ ],
},
"title": "",
"type": "string",
diff --git a/src/services/AppStore.ts b/src/services/AppStore.ts
index 6c3457cd..082557f3 100644
--- a/src/services/AppStore.ts
+++ b/src/services/AppStore.ts
@@ -1,32 +1,26 @@
import { Lambda, observe } from 'mobx';
-import { OpenAPISpec } from '../types';
+import type { OpenAPISpec } from '../types';
import { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
import { history } from './HistoryService';
import { MarkerService } from './MarkerService';
import { MenuStore } from './MenuStore';
import { SpecStore } from './models';
-import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOptions';
+import { RedocNormalizedOptions } from './RedocNormalizedOptions';
+import type { RedocRawOptions } from './RedocNormalizedOptions';
import { ScrollService } from './ScrollService';
import { SearchStore } from './SearchStore';
import { SchemaDefinition } from '../components/SchemaDefinition/SchemaDefinition';
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,
+ OLD_SECURITY_DEFINITIONS_JSX_NAME,
+} from '../utils/openapi';
import { IS_BROWSER } from '../utils';
-
-export interface StoreState {
- menu: {
- activeItemIdx: number;
- };
- spec: {
- url?: string;
- data: any;
- };
- searchIndex: any;
- options: RedocRawOptions;
-}
+import type { StoreState } from './types';
export async function createStore(
spec: object,
@@ -160,6 +154,12 @@ const DEFAULT_OPTIONS: RedocRawOptions = {
securitySchemes: store.spec.securitySchemes,
}),
},
+ [OLD_SECURITY_DEFINITIONS_JSX_NAME]: {
+ component: SecurityDefs,
+ propsSelector: (store: AppStore) => ({
+ securitySchemes: store.spec.securitySchemes,
+ }),
+ },
[SCHEMA_DEFINITION_JSX_NAME]: {
component: SchemaDefinition,
propsSelector: (store: AppStore) => ({
diff --git a/src/services/Labels.ts b/src/services/Labels.ts
index 378ef143..345db271 100644
--- a/src/services/Labels.ts
+++ b/src/services/Labels.ts
@@ -1,25 +1,4 @@
-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;
+import type { LabelsConfig, LabelsConfigRaw } from './types';
const labels: LabelsConfig = {
enum: 'Enum',
diff --git a/src/services/MarkdownRenderer.ts b/src/services/MarkdownRenderer.ts
index 80408ab7..ba0c3d77 100644
--- a/src/services/MarkdownRenderer.ts
+++ b/src/services/MarkdownRenderer.ts
@@ -1,9 +1,8 @@
-import * as React from 'react';
import { marked } from 'marked';
import { highlight, safeSlugify, unescapeHTMLChars } from '../utils';
-import { AppStore } from './AppStore';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
+import type { MarkdownHeading, MDXComponentMeta } from './types';
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 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) {
return ``;
}
@@ -78,7 +63,7 @@ export class MarkdownRenderer {
parentId?: string,
): MarkdownHeading {
name = unescapeHTMLChars(name);
- const item = {
+ const item: MarkdownHeading = {
id: parentId
? `${parentId}/${safeSlugify(name)}`
: `${this.parentId || 'section'}/${safeSlugify(name)}`,
@@ -105,7 +90,7 @@ export class MarkdownRenderer {
attachHeadingsDescriptions(rawText: string) {
const buildRegexp = (heading: MarkdownHeading) => {
return new RegExp(
- `##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}\s*(\n|\r\n)`,
+ `##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}\s*(\n|\r\n|$|\s*)`,
);
};
diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts
index cc428c07..95b12ee3 100644
--- a/src/services/MenuBuilder.ts
+++ b/src/services/MenuBuilder.ts
@@ -1,41 +1,12 @@
-import {
- OpenAPIOperation,
- OpenAPIParameter,
- OpenAPISpec,
- OpenAPITag,
- Referenced,
- OpenAPIServer,
- OpenAPIPaths,
-} from '../types';
+import type { OpenAPISpec, OpenAPIPaths } from '../types';
import { isOperationName, JsonPointer, alphabeticallyByProp } from '../utils';
import { MarkdownRenderer } from './MarkdownRenderer';
import { GroupModel, OperationModel } from './models';
-import { OpenAPIParser } from './OpenAPIParser';
-import { RedocNormalizedOptions } from './RedocNormalizedOptions';
-
-export type TagInfo = OpenAPITag & {
- operations: ExtendedOpenAPIOperation[];
- used?: boolean;
-};
-
-export type ExtendedOpenAPIOperation = {
- pointer: string;
- pathName: string;
- httpVerb: string;
- pathParameters: Array>;
- pathServers: Array | undefined;
- isWebhook: boolean;
-} & OpenAPIOperation;
-
-export type TagsInfoMap = Record;
-
-export interface TagGroup {
- name: string;
- tags: string[];
-}
+import type { OpenAPIParser } from './OpenAPIParser';
+import type { RedocNormalizedOptions } from './RedocNormalizedOptions';
+import type { ContentItemModel, TagGroup, TagInfo, TagsInfoMap } from './types';
export const GROUP_DEPTH = 0;
-export type ContentItemModel = GroupModel | OperationModel;
export class MenuBuilder {
/**
@@ -239,7 +210,7 @@ export class MenuBuilder {
for (const operationName of operations) {
const operationInfo = path[operationName];
if (path.$ref) {
- const resolvedPaths = parser.deref(path as OpenAPIPaths);
+ const { resolved: resolvedPaths } = parser.deref(path as OpenAPIPaths);
getTags(parser, { [pathName]: resolvedPaths }, isWebhook);
continue;
}
diff --git a/src/services/MenuStore.ts b/src/services/MenuStore.ts
index 75a9956d..34fb5e08 100644
--- a/src/services/MenuStore.ts
+++ b/src/services/MenuStore.ts
@@ -1,37 +1,15 @@
import { action, observable, makeObservable } from 'mobx';
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 { ScrollService } from './ScrollService';
-
-import { escapeHTMLAttrChars, flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
import { GROUP_DEPTH } from './MenuBuilder';
-export type MenuItemGroupType = 'group' | 'tag' | 'section';
-export type MenuItemType = MenuItemGroupType | 'operation';
+import type { SpecStore } from './models';
+import type { ScrollService } from './ScrollService';
+import type { IMenuItem } from './types';
/** 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';
diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts
index 15cd5239..61a755d5 100644
--- a/src/services/OpenAPIParser.ts
+++ b/src/services/OpenAPIParser.ts
@@ -1,45 +1,29 @@
-import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
-
-import { isArray, isBoolean, IS_BROWSER } from '../utils/';
+import type { OpenAPIRef, OpenAPISchema, OpenAPISpec } from '../types';
+import { IS_BROWSER, getDefinitionName } from '../utils/';
import { JsonPointer } from '../utils/JsonPointer';
-import { getDefinitionName, isNamedDefinition } from '../utils/openapi';
+
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];
- }
-}
+const MAX_DEREF_DEPTH = 999; // prevent circular detection crashes by adding hard limit on deref depth
/**
* 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 {
specUrl?: string;
spec: OpenAPISpec;
- private _refCounter: RefCounter = new RefCounter();
- private allowMergeRefs: boolean = false;
+ private readonly allowMergeRefs: boolean = false;
constructor(
spec: OpenAPISpec,
@@ -51,13 +35,13 @@ export class OpenAPIParser {
this.spec = spec;
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') {
- this.specUrl = new URL(specUrl, href).href;
+ this.specUrl = href ? new URL(specUrl, href).href : specUrl;
}
}
- validate(spec: any) {
+ validate(spec: GenericObject): void {
if (spec.openapi === undefined) {
throw new Error('Document must be valid OpenAPI 3.0.0 definition');
}
@@ -86,101 +70,92 @@ export class OpenAPIParser {
/**
* checks if the object is OpenAPI reference (contains $ref property)
*/
- isRef(obj: any): obj is OpenAPIRef {
+ isRef(obj: OpenAPIRef | T): obj is OpenAPIRef {
if (!obj) {
return false;
}
+ obj = obj;
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(ref: Referenced) {
- if (!this.isRef(ref)) {
- return;
- }
- this._refCounter.exit(ref.$ref);
- }
-
/**
* Resolve given reference object or return as is if it is not a reference
* @param obj object to dereference
* @param forceCircular whether to dereference even if it is circular ref
+ * @param mergeAsAllOf
*/
- deref(obj: OpenAPIRef | T, forceCircular = false, mergeAsAllOf = false): T {
+ deref(
+ 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)) {
const schemaName = getDefinitionName(obj.$ref);
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(obj.$ref)!;
- const visited = this._refCounter.visited(obj.$ref);
- this._refCounter.visit(obj.$ref);
- if (visited && !forceCircular) {
- // circular reference detected
- // tslint:disable-next-line
- return Object.assign({}, resolved, { 'x-circular-ref': true });
+ let resolved = this.byRef(obj.$ref);
+ if (!resolved) {
+ throw new Error(`Failed to resolve $ref "${obj.$ref}"`);
}
- // 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);
+
+ let refsStack = baseRefsStack;
+ if (baseRefsStack.includes(obj.$ref) || baseRefsStack.length > MAX_DEREF_DEPTH) {
+ resolved = Object.assign({}, resolved, { 'x-circular-ref': true });
+ } else if (this.isRef(resolved)) {
+ const res = this.deref(resolved, baseRefsStack, mergeAsAllOf);
+ refsStack = res.refsStack;
+ resolved = res.resolved;
}
- return this.allowMergeRefs ? this.mergeRefs(obj, resolved, mergeAsAllOf) : result;
+
+ refsStack = pushRef(baseRefsStack, obj.$ref);
+ resolved = this.allowMergeRefs ? this.mergeRefs(obj, resolved, mergeAsAllOf) : resolved;
+
+ return { resolved, refsStack };
}
- return obj;
+ return {
+ resolved: obj,
+ refsStack: concatRefStacks(baseRefsStack, objRefsStack),
+ };
}
- shallowDeref(obj: OpenAPIRef | T): T {
- if (this.isRef(obj)) {
- const schemaName = getDefinitionName(obj.$ref);
- if (schemaName && this.options.ignoreNamedSchemas.has(schemaName)) {
- return { type: 'object', title: schemaName } as T;
- }
- const resolved = this.byRef(obj.$ref);
- return this.allowMergeRefs ? this.mergeRefs(obj, resolved, false) : (resolved as T);
- }
- return obj;
- }
-
- mergeRefs(ref, resolved, mergeAsAllOf: boolean) {
+ mergeRefs(ref: OpenAPIRef, resolved: T, mergeAsAllOf: boolean): T {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { $ref, ...rest } = ref;
const keys = Object.keys(rest);
if (keys.length === 0) {
- if (this.isRef(resolved)) {
- return this.shallowDeref(resolved);
- }
return resolved;
}
if (
mergeAsAllOf &&
- keys.some(k => k !== 'description' && k !== 'title' && k !== 'externalDocs')
+ keys.some(
+ k =>
+ ![
+ 'description',
+ 'title',
+ 'externalDocs',
+ 'x-refsStack',
+ 'x-parentRefs',
+ 'readOnly',
+ 'writeOnly',
+ ].includes(k),
+ )
) {
+ const { description, title, readOnly, writeOnly, ...restSchema } = rest as OpenAPISchema;
return {
- allOf: [rest, resolved],
- };
+ allOf: [{ description, title, readOnly, writeOnly }, resolved, restSchema],
+ } as T;
} else {
// small optimization
return {
- ...resolved,
+ ...(resolved as GenericObject),
...rest,
- };
+ } as T;
}
}
@@ -189,18 +164,18 @@ export class OpenAPIParser {
* @param schema schema with allOF
* @param $ref pointer of the schema
* @param forceCircular whether to dereference children even if it is a circular ref
+ * @param used$Refs
*/
mergeAllOf(
- schema: OpenAPISchema,
- $ref?: string,
- forceCircular: boolean = false,
- used$Refs = new Set(),
+ schema: MergedOpenAPISchema,
+ $ref: string | undefined,
+ refsStack: string[],
): MergedOpenAPISchema {
- if ($ref) {
- used$Refs.add($ref);
+ if (schema['x-circular-ref']) {
+ return schema;
}
- schema = this.hoistOneOfs(schema);
+ schema = this.hoistOneOfs(schema, refsStack);
if (schema.allOf === undefined) {
return schema;
@@ -208,8 +183,8 @@ export class OpenAPIParser {
let receiver: MergedOpenAPISchema = {
...schema,
+ 'x-parentRefs': [],
allOf: undefined,
- parentRefs: [],
title: schema.title || getDefinitionName($ref),
};
@@ -221,36 +196,49 @@ export class OpenAPIParser {
receiver.items = { ...receiver.items };
}
- const allOfSchemas = schema.allOf
- .map(subSchema => {
- if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) {
- return undefined;
- }
+ const allOfSchemas = uniqByPropIncludeMissing(
+ schema.allOf
+ .map((subSchema: OpenAPISchema) => {
+ const { resolved, refsStack: subRefsStack } = this.deref(subSchema, refsStack, true);
- const resolved = this.deref(subSchema, forceCircular, true);
- const subRef = subSchema.$ref || undefined;
- const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs);
- receiver.parentRefs!.push(...(subMerged.parentRefs || []));
- return {
- $ref: subRef,
- schema: subMerged,
- };
- })
- .filter(child => child !== undefined) as Array<{
- $ref: string | undefined;
- schema: MergedOpenAPISchema;
- }>;
+ 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;
+ }
+ if (subRef) {
+ // collect information for implicit descriminator lookup
+ receiver['x-parentRefs']?.push(...(subMerged['x-parentRefs'] || []), subRef);
+ }
+ return {
+ $ref: subRef,
+ refsStack: pushRef(subRefsStack, subRef),
+ schema: subMerged,
+ };
+ })
+ .filter(child => child !== undefined) as Array<{
+ schema: MergedOpenAPISchema;
+ refsStack: string[];
+ $ref?: string;
+ }>,
+ '$ref',
+ );
- for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) {
+ for (const { schema: subSchema, refsStack: subRefsStack } of allOfSchemas) {
const {
type,
enum: enumProperty,
properties,
items,
required,
+ title,
+ description,
+ readOnly,
+ writeOnly,
oneOf,
anyOf,
- title,
+ 'x-circular-ref': isCircular,
...otherConstraints
} = subSchema;
@@ -268,49 +256,57 @@ export class OpenAPIParser {
if (enumProperty !== undefined) {
if (Array.isArray(enumProperty) && Array.isArray(receiver.enum)) {
- receiver.enum = [...enumProperty, ...receiver.enum];
+ receiver.enum = Array.from(new Set([...enumProperty, ...receiver.enum]));
} else {
receiver.enum = enumProperty;
}
}
- if (properties !== undefined) {
+ if (properties !== undefined && typeof properties === 'object') {
receiver.properties = receiver.properties || {};
for (const prop in properties) {
+ const propRefsStack = concatRefStacks(subRefsStack, properties[prop]?.['x-refsStack']);
if (!receiver.properties[prop]) {
- receiver.properties[prop] = properties[prop];
- } else {
+ receiver.properties[prop] = {
+ ...properties[prop],
+ 'x-refsStack': propRefsStack,
+ } as MergedOpenAPISchema;
+ } else if (!isCircular) {
// merge inner properties
const mergedProp = this.mergeAllOf(
- { allOf: [receiver.properties[prop], properties[prop]] },
+ {
+ allOf: [
+ receiver.properties[prop],
+ { ...properties[prop], 'x-refsStack': propRefsStack } as any,
+ ],
+ 'x-refsStack': propRefsStack,
+ },
$ref + '/properties/' + prop,
+ propRefsStack,
);
receiver.properties[prop] = mergedProp;
- this.exitParents(mergedProp); // every prop resolution should have separate recursive stack
}
}
}
- if (items !== undefined) {
- const receiverItems = isBoolean(receiver.items)
- ? { items: receiver.items }
- : receiver.items
- ? (Object.assign({}, receiver.items) as OpenAPISchema)
- : {};
- const subSchemaItems = isBoolean(items)
- ? { items }
- : (Object.assign({}, items) as OpenAPISchema);
+ if (items !== undefined && !isCircular) {
+ const receiverItems =
+ typeof receiver.items === 'boolean'
+ ? {}
+ : (Object.assign({}, receiver.items) as OpenAPISchema);
+ const subSchemaItems =
+ typeof subSchema.items === 'boolean'
+ ? {}
+ : (Object.assign({}, subSchema.items) as OpenAPISchema);
// merge inner properties
receiver.items = this.mergeAllOf(
- { allOf: [receiverItems, subSchemaItems] },
+ {
+ allOf: [receiverItems, subSchemaItems],
+ },
$ref + '/items',
+ subRefsStack,
);
}
-
- if (required !== undefined) {
- receiver.required = (receiver.required || []).concat(required);
- }
-
if (oneOf !== undefined) {
receiver.oneOf = oneOf;
}
@@ -319,18 +315,21 @@ export class OpenAPIParser {
receiver.anyOf = anyOf;
}
+ if (required !== undefined) {
+ receiver.required = [...(receiver.required || []), ...required];
+ }
+
// merge rest of constraints
// TODO: do more intelligent merge
- receiver = { ...receiver, title: receiver.title || title, ...otherConstraints };
-
- if (subSchemaRef) {
- receiver.parentRefs!.push(subSchemaRef);
- if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) {
- // this is not so correct behaviour. commented out for now
- // ref: https://github.com/Redocly/redoc/issues/601
- // receiver.title = JsonPointer.baseName(subSchemaRef);
- }
- }
+ receiver = {
+ ...receiver,
+ title: receiver.title || title,
+ description: receiver.description || description,
+ readOnly: receiver.readOnly !== undefined ? receiver.readOnly : readOnly,
+ writeOnly: receiver.writeOnly !== undefined ? receiver.writeOnly : writeOnly,
+ 'x-circular-ref': receiver['x-circular-ref'] || isCircular,
+ ...otherConstraints,
+ };
}
return receiver;
@@ -345,10 +344,12 @@ export class OpenAPIParser {
const res: Record = {};
const schemas = (this.spec.components && this.spec.components.schemas) || {};
for (const defName in schemas) {
- const def = this.deref(schemas[defName]);
+ const { resolved: def } = this.deref(schemas[defName]);
if (
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];
}
@@ -356,13 +357,7 @@ export class OpenAPIParser {
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, refsStack: string[]) {
if (schema.allOf === undefined) {
return schema;
}
@@ -370,19 +365,15 @@ export class OpenAPIParser {
const allOf = schema.allOf;
for (let i = 0; i < allOf.length; i++) {
const sub = allOf[i];
- if (isArray(sub.oneOf)) {
+ if (Array.isArray(sub.oneOf)) {
const beforeAllOf = allOf.slice(0, i);
const afterAllOf = allOf.slice(i + 1);
return {
- oneOf: sub.oneOf.map(part => {
- const merged = this.mergeAllOf({
+ oneOf: sub.oneOf.map((part: OpenAPISchema) => {
+ return {
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;
+ 'x-refsStack': refsStack,
+ };
}),
};
}
@@ -391,3 +382,15 @@ export class OpenAPIParser {
return schema;
}
}
+
+/**
+ * Unique array by property, missing properties are included
+ */
+function uniqByPropIncludeMissing(arr: T[], prop: keyof T): T[] {
+ const seen = new Set();
+ return arr.filter(item => {
+ const k = item[prop];
+ if (!k) return true;
+ return k && !seen.has(k) && seen.add(k);
+ });
+}
diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts
index 3efcbac7..4a219eef 100644
--- a/src/services/RedocNormalizedOptions.ts
+++ b/src/services/RedocNormalizedOptions.ts
@@ -2,14 +2,9 @@ import defaultTheme, { ResolvedThemeInterface, resolveTheme, ThemeInterface } fr
import { querySelector } from '../utils/dom';
import { isArray, isNumeric, mergeObjects } from '../utils/helpers';
-import { LabelsConfigRaw, setRedocLabels } from './Labels';
-import { MDXComponentMeta } from './MarkdownRenderer';
-
-export enum SideNavStyleEnum {
- SummaryOnly = 'summary-only',
- PathOnly = 'path-only',
- IdOnly = 'id-only',
-}
+import { setRedocLabels } from './Labels';
+import { SideNavStyleEnum } from './types';
+import type { LabelsConfigRaw, MDXComponentMeta } from './types';
export interface RedocRawOptions {
theme?: ThemeInterface;
diff --git a/src/services/ScrollService.ts b/src/services/ScrollService.ts
index 1843ea37..bbfa20fc 100644
--- a/src/services/ScrollService.ts
+++ b/src/services/ScrollService.ts
@@ -2,7 +2,7 @@ import { bind } from 'decko';
import * as EventEmitter from 'eventemitter3';
import { IS_BROWSER, querySelector, Throttle } from '../utils';
-import { RedocNormalizedOptions } from './RedocNormalizedOptions';
+import type { RedocNormalizedOptions } from './RedocNormalizedOptions';
const EVENT = 'scroll';
diff --git a/src/services/SearchStore.ts b/src/services/SearchStore.ts
index 927bc14c..feb19c69 100644
--- a/src/services/SearchStore.ts
+++ b/src/services/SearchStore.ts
@@ -1,6 +1,6 @@
import { IS_BROWSER } from '../utils/';
-import { IMenuItem } from './MenuStore';
-import { OperationModel } from './models';
+import type { IMenuItem } from './types';
+import type { OperationModel } from './models';
import Worker from './SearchWorker.worker';
@@ -26,7 +26,7 @@ export class SearchStore {
const recurse = items => {
items.forEach(group => {
if (group.type !== 'group') {
- this.add(group.name, group.description || '', group.id);
+ this.add(group.name, (group.description || '').concat(' ', group.path || ''), group.id);
}
recurse(group.items);
});
diff --git a/src/services/SearchWorker.worker.ts b/src/services/SearchWorker.worker.ts
index a4a6da4b..5b535656 100644
--- a/src/services/SearchWorker.worker.ts
+++ b/src/services/SearchWorker.worker.ts
@@ -1,4 +1,5 @@
import * as lunr from 'lunr';
+import type { SearchResult } from './types';
/* just for better typings */
export default class Worker {
@@ -11,17 +12,6 @@ export default class Worker {
fromExternalJS = fromExternalJS;
}
-export interface SearchDocument {
- title: string;
- description: string;
- id: string;
-}
-
-export interface SearchResult {
- meta: T;
- score: number;
-}
-
let store: any[] = [];
lunr.tokenizer.separator = /\s+/;
@@ -47,7 +37,10 @@ function initEmpty() {
initEmpty();
-const expandTerm = term => '*' + lunr.stemmer(new lunr.Token(term, {})) + '*';
+const expandTerm = term => {
+ const token = lunr.trimmer(new lunr.Token(term, {}));
+ return '*' + lunr.stemmer(token) + '*';
+};
export function add(title: string, description: string, meta?: T) {
const ref = store.push(meta) - 1;
diff --git a/src/services/SpecStore.ts b/src/services/SpecStore.ts
index 20023ce0..d0de7a9c 100644
--- a/src/services/SpecStore.ts
+++ b/src/services/SpecStore.ts
@@ -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 { WebhookModel } from './models/Webhook';
import { SecuritySchemesModel } from './models/SecuritySchemes';
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
*/
diff --git a/src/services/__tests__/MarkdownRenderer.test.ts b/src/services/__tests__/MarkdownRenderer.test.ts
index efd0d1fb..777822c0 100644
--- a/src/services/__tests__/MarkdownRenderer.test.ts
+++ b/src/services/__tests__/MarkdownRenderer.test.ts
@@ -1,4 +1,5 @@
-import { MarkdownRenderer, MDXComponentMeta } from '../MarkdownRenderer';
+import type { MDXComponentMeta } from '../types';
+import { MarkdownRenderer } from '../MarkdownRenderer';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
const TestComponent = () => null;
@@ -96,4 +97,22 @@ describe('Markdown renderer', () => {
expect(part.component).toBe(TestComponent);
expect(part.props).toEqual({ children: ' Test Test ' });
});
+
+ test('should properly extract title from text', () => {
+ const rawTexts = ['text before\n# Test', 'text before\n # Test', 'text before\n# Test\n'];
+ rawTexts.forEach(text => {
+ const headings = renderer.extractHeadings(text);
+ expect(headings).toHaveLength(1);
+ expect(headings[0].name).toEqual('Test');
+ expect(headings[0].description).toEqual('');
+ });
+
+ const rawTexts2 = ['# Test \n text after', '# Test \ntext after'];
+ rawTexts2.forEach(text => {
+ const headings = renderer.extractHeadings(text);
+ expect(headings).toHaveLength(1);
+ expect(headings[0].name).toEqual('Test');
+ expect(headings[0].description).toEqual('text after');
+ });
+ });
});
diff --git a/src/services/__tests__/OpenAPIParser.test.ts b/src/services/__tests__/OpenAPIParser.test.ts
index 24fbc330..d32e0f8e 100644
--- a/src/services/__tests__/OpenAPIParser.test.ts
+++ b/src/services/__tests__/OpenAPIParser.test.ts
@@ -41,14 +41,14 @@ describe('Models', () => {
expect(schema.title).toEqual('Foo');
});
- test('should merge oneOff to inside allOff', () => {
+ test('should merge oneOf to inside allOff', () => {
// TODO: should hoist
const spec = require('./fixtures/mergeAllOf.json');
parser = new OpenAPIParser(spec, undefined, opts);
const schema = parser.mergeAllOf(spec.components.schemas.Case4);
expect(schema.title).toEqual('Foo');
- expect(schema.parentRefs).toHaveLength(1);
- expect(schema.parentRefs[0]).toEqual('#/components/schemas/Ref');
+ expect(schema['x-parentRefs']).toHaveLength(1);
+ expect(schema['x-parentRefs'][0]).toEqual('#/components/schemas/Ref');
expect(schema.oneOf).toEqual([{ title: 'Bar' }, { title: 'Baz' }]);
});
@@ -60,7 +60,7 @@ describe('Models', () => {
description: 'Overriden description',
};
- expect(parser.shallowDeref(schemaOrRef)).toMatchSnapshot();
+ expect(parser.deref(schemaOrRef)).toMatchSnapshot();
});
test('should correct resolve double $ref if no need sibling', () => {
@@ -70,7 +70,7 @@ describe('Models', () => {
$ref: '#/components/schemas/Parent',
};
- expect(parser.deref(schemaOrRef, false, true)).toMatchSnapshot();
+ expect(parser.deref(schemaOrRef, [], true)).toMatchSnapshot();
});
});
});
diff --git a/src/services/__tests__/__snapshots__/OpenAPIParser.test.ts.snap b/src/services/__tests__/__snapshots__/OpenAPIParser.test.ts.snap
index 8b5ea018..3b983a1f 100644
--- a/src/services/__tests__/__snapshots__/OpenAPIParser.test.ts.snap
+++ b/src/services/__tests__/__snapshots__/OpenAPIParser.test.ts.snap
@@ -2,12 +2,17 @@
exports[`Models Schema should correct resolve double $ref if no need sibling 1`] = `
Object {
- "properties": Object {
- "test": Object {
- "type": "string",
+ "refsStack": Array [
+ "#/components/schemas/Parent",
+ ],
+ "resolved": Object {
+ "properties": Object {
+ "test": Object {
+ "type": "string",
+ },
},
+ "type": "object",
},
- "type": "object",
}
`;
@@ -15,84 +20,84 @@ exports[`Models Schema should hoist oneOfs when mergin allOf 1`] = `
Object {
"oneOf": Array [
Object {
- "oneOf": Array [
+ "allOf": Array [
Object {
- "allOf": undefined,
- "parentRefs": Array [],
"properties": Object {
- "extra": Object {
- "type": "string",
- },
- "password": Object {
- "description": "The user's password",
- "type": "string",
- },
"username": Object {
"description": "The user's name",
"type": "string",
},
},
- "title": undefined,
},
Object {
- "allOf": undefined,
- "parentRefs": Array [],
"properties": Object {
"extra": Object {
"type": "string",
},
- "mobile": Object {
- "description": "The user's mobile",
- "type": "string",
- },
- "username": Object {
- "description": "The user's name",
- "type": "string",
- },
},
- "title": undefined,
+ },
+ Object {
+ "oneOf": Array [
+ Object {
+ "properties": Object {
+ "password": Object {
+ "description": "The user's password",
+ "type": "string",
+ },
+ },
+ },
+ Object {
+ "properties": Object {
+ "mobile": Object {
+ "description": "The user's mobile",
+ "type": "string",
+ },
+ },
+ },
+ ],
},
],
+ "x-refsStack": undefined,
},
Object {
- "oneOf": Array [
+ "allOf": Array [
Object {
- "allOf": undefined,
- "parentRefs": Array [],
"properties": Object {
"email": Object {
"description": "The user's email",
"type": "string",
},
- "extra": Object {
- "type": "string",
- },
- "password": Object {
- "description": "The user's password",
- "type": "string",
- },
},
- "title": undefined,
},
Object {
- "allOf": undefined,
- "parentRefs": Array [],
"properties": Object {
- "email": Object {
- "description": "The user's email",
- "type": "string",
- },
"extra": Object {
"type": "string",
},
- "mobile": Object {
- "description": "The user's mobile",
- "type": "string",
- },
},
- "title": undefined,
+ },
+ Object {
+ "oneOf": Array [
+ Object {
+ "properties": Object {
+ "password": Object {
+ "description": "The user's password",
+ "type": "string",
+ },
+ },
+ },
+ Object {
+ "properties": Object {
+ "mobile": Object {
+ "description": "The user's mobile",
+ "type": "string",
+ },
+ },
+ },
+ ],
},
],
+ "x-refsStack": undefined,
},
],
}
@@ -100,7 +105,12 @@ Object {
exports[`Models Schema should override description from $ref of the referenced component, when sibling description exists 1`] = `
Object {
- "description": "Overriden description",
- "type": "object",
+ "refsStack": Array [
+ "#/components/schemas/Test",
+ ],
+ "resolved": Object {
+ "description": "Overriden description",
+ "type": "object",
+ },
}
`;
diff --git a/src/services/__tests__/fixtures/3.1/patternProperties.json b/src/services/__tests__/fixtures/3.1/patternProperties.json
index ec686421..39928813 100644
--- a/src/services/__tests__/fixtures/3.1/patternProperties.json
+++ b/src/services/__tests__/fixtures/3.1/patternProperties.json
@@ -25,6 +25,26 @@
}
}
}
+ },
+ "properties": {
+ "nestedObjectProp": {
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "type": "integer"
+ }
+ }
+ },
+ "nestedArrayProp": {
+ "type": "array",
+ "items": {
+ "patternProperties": {
+ ".*": {
+ "type": "string"
+ }
+ }
+ }
+ }
}
}
}
diff --git a/src/services/__tests__/fixtures/3.1/prefixItems.json b/src/services/__tests__/fixtures/3.1/prefixItems.json
index cd527297..9ad145e5 100644
--- a/src/services/__tests__/fixtures/3.1/prefixItems.json
+++ b/src/services/__tests__/fixtures/3.1/prefixItems.json
@@ -140,6 +140,14 @@
"$ref": "#/components/schemas/Tag"
}
},
+ "Case7": {
+ "type": "object",
+ "properties": {
+ "array_field": {
+ "$ref": "#/components/schemas/AnyArray"
+ }
+ }
+ },
"Cat": {
"type": "object",
"properties": {
@@ -178,6 +186,10 @@
"type": "integer",
"format": "int64",
"readOnly": true
+ },
+ "AnyArray": {
+ "type": "array",
+ "items": true
}
}
}
diff --git a/src/services/__tests__/fixtures/arrayItems.json b/src/services/__tests__/fixtures/arrayItems.json
index 51da75ea..c0cb7dce 100644
--- a/src/services/__tests__/fixtures/arrayItems.json
+++ b/src/services/__tests__/fixtures/arrayItems.json
@@ -140,6 +140,14 @@
"$ref": "#/components/schemas/Tag"
}
},
+ "Case7": {
+ "type": "object",
+ "properties": {
+ "array_field": {
+ "$ref": "#/components/schemas/AnyArray"
+ }
+ }
+ },
"Cat": {
"type": "object",
"properties": {
@@ -178,6 +186,10 @@
"type": "integer",
"format": "int64",
"readOnly": true
+ },
+ "AnyArray": {
+ "type": "array",
+ "items": true
}
}
}
diff --git a/src/services/__tests__/models/ApiInfo.test.ts b/src/services/__tests__/models/ApiInfo.test.ts
index 4a67139e..867d50e9 100644
--- a/src/services/__tests__/models/ApiInfo.test.ts
+++ b/src/services/__tests__/models/ApiInfo.test.ts
@@ -47,6 +47,30 @@ describe('Models', () => {
expect(info.summary).toEqual('Test summary\nsome text\n## Heading\n test');
});
+ test('should correctly populate description when 2nd line is started by white space', () => {
+ parser.spec = {
+ openapi: '3.0.0',
+ info: {
+ description: 'text before\n # Test',
+ },
+ } as any;
+
+ const info = new ApiInfoModel(parser);
+ expect(info.description).toEqual('text before\n');
+ });
+
+ test('should correctly populate description when 2nd line is only white space', () => {
+ parser.spec = {
+ openapi: '3.0.0',
+ info: {
+ description: 'text before\n \n # Test',
+ },
+ } as any;
+
+ const info = new ApiInfoModel(parser);
+ expect(info.description).toEqual('text before\n');
+ });
+
test('should correctly populate license identifier', () => {
parser.spec = {
openapi: '3.1.0',
diff --git a/src/services/__tests__/models/RequestBody.test.ts b/src/services/__tests__/models/RequestBody.test.ts
index 554a1222..e8720c9b 100644
--- a/src/services/__tests__/models/RequestBody.test.ts
+++ b/src/services/__tests__/models/RequestBody.test.ts
@@ -1,3 +1,5 @@
+import { parseYaml } from '@redocly/openapi-core';
+import { outdent } from 'outdent';
import { RequestBodyModel } from '../../models/RequestBody';
import { OpenAPIParser } from '../../OpenAPIParser';
import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
@@ -23,5 +25,55 @@ describe('Models', () => {
expect(consoleError).not.toHaveBeenCalled();
expect(req).toEqual({ description: '', required: false });
});
+
+ test('should have correct field data when it includes allOf', () => {
+ const spec = parseYaml(outdent`
+ openapi: 3.0.0
+ paths:
+ /user:
+ post:
+ tags:
+ - user
+ summary: Create user
+ description: This can only be done by the logged in user.
+ operationId: createUser
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ description: Created user object
+ required: true
+ components:
+ schemas:
+ User:
+ allOf:
+ - type: object
+ properties:
+ name:
+ type: string
+ description: correct description name
+ readOnly: false
+ writeOnly: false
+ allOf:
+ - $ref: '#/components/schemas/NameField'
+ NameField:
+ description: name description
+ readOnly: true
+ writeOnly: true
+ `) as any;
+
+ parser = new OpenAPIParser(spec, undefined, opts);
+ const req = new RequestBodyModel({
+ parser,
+ infoOrRef: spec.paths['/user'].post.requestBody,
+ options: opts,
+ isEvent: false,
+ });
+ const nameField = req.content?.mediaTypes[0].schema?.fields?.[0];
+ expect(nameField?.schema.readOnly).toBe(false);
+ expect(nameField?.schema.writeOnly).toBe(false);
+ expect(nameField?.description).toMatchInlineSnapshot(`"correct description name"`);
+ });
});
});
diff --git a/src/services/__tests__/models/Response.test.ts b/src/services/__tests__/models/Response.test.ts
index 26ec21b7..67f79003 100644
--- a/src/services/__tests__/models/Response.test.ts
+++ b/src/services/__tests__/models/Response.test.ts
@@ -1,3 +1,6 @@
+import { parseYaml } from '@redocly/openapi-core';
+import { outdent } from 'outdent';
+
import { ResponseModel } from '../../models/Response';
import { OpenAPIParser } from '../../OpenAPIParser';
import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
@@ -53,5 +56,81 @@ describe('Models', () => {
expect(Object.keys(resp.extensions).length).toEqual(1);
expect(resp.extensions['x-example']).toEqual({ a: 1 });
});
+
+ test('should get correct sibling in responses for openapi 3.1', () => {
+ const spec = parseYaml(outdent`
+ openapi: 3.1.0
+ paths:
+ /test:
+ get:
+ operationId: test
+ responses:
+ '200':
+ description: Overridden description
+ $ref: "#/components/responses/Successful"
+ components:
+ responses:
+ Successful:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ successful:
+ type: boolean
+ `) as any;
+
+ parser = new OpenAPIParser(spec, undefined, opts);
+ const code = '200';
+ const responseModel = new ResponseModel({
+ parser: parser,
+ code: code,
+ defaultAsError: false,
+ infoOrRef: spec.paths['/test'].get.responses[code],
+ options: opts,
+ isEvent: false,
+ });
+
+ expect(responseModel.summary).toBe('Overridden description');
+ });
+
+ test('should not override description in responses for openapi 3.0', () => {
+ const spec = parseYaml(outdent`
+ openapi: 3.0.0
+ paths:
+ /test:
+ get:
+ operationId: test
+ responses:
+ '200':
+ description: Overridden description
+ $ref: "#/components/responses/Successful"
+ components:
+ responses:
+ Successful:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ successful:
+ type: boolean
+ `) as any;
+
+ parser = new OpenAPIParser(spec, undefined, opts);
+ const code = '200';
+ const responseModel = new ResponseModel({
+ parser: parser,
+ code: code,
+ defaultAsError: false,
+ infoOrRef: spec.paths['/test'].get.responses[code],
+ options: opts,
+ isEvent: false,
+ });
+
+ expect(responseModel.summary).toBe('successful operation');
+ });
});
});
diff --git a/src/services/__tests__/models/Schema.circular.test.ts b/src/services/__tests__/models/Schema.circular.test.ts
new file mode 100644
index 00000000..404065b6
--- /dev/null
+++ b/src/services/__tests__/models/Schema.circular.test.ts
@@ -0,0 +1,730 @@
+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: