feat: Option to reverse readOnly/writeOnly properties #1720

This commit is contained in:
Andriy Zaleskyy 2021-08-27 15:41:27 +03:00
parent 1487b24776
commit 2a0b0866ef
8 changed files with 51 additions and 29 deletions

View File

@ -248,7 +248,7 @@ You can use all of the following options with standalone version on <redoc> tag
* `onlyRequiredInSamples` - shows only required fields in request samples. * `onlyRequiredInSamples` - shows only required fields in request samples.
* `pathInMiddlePanel` - show path link and HTTP verb in the middle panel instead of the right one. * `pathInMiddlePanel` - show path link and HTTP verb in the middle panel instead of the right one.
* `requiredPropsFirst` - show required properties first ordered in the same order as in `required` array. * `requiredPropsFirst` - show required properties first ordered in the same order as in `required` array.
* `reverseEventsReadOnlyProps`, `reverseEventsWriteOnlyProps` - reverse readOnly/writeOnly schema key for webhooks and callbacks. * `reverseEventsReadWriteProps` - reverse readOnly/writeOnly schema key for webhooks and callbacks.
* `scrollYOffset` - If set, specifies a vertical scroll-offset. This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc; * `scrollYOffset` - If set, specifies a vertical scroll-offset. This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc;
`scrollYOffset` can be specified in various ways: `scrollYOffset` can be specified in various ways:
* **number**: A fixed number of pixels to be used as offset. * **number**: A fixed number of pixels to be used as offset.

View File

@ -1193,7 +1193,7 @@ x-webhooks:
summary: New pet summary: New pet
description: Information about a new pet in the systems description: Information about a new pet in the systems
operationId: newPet operationId: newPet
tags: tags:
- pet - pet
requestBody: requestBody:
content: content:
@ -1202,4 +1202,4 @@ x-webhooks:
$ref: "#/components/schemas/Pet" $ref: "#/components/schemas/Pet"
responses: responses:
"200": "200":
description: Return a 200 status to indicate that the data was received successfully description: Return a 200 status to indicate that the data was received successfully

View File

@ -69,13 +69,14 @@ function DropdownWithinHeader(props) {
export function BodyContent(props: { content: MediaContentModel; description?: string }): JSX.Element { export function BodyContent(props: { content: MediaContentModel; description?: string }): JSX.Element {
const { content, description } = props; const { content, description } = props;
const { isRequestType } = content;
return ( return (
<MediaTypesSwitch content={content} renderDropdown={DropdownWithinHeader}> <MediaTypesSwitch content={content} renderDropdown={DropdownWithinHeader}>
{({ schema }) => { {({ schema }) => {
return ( return (
<> <>
{description !== undefined && <Markdown source={description} />} {description !== undefined && <Markdown source={description} />}
<Schema skipReadOnly={true} key="schema" schema={schema} /> <Schema skipReadOnly={isRequestType} key="schema" schema={schema} />
</> </>
); );
}} }}

View File

@ -42,6 +42,7 @@ export interface RedocRawOptions {
maxDisplayedEnumValues?: number; maxDisplayedEnumValues?: number;
ignoreNamedSchemas?: string[] | string; ignoreNamedSchemas?: string[] | string;
hideSchemaPattern?: boolean; hideSchemaPattern?: boolean;
reverseEventsReadWriteProps?: boolean;
} }
export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean { export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
@ -49,7 +50,7 @@ export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean
return defaultValue || false; return defaultValue || false;
} }
if (typeof val === 'string') { if (typeof val === 'string') {
return val === 'false' ? false : true; return val !== 'false';
} }
return val; return val;
} }
@ -196,6 +197,7 @@ export class RedocNormalizedOptions {
ignoreNamedSchemas: Set<string>; ignoreNamedSchemas: Set<string>;
hideSchemaPattern: boolean; hideSchemaPattern: boolean;
reverseEventsReadWriteProps: boolean;
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) { constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
raw = { ...defaults, ...raw }; raw = { ...defaults, ...raw };
@ -257,5 +259,6 @@ export class RedocNormalizedOptions {
: raw.ignoreNamedSchemas?.split(',').map((s) => s.trim()); : raw.ignoreNamedSchemas?.split(',').map((s) => s.trim());
this.ignoreNamedSchemas = new Set(ignoreNamedSchemas); this.ignoreNamedSchemas = new Set(ignoreNamedSchemas);
this.hideSchemaPattern = argValueToBoolean(raw.hideSchemaPattern); this.hideSchemaPattern = argValueToBoolean(raw.hideSchemaPattern);
this.reverseEventsReadWriteProps = argValueToBoolean(raw.reverseEventsReadWriteProps);
} }
} }

View File

@ -51,8 +51,8 @@ export class MediaTypeModel {
generateExample(parser: OpenAPIParser, info: OpenAPIMediaType) { generateExample(parser: OpenAPIParser, info: OpenAPIMediaType) {
const samplerOptions = { const samplerOptions = {
skipReadOnly: this.isRequestType, skipReadOnly: this.isRequestType,
skipNonRequired: this.isRequestType && this.onlyRequiredInSamples,
skipWriteOnly: !this.isRequestType, skipWriteOnly: !this.isRequestType,
skipNonRequired: this.isRequestType && this.onlyRequiredInSamples,
maxSampleDepth: 10, maxSampleDepth: 10,
}; };
if (this.schema && this.schema.oneOf) { if (this.schema && this.schema.oneOf) {

View File

@ -76,6 +76,7 @@ export class OperationModel implements IMenuItem {
extensions: Record<string, any>; extensions: Record<string, any>;
isCallback: boolean; isCallback: boolean;
isWebhook: boolean; isWebhook: boolean;
reverseEventsReadWriteProps: boolean;
constructor( constructor(
private parser: OpenAPIParser, private parser: OpenAPIParser,
@ -98,7 +99,9 @@ export class OperationModel implements IMenuItem {
this.operationId = operationSpec.operationId; this.operationId = operationSpec.operationId;
this.path = operationSpec.pathName; this.path = operationSpec.pathName;
this.isCallback = isCallback; this.isCallback = isCallback;
this.isWebhook = !!operationSpec.isWebhook; this.isWebhook = operationSpec.isWebhook;
this.reverseEventsReadWriteProps = options.reverseEventsReadWriteProps &&
(this.isCallback || this.isWebhook);
this.name = getOperationSummary(operationSpec); this.name = getOperationSummary(operationSpec);
@ -171,8 +174,12 @@ export class OperationModel implements IMenuItem {
@memoize @memoize
get requestBody() { get requestBody() {
return ( return (
this.operationSpec.requestBody && this.operationSpec.requestBody && new RequestBodyModel({
new RequestBodyModel(this.parser, this.operationSpec.requestBody, this.options) parser: this.parser,
infoOrRef: this.operationSpec.requestBody,
options: this.options,
reverseEventsReadWriteProps: this.reverseEventsReadWriteProps,
})
); );
} }
@ -240,13 +247,14 @@ export class OperationModel implements IMenuItem {
return isStatusCode(code); return isStatusCode(code);
}) // filter out other props (e.g. x-props) }) // filter out other props (e.g. x-props)
.map((code) => { .map((code) => {
return new ResponseModel( return new ResponseModel({
this.parser, parser: this.parser,
code, code,
hasSuccessResponses, defaultAsError: hasSuccessResponses,
this.operationSpec.responses[code], infoOrRef: this.operationSpec.responses[code],
this.options, options: this.options,
); reverseEventsReadWriteProps: this.reverseEventsReadWriteProps,
});
}); });
} }

View File

@ -4,22 +4,27 @@ import { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { MediaContentModel } from './MediaContent'; import { MediaContentModel } from './MediaContent';
type RequestBodyProps = {
parser: OpenAPIParser;
infoOrRef: Referenced<OpenAPIRequestBody>;
options: RedocNormalizedOptions;
reverseEventsReadWriteProps: boolean;
}
export class RequestBodyModel { export class RequestBodyModel {
description: string; description: string;
required: boolean; required: boolean;
content?: MediaContentModel; content?: MediaContentModel;
constructor( constructor(props: RequestBodyProps) {
parser: OpenAPIParser, const { parser, infoOrRef, options, reverseEventsReadWriteProps } = props;
infoOrRef: Referenced<OpenAPIRequestBody>, const isRequest = reverseEventsReadWriteProps ? false : true;
options: RedocNormalizedOptions,
) {
const info = parser.deref(infoOrRef); const info = parser.deref(infoOrRef);
this.description = info.description || ''; this.description = info.description || '';
this.required = !!info.required; this.required = !!info.required;
parser.exitRef(infoOrRef); parser.exitRef(infoOrRef);
if (info.content !== undefined) { if (info.content !== undefined) {
this.content = new MediaContentModel(parser, info.content, true, options); this.content = new MediaContentModel(parser, info.content, isRequest, options);
} }
} }
} }

View File

@ -8,6 +8,15 @@ import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { FieldModel } from './Field'; import { FieldModel } from './Field';
import { MediaContentModel } from './MediaContent'; import { MediaContentModel } from './MediaContent';
type ResponseProps = {
parser: OpenAPIParser,
code: string,
defaultAsError: boolean,
infoOrRef: Referenced<OpenAPIResponse>,
options: RedocNormalizedOptions,
reverseEventsReadWriteProps: boolean,
}
export class ResponseModel { export class ResponseModel {
@observable @observable
expanded: boolean = false; expanded: boolean = false;
@ -19,13 +28,9 @@ export class ResponseModel {
type: string; type: string;
headers: FieldModel[] = []; headers: FieldModel[] = [];
constructor( constructor(props: ResponseProps) {
parser: OpenAPIParser, const { parser, code, defaultAsError, infoOrRef, options, reverseEventsReadWriteProps } = props;
code: string, const isRequest = reverseEventsReadWriteProps ? true : false;
defaultAsError: boolean,
infoOrRef: Referenced<OpenAPIResponse>,
options: RedocNormalizedOptions,
) {
makeObservable(this); makeObservable(this);
this.expanded = options.expandResponses === 'all' || options.expandResponses[code]; this.expanded = options.expandResponses === 'all' || options.expandResponses[code];
@ -34,7 +39,7 @@ export class ResponseModel {
parser.exitRef(infoOrRef); parser.exitRef(infoOrRef);
this.code = code; this.code = code;
if (info.content !== undefined) { 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) { if (info['x-summary'] !== undefined) {