mirror of
				https://github.com/Redocly/redoc.git
				synced 2025-11-01 00:07:32 +03:00 
			
		
		
		
	feet: add the option to render vendor extensions (#552)
* add the option to render vendor extensions * refactor Extensions, move Redoc extension list to utils file * feat: new option showExtensions (support list of extensions)
This commit is contained in:
		
							parent
							
								
									fe3383d1a3
								
							
						
					
					
						commit
						e9610e92d4
					
				|  | @ -215,6 +215,7 @@ 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. | ||||
| * `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 | ||||
|  |  | |||
							
								
								
									
										41
									
								
								src/components/Fields/Extensions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/components/Fields/Extensions.tsx
									
									
									
									
									
										Normal 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> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | @ -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<FieldProps> { | |||
|         <FieldDetail label={'Default:'} value={schema.default} /> | ||||
|         {!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '} | ||||
|         {showExamples && <FieldDetail label={'Example:'} value={example} />} | ||||
|         {<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />} | ||||
|         <div> | ||||
|           <Markdown compact={true} source={description} /> | ||||
|         </div> | ||||
|  |  | |||
|  | @ -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<OperationProps> { | |||
|                   {externalDocs && <ExternalDocumentation externalDocs={externalDocs} />} | ||||
|                 </Description> | ||||
|               )} | ||||
|               <Extensions extensions={operation.extensions} /> | ||||
|               <SecurityRequirements securities={operation.security} /> | ||||
|               <Parameters parameters={operation.parameters} body={operation.requestBody} /> | ||||
|               <ResponsesList responses={operation.responses} /> | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<any>; | ||||
| 
 | ||||
|   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 | ||||
|  |  | |||
|  | @ -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<any>; | ||||
| 
 | ||||
|   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); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  |  | |||
|  | @ -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<any>; | ||||
| 
 | ||||
|   /** | ||||
|    * @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); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  |  | |||
|  | @ -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<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; | ||||
|     }, {}); | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user