diff --git a/README.md b/README.md index 98d1c39a..c2a810c4 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) @@ -215,6 +215,7 @@ 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. +* `showExtensions` - show vendor extensions ("x-" fields). Extensions used by ReDoc are ignored. * `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 @@ -242,4 +243,4 @@ Redoc.init('http://petstore.swagger.io/v2/swagger.json', { ----------- ## Development -see [CONTRIBUTING.md](.github/CONTRIBUTING.md) \ No newline at end of file +see [CONTRIBUTING.md](.github/CONTRIBUTING.md) diff --git a/src/components/Fields/Extensions.tsx b/src/components/Fields/Extensions.tsx new file mode 100644 index 00000000..d39e40c6 --- /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 838351ec..32371688 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/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index 320a6621..1283a6ad 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/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index 851d4b1b..c0368a3b 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -17,6 +17,7 @@ export interface RedocRawOptions { hideLoading?: boolean | string; hideDownloadButton?: boolean | string; disableSearch?: boolean | string; + showExtensions?: boolean | string | string[]; unstable_ignoreMimeParameters?: boolean; @@ -88,6 +89,21 @@ 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; @@ -99,6 +115,7 @@ export class RedocNormalizedOptions { untrustedSpec: boolean; hideDownloadButton: boolean; disableSearch: boolean; + showExtensions: boolean | string[]; /* tslint:disable-next-line */ unstable_ignoreMimeParameters: boolean; @@ -124,6 +141,7 @@ export class RedocNormalizedOptions { this.untrustedSpec = argValueToBoolean(raw.untrustedSpec); this.hideDownloadButton = argValueToBoolean(raw.hideDownloadButton); this.disableSearch = argValueToBoolean(raw.disableSearch); + this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); diff --git a/src/services/models/Field.ts b/src/services/models/Field.ts index 192c9ee2..f54a3cf8 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 bd12a064..de558086 100644 --- a/src/services/models/Operation.ts +++ b/src/services/models/Operation.ts @@ -12,6 +12,7 @@ import { } from '../../types'; import { + extractExtensions, getOperationSummary, getStatusCodeType, isStatusCode, @@ -61,6 +62,7 @@ export class OperationModel implements IMenuItem { servers: OpenAPIServer[]; security: SecurityRequirementModel[]; codeSamples: OpenAPIXCodeSample[]; + extensions: Dict; constructor( private parser: OpenAPIParser, @@ -101,6 +103,10 @@ export class OperationModel implements IMenuItem { this.security = (operationSpec.security || parser.spec.security || []).map( security => new SecurityRequirementModel(security, parser), ); + + if (options.showExtensions) { + this.extensions = extractExtensions(operationSpec, options.showExtensions); + } } /** diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index 32a96a49..7d99c437 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -14,6 +14,7 @@ import { isPrimitiveType, JsonPointer, sortByRequired, + extractExtensions, } from '../../utils/'; // TODO: refactor this model, maybe use getters instead of copying all the values @@ -54,6 +55,7 @@ export class SchemaModel { rawSchema: OpenAPISchema; schema: MergedOpenAPISchema; + extensions?: Dict; /** * @param isChild if schema discriminator Child @@ -77,6 +79,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); + } } /** diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index 3d91ac12..16c74a76 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -286,3 +286,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; + }, {}); +}