diff --git a/README.md b/README.md index 2804c8e572..2b866a7bbc 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ **OpenAPI/Swagger-generated API Reference Documentation** - [![Build Status](https://travis-ci.org/Rebilly/ReDoc.svg?branch=master)](https://travis-ci.org/Rebilly/ReDoc) [![Coverage Status](https://coveralls.io/repos/Rebilly/ReDoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Rebilly/ReDoc?branch=master) [![dependencies Status](https://david-dm.org/Rebilly/ReDoc/status.svg)](https://david-dm.org/Rebilly/ReDoc) [![devDependencies Status](https://david-dm.org/Rebilly/ReDoc/dev-status.svg)](https://david-dm.org/Rebilly/ReDoc#info=devDependencies) [![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Rebilly/ReDoc/blob/master/LICENSE) + [![Build Status](https://travis-ci.org/Rebilly/ReDoc.svg?branch=master)](https://travis-ci.org/Rebilly/ReDoc) [![Coverage Status](https://coveralls.io/repos/Rebilly/ReDoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Rebilly/ReDoc?branch=master) [![dependencies Status](https://david-dm.org/Rebilly/ReDoc/status.svg)](https://david-dm.org/Rebilly/ReDoc) [![devDependencies Status](https://david-dm.org/Rebilly/ReDoc/dev-status.svg)](https://david-dm.org/Rebilly/ReDoc#info=devDependencies) [![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Rebilly/ReDoc/blob/master/LICENSE) - [![bundle size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js?compression=gzip&max=300000)](https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js) [![npm](https://img.shields.io/npm/dm/redoc.svg)](https://www.npmjs.com/package/redoc) [![](https://data.jsdelivr.com/v1/package/npm/redoc/badge)](https://www.jsdelivr.com/package/npm/redoc) + [![bundle size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js?compression=gzip&max=300000)](https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js) [![npm](https://img.shields.io/npm/dm/redoc.svg)](https://www.npmjs.com/package/redoc) [![](https://data.jsdelivr.com/v1/package/npm/redoc/badge)](https://www.jsdelivr.com/package/npm/redoc) @@ -135,6 +135,10 @@ For npm: ## 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: ```js @@ -215,6 +219,8 @@ You can use all of the following options with standalone version on tag * `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. * `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 * `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 diff --git a/cli/package.json b/cli/package.json index 2b51d418bb..357bb92641 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "redoc-cli", - "version": "0.6.2", + "version": "0.6.4", "description": "ReDoc's Command Line Interface", "main": "index.js", "bin": "index.js", @@ -15,6 +15,7 @@ "react": "^16.4.2", "react-dom": "^16.4.2", "redoc": "^2.0.0-alpha.37", + "styled-components": "^3.4.0", "tslib": "^1.9.3", "yargs": "^12.0.1" }, diff --git a/demo/webpack.config.ts b/demo/webpack.config.ts index edd661b006..9fef0db086 100644 --- a/demo/webpack.config.ts +++ b/demo/webpack.config.ts @@ -22,6 +22,7 @@ const tsLoader = env => ({ options: { compilerOptions: { 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: { loader: 'ts-loader', options: { diff --git a/package.json b/package.json index 333609fb75..fbe068a331 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "json-schema-ref-parser": "^5.1.2", "lunr": "^2.3.2", "mark.js": "^8.11.1", - "marked": "0.3.18", + "marked": "^0.5.1", "memoize-one": "^4.0.0", "mobx-react": "^5.2.5", "openapi-sampler": "1.0.0-beta.14", diff --git a/src/components/Fields/Extensions.tsx b/src/components/Fields/Extensions.tsx new file mode 100644 index 0000000000..d39e40c69f --- /dev/null +++ b/src/components/Fields/Extensions.tsx @@ -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 { + render() { + return ( + + {options => ( + <> + {options.showExtensions && + Object.keys(this.props.extensions).map(key => ( + + {key}:{' '} + {JSON.stringify(this.props.extensions[key])} + + ))} + + )} + + ); + } +} diff --git a/src/components/Fields/FieldDetails.tsx b/src/components/Fields/FieldDetails.tsx index 838351ec30..32371688e8 100644 --- a/src/components/Fields/FieldDetails.tsx +++ b/src/components/Fields/FieldDetails.tsx @@ -12,6 +12,7 @@ import { import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { Markdown } from '../Markdown/Markdown'; import { EnumValues } from './EnumValues'; +import { Extensions } from './Extensions'; import { FieldProps } from './Field'; import { ConstraintsView } from './FieldContstraints'; import { FieldDetail } from './FieldDetail'; @@ -51,6 +52,7 @@ export class FieldDetails extends React.PureComponent { {!renderDiscriminatorSwitch && }{' '} {showExamples && } + {}
diff --git a/src/components/Markdown/AdvancedMarkdown.tsx b/src/components/Markdown/AdvancedMarkdown.tsx index 0b1bbed778..d7714f5e60 100644 --- a/src/components/Markdown/AdvancedMarkdown.tsx +++ b/src/components/Markdown/AdvancedMarkdown.tsx @@ -42,7 +42,7 @@ export class AdvancedMarkdown extends React.Component { { key: idx }, ); } - return ; + return ; }); } } diff --git a/src/components/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index 320a6621d6..1283a6ad89 100644 --- a/src/components/Operation/Operation.tsx +++ b/src/components/Operation/Operation.tsx @@ -18,6 +18,7 @@ import { ResponseSamples } from '../ResponseSamples/ResponseSamples'; import { OperationModel as OperationType } from '../../services/models'; import styled from '../../styled-components'; +import { Extensions } from '../Fields/Extensions'; const OperationRow = styled(Row)` backface-visibility: hidden; @@ -58,6 +59,7 @@ export class Operation extends React.Component { {externalDocs && } )} + diff --git a/src/services/MarkdownRenderer.ts b/src/services/MarkdownRenderer.ts index 3494415b48..a944dac3e7 100644 --- a/src/services/MarkdownRenderer.ts +++ b/src/services/MarkdownRenderer.ts @@ -13,14 +13,18 @@ marked.setOptions({ }, }); -export const LEGACY_REGEXP = '^\\s*\\s*$'; -export const MDX_COMPONENT_REGEXP = '^\\s*<{component}\\s*?/>\\s*$'; +export const LEGACY_REGEXP = '^ {0,3}\\s*$'; + +// prettier-ignore +export const MDX_COMPONENT_REGEXP = '(?:^ {0,3}<({component})([\\s\\S]*?)>([\\s\\S]*?)' // with children + + '|^ {0,3}<({component})([\\s\\S]*?)(?:/>|\\n{2,}))'; // self-closing + export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')'; export interface MDXComponentMeta { component: React.ComponentType; propsSelector: (store?: AppStore) => any; - attrs?: object; + props?: object; } export interface MarkdownHeading { @@ -37,11 +41,8 @@ export function buildComponentComment(name: string) { export class MarkdownRenderer { static containsComponent(rawText: string, componentName: string) { - const anyCompRegexp = new RegExp( - COMPONENT_REGEXP.replace(/{component}/g, componentName), - 'gmi', - ); - return anyCompRegexp.test(rawText); + const compRegexp = new RegExp(COMPONENT_REGEXP.replace(/{component}/g, componentName), 'gmi'); + return compRegexp.test(rawText); } headings: MarkdownHeading[] = []; @@ -147,32 +148,41 @@ export class MarkdownRenderer { return res; } - // TODO: rewrite this completelly! Regexp-based 👎 - // Use marked ecosystem + // regexp-based 👎: remark is slow and too big so for now using marked + regexps soup renderMdWithComponents(rawText: string): Array { const components = this.options && this.options.allowedMdComponents; if (!components || Object.keys(components).length === 0) { 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( - COMPONENT_REGEXP.replace(/{component}/g, '(' + names + '.*?)'), - 'gmi', - ); - let match = anyCompRegexp.exec(rawText); + const htmlParts: string[] = []; + const componentDefs: MDXComponentMeta[] = []; + + let match = componentsRegexp.exec(rawText); + let lasxtIdx = 0; while (match) { - componentDefs.push(match[1] || match[2]); - match = anyCompRegexp.exec(rawText); + htmlParts.push(rawText.substring(lasxtIdx, match.index)); + 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[] = []; for (let i = 0; i < htmlParts.length; i++) { const htmlPart = htmlParts[i]; @@ -180,46 +190,37 @@ export class MarkdownRenderer { res.push(this.renderMd(htmlPart)); } if (componentDefs[i]) { - const { componentName, attrs } = parseComponent(componentDefs[i]); - if (!componentName) { - continue; - } - res.push({ - ...components[componentName], - attrs, - }); + res.push(componentDefs[i]); } } return res; } } -function parseComponent( - htmlTag: string, -): { - componentName?: string; - attrs: any; -} { - const match = /([\w_-]+)(\s+[\w_-]+\s*={[^}]*?})*/.exec(htmlTag); - if (match === null || match.length <= 1) { - return { componentName: undefined, attrs: {} }; +function parseProps(props: string): object { + if (!props) { + return {}; } - 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()); - // tslint:disable-next-line - attrs[name] = value.startsWith('{') ? eval(value.substr(1, value.length - 2)) : eval(value); + const regex = /([\w-]+)\s*=\s*(?:{([^}]+?)}|"([^"]+?)")/gim; + const parsed = {}; + let match; + // tslint:disable-next-line + 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 */ + } + parsed[match[1]] = val; + } } - return { - componentName, - attrs, - }; + + return parsed; } diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index df26333094..18623976c8 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -10,6 +10,7 @@ export interface RedocRawOptions { hideHostname?: boolean | string; expandResponses?: string | 'all'; requiredPropsFirst?: boolean | string; + sortPropsAlphabetically?: boolean | string; noAutoAuth?: boolean | string; nativeScrollbars?: boolean | string; pathInMiddlePanel?: boolean | string; @@ -18,6 +19,7 @@ export interface RedocRawOptions { hideDownloadButton?: boolean | string; disableSearch?: boolean | string; onlyRequiredInSamples?: boolean | string; + showExtensions?: boolean | string | string[]; unstable_ignoreMimeParameters?: boolean; @@ -89,11 +91,27 @@ export class RedocNormalizedOptions { 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; scrollYOffset: () => number; hideHostname: boolean; expandResponses: { [code: string]: boolean } | 'all'; requiredPropsFirst: boolean; + sortPropsAlphabetically: boolean; noAutoAuth: boolean; nativeScrollbars: boolean; pathInMiddlePanel: boolean; @@ -101,6 +119,7 @@ export class RedocNormalizedOptions { hideDownloadButton: boolean; disableSearch: boolean; onlyRequiredInSamples: boolean; + showExtensions: boolean | string[]; /* tslint:disable-next-line */ unstable_ignoreMimeParameters: boolean; @@ -120,6 +139,7 @@ export class RedocNormalizedOptions { this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname); this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses); this.requiredPropsFirst = argValueToBoolean(raw.requiredPropsFirst); + this.sortPropsAlphabetically = argValueToBoolean(raw.sortPropsAlphabetically); this.noAutoAuth = argValueToBoolean(raw.noAutoAuth); this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars); this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel); @@ -127,6 +147,7 @@ export class RedocNormalizedOptions { this.hideDownloadButton = argValueToBoolean(raw.hideDownloadButton); this.disableSearch = argValueToBoolean(raw.disableSearch); this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples); + this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); diff --git a/src/services/__tests__/MarkdownRenderer.test.ts b/src/services/__tests__/MarkdownRenderer.test.ts index 89f308c69c..efd0d1fb3a 100644 --- a/src/services/__tests__/MarkdownRenderer.test.ts +++ b/src/services/__tests__/MarkdownRenderer.test.ts @@ -53,11 +53,47 @@ describe('Markdown renderer', () => { }); test('renderMdWithComponents should parse attribute names', () => { - const source = ''; + const source = ''; const parts = renderer.renderMdWithComponents(source); expect(parts).toHaveLength(1); const part = parts[0] as MDXComponentMeta; 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 = ''; + 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 = ''; + 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 = ' Test 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({ children: ' Test Test ' }); + }); + + test('renderMdWithComponents should parse children', () => { + const source = ' Test 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({ children: ' Test Test ' }); }); }); diff --git a/src/services/models/Field.ts b/src/services/models/Field.ts index 192c9ee21b..f54a3cf85b 100644 --- a/src/services/models/Field.ts +++ b/src/services/models/Field.ts @@ -3,6 +3,7 @@ import { action, observable } from 'mobx'; import { OpenAPIParameter, Referenced } from '../../types'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; +import { extractExtensions } from '../../utils/openapi'; import { OpenAPIParser } from '../OpenAPIParser'; import { SchemaModel } from './Schema'; @@ -21,6 +22,7 @@ export class FieldModel { deprecated: boolean; in?: string; kind: string; + extensions?: Dict; constructor( parser: OpenAPIParser, @@ -40,6 +42,10 @@ export class FieldModel { this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated; parser.exitRef(infoOrRef); + + if (options.showExtensions) { + this.extensions = extractExtensions(info, options.showExtensions); + } } @action diff --git a/src/services/models/Operation.ts b/src/services/models/Operation.ts index a63fa77655..3e31a3677b 100644 --- a/src/services/models/Operation.ts +++ b/src/services/models/Operation.ts @@ -4,9 +4,15 @@ import { IMenuItem } from '../MenuStore'; import { GroupModel } from './Group.model'; import { SecurityRequirementModel } from './SecurityRequirement'; -import { OpenAPIExternalDocumentation, OpenAPIServer, OpenAPIXCodeSample } from '../../types'; +import { + OpenAPIExternalDocumentation, + OpenAPIPath, + OpenAPIServer, + OpenAPIXCodeSample, +} from '../../types'; import { + extractExtensions, getOperationSummary, getStatusCodeType, isStatusCode, @@ -14,6 +20,7 @@ import { memoize, mergeParams, normalizeServers, + sortByField, sortByRequired, } from '../../utils'; import { ContentItemModel, ExtendedOpenAPIOperation } from '../MenuBuilder'; @@ -56,6 +63,7 @@ export class OperationModel implements IMenuItem { servers: OpenAPIServer[]; security: SecurityRequirementModel[]; codeSamples: OpenAPIXCodeSample[]; + extensions: Dict; constructor( private parser: OpenAPIParser, @@ -83,14 +91,23 @@ export class OperationModel implements IMenuItem { this.operationId = operationSpec.operationId; this.codeSamples = operationSpec['x-code-samples'] || []; this.path = operationSpec.pathName; + + const pathInfo = parser.byRef( + JsonPointer.compile(['paths', operationSpec.pathName]), + ); + this.servers = normalizeServers( parser.specUrl, - operationSpec.servers || parser.spec.servers || [], + operationSpec.servers || (pathInfo && pathInfo.servers) || parser.spec.servers || [], ); this.security = (operationSpec.security || parser.spec.security || []).map( 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 ).map(paramOrRef => new FieldModel(this.parser, paramOrRef, this.pointer, this.options)); + if (this.options.sortPropsAlphabetically) { + sortByField(_parameters, 'name'); + } if (this.options.requiredPropsFirst) { sortByRequired(_parameters); } diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index 69f0e0b098..e5b69fcb85 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -9,10 +9,12 @@ import { FieldModel } from './Field'; import { MergedOpenAPISchema } from '../'; import { detectType, + extractExtensions, humanizeConstraints, isNamedDefinition, isPrimitiveType, JsonPointer, + sortByField, sortByRequired, } from '../../utils/'; @@ -54,6 +56,7 @@ export class SchemaModel { rawSchema: OpenAPISchema; schema: MergedOpenAPISchema; + extensions?: Dict; /** * @param isChild if schema discriminator Child @@ -77,6 +80,10 @@ export class SchemaModel { // exit all the refs visited during allOf traverse parser.exitRef({ $ref: parent$ref }); } + + if (options.showExtensions) { + this.extensions = extractExtensions(this.schema, options.showExtensions); + } } /** @@ -161,9 +168,10 @@ export class SchemaModel { (variant, idx) => new SchemaModel( 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 }], } as OpenAPISchema, this.pointer + '/oneOf/' + idx, @@ -254,8 +262,12 @@ function buildFields( ); }); + if (options.sortPropsAlphabetically) { + sortByField(fields, 'name'); + } 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) { diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index 3d91ac1292..7e1721323f 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -196,13 +196,19 @@ export function sortByRequired( } else if (a.required && !b.required) { return -1; } 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 { return 0; } }); } +export function sortByField(fields: Array<{ [P in T]: string }>, param: T) { + fields.sort((a, b) => { + return a[param].localeCompare(b[param]); + }); +} + export function mergeParams( parser: OpenAPIParser, pathParams: Array> = [], @@ -286,3 +292,34 @@ export const shortenHTTPVerb = verb => delete: 'del', options: 'opts', }[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 { + 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; + }, {}); +} diff --git a/yarn.lock b/yarn.lock index 5a341c9226..64bf4fa0c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5580,9 +5580,9 @@ mark.js@^8.11.1: version "8.11.1" resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5" -marked@0.3.18: - version "0.3.18" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.18.tgz#3ef058cd926101849b92a7a7c15db18c7fc76b2f" +marked@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.5.1.tgz#062f43b88b02ee80901e8e8d8e6a620ddb3aa752" math-random@^1.0.1: version "1.0.1"