From a8e0c296852661dec1dcad2388d7589f9e0d3609 Mon Sep 17 00:00:00 2001 From: Andriy Zaleskyy Date: Tue, 31 Aug 2021 15:39:55 +0300 Subject: [PATCH] fix: Schema for events incorrectly omits readOnly and includes writeOnly (#1720 #1540) --- demo/openapi.yaml | 4 +-- src/components/Parameters/Parameters.tsx | 3 ++- src/services/RedocNormalizedOptions.ts | 2 +- .../__tests__/models/RequestBody.test.ts | 27 +++++++++++++++++++ .../__tests__/models/Response.test.ts | 22 ++++++++++----- src/services/models/MediaType.ts | 2 +- src/services/models/Operation.ts | 25 ++++++++++------- src/services/models/RequestBody.ts | 17 +++++++----- src/services/models/Response.ts | 21 +++++++++------ 9 files changed, 88 insertions(+), 35 deletions(-) create mode 100644 src/services/__tests__/models/RequestBody.test.ts diff --git a/demo/openapi.yaml b/demo/openapi.yaml index 74c16b70..c570cc60 100644 --- a/demo/openapi.yaml +++ b/demo/openapi.yaml @@ -1193,7 +1193,7 @@ x-webhooks: summary: New pet description: Information about a new pet in the systems operationId: newPet - tags: + tags: - pet requestBody: content: @@ -1202,4 +1202,4 @@ x-webhooks: $ref: "#/components/schemas/Pet" responses: "200": - description: Return a 200 status to indicate that the data was received successfully \ No newline at end of file + description: Return a 200 status to indicate that the data was received successfully diff --git a/src/components/Parameters/Parameters.tsx b/src/components/Parameters/Parameters.tsx index 17ab9c48..eb3d2d61 100644 --- a/src/components/Parameters/Parameters.tsx +++ b/src/components/Parameters/Parameters.tsx @@ -69,13 +69,14 @@ function DropdownWithinHeader(props) { export function BodyContent(props: { content: MediaContentModel; description?: string }): JSX.Element { const { content, description } = props; + const { isRequestType } = content; return ( {({ schema }) => { return ( <> {description !== undefined && } - + ); }} diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index e310a9ad..c2407b97 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -49,7 +49,7 @@ export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean return defaultValue || false; } if (typeof val === 'string') { - return val === 'false' ? false : true; + return val !== 'false'; } return val; } diff --git a/src/services/__tests__/models/RequestBody.test.ts b/src/services/__tests__/models/RequestBody.test.ts new file mode 100644 index 00000000..554a1222 --- /dev/null +++ b/src/services/__tests__/models/RequestBody.test.ts @@ -0,0 +1,27 @@ +import { RequestBodyModel } from '../../models/RequestBody'; +import { OpenAPIParser } from '../../OpenAPIParser'; +import { RedocNormalizedOptions } from '../../RedocNormalizedOptions'; + +const opts = new RedocNormalizedOptions({}); +describe('Models', () => { + describe('RequestBodyModel', () => { + let parser, props; + + beforeEach(() => { + parser = new OpenAPIParser({ openapi: '3.0.0' } as any, undefined, opts); + props = { + parser, + infoOrRef: {}, + options: opts, + isEvent: false, + }; + }); + + test('should work with default props', () => { + const consoleError = jest.spyOn(global.console, 'error'); + const req = new RequestBodyModel(props); + expect(consoleError).not.toHaveBeenCalled(); + expect(req).toEqual({ description: '', required: false }); + }); + }); +}); diff --git a/src/services/__tests__/models/Response.test.ts b/src/services/__tests__/models/Response.test.ts index 3fb220f2..ae3c1fd1 100644 --- a/src/services/__tests__/models/Response.test.ts +++ b/src/services/__tests__/models/Response.test.ts @@ -5,30 +5,38 @@ import { RedocNormalizedOptions } from '../../RedocNormalizedOptions'; const opts = new RedocNormalizedOptions({}); describe('Models', () => { describe('ResponseModel', () => { - let parser; + let parser, props; beforeEach(() => { parser = new OpenAPIParser({ openapi: '3.0.0' } as any, undefined, opts); + props = { + parser, + defaultAsError: false, + infoOrRef: {}, + options: opts, + isEvent: false, + code: 'default', + }; }); test('should calculate response type based on code', () => { - let resp = new ResponseModel(parser, '200', false, {}, opts); + let resp = new ResponseModel({...props, code: '200' }); expect(resp.type).toEqual('success'); - resp = new ResponseModel(parser, '120', false, {}, opts); + resp = new ResponseModel({...props, code: '120' }); expect(resp.type).toEqual('info'); - resp = new ResponseModel(parser, '301', false, {}, opts); + resp = new ResponseModel({...props, code: '301' }); expect(resp.type).toEqual('redirect'); - resp = new ResponseModel(parser, '400', false, {}, opts); + resp = new ResponseModel({...props, code: '400' }); expect(resp.type).toEqual('error'); }); test('default should be successful by default', () => { - const resp = new ResponseModel(parser, 'default', false, {}, opts); + const resp = new ResponseModel({...props, code: 'default' }); expect(resp.type).toEqual('success'); }); test('default should be error if defaultAsError is true', () => { - const resp = new ResponseModel(parser, 'default', true, {}, opts); + const resp = new ResponseModel({...props, code: 'default', defaultAsError: true }); expect(resp.type).toEqual('error'); }); }); diff --git a/src/services/models/MediaType.ts b/src/services/models/MediaType.ts index 9283fbae..8987fbbb 100644 --- a/src/services/models/MediaType.ts +++ b/src/services/models/MediaType.ts @@ -51,8 +51,8 @@ export class MediaTypeModel { generateExample(parser: OpenAPIParser, info: OpenAPIMediaType) { const samplerOptions = { skipReadOnly: this.isRequestType, - skipNonRequired: this.isRequestType && this.onlyRequiredInSamples, skipWriteOnly: !this.isRequestType, + skipNonRequired: this.isRequestType && this.onlyRequiredInSamples, maxSampleDepth: 10, }; if (this.schema && this.schema.oneOf) { diff --git a/src/services/models/Operation.ts b/src/services/models/Operation.ts index d00461a1..5b1b5aca 100644 --- a/src/services/models/Operation.ts +++ b/src/services/models/Operation.ts @@ -76,6 +76,7 @@ export class OperationModel implements IMenuItem { extensions: Record; isCallback: boolean; isWebhook: boolean; + isEvent: boolean; constructor( private parser: OpenAPIParser, @@ -98,7 +99,8 @@ export class OperationModel implements IMenuItem { this.operationId = operationSpec.operationId; this.path = operationSpec.pathName; this.isCallback = isCallback; - this.isWebhook = !!operationSpec.isWebhook; + this.isWebhook = operationSpec.isWebhook; + this.isEvent = this.isCallback || this.isWebhook; this.name = getOperationSummary(operationSpec); @@ -171,8 +173,12 @@ export class OperationModel implements IMenuItem { @memoize get requestBody() { return ( - this.operationSpec.requestBody && - new RequestBodyModel(this.parser, this.operationSpec.requestBody, this.options) + this.operationSpec.requestBody && new RequestBodyModel({ + parser: this.parser, + infoOrRef: this.operationSpec.requestBody, + options: this.options, + isEvent: this.isEvent, + }) ); } @@ -240,13 +246,14 @@ export class OperationModel implements IMenuItem { return isStatusCode(code); }) // filter out other props (e.g. x-props) .map((code) => { - return new ResponseModel( - this.parser, + return new ResponseModel({ + parser: this.parser, code, - hasSuccessResponses, - this.operationSpec.responses[code], - this.options, - ); + defaultAsError: hasSuccessResponses, + infoOrRef: this.operationSpec.responses[code], + options: this.options, + isEvent: this.isEvent, + }); }); } diff --git a/src/services/models/RequestBody.ts b/src/services/models/RequestBody.ts index f3b45959..3ca3dc21 100644 --- a/src/services/models/RequestBody.ts +++ b/src/services/models/RequestBody.ts @@ -4,22 +4,27 @@ import { OpenAPIParser } from '../OpenAPIParser'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { MediaContentModel } from './MediaContent'; +type RequestBodyProps = { + parser: OpenAPIParser; + infoOrRef: Referenced; + options: RedocNormalizedOptions; + isEvent: boolean; +} + export class RequestBodyModel { description: string; required: boolean; content?: MediaContentModel; - constructor( - parser: OpenAPIParser, - infoOrRef: Referenced, - options: RedocNormalizedOptions, - ) { + constructor(props: RequestBodyProps) { + const { parser, infoOrRef, options, isEvent } = props; + const isRequest = isEvent ? false : true; const info = parser.deref(infoOrRef); this.description = info.description || ''; this.required = !!info.required; parser.exitRef(infoOrRef); if (info.content !== undefined) { - this.content = new MediaContentModel(parser, info.content, true, options); + this.content = new MediaContentModel(parser, info.content, isRequest, options); } } } diff --git a/src/services/models/Response.ts b/src/services/models/Response.ts index f50ee0f1..d3ed554a 100644 --- a/src/services/models/Response.ts +++ b/src/services/models/Response.ts @@ -8,6 +8,15 @@ import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { FieldModel } from './Field'; import { MediaContentModel } from './MediaContent'; +type ResponseProps = { + parser: OpenAPIParser, + code: string, + defaultAsError: boolean, + infoOrRef: Referenced, + options: RedocNormalizedOptions, + isEvent: boolean, +} + export class ResponseModel { @observable expanded: boolean = false; @@ -19,13 +28,9 @@ export class ResponseModel { type: string; headers: FieldModel[] = []; - constructor( - parser: OpenAPIParser, - code: string, - defaultAsError: boolean, - infoOrRef: Referenced, - options: RedocNormalizedOptions, - ) { + constructor(props: ResponseProps) { + const { parser, code, defaultAsError, infoOrRef, options, isEvent } = props; + const isRequest = isEvent ? true : false; makeObservable(this); this.expanded = options.expandResponses === 'all' || options.expandResponses[code]; @@ -34,7 +39,7 @@ export class ResponseModel { parser.exitRef(infoOrRef); this.code = code; if (info.content !== undefined) { - this.content = new MediaContentModel(parser, info.content, false, options); + this.content = new MediaContentModel(parser, info.content, isRequest, options); } if (info['x-summary'] !== undefined) {