From 918e2414f619b3a8335da96d006df12059d6e454 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Thu, 8 Oct 2020 13:44:00 -0700 Subject: [PATCH 1/5] Corrected responses to support $ref --- src/types/open-api.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/open-api.d.ts b/src/types/open-api.d.ts index aefc0b7322..e5a7d72eb0 100644 --- a/src/types/open-api.d.ts +++ b/src/types/open-api.d.ts @@ -181,7 +181,7 @@ export interface OpenAPIRequestBody { } export interface OpenAPIResponses { - [code: string]: OpenAPIResponse; + [code: string]: Referenced; } export interface OpenAPIResponse { From dbaab9e30611b5be1c32689c57264c203532e5a7 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Thu, 8 Oct 2020 13:44:56 -0700 Subject: [PATCH 2/5] Add showExtentions for Responses --- src/components/Responses/Response.tsx | 4 ++-- src/components/Responses/ResponseDetails.tsx | 4 +++- src/services/models/Response.ts | 11 ++++++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/Responses/Response.tsx b/src/components/Responses/Response.tsx index 58e0ae32e2..6d1e1a28c4 100644 --- a/src/components/Responses/Response.tsx +++ b/src/components/Responses/Response.tsx @@ -12,11 +12,11 @@ export class ResponseView extends React.Component<{ response: ResponseModel }> { }; render() { - const { headers, type, summary, description, code, expanded, content } = this.props.response; + const { extensions, headers, type, summary, description, code, expanded, content } = this.props.response; const mimes = content === undefined ? [] : content.mediaTypes.filter(mime => mime.schema !== undefined); - const empty = headers.length === 0 && mimes.length === 0 && !description; + const empty = Object.keys(extensions).length === 0 && headers.length === 0 && mimes.length === 0 && !description; return (
diff --git a/src/components/Responses/ResponseDetails.tsx b/src/components/Responses/ResponseDetails.tsx index 821fc2da99..3794eba1da 100644 --- a/src/components/Responses/ResponseDetails.tsx +++ b/src/components/Responses/ResponseDetails.tsx @@ -7,15 +7,17 @@ import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel'; import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch'; import { Schema } from '../Schema'; +import { Extensions } from '../Fields/Extensions'; import { Markdown } from '../Markdown/Markdown'; import { ResponseHeaders } from './ResponseHeaders'; export class ResponseDetails extends React.PureComponent<{ response: ResponseModel }> { render() { - const { description, headers, content } = this.props.response; + const { description, extensions, headers, content } = this.props.response; return ( <> {description && } + {({ schema }) => { diff --git a/src/services/models/Response.ts b/src/services/models/Response.ts index 3fc4d06c0a..acd10c0d6c 100644 --- a/src/services/models/Response.ts +++ b/src/services/models/Response.ts @@ -2,7 +2,10 @@ import { action, observable } from 'mobx'; import { OpenAPIResponse, Referenced } from '../../types'; -import { getStatusCodeType } from '../../utils'; +import { + getStatusCodeType, + extractExtensions +} from '../../utils'; import { OpenAPIParser } from '../OpenAPIParser'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { FieldModel } from './Field'; @@ -19,6 +22,8 @@ export class ResponseModel { type: string; headers: FieldModel[] = []; + extensions: Record; + constructor( parser: OpenAPIParser, code: string, @@ -52,6 +57,10 @@ export class ResponseModel { return new FieldModel(parser, { ...header, name }, '', options); }); } + + if (options.showExtensions) { + this.extensions = extractExtensions(info, options.showExtensions); + } } @action From 69f1974a6cf61e0d3262bc8d7518f51aba1a9263 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Thu, 8 Oct 2020 18:39:29 -0700 Subject: [PATCH 3/5] Add showExtentions support for Responses --- src/components/Responses/Response.tsx | 5 +++-- src/components/Responses/ResponseDetails.tsx | 4 +++- src/services/__tests__/models/Response.test.ts | 7 +++++++ src/services/models/Response.ts | 11 ++++++++++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/components/Responses/Response.tsx b/src/components/Responses/Response.tsx index 58e0ae32e2..bc6ece8bd4 100644 --- a/src/components/Responses/Response.tsx +++ b/src/components/Responses/Response.tsx @@ -12,11 +12,12 @@ export class ResponseView extends React.Component<{ response: ResponseModel }> { }; render() { - const { headers, type, summary, description, code, expanded, content } = this.props.response; + const { extensions, headers, type, summary, description, code, expanded, content } = this.props.response; const mimes = content === undefined ? [] : content.mediaTypes.filter(mime => mime.schema !== undefined); - const empty = headers.length === 0 && mimes.length === 0 && !description; + const empty = (!extensions || Object.keys(extensions).length === 0) && + headers.length === 0 && mimes.length === 0 && !description; return (
diff --git a/src/components/Responses/ResponseDetails.tsx b/src/components/Responses/ResponseDetails.tsx index 821fc2da99..3794eba1da 100644 --- a/src/components/Responses/ResponseDetails.tsx +++ b/src/components/Responses/ResponseDetails.tsx @@ -7,15 +7,17 @@ import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel'; import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch'; import { Schema } from '../Schema'; +import { Extensions } from '../Fields/Extensions'; import { Markdown } from '../Markdown/Markdown'; import { ResponseHeaders } from './ResponseHeaders'; export class ResponseDetails extends React.PureComponent<{ response: ResponseModel }> { render() { - const { description, headers, content } = this.props.response; + const { description, extensions, headers, content } = this.props.response; return ( <> {description && } + {({ schema }) => { diff --git a/src/services/__tests__/models/Response.test.ts b/src/services/__tests__/models/Response.test.ts index 3fb220f2df..ce9017f218 100644 --- a/src/services/__tests__/models/Response.test.ts +++ b/src/services/__tests__/models/Response.test.ts @@ -31,5 +31,12 @@ describe('Models', () => { const resp = new ResponseModel(parser, 'default', true, {}, opts); expect(resp.type).toEqual('error'); }); + + test('should be error if showExtensions is true', () => { + const options = new RedocNormalizedOptions({ showExtensions: true }); + const resp = new ResponseModel(parser, 'default', true, { 'x-example': {a: 1} } as any, options); + expect(Object.keys(resp.extensions).length).toEqual(1); + expect(resp.extensions['x-example']).toEqual({a: 1}); + }); }); }); diff --git a/src/services/models/Response.ts b/src/services/models/Response.ts index 3fc4d06c0a..77de3599a9 100644 --- a/src/services/models/Response.ts +++ b/src/services/models/Response.ts @@ -2,7 +2,10 @@ import { action, observable } from 'mobx'; import { OpenAPIResponse, Referenced } from '../../types'; -import { getStatusCodeType } from '../../utils'; +import { + extractExtensions, + getStatusCodeType, +} from '../../utils'; import { OpenAPIParser } from '../OpenAPIParser'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { FieldModel } from './Field'; @@ -19,6 +22,8 @@ export class ResponseModel { type: string; headers: FieldModel[] = []; + extensions: Record; + constructor( parser: OpenAPIParser, code: string, @@ -52,6 +57,10 @@ export class ResponseModel { return new FieldModel(parser, { ...header, name }, '', options); }); } + + if (options.showExtensions) { + this.extensions = extractExtensions(info, options.showExtensions); + } } @action From 4716620ab4e9743f5206f7c6b6d7c0d3fb850756 Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Tue, 13 Oct 2020 17:10:21 +0300 Subject: [PATCH 4/5] Update src/services/__tests__/models/Response.test.ts Co-authored-by: Giles Wells --- src/services/__tests__/models/Response.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/__tests__/models/Response.test.ts b/src/services/__tests__/models/Response.test.ts index ce9017f218..33cabb2c7e 100644 --- a/src/services/__tests__/models/Response.test.ts +++ b/src/services/__tests__/models/Response.test.ts @@ -32,7 +32,7 @@ describe('Models', () => { expect(resp.type).toEqual('error'); }); - test('should be error if showExtensions is true', () => { + test('ensure extensions are shown if showExtensions is true', () => { const options = new RedocNormalizedOptions({ showExtensions: true }); const resp = new ResponseModel(parser, 'default', true, { 'x-example': {a: 1} } as any, options); expect(Object.keys(resp.extensions).length).toEqual(1); From 08562e77562407a4f59120bee6618de29b767f34 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Mon, 26 Oct 2020 19:27:57 -0700 Subject: [PATCH 5/5] Add expandSchemas to expand all properties for all Schemas --- e2e/expandSchemas.html | 8 +++++ e2e/integration/expandSchemas.e2e.ts | 17 ++++++++++ src/services/RedocNormalizedOptions.ts | 7 ++++ .../__tests__/fixtures/expandSchemas.json | 20 +++++++++++ src/services/__tests__/models/Schema.test.ts | 20 +++++++++++ src/services/models/Schema.ts | 34 ++++++++++--------- 6 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 e2e/expandSchemas.html create mode 100644 e2e/integration/expandSchemas.e2e.ts create mode 100644 src/services/__tests__/fixtures/expandSchemas.json diff --git a/e2e/expandSchemas.html b/e2e/expandSchemas.html new file mode 100644 index 0000000000..12ec60731e --- /dev/null +++ b/e2e/expandSchemas.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/e2e/integration/expandSchemas.e2e.ts b/e2e/integration/expandSchemas.e2e.ts new file mode 100644 index 0000000000..2267086812 --- /dev/null +++ b/e2e/integration/expandSchemas.e2e.ts @@ -0,0 +1,17 @@ +describe('Schemas', () => { + it('expandSchemas != true', () => { + cy.visit('e2e/standalone.html'); + + cy.get('.api-content') + .find('.expanded') + .should('have.length', 0); + }); + + it('expandSchemas == true', () => { + cy.visit('e2e/expandSchemas.html'); + + cy.get('.api-content') + .find('.expanded') + .should('have.length', 146); + }); +}); diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index 126e313aa4..eb6d15e7de 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -41,6 +41,7 @@ export interface RedocRawOptions { expandDefaultServerVariables?: boolean; maxDisplayedEnumValues?: number; ignoreNamedSchemas?: string[] | string; + expandSchemas?: boolean; } function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean { @@ -86,6 +87,10 @@ export class RedocNormalizedOptions { return !!value; } + static normalizeExpandSchemas(value: RedocRawOptions['expandSchemas']): boolean { + return !!value; + } + static normalizeScrollYOffset(value: RedocRawOptions['scrollYOffset']): () => number { // just number is not valid selector and leads to crash so checking if isNumeric here if (typeof value === 'string' && !isNumeric(value)) { @@ -194,6 +199,7 @@ export class RedocNormalizedOptions { maxDisplayedEnumValues?: number; ignoreNamedSchemas: Set; + expandSchemas: boolean; constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) { raw = { ...defaults, ...raw }; @@ -252,5 +258,6 @@ export class RedocNormalizedOptions { this.maxDisplayedEnumValues = argValueToNumber(raw.maxDisplayedEnumValues); const ignoreNamedSchemas = Array.isArray(raw.ignoreNamedSchemas) ? raw.ignoreNamedSchemas : raw.ignoreNamedSchemas?.split(',').map(s => s.trim()); this.ignoreNamedSchemas = new Set(ignoreNamedSchemas); + this.expandSchemas = RedocNormalizedOptions.normalizeExpandSchemas(raw.expandSchemas); } } diff --git a/src/services/__tests__/fixtures/expandSchemas.json b/src/services/__tests__/fixtures/expandSchemas.json new file mode 100644 index 0000000000..602d9f91b5 --- /dev/null +++ b/src/services/__tests__/fixtures/expandSchemas.json @@ -0,0 +1,20 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0", + "title": "Foo" + }, + "components": { + "schemas": { + "Foo": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": true + } + } + } +} diff --git a/src/services/__tests__/models/Schema.test.ts b/src/services/__tests__/models/Schema.test.ts index 460ca7c110..2edf210b55 100644 --- a/src/services/__tests__/models/Schema.test.ts +++ b/src/services/__tests__/models/Schema.test.ts @@ -40,5 +40,25 @@ describe('Models', () => { expect(schema.oneOf).toHaveLength(2); expect(schema.displayType).toBe('(Array of strings or numbers) or string'); }); + + test('expandSchemas != true', () => { + const spec = require('../fixtures/expandSchemas.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Foo, '', opts); + expect(schema.fields).toHaveLength(2); + expect(schema.fields![0].expanded).toEqual(false); + expect(schema.fields![1].expanded).toEqual(false); + }); + + test('expandSchemas == true', () => { + const opts = new RedocNormalizedOptions({ expandSchemas: true}); + + const spec = require('../fixtures/expandSchemas.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Foo, '', opts); + expect(schema.fields).toHaveLength(2); + expect(schema.fields![0].expanded).toEqual(true); + expect(schema.fields![1].expanded).toEqual(true); + }); }); }); diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index dd4a4d6f35..f765e9cfc1 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -348,7 +348,7 @@ function buildFields( const required = schema.required === undefined ? false : schema.required.indexOf(fieldName) > -1; - return new FieldModel( + const fieldModel = new FieldModel( parser, { name: fieldName, @@ -361,6 +361,8 @@ function buildFields( $ref + '/properties/' + fieldName, options, ); + fieldModel.expanded = options.expandSchemas; + return fieldModel; }); if (options.sortPropsAlphabetically) { @@ -372,22 +374,22 @@ function buildFields( } if (typeof additionalProps === 'object' || additionalProps === true) { - fields.push( - new FieldModel( - parser, - { - name: (typeof additionalProps === 'object' - ? additionalProps['x-additionalPropertiesName'] || 'property name' - : 'property name' - ).concat('*'), - required: false, - schema: additionalProps === true ? {} : additionalProps, - kind: 'additionalProperties', - }, - $ref + '/additionalProperties', - options, - ), + const fieldModel = new FieldModel( + parser, + { + name: (typeof additionalProps === 'object' + ? additionalProps['x-additionalPropertiesName'] || 'property name' + : 'property name' + ).concat('*'), + required: false, + schema: additionalProps === true ? {} : additionalProps, + kind: 'additionalProperties', + }, + $ref + '/additionalProperties', + options, ); + fieldModel.expanded = options.expandSchemas; + fields.push(fieldModel); } return fields;