diff --git a/package-lock.json b/package-lock.json
index e818de0d..a779927a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -81,6 +81,7 @@
"license-checker": "^25.0.1",
"lodash.noop": "^3.0.1",
"mobx": "^6.3.2",
+ "outdent": "^0.8.0",
"prettier": "^2.3.2",
"pretty-quick": "^3.0.0",
"raf": "^3.4.1",
@@ -14282,6 +14283,12 @@
"integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=",
"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": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz",
@@ -29936,6 +29943,12 @@
"integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=",
"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": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz",
diff --git a/package.json b/package.json
index 799d55f3..7bdc4bb6 100644
--- a/package.json
+++ b/package.json
@@ -110,6 +110,7 @@
"license-checker": "^25.0.1",
"lodash.noop": "^3.0.1",
"mobx": "^6.3.2",
+ "outdent": "^0.8.0",
"prettier": "^2.3.2",
"pretty-quick": "^3.0.0",
"raf": "^3.4.1",
@@ -186,10 +187,12 @@
"coveragePathIgnorePatterns": [
"\\.d\\.ts$",
"/benchmark/",
- "/node_modules/"
+ "/node_modules/",
+ "src/services/__tests__/models/helpers.ts"
],
"modulePathIgnorePatterns": [
- "/benchmark/"
+ "/benchmark/",
+ "src/services/__tests__/models/helpers.ts"
],
"snapshotSerializers": [
"enzyme-to-json/serializer"
diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx
index 2766887a..9c2eae74 100644
--- a/src/components/ContentItems/ContentItems.tsx
+++ b/src/components/ContentItems/ContentItems.tsx
@@ -4,8 +4,8 @@ import * as React from 'react';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown';
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
-import { ContentItemModel } from '../../services/MenuBuilder';
-import { GroupModel, OperationModel } from '../../services/models';
+import type { ContentItemModel } from '../../services';
+import type { GroupModel, OperationModel } from '../../services/models';
import { Operation } from '../Operation/Operation';
@observer
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 ? (
+
+ ) : (
;
+import type { LabelsConfig, LabelsConfigRaw } from './types';
const labels: LabelsConfig = {
enum: 'Enum',
diff --git a/src/services/MarkdownRenderer.ts b/src/services/MarkdownRenderer.ts
index 08b5e692..4764e129 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 ``;
}
diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts
index 56c4ad70..fb8cf5a5 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 21da1ef2..5894b0f4 100644
--- a/src/services/OpenAPIParser.ts
+++ b/src/services/OpenAPIParser.ts
@@ -1,45 +1,28 @@
-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';
-
-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];
- }
-}
+import type { MergedOpenAPISchema } from './types';
/**
* 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 _refCounter: RefCounter = new RefCounter();
+ private readonly allowMergeRefs: boolean = false;
constructor(
spec: OpenAPISpec,
@@ -51,13 +34,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;
}
}
- 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 +69,82 @@ 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)) {
+ 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'].includes(k),
+ )
) {
return {
allOf: [resolved, rest],
- };
+ } as T;
} else {
// small optimization
return {
- ...resolved,
+ ...(resolved as GenericObject),
...rest,
- };
+ } as T;
}
}
@@ -189,15 +153,15 @@ 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);
@@ -208,8 +172,8 @@ export class OpenAPIParser {
let receiver: MergedOpenAPISchema = {
...schema,
+ 'x-parentRefs': [],
allOf: undefined,
- parentRefs: [],
title: schema.title || getDefinitionName($ref),
};
@@ -222,36 +186,41 @@ export class OpenAPIParser {
}
const allOfSchemas = schema.allOf
- .map(subSchema => {
- if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) {
+ .map((subSchema: OpenAPISchema) => {
+ 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;
}
-
- 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 || []));
+ 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<{
- $ref: string | undefined;
schema: MergedOpenAPISchema;
+ refsStack: string[];
}>;
- for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) {
+ for (const { schema: subSchema, refsStack: subRefsStack } of allOfSchemas) {
const {
type,
- format,
enum: enumProperty,
properties,
items,
required,
+ title,
oneOf,
anyOf,
- title,
+ 'x-circular-ref': isCircular,
...otherConstraints
} = subSchema;
@@ -264,7 +233,6 @@ export class OpenAPIParser {
receiver.type = [...type, ...receiver.type];
} else {
receiver.type = type;
- receiver.format = format;
}
}
@@ -276,43 +244,51 @@ export class OpenAPIParser {
}
}
- 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,
+ },
$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) {
+ // FIXME: this is invalid here, we need to fix it in separate PR
+ const receiverItems =
+ typeof receiver.items === 'boolean'
+ ? { items: receiver.items }
+ : receiver.items
+ ? (Object.assign({}, receiver.items) as OpenAPISchema)
+ : {};
+ const subSchemaItems =
+ typeof subSchema.items === 'boolean'
+ ? { items: subSchema.items }
+ : (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;
}
@@ -321,18 +297,18 @@ 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,
+ 'x-circular-ref': receiver['x-circular-ref'] || isCircular,
+ ...otherConstraints,
+ };
}
return receiver;
@@ -347,10 +323,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];
}
@@ -358,12 +336,6 @@ export class OpenAPIParser {
return res;
}
- exitParents(shema: MergedOpenAPISchema) {
- for (const parent$ref of shema.parentRefs || []) {
- this.exitRef({ $ref: parent$ref });
- }
- }
-
private hoistOneOfs(schema: OpenAPISchema) {
if (schema.allOf === undefined) {
return schema;
@@ -372,19 +344,14 @@ 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;
+ };
}),
};
}
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..4600c0ec 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';
diff --git a/src/services/SearchWorker.worker.ts b/src/services/SearchWorker.worker.ts
index a4a6da4b..764370f1 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+/;
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..669a8fb0 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;
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..889d2359 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,82 +20,80 @@ 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",
+ },
+ },
+ },
+ ],
},
],
},
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",
+ },
+ },
+ },
+ ],
},
],
},
@@ -100,7 +103,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__/models/Schema.circular.test.ts b/src/services/__tests__/models/Schema.circular.test.ts
new file mode 100644
index 00000000..11f40127
--- /dev/null
+++ b/src/services/__tests__/models/Schema.circular.test.ts
@@ -0,0 +1,486 @@
+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: