Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ You can use all of the following options with standalone version on <redoc> tag
* **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset.
* **function**: A getter function. Must return a number representing the offset (in pixels).
* `showExtensions` - show vendor extensions ("x-" fields). Extensions used by ReDoc are ignored. Can be boolean or an array of `string` with names of extensions to display.
* `showSchemas` - Show all top-level Schemas under group in menu, default `false`.
* `sortPropsAlphabetically` - sort properties alphabetically.
* `payloadSampleIdx` - if set, payload sample will be inserted at this index or last. Indexes start from 0.
* `theme` - ReDoc theme. For details check [theme docs](#redoc-theme-object).
Expand Down
14 changes: 14 additions & 0 deletions e2e/integration/menu.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,18 @@ describe('Menu', () => {
cy.contains('[role=menuitem].-depth1', 'store').click({ force: true });
petItem().should('not.have.class', 'active');
});

it('should omit Schemas group when showSchemas == false, which is default', () => {
cy.get('.menu-content').should('exist');
cy.get('[data-item-id="section/Schemas"]').should('not.exist');
cy.get('[type=section] [title=HoneyBee]').should('not.exist');
});

it('should include Schemas group and sections when showSchemas == true', () => {
cy.visit('e2e/showSchemas.html');

cy.get('.menu-content').should('exist');
cy.get('[data-item-id="section/Schemas"]').should('exist');
cy.get('[type=section] [title=HoneyBee]').should('exist');
});
});
8 changes: 8 additions & 0 deletions e2e/showSchemas.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>

<body>
<redoc spec-url="../demo/openapi.yaml" show-schemas="true"></redoc>
<script src="../bundles/redoc.standalone.js"></script>
</body>

</html>
5 changes: 3 additions & 2 deletions src/components/Responses/Response.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
Expand Down
4 changes: 3 additions & 1 deletion src/components/Responses/ResponseDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && <Markdown source={description} />}
<Extensions extensions={extensions} />
<ResponseHeaders headers={headers} />
<MediaTypesSwitch content={content} renderDropdown={this.renderDropdown}>
{({ schema }) => {
Expand Down
16 changes: 16 additions & 0 deletions src/services/MenuBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ export class MenuBuilder {
} else {
items.push(...MenuBuilder.getTagsItems(parser, tagsMap, undefined, undefined, options));
}

if (options.showSchemas && spec.components?.schemas) {
// Ignore entries that are side-effects of Swagger 2 conversion
const titles = Object.keys(spec.components?.schemas).filter((title) => {
return !/^schema[0-9]*$/.test(title);
});

let markdown = '# Schemas\n\n';
titles.sort();
for (const title of titles) {
markdown += `## ${title}\n`;
markdown += `<SchemaDefinition schemaRef="#/components/schemas/${title}" />\n\n`;
}
items.push(...MenuBuilder.addMarkdownItems(markdown, undefined, 1, options));
}

return items;
}

Expand Down
7 changes: 7 additions & 0 deletions src/services/RedocNormalizedOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface RedocRawOptions {
expandDefaultServerVariables?: boolean;
maxDisplayedEnumValues?: number;
ignoreNamedSchemas?: string[] | string;
showSchemas?: boolean;
hideSchemaPattern?: boolean;
}

Expand Down Expand Up @@ -87,6 +88,10 @@ export class RedocNormalizedOptions {
return !!value;
}

static normalizeShowSchemas(value: RedocRawOptions['showSchemas']): 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)) {
Expand Down Expand Up @@ -195,6 +200,7 @@ export class RedocNormalizedOptions {
maxDisplayedEnumValues?: number;

ignoreNamedSchemas: Set<string>;
showSchemas: boolean;
hideSchemaPattern: boolean;

constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
Expand Down Expand Up @@ -256,6 +262,7 @@ export class RedocNormalizedOptions {
? raw.ignoreNamedSchemas
: raw.ignoreNamedSchemas?.split(',').map((s) => s.trim());
this.ignoreNamedSchemas = new Set(ignoreNamedSchemas);
this.showSchemas = RedocNormalizedOptions.normalizeShowSchemas(raw.showSchemas);
this.hideSchemaPattern = argValueToBoolean(raw.hideSchemaPattern);
}
}
25 changes: 25 additions & 0 deletions src/services/__tests__/MenuBuilder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { MenuBuilder } from '../MenuBuilder';

describe('MenuBuilder', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const spec = require('./fixtures/showSchemas.json');

test('should omit Schemas group when showSchemas != true', () => {
const opts = new RedocNormalizedOptions({});
const parser = new OpenAPIParser(spec, undefined, opts);
const menuItems = MenuBuilder.buildStructure(parser, opts);
expect(menuItems.length).toEqual(1);
});

test('should build Schemas group when showSchemas == true', () => {
const opts = new RedocNormalizedOptions({ showSchemas: true });
const parser = new OpenAPIParser(spec, undefined, opts);
const menuItems = MenuBuilder.buildStructure(parser, opts);
expect(menuItems.length).toEqual(2);
expect(menuItems[1].name).toEqual('Schemas');
expect(menuItems[1].items.length).toEqual(1);
expect(menuItems[1].items[0].name).toEqual('Foo');
});
});
37 changes: 37 additions & 0 deletions src/services/__tests__/fixtures/showSchemas.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.0",
"title": "Foo"
},
"paths": {
"/foo": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/Foo"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Foo": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
}
7 changes: 7 additions & 0 deletions src/services/__tests__/models/Response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,12 @@ describe('Models', () => {
const resp = new ResponseModel(parser, 'default', true, {}, opts);
expect(resp.type).toEqual('error');
});

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);
expect(resp.extensions['x-example']).toEqual({a: 1});
});
});
});
11 changes: 10 additions & 1 deletion src/services/models/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { action, observable, makeObservable } 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';
Expand All @@ -19,6 +22,8 @@ export class ResponseModel {
type: string;
headers: FieldModel[] = [];

extensions: Record<string, any>;

constructor(
parser: OpenAPIParser,
code: string,
Expand Down Expand Up @@ -54,6 +59,10 @@ export class ResponseModel {
return new FieldModel(parser, { ...header, name }, '', options);
});
}

if (options.showExtensions) {
this.extensions = extractExtensions(info, options.showExtensions);
}
}

@action
Expand Down
2 changes: 1 addition & 1 deletion src/types/open-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export interface OpenAPIRequestBody {
}

export interface OpenAPIResponses {
[code: string]: OpenAPIResponse;
[code: string]: Referenced<OpenAPIResponse>;
}

export interface OpenAPIResponse {
Expand Down