diff --git a/src/components/__tests__/DiscriminatorDropdown.test.tsx b/src/components/__tests__/DiscriminatorDropdown.test.tsx index 6c02d1c6..a15c5057 100644 --- a/src/components/__tests__/DiscriminatorDropdown.test.tsx +++ b/src/components/__tests__/DiscriminatorDropdown.test.tsx @@ -50,7 +50,7 @@ describe('Components', () => { }} />, ); - expect(filterPropsDeep(toJson(schemaView), ['field.schema.options'])).toMatchSnapshot(); + expect(filterPropsDeep(toJson(schemaView), ['field.schema.options', 'field.schema.parent', 'field.schema.parser'])).toMatchSnapshot(); }); }); }); diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap index 745c4cad..92e150fc 100644 --- a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap @@ -27,10 +27,12 @@ exports[`Components SchemaView discriminator should correctly render discriminat "example": undefined, "externalDocs": undefined, "format": undefined, - "isCircular": undefined, + "isCircular": false, "isPrimitive": true, "nullable": false, "options": "<<>>", + "parent": "<<>>", + "parser": "<<>>", "pattern": undefined, "pointer": "#/components/schemas/Dog/properties/packSize", "rawSchema": Object { @@ -77,10 +79,12 @@ exports[`Components SchemaView discriminator should correctly render discriminat "example": undefined, "externalDocs": undefined, "format": undefined, - "isCircular": undefined, + "isCircular": false, "isPrimitive": true, "nullable": false, "options": "<<>>", + "parent": "<<>>", + "parser": "<<>>", "pattern": undefined, "pointer": "#/components/schemas/Dog/properties/type", "rawSchema": Object { diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts index 51a7ce07..7affc1bc 100644 --- a/src/services/OpenAPIParser.ts +++ b/src/services/OpenAPIParser.ts @@ -177,6 +177,11 @@ export class OpenAPIParser { 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; + } + return this.byRef(obj.$ref)!; } return obj; diff --git a/src/services/models/Field.ts b/src/services/models/Field.ts index ab30d094..263c6e78 100644 --- a/src/services/models/Field.ts +++ b/src/services/models/Field.ts @@ -60,6 +60,7 @@ export class FieldModel { infoOrRef: Referenced & { name?: string; kind?: string }, pointer: string, options: RedocNormalizedOptions, + parentSchema?: SchemaModel, ) { makeObservable(this); @@ -76,7 +77,7 @@ export class FieldModel { fieldSchema = info.content[serializationMime] && info.content[serializationMime].schema; } - this.schema = new SchemaModel(parser, fieldSchema || {}, pointer, options); + this.schema = new SchemaModel(parser, fieldSchema || {}, pointer, options, false, parentSchema); this.description = info.description === undefined ? this.schema.description || '' : info.description; this.example = info.example || this.schema.example; diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index dd4a4d6f..5ec0c591 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -48,7 +48,7 @@ export class SchemaModel { constraints: string[]; - fields?: FieldModel[]; + private _fields?: FieldModel[]; items?: SchemaModel; oneOf?: SchemaModel[]; @@ -66,21 +66,20 @@ export class SchemaModel { * When true forces dereferencing in allOfs even if circular */ constructor( - parser: OpenAPIParser, + private parser: OpenAPIParser, schemaOrRef: Referenced, pointer: string, private options: RedocNormalizedOptions, isChild: boolean = false, + private parent?: SchemaModel, ) { makeObservable(this); this.pointer = schemaOrRef.$ref || pointer || ''; - this.rawSchema = parser.deref(schemaOrRef); + this.rawSchema = parser.shallowDeref(schemaOrRef); + this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild); - this.init(parser, isChild); - - parser.exitRef(schemaOrRef); parser.exitParents(this.schema); if (options.showExtensions) { @@ -97,9 +96,35 @@ export class SchemaModel { this.activeOneOf = idx; } + get fields() { + if (this.isCircular) { + return undefined; + } + + if (!this._fields && this.type === 'object') { + this._fields = buildFields(this.parser, this.schema, this.pointer, this.options, this); + } + return this._fields; + } + + // check circular refs for lazy fields + hasCircularParent($refs: string[]) { + if (this.parent) { + const res = $refs.some($ref => { + const parent = this.parent!; + if (parent.pointer === $ref) return true; + if (parent.schema.parentRefs?.some?.(parentRef => parentRef === $ref)) return true; + }) + if (res) return true; + if (this.parent.hasCircularParent($refs)) return true; + } + + return false; + } + init(parser: OpenAPIParser, isChild: boolean) { const schema = this.schema; - this.isCircular = schema['x-circular-ref']; + this.isCircular = schema['x-circular-ref'] || this.hasCircularParent([this.pointer, ...(this.schema.parentRefs || [])]); this.title = schema.title || (isNamedDefinition(this.pointer) && JsonPointer.baseName(this.pointer)) || ''; @@ -154,10 +179,8 @@ export class SchemaModel { return; } - if (this.type === 'object') { - this.fields = buildFields(parser, schema, this.pointer, this.options); - } else if (this.type === 'array' && schema.items) { - this.items = new SchemaModel(parser, schema.items, this.pointer + '/items', this.options); + if (this.type === 'array' && schema.items) { + this.items = new SchemaModel(parser, schema.items, this.pointer + '/items', this.options, false, this); this.displayType = pluralizeType(this.items.displayType); this.displayFormat = this.items.format; this.typePrefix = this.items.typePrefix + l('arrayOf'); @@ -199,6 +222,8 @@ export class SchemaModel { } as OpenAPISchema, this.pointer + '/oneOf/' + idx, this.options, + false, + this ); parser.exitRef(variant); @@ -319,7 +344,7 @@ export class SchemaModel { } this.oneOf = refs.map(({ $ref, name }) => { - const innerSchema = new SchemaModel(parser, parser.byRef($ref)!, $ref, this.options, true); + const innerSchema = new SchemaModel(parser, parser.byRef($ref)!, $ref, this.options, true, this.parent); innerSchema.title = name; return innerSchema; }); @@ -331,6 +356,7 @@ function buildFields( schema: OpenAPISchema, $ref: string, options: RedocNormalizedOptions, + parent?: SchemaModel ): FieldModel[] { const props = schema.properties || {}; const additionalProps = schema.additionalProperties; @@ -360,6 +386,7 @@ function buildFields( }, $ref + '/properties/' + fieldName, options, + parent ); }); @@ -386,6 +413,7 @@ function buildFields( }, $ref + '/additionalProperties', options, + parent ), ); }