Merge branch 'master' into only-required-parameter

This commit is contained in:
TATSUNO Yasuhiro 2018-10-04 21:34:46 +09:00 committed by GitHub
commit 84f3a2505f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 261 additions and 75 deletions

View File

@ -135,6 +135,10 @@ For npm:
## Usage as a React component ## Usage as a React component
Install peer dependencies required by ReDoc if you don't have them installed already:
npm i react react-dom mobx@^4.2.0 styled-components
Import `RedocStandalone` component from 'redoc' module: Import `RedocStandalone` component from 'redoc' module:
```js ```js
@ -215,6 +219,8 @@ You can use all of the following options with standalone version on <redoc> tag
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition. * `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time. * `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
* `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.
* `sortPropsAlphabetically` - sort properties alphabetically
* `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
* `noAutoAuth` - do not inject Authentication section automatically * `noAutoAuth` - do not inject Authentication section automatically
* `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
* `hideLoading` - do not show loading animation. Useful for small docs * `hideLoading` - do not show loading animation. Useful for small docs

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.6.2", "version": "0.6.4",
"description": "ReDoc's Command Line Interface", "description": "ReDoc's Command Line Interface",
"main": "index.js", "main": "index.js",
"bin": "index.js", "bin": "index.js",
@ -15,6 +15,7 @@
"react": "^16.4.2", "react": "^16.4.2",
"react-dom": "^16.4.2", "react-dom": "^16.4.2",
"redoc": "^2.0.0-alpha.37", "redoc": "^2.0.0-alpha.37",
"styled-components": "^3.4.0",
"tslib": "^1.9.3", "tslib": "^1.9.3",
"yargs": "^12.0.1" "yargs": "^12.0.1"
}, },

View File

@ -22,6 +22,7 @@ const tsLoader = env => ({
options: { options: {
compilerOptions: { compilerOptions: {
module: env.bench ? 'esnext' : 'es2015', module: env.bench ? 'esnext' : 'es2015',
declaration: false,
}, },
}, },
}); });
@ -108,7 +109,7 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
}, },
}, },
{ {
test: /node_modules\/(swagger2openapi|reftools)\/.*\.js$/, test: /node_modules\/(swagger2openapi|reftools|oas-resolver|oas-kit-common|oas-schema-walker)\/.*\.js$/,
use: { use: {
loader: 'ts-loader', loader: 'ts-loader',
options: { options: {

View File

@ -136,7 +136,7 @@
"json-schema-ref-parser": "^5.1.2", "json-schema-ref-parser": "^5.1.2",
"lunr": "^2.3.2", "lunr": "^2.3.2",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "0.3.18", "marked": "^0.5.1",
"memoize-one": "^4.0.0", "memoize-one": "^4.0.0",
"mobx-react": "^5.2.5", "mobx-react": "^5.2.5",
"openapi-sampler": "1.0.0-beta.14", "openapi-sampler": "1.0.0-beta.14",

View File

@ -0,0 +1,41 @@
import * as React from 'react';
import styled from '../../styled-components';
import { OptionsContext } from '../OptionsProvider';
import { StyledMarkdownBlock } from '../Markdown/styled.elements';
const Extension = styled(StyledMarkdownBlock)`
opacity: 0.9;
margin: 2px 0;
`;
const ExtensionLable = styled.span`
font-style: italic;
`;
export interface ExtensionsProps {
extensions: {
[k: string]: any;
};
}
export class Extensions extends React.PureComponent<ExtensionsProps> {
render() {
return (
<OptionsContext.Consumer>
{options => (
<>
{options.showExtensions &&
Object.keys(this.props.extensions).map(key => (
<Extension key={key}>
<ExtensionLable>{key}</ExtensionLable>:{' '}
<code>{JSON.stringify(this.props.extensions[key])}</code>
</Extension>
))}
</>
)}
</OptionsContext.Consumer>
);
}
}

View File

@ -12,6 +12,7 @@ import {
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Markdown } from '../Markdown/Markdown'; import { Markdown } from '../Markdown/Markdown';
import { EnumValues } from './EnumValues'; import { EnumValues } from './EnumValues';
import { Extensions } from './Extensions';
import { FieldProps } from './Field'; import { FieldProps } from './Field';
import { ConstraintsView } from './FieldContstraints'; import { ConstraintsView } from './FieldContstraints';
import { FieldDetail } from './FieldDetail'; import { FieldDetail } from './FieldDetail';
@ -51,6 +52,7 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
<FieldDetail label={'Default:'} value={schema.default} /> <FieldDetail label={'Default:'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '} {!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
{showExamples && <FieldDetail label={'Example:'} value={example} />} {showExamples && <FieldDetail label={'Example:'} value={example} />}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}
<div> <div>
<Markdown compact={true} source={description} /> <Markdown compact={true} source={description} />
</div> </div>

View File

@ -42,7 +42,7 @@ export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
{ key: idx }, { key: idx },
); );
} }
return <part.component key={idx} {...{ ...part.attrs, ...part.propsSelector(store) }} />; return <part.component key={idx} {...{ ...part.props, ...part.propsSelector(store) }} />;
}); });
} }
} }

View File

@ -18,6 +18,7 @@ import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
import { OperationModel as OperationType } from '../../services/models'; import { OperationModel as OperationType } from '../../services/models';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { Extensions } from '../Fields/Extensions';
const OperationRow = styled(Row)` const OperationRow = styled(Row)`
backface-visibility: hidden; backface-visibility: hidden;
@ -58,6 +59,7 @@ export class Operation extends React.Component<OperationProps> {
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />} {externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
</Description> </Description>
)} )}
<Extensions extensions={operation.extensions} />
<SecurityRequirements securities={operation.security} /> <SecurityRequirements securities={operation.security} />
<Parameters parameters={operation.parameters} body={operation.requestBody} /> <Parameters parameters={operation.parameters} body={operation.requestBody} />
<ResponsesList responses={operation.responses} /> <ResponsesList responses={operation.responses} />

View File

@ -13,14 +13,18 @@ marked.setOptions({
}, },
}); });
export const LEGACY_REGEXP = '^\\s*<!-- ReDoc-Inject:\\s+?<{component}\\s*?/?>\\s+?-->\\s*$'; export const LEGACY_REGEXP = '^ {0,3}<!-- ReDoc-Inject:\\s+?<({component}).*?/?>\\s+?-->\\s*$';
export const MDX_COMPONENT_REGEXP = '^\\s*<{component}\\s*?/>\\s*$';
// prettier-ignore
export const MDX_COMPONENT_REGEXP = '(?:^ {0,3}<({component})([\\s\\S]*?)>([\\s\\S]*?)</\\2>' // with children
+ '|^ {0,3}<({component})([\\s\\S]*?)(?:/>|\\n{2,}))'; // self-closing
export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')'; export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')';
export interface MDXComponentMeta { export interface MDXComponentMeta {
component: React.ComponentType; component: React.ComponentType;
propsSelector: (store?: AppStore) => any; propsSelector: (store?: AppStore) => any;
attrs?: object; props?: object;
} }
export interface MarkdownHeading { export interface MarkdownHeading {
@ -37,11 +41,8 @@ export function buildComponentComment(name: string) {
export class MarkdownRenderer { export class MarkdownRenderer {
static containsComponent(rawText: string, componentName: string) { static containsComponent(rawText: string, componentName: string) {
const anyCompRegexp = new RegExp( const compRegexp = new RegExp(COMPONENT_REGEXP.replace(/{component}/g, componentName), 'gmi');
COMPONENT_REGEXP.replace(/{component}/g, componentName), return compRegexp.test(rawText);
'gmi',
);
return anyCompRegexp.test(rawText);
} }
headings: MarkdownHeading[] = []; headings: MarkdownHeading[] = [];
@ -147,32 +148,41 @@ export class MarkdownRenderer {
return res; return res;
} }
// TODO: rewrite this completelly! Regexp-based 👎 // regexp-based 👎: remark is slow and too big so for now using marked + regexps soup
// Use marked ecosystem
renderMdWithComponents(rawText: string): Array<string | MDXComponentMeta> { renderMdWithComponents(rawText: string): Array<string | MDXComponentMeta> {
const components = this.options && this.options.allowedMdComponents; const components = this.options && this.options.allowedMdComponents;
if (!components || Object.keys(components).length === 0) { if (!components || Object.keys(components).length === 0) {
return [this.renderMd(rawText)]; return [this.renderMd(rawText)];
} }
const componentDefs: string[] = []; const names = Object.keys(components).join('|');
const names = '(?:' + Object.keys(components).join('|') + ')'; const componentsRegexp = new RegExp(COMPONENT_REGEXP.replace(/{component}/g, names), 'mig');
const anyCompRegexp = new RegExp( const htmlParts: string[] = [];
COMPONENT_REGEXP.replace(/{component}/g, '(' + names + '.*?)'), const componentDefs: MDXComponentMeta[] = [];
'gmi',
); let match = componentsRegexp.exec(rawText);
let match = anyCompRegexp.exec(rawText); let lasxtIdx = 0;
while (match) { while (match) {
componentDefs.push(match[1] || match[2]); htmlParts.push(rawText.substring(lasxtIdx, match.index));
match = anyCompRegexp.exec(rawText); lasxtIdx = componentsRegexp.lastIndex;
} const compName = match[1] || match[2] || match[5];
const componentMeta = components[compName];
const props = match[3] || match[6];
const children = match[4];
if (componentMeta) {
componentDefs.push({
component: componentMeta.component,
propsSelector: componentMeta.propsSelector,
props: { ...parseProps(props), ...componentMeta.props, children },
});
}
match = componentsRegexp.exec(rawText);
}
htmlParts.push(rawText.substring(lasxtIdx));
const splitCompRegexp = new RegExp(
COMPONENT_REGEXP.replace(/{component}/g, names + '.*?'),
'mi',
);
const htmlParts = rawText.split(splitCompRegexp);
const res: any[] = []; const res: any[] = [];
for (let i = 0; i < htmlParts.length; i++) { for (let i = 0; i < htmlParts.length; i++) {
const htmlPart = htmlParts[i]; const htmlPart = htmlParts[i];
@ -180,46 +190,37 @@ export class MarkdownRenderer {
res.push(this.renderMd(htmlPart)); res.push(this.renderMd(htmlPart));
} }
if (componentDefs[i]) { if (componentDefs[i]) {
const { componentName, attrs } = parseComponent(componentDefs[i]); res.push(componentDefs[i]);
if (!componentName) {
continue;
}
res.push({
...components[componentName],
attrs,
});
} }
} }
return res; return res;
} }
} }
function parseComponent( function parseProps(props: string): object {
htmlTag: string, if (!props) {
): { return {};
componentName?: string;
attrs: any;
} {
const match = /([\w_-]+)(\s+[\w_-]+\s*={[^}]*?})*/.exec(htmlTag);
if (match === null || match.length <= 1) {
return { componentName: undefined, attrs: {} };
} }
const componentName = match[1];
const attrs = {};
for (let i = 2; i < match.length; i++) {
if (!match[i]) {
continue;
}
const [name, value] = match[i]
.trim()
.split('=')
.map(p => p.trim());
const regex = /([\w-]+)\s*=\s*(?:{([^}]+?)}|"([^"]+?)")/gim;
const parsed = {};
let match;
// tslint:disable-next-line // tslint:disable-next-line
attrs[name] = value.startsWith('{') ? eval(value.substr(1, value.length - 2)) : eval(value); while ((match = regex.exec(props)) !== null) {
if (match[3]) {
// string prop match (in double quotes)
parsed[match[1]] = match[3];
} else if (match[2]) {
// jsx prop match (in curly braces)
let val;
try {
val = JSON.parse(match[2]);
} catch (e) {
/* noop */
} }
return { parsed[match[1]] = val;
componentName, }
attrs, }
};
return parsed;
} }

View File

@ -10,6 +10,7 @@ export interface RedocRawOptions {
hideHostname?: boolean | string; hideHostname?: boolean | string;
expandResponses?: string | 'all'; expandResponses?: string | 'all';
requiredPropsFirst?: boolean | string; requiredPropsFirst?: boolean | string;
sortPropsAlphabetically?: boolean | string;
noAutoAuth?: boolean | string; noAutoAuth?: boolean | string;
nativeScrollbars?: boolean | string; nativeScrollbars?: boolean | string;
pathInMiddlePanel?: boolean | string; pathInMiddlePanel?: boolean | string;
@ -18,6 +19,7 @@ export interface RedocRawOptions {
hideDownloadButton?: boolean | string; hideDownloadButton?: boolean | string;
disableSearch?: boolean | string; disableSearch?: boolean | string;
onlyRequiredInSamples?: boolean | string; onlyRequiredInSamples?: boolean | string;
showExtensions?: boolean | string | string[];
unstable_ignoreMimeParameters?: boolean; unstable_ignoreMimeParameters?: boolean;
@ -89,11 +91,27 @@ export class RedocNormalizedOptions {
return () => 0; return () => 0;
} }
static normalizeShowExtensions(value: RedocRawOptions['showExtensions']): string[] | boolean {
if (typeof value === 'undefined') {
return false;
}
if (value === '') {
return true;
}
if (typeof value === 'string') {
return value.split(',').map(ext => ext.trim());
}
return value;
}
theme: ResolvedThemeInterface; theme: ResolvedThemeInterface;
scrollYOffset: () => number; scrollYOffset: () => number;
hideHostname: boolean; hideHostname: boolean;
expandResponses: { [code: string]: boolean } | 'all'; expandResponses: { [code: string]: boolean } | 'all';
requiredPropsFirst: boolean; requiredPropsFirst: boolean;
sortPropsAlphabetically: boolean;
noAutoAuth: boolean; noAutoAuth: boolean;
nativeScrollbars: boolean; nativeScrollbars: boolean;
pathInMiddlePanel: boolean; pathInMiddlePanel: boolean;
@ -101,6 +119,7 @@ export class RedocNormalizedOptions {
hideDownloadButton: boolean; hideDownloadButton: boolean;
disableSearch: boolean; disableSearch: boolean;
onlyRequiredInSamples: boolean; onlyRequiredInSamples: boolean;
showExtensions: boolean | string[];
/* tslint:disable-next-line */ /* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean; unstable_ignoreMimeParameters: boolean;
@ -120,6 +139,7 @@ export class RedocNormalizedOptions {
this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname); this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname);
this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses); this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses);
this.requiredPropsFirst = argValueToBoolean(raw.requiredPropsFirst); this.requiredPropsFirst = argValueToBoolean(raw.requiredPropsFirst);
this.sortPropsAlphabetically = argValueToBoolean(raw.sortPropsAlphabetically);
this.noAutoAuth = argValueToBoolean(raw.noAutoAuth); this.noAutoAuth = argValueToBoolean(raw.noAutoAuth);
this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars); this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars);
this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel); this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel);
@ -127,6 +147,7 @@ export class RedocNormalizedOptions {
this.hideDownloadButton = argValueToBoolean(raw.hideDownloadButton); this.hideDownloadButton = argValueToBoolean(raw.hideDownloadButton);
this.disableSearch = argValueToBoolean(raw.disableSearch); this.disableSearch = argValueToBoolean(raw.disableSearch);
this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples); this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples);
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);

View File

@ -58,6 +58,42 @@ describe('Markdown renderer', () => {
expect(parts).toHaveLength(1); expect(parts).toHaveLength(1);
const part = parts[0] as MDXComponentMeta; const part = parts[0] as MDXComponentMeta;
expect(part.component).toBe(TestComponent); expect(part.component).toBe(TestComponent);
expect(part.attrs).toEqual({ pointer: 'test' }); expect(part.props).toEqual({ pointer: 'test' });
});
test('renderMdWithComponents should parse string attribute names', () => {
const source = '<security-definitions pointer="test" />';
const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(1);
const part = parts[0] as MDXComponentMeta;
expect(part.component).toBe(TestComponent);
expect(part.props).toEqual({ pointer: 'test' });
});
test('renderMdWithComponents should parse string attribute with spaces new-lines', () => {
const source = '<security-definitions \n pointer = "test" \n flag-dash={ \nfalse } />';
const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(1);
const part = parts[0] as MDXComponentMeta;
expect(part.component).toBe(TestComponent);
expect(part.props).toEqual({ pointer: 'test', 'flag-dash': false });
});
test('renderMdWithComponents should parse children', () => {
const source = '<security-definitions> Test Test </security-definitions>';
const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(1);
const part = parts[0] as MDXComponentMeta;
expect(part.component).toBe(TestComponent);
expect(part.props).toEqual({ children: ' Test Test ' });
});
test('renderMdWithComponents should parse children', () => {
const source = '<security-definitions> Test Test </security-definitions>';
const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(1);
const part = parts[0] as MDXComponentMeta;
expect(part.component).toBe(TestComponent);
expect(part.props).toEqual({ children: ' Test Test ' });
}); });
}); });

View File

@ -3,6 +3,7 @@ import { action, observable } from 'mobx';
import { OpenAPIParameter, Referenced } from '../../types'; import { OpenAPIParameter, Referenced } from '../../types';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { extractExtensions } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
import { SchemaModel } from './Schema'; import { SchemaModel } from './Schema';
@ -21,6 +22,7 @@ export class FieldModel {
deprecated: boolean; deprecated: boolean;
in?: string; in?: string;
kind: string; kind: string;
extensions?: Dict<any>;
constructor( constructor(
parser: OpenAPIParser, parser: OpenAPIParser,
@ -40,6 +42,10 @@ export class FieldModel {
this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated; this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated;
parser.exitRef(infoOrRef); parser.exitRef(infoOrRef);
if (options.showExtensions) {
this.extensions = extractExtensions(info, options.showExtensions);
}
} }
@action @action

View File

@ -4,9 +4,15 @@ import { IMenuItem } from '../MenuStore';
import { GroupModel } from './Group.model'; import { GroupModel } from './Group.model';
import { SecurityRequirementModel } from './SecurityRequirement'; import { SecurityRequirementModel } from './SecurityRequirement';
import { OpenAPIExternalDocumentation, OpenAPIServer, OpenAPIXCodeSample } from '../../types'; import {
OpenAPIExternalDocumentation,
OpenAPIPath,
OpenAPIServer,
OpenAPIXCodeSample,
} from '../../types';
import { import {
extractExtensions,
getOperationSummary, getOperationSummary,
getStatusCodeType, getStatusCodeType,
isStatusCode, isStatusCode,
@ -14,6 +20,7 @@ import {
memoize, memoize,
mergeParams, mergeParams,
normalizeServers, normalizeServers,
sortByField,
sortByRequired, sortByRequired,
} from '../../utils'; } from '../../utils';
import { ContentItemModel, ExtendedOpenAPIOperation } from '../MenuBuilder'; import { ContentItemModel, ExtendedOpenAPIOperation } from '../MenuBuilder';
@ -56,6 +63,7 @@ export class OperationModel implements IMenuItem {
servers: OpenAPIServer[]; servers: OpenAPIServer[];
security: SecurityRequirementModel[]; security: SecurityRequirementModel[];
codeSamples: OpenAPIXCodeSample[]; codeSamples: OpenAPIXCodeSample[];
extensions: Dict<any>;
constructor( constructor(
private parser: OpenAPIParser, private parser: OpenAPIParser,
@ -83,14 +91,23 @@ export class OperationModel implements IMenuItem {
this.operationId = operationSpec.operationId; this.operationId = operationSpec.operationId;
this.codeSamples = operationSpec['x-code-samples'] || []; this.codeSamples = operationSpec['x-code-samples'] || [];
this.path = operationSpec.pathName; this.path = operationSpec.pathName;
const pathInfo = parser.byRef<OpenAPIPath>(
JsonPointer.compile(['paths', operationSpec.pathName]),
);
this.servers = normalizeServers( this.servers = normalizeServers(
parser.specUrl, parser.specUrl,
operationSpec.servers || parser.spec.servers || [], operationSpec.servers || (pathInfo && pathInfo.servers) || parser.spec.servers || [],
); );
this.security = (operationSpec.security || parser.spec.security || []).map( this.security = (operationSpec.security || parser.spec.security || []).map(
security => new SecurityRequirementModel(security, parser), security => new SecurityRequirementModel(security, parser),
); );
if (options.showExtensions) {
this.extensions = extractExtensions(operationSpec, options.showExtensions);
}
} }
/** /**
@ -136,6 +153,9 @@ export class OperationModel implements IMenuItem {
// TODO: fix pointer // TODO: fix pointer
).map(paramOrRef => new FieldModel(this.parser, paramOrRef, this.pointer, this.options)); ).map(paramOrRef => new FieldModel(this.parser, paramOrRef, this.pointer, this.options));
if (this.options.sortPropsAlphabetically) {
sortByField(_parameters, 'name');
}
if (this.options.requiredPropsFirst) { if (this.options.requiredPropsFirst) {
sortByRequired(_parameters); sortByRequired(_parameters);
} }

View File

@ -9,10 +9,12 @@ import { FieldModel } from './Field';
import { MergedOpenAPISchema } from '../'; import { MergedOpenAPISchema } from '../';
import { import {
detectType, detectType,
extractExtensions,
humanizeConstraints, humanizeConstraints,
isNamedDefinition, isNamedDefinition,
isPrimitiveType, isPrimitiveType,
JsonPointer, JsonPointer,
sortByField,
sortByRequired, sortByRequired,
} from '../../utils/'; } from '../../utils/';
@ -54,6 +56,7 @@ export class SchemaModel {
rawSchema: OpenAPISchema; rawSchema: OpenAPISchema;
schema: MergedOpenAPISchema; schema: MergedOpenAPISchema;
extensions?: Dict<any>;
/** /**
* @param isChild if schema discriminator Child * @param isChild if schema discriminator Child
@ -77,6 +80,10 @@ export class SchemaModel {
// exit all the refs visited during allOf traverse // exit all the refs visited during allOf traverse
parser.exitRef({ $ref: parent$ref }); parser.exitRef({ $ref: parent$ref });
} }
if (options.showExtensions) {
this.extensions = extractExtensions(this.schema, options.showExtensions);
}
} }
/** /**
@ -161,9 +168,10 @@ export class SchemaModel {
(variant, idx) => (variant, idx) =>
new SchemaModel( new SchemaModel(
parser, parser,
{
// merge base schema into each of oneOf's subschemas // merge base schema into each of oneOf's subschemas
...variant, {
// variant may already have allOf so merge it to not get overwritten
...parser.mergeAllOf(variant, this.pointer + '/oneOf/' + idx),
allOf: [{ ...this.schema, oneOf: undefined, anyOf: undefined }], allOf: [{ ...this.schema, oneOf: undefined, anyOf: undefined }],
} as OpenAPISchema, } as OpenAPISchema,
this.pointer + '/oneOf/' + idx, this.pointer + '/oneOf/' + idx,
@ -254,8 +262,12 @@ function buildFields(
); );
}); });
if (options.sortPropsAlphabetically) {
sortByField(fields, 'name');
}
if (options.requiredPropsFirst) { if (options.requiredPropsFirst) {
sortByRequired(fields, schema.required); // if not sort alphabetically sort in the order from required keyword
sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined);
} }
if (typeof additionalProps === 'object' || additionalProps === true) { if (typeof additionalProps === 'object' || additionalProps === true) {

View File

@ -196,13 +196,19 @@ export function sortByRequired(
} else if (a.required && !b.required) { } else if (a.required && !b.required) {
return -1; return -1;
} else if (a.required && b.required) { } else if (a.required && b.required) {
return order.indexOf(a.name) > order.indexOf(b.name) ? 1 : -1; return order.indexOf(a.name) - order.indexOf(b.name);
} else { } else {
return 0; return 0;
} }
}); });
} }
export function sortByField<T extends string>(fields: Array<{ [P in T]: string }>, param: T) {
fields.sort((a, b) => {
return a[param].localeCompare(b[param]);
});
}
export function mergeParams( export function mergeParams(
parser: OpenAPIParser, parser: OpenAPIParser,
pathParams: Array<Referenced<OpenAPIParameter>> = [], pathParams: Array<Referenced<OpenAPIParameter>> = [],
@ -286,3 +292,34 @@ export const shortenHTTPVerb = verb =>
delete: 'del', delete: 'del',
options: 'opts', options: 'opts',
}[verb] || verb); }[verb] || verb);
export function isRedocExtension(key: string): boolean {
const redocExtensions = {
'x-circular-ref': true,
'x-code-samples': true,
'x-displayName': true,
'x-examples': true,
'x-ignoredHeaderParameters': true,
'x-logo': true,
'x-nullable': true,
'x-servers': true,
'x-tagGroups': true,
'x-traitTag': true,
};
return key in redocExtensions;
}
export function extractExtensions(obj: object, showExtensions: string[] | true): Dict<any> {
return Object.keys(obj)
.filter(key => {
if (showExtensions === true) {
return key.startsWith('x-') && !isRedocExtension(key);
}
return key.startsWith('x-') && showExtensions.indexOf(key) > -1;
})
.reduce((acc, key) => {
acc[key] = obj[key];
return acc;
}, {});
}

View File

@ -5580,9 +5580,9 @@ mark.js@^8.11.1:
version "8.11.1" version "8.11.1"
resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5" resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5"
marked@0.3.18: marked@^0.5.1:
version "0.3.18" version "0.5.1"
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.18.tgz#3ef058cd926101849b92a7a7c15db18c7fc76b2f" resolved "https://registry.yarnpkg.com/marked/-/marked-0.5.1.tgz#062f43b88b02ee80901e8e8d8e6a620ddb3aa752"
math-random@^1.0.1: math-random@^1.0.1:
version "1.0.1" version "1.0.1"