mirror of
				https://github.com/Redocly/redoc.git
				synced 2025-10-30 23:37:28 +03:00 
			
		
		
		
	feat: add webhooks support (#1304)
This commit is contained in:
		
							parent
							
								
									171711f79c
								
							
						
					
					
						commit
						41f81b4d96
					
				|  | @ -1187,3 +1187,19 @@ components: | ||||||
|         shipDate: '2018-10-19T16:46:45Z' |         shipDate: '2018-10-19T16:46:45Z' | ||||||
|         status: placed |         status: placed | ||||||
|         complete: false |         complete: false | ||||||
|  | x-webhooks: | ||||||
|  |   newPet: | ||||||
|  |     post: | ||||||
|  |       summary: New pet | ||||||
|  |       description: Information about a new pet in the systems | ||||||
|  |       operationId: newPet | ||||||
|  |       tags:  | ||||||
|  |         - pet | ||||||
|  |       requestBody: | ||||||
|  |         content: | ||||||
|  |           application/json: | ||||||
|  |             schema: | ||||||
|  |               $ref: "#/components/schemas/Pet" | ||||||
|  |       responses: | ||||||
|  |         "200": | ||||||
|  |           description: Return a 200 status to indicate that the data was received successfully | ||||||
|  | @ -6,7 +6,7 @@ describe('Menu', () => { | ||||||
|   it('should have valid items count', () => { |   it('should have valid items count', () => { | ||||||
|     cy.get('.menu-content') |     cy.get('.menu-content') | ||||||
|       .find('li') |       .find('li') | ||||||
|       .should('have.length', 6 + (2 + 8 + 1 + 4 + 2) + (1 + 8) + 1); |       .should('have.length', 34); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   it('should sync active menu items while scroll', () => { |   it('should sync active menu items while scroll', () => { | ||||||
|  |  | ||||||
|  | @ -51,10 +51,17 @@ export const ShelfIcon = styled(IntShelfIcon)` | ||||||
| 
 | 
 | ||||||
| export const Badge = styled.span<{ type: string }>` | export const Badge = styled.span<{ type: string }>` | ||||||
|   display: inline-block; |   display: inline-block; | ||||||
|   padding: 0 5px; |   padding: 2px 8px; | ||||||
|   margin: 0; |   margin: 0; | ||||||
|   background-color: ${props => props.theme.colors[props.type].main}; |   background-color: ${props => props.theme.colors[props.type].main}; | ||||||
|   color: ${props => props.theme.colors[props.type].contrastText}; |   color: ${props => props.theme.colors[props.type].contrastText}; | ||||||
|   font-size: ${props => props.theme.typography.code.fontSize}; |   font-size: ${props => props.theme.typography.code.fontSize}; | ||||||
|   vertical-align: text-top; |   vertical-align: middle; | ||||||
|  |   line-height: 1.6; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   font-weight: ${({ theme }) => theme.typography.fontWeightBold}; | ||||||
|  |   font-size: 12px; | ||||||
|  |   + span[type] { | ||||||
|  |     margin-left: 4px; | ||||||
|  |   } | ||||||
| `;
 | `;
 | ||||||
|  |  | ||||||
|  | @ -37,19 +37,22 @@ export class Operation extends React.Component<OperationProps> { | ||||||
|   render() { |   render() { | ||||||
|     const { operation } = this.props; |     const { operation } = this.props; | ||||||
| 
 | 
 | ||||||
|     const { name: summary, description, deprecated, externalDocs } = operation; |     const { name: summary, description, deprecated, externalDocs, isWebhook } = operation; | ||||||
|     const hasDescription = !!(description || externalDocs); |     const hasDescription = !!(description || externalDocs); | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <OptionsContext.Consumer> |       <OptionsContext.Consumer> | ||||||
|         {options => ( |         {(options) => ( | ||||||
|           <OperationRow> |           <OperationRow> | ||||||
|             <MiddlePanel> |             <MiddlePanel> | ||||||
|               <H2> |               <H2> | ||||||
|                 <ShareLink to={operation.id} /> |                 <ShareLink to={operation.id} /> | ||||||
|                 {summary} {deprecated && <Badge type="warning"> Deprecated </Badge>} |                 {summary} {deprecated && <Badge type="warning"> Deprecated </Badge>} | ||||||
|  |                 {isWebhook && <Badge type="primary"> Webhook </Badge>} | ||||||
|               </H2> |               </H2> | ||||||
|               {options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />} |               {options.pathInMiddlePanel && !isWebhook && ( | ||||||
|  |                 <Endpoint operation={operation} inverted={true} /> | ||||||
|  |               )} | ||||||
|               {hasDescription && ( |               {hasDescription && ( | ||||||
|                 <Description> |                 <Description> | ||||||
|                   {description !== undefined && <Markdown source={description} />} |                   {description !== undefined && <Markdown source={description} />} | ||||||
|  | @ -63,7 +66,7 @@ export class Operation extends React.Component<OperationProps> { | ||||||
|               <CallbacksList callbacks={operation.callbacks} /> |               <CallbacksList callbacks={operation.callbacks} /> | ||||||
|             </MiddlePanel> |             </MiddlePanel> | ||||||
|             <DarkRightPanel> |             <DarkRightPanel> | ||||||
|               {!options.pathInMiddlePanel && <Endpoint operation={operation} />} |               {!options.pathInMiddlePanel && !isWebhook && <Endpoint operation={operation} />} | ||||||
|               <RequestSamples operation={operation} /> |               <RequestSamples operation={operation} /> | ||||||
|               <ResponseSamples operation={operation} /> |               <ResponseSamples operation={operation} /> | ||||||
|               <CallbackSamples callbacks={operation.callbacks} /> |               <CallbackSamples callbacks={operation.callbacks} /> | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ import { IMenuItem, OperationModel } from '../../services'; | ||||||
| import { shortenHTTPVerb } from '../../utils/openapi'; | import { shortenHTTPVerb } from '../../utils/openapi'; | ||||||
| import { MenuItems } from './MenuItems'; | import { MenuItems } from './MenuItems'; | ||||||
| import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements'; | import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements'; | ||||||
|  | import { l } from '../../services/Labels'; | ||||||
| 
 | 
 | ||||||
| export interface MenuItemProps { | export interface MenuItemProps { | ||||||
|   item: IMenuItem; |   item: IMenuItem; | ||||||
|  | @ -90,7 +91,11 @@ export class OperationMenuItemContent extends React.Component<OperationMenuItemC | ||||||
|         deprecated={item.deprecated} |         deprecated={item.deprecated} | ||||||
|         ref={this.ref} |         ref={this.ref} | ||||||
|       > |       > | ||||||
|         <OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge> |         {item.isWebhook ? ( | ||||||
|  |           <OperationBadge type="hook">{l('webhook')}</OperationBadge> | ||||||
|  |         ) : ( | ||||||
|  |           <OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge> | ||||||
|  |         )} | ||||||
|         <MenuItemTitle width="calc(100% - 38px)"> |         <MenuItemTitle width="calc(100% - 38px)"> | ||||||
|           {item.name} |           {item.name} | ||||||
|           {this.props.children} |           {this.props.children} | ||||||
|  |  | ||||||
|  | @ -60,6 +60,10 @@ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({ | ||||||
|   &.head { |   &.head { | ||||||
|     background-color: ${props => props.theme.colors.http.head}; |     background-color: ${props => props.theme.colors.http.head}; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   &.hook { | ||||||
|  |     background-color: ${props => props.theme.colors.primary.main}; | ||||||
|  |   } | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
| function menuItemActiveBg(depth, { theme }: { theme: ResolvedThemeInterface }): string { | function menuItemActiveBg(depth, { theme }: { theme: ResolvedThemeInterface }): string { | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ export interface LabelsConfig { | ||||||
|   nullable: string; |   nullable: string; | ||||||
|   recursive: string; |   recursive: string; | ||||||
|   arrayOf: string; |   arrayOf: string; | ||||||
|  |   webhook: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type LabelsConfigRaw = Partial<LabelsConfig>; | export type LabelsConfigRaw = Partial<LabelsConfig>; | ||||||
|  | @ -22,6 +23,7 @@ const labels: LabelsConfig = { | ||||||
|   nullable: 'Nullable', |   nullable: 'Nullable', | ||||||
|   recursive: 'Recursive', |   recursive: 'Recursive', | ||||||
|   arrayOf: 'Array of ', |   arrayOf: 'Array of ', | ||||||
|  |   webhook: 'Event', | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export function setRedocLabels(_labels?: LabelsConfigRaw) { | export function setRedocLabels(_labels?: LabelsConfigRaw) { | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import { | ||||||
|   OpenAPITag, |   OpenAPITag, | ||||||
|   Referenced, |   Referenced, | ||||||
|   OpenAPIServer, |   OpenAPIServer, | ||||||
|  |   OpenAPIPaths, | ||||||
| } from '../types'; | } from '../types'; | ||||||
| import { | import { | ||||||
|   isOperationName, |   isOperationName, | ||||||
|  | @ -28,6 +29,7 @@ export type ExtendedOpenAPIOperation = { | ||||||
|   httpVerb: string; |   httpVerb: string; | ||||||
|   pathParameters: Array<Referenced<OpenAPIParameter>>; |   pathParameters: Array<Referenced<OpenAPIParameter>>; | ||||||
|   pathServers: Array<OpenAPIServer> | undefined; |   pathServers: Array<OpenAPIServer> | undefined; | ||||||
|  |   isWebhook: boolean; | ||||||
| } & OpenAPIOperation; | } & OpenAPIOperation; | ||||||
| 
 | 
 | ||||||
| export type TagsInfoMap = Record<string, TagInfo>; | export type TagsInfoMap = Record<string, TagInfo>; | ||||||
|  | @ -219,43 +221,49 @@ export class MenuBuilder { | ||||||
|       tags[tag.name] = { ...tag, operations: [] }; |       tags[tag.name] = { ...tag, operations: [] }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const paths = spec.paths; |     getTags(spec.paths); | ||||||
|     for (const pathName of Object.keys(paths)) { |     if (spec['x-webhooks']) { | ||||||
|       const path = paths[pathName]; |       getTags(spec['x-webhooks'], true); | ||||||
|       const operations = Object.keys(path).filter(isOperationName); |     } | ||||||
|       for (const operationName of operations) { |  | ||||||
|         const operationInfo = path[operationName]; |  | ||||||
|         let operationTags = operationInfo.tags; |  | ||||||
| 
 | 
 | ||||||
|         if (!operationTags || !operationTags.length) { |     function getTags(paths: OpenAPIPaths, isWebhook?: boolean) { | ||||||
|           // empty tag
 |       for (const pathName of Object.keys(paths)) { | ||||||
|           operationTags = ['']; |         const path = paths[pathName]; | ||||||
|         } |         const operations = Object.keys(path).filter(isOperationName); | ||||||
|  |         for (const operationName of operations) { | ||||||
|  |           const operationInfo = path[operationName]; | ||||||
|  |           let operationTags = operationInfo.tags; | ||||||
| 
 | 
 | ||||||
|         for (const tagName of operationTags) { |           if (!operationTags || !operationTags.length) { | ||||||
|           let tag = tags[tagName]; |             // empty tag
 | ||||||
|           if (tag === undefined) { |             operationTags = ['']; | ||||||
|             tag = { |  | ||||||
|               name: tagName, |  | ||||||
|               operations: [], |  | ||||||
|             }; |  | ||||||
|             tags[tagName] = tag; |  | ||||||
|           } |           } | ||||||
|           if (tag['x-traitTag']) { | 
 | ||||||
|             continue; |           for (const tagName of operationTags) { | ||||||
|  |             let tag = tags[tagName]; | ||||||
|  |             if (tag === undefined) { | ||||||
|  |               tag = { | ||||||
|  |                 name: tagName, | ||||||
|  |                 operations: [], | ||||||
|  |               }; | ||||||
|  |               tags[tagName] = tag; | ||||||
|  |             } | ||||||
|  |             if (tag['x-traitTag']) { | ||||||
|  |               continue; | ||||||
|  |             } | ||||||
|  |             tag.operations.push({ | ||||||
|  |               ...operationInfo, | ||||||
|  |               pathName, | ||||||
|  |               pointer: JsonPointer.compile(['paths', pathName, operationName]), | ||||||
|  |               httpVerb: operationName, | ||||||
|  |               pathParameters: path.parameters || [], | ||||||
|  |               pathServers: path.servers, | ||||||
|  |               isWebhook: !!isWebhook, | ||||||
|  |             }); | ||||||
|           } |           } | ||||||
|           tag.operations.push({ |  | ||||||
|             ...operationInfo, |  | ||||||
|             pathName, |  | ||||||
|             pointer: JsonPointer.compile(['paths', pathName, operationName]), |  | ||||||
|             httpVerb: operationName, |  | ||||||
|             pathParameters: path.parameters || [], |  | ||||||
|             pathServers: path.servers, |  | ||||||
|           }); |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     return tags; |     return tags; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ import { OpenAPIExternalDocumentation, OpenAPISpec } from '../types'; | ||||||
| 
 | 
 | ||||||
| import { ContentItemModel, MenuBuilder } from './MenuBuilder'; | import { ContentItemModel, MenuBuilder } from './MenuBuilder'; | ||||||
| import { ApiInfoModel } from './models/ApiInfo'; | import { ApiInfoModel } from './models/ApiInfo'; | ||||||
|  | import { WebhookModel } from './models/Webhook'; | ||||||
| import { SecuritySchemesModel } from './models/SecuritySchemes'; | import { SecuritySchemesModel } from './models/SecuritySchemes'; | ||||||
| import { OpenAPIParser } from './OpenAPIParser'; | import { OpenAPIParser } from './OpenAPIParser'; | ||||||
| import { RedocNormalizedOptions } from './RedocNormalizedOptions'; | import { RedocNormalizedOptions } from './RedocNormalizedOptions'; | ||||||
|  | @ -15,6 +16,7 @@ export class SpecStore { | ||||||
|   externalDocs?: OpenAPIExternalDocumentation; |   externalDocs?: OpenAPIExternalDocumentation; | ||||||
|   contentItems: ContentItemModel[]; |   contentItems: ContentItemModel[]; | ||||||
|   securitySchemes: SecuritySchemesModel; |   securitySchemes: SecuritySchemesModel; | ||||||
|  |   webhooks?: WebhookModel; | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     spec: OpenAPISpec, |     spec: OpenAPISpec, | ||||||
|  | @ -26,5 +28,6 @@ export class SpecStore { | ||||||
|     this.externalDocs = this.parser.spec.externalDocs; |     this.externalDocs = this.parser.spec.externalDocs; | ||||||
|     this.contentItems = MenuBuilder.buildStructure(this.parser, this.options); |     this.contentItems = MenuBuilder.buildStructure(this.parser, this.options); | ||||||
|     this.securitySchemes = new SecuritySchemesModel(this.parser); |     this.securitySchemes = new SecuritySchemesModel(this.parser); | ||||||
|  |     this.webhooks = new WebhookModel(this.parser, options, this.parser.spec['x-webhooks']); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -75,6 +75,7 @@ export class OperationModel implements IMenuItem { | ||||||
|   security: SecurityRequirementModel[]; |   security: SecurityRequirementModel[]; | ||||||
|   extensions: Record<string, any>; |   extensions: Record<string, any>; | ||||||
|   isCallback: boolean; |   isCallback: boolean; | ||||||
|  |   isWebhook: boolean; | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     private parser: OpenAPIParser, |     private parser: OpenAPIParser, | ||||||
|  | @ -95,6 +96,7 @@ export class OperationModel implements IMenuItem { | ||||||
|     this.operationId = operationSpec.operationId; |     this.operationId = operationSpec.operationId; | ||||||
|     this.path = operationSpec.pathName; |     this.path = operationSpec.pathName; | ||||||
|     this.isCallback = isCallback; |     this.isCallback = isCallback; | ||||||
|  |     this.isWebhook = !!operationSpec.isWebhook; | ||||||
| 
 | 
 | ||||||
|     this.name = getOperationSummary(operationSpec); |     this.name = getOperationSummary(operationSpec); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										38
									
								
								src/services/models/Webhook.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/services/models/Webhook.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | ||||||
|  | import { OpenAPIPath, Referenced } from '../../types'; | ||||||
|  | import { OpenAPIParser } from '../OpenAPIParser'; | ||||||
|  | import { OperationModel } from './Operation'; | ||||||
|  | import { isOperationName } from '../..'; | ||||||
|  | import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; | ||||||
|  | 
 | ||||||
|  | export class WebhookModel { | ||||||
|  |   operations: OperationModel[] = []; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     parser: OpenAPIParser, | ||||||
|  |     options: RedocNormalizedOptions, | ||||||
|  |     infoOrRef?: Referenced<OpenAPIPath>, | ||||||
|  |   ) { | ||||||
|  |     const webhooks = parser.deref<OpenAPIPath>(infoOrRef || {}); | ||||||
|  |     parser.exitRef(infoOrRef); | ||||||
|  | 
 | ||||||
|  |     for (const webhookName of Object.keys(webhooks)) { | ||||||
|  |       const webhook = webhooks[webhookName]; | ||||||
|  |       const operations = Object.keys(webhook).filter(isOperationName); | ||||||
|  |       for (const operationName of operations) { | ||||||
|  |         const operationInfo = webhook[operationName]; | ||||||
|  |         const operation = new OperationModel( | ||||||
|  |           parser, | ||||||
|  |           { | ||||||
|  |             ...operationInfo, | ||||||
|  |             httpVerb: operationName, | ||||||
|  |           }, | ||||||
|  |           undefined, | ||||||
|  |           options, | ||||||
|  |           false, | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         this.operations.push(operation); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								src/types/open-api.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/types/open-api.d.ts
									
									
									
									
										vendored
									
									
								
							|  | @ -9,6 +9,7 @@ export interface OpenAPISpec { | ||||||
|   security?: OpenAPISecurityRequirement[]; |   security?: OpenAPISecurityRequirement[]; | ||||||
|   tags?: OpenAPITag[]; |   tags?: OpenAPITag[]; | ||||||
|   externalDocs?: OpenAPIExternalDocumentation; |   externalDocs?: OpenAPIExternalDocumentation; | ||||||
|  |   'x-webhooks'?: OpenAPIPaths; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface OpenAPIInfo { | export interface OpenAPIInfo { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user