From c094d1990f34a66484bced7677089a2e8b9c723c Mon Sep 17 00:00:00 2001 From: Anya Stasiuk Date: Fri, 19 Jun 2020 14:06:02 +0300 Subject: [PATCH] feat: add webhooks support --- demo/openapi.yaml | 16 +++++ src/common-elements/shelfs.tsx | 11 +++- src/components/Operation/Operation.tsx | 5 +- src/components/SideMenu/MenuItem.tsx | 6 +- src/components/SideMenu/styled.elements.ts | 4 ++ src/services/MenuBuilder.ts | 68 ++++++++++++---------- src/services/SpecStore.ts | 3 + src/services/models/Operation.ts | 2 + src/services/models/Webhook.ts | 38 ++++++++++++ src/types/open-api.d.ts | 1 + 10 files changed, 119 insertions(+), 35 deletions(-) create mode 100644 src/services/models/Webhook.ts diff --git a/demo/openapi.yaml b/demo/openapi.yaml index 46b62e62..74c16b70 100644 --- a/demo/openapi.yaml +++ b/demo/openapi.yaml @@ -1187,3 +1187,19 @@ components: shipDate: '2018-10-19T16:46:45Z' status: placed 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 \ No newline at end of file diff --git a/src/common-elements/shelfs.tsx b/src/common-elements/shelfs.tsx index 649bc904..3359a130 100644 --- a/src/common-elements/shelfs.tsx +++ b/src/common-elements/shelfs.tsx @@ -50,10 +50,17 @@ export const ShelfIcon = styled(IntShelfIcon)` export const Badge = styled.span<{ type: string }>` display: inline-block; - padding: 0 5px; + padding: 2px 8px; margin: 0; background-color: ${props => props.theme.colors[props.type].main}; color: ${props => props.theme.colors[props.type].contrastText}; 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; + } `; diff --git a/src/components/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index 6da94e1c..53bc53df 100644 --- a/src/components/Operation/Operation.tsx +++ b/src/components/Operation/Operation.tsx @@ -37,7 +37,7 @@ export class Operation extends React.Component { render() { const { operation } = this.props; - const { name: summary, description, deprecated, externalDocs } = operation; + const { name: summary, description, deprecated, externalDocs, isWebhook } = operation; const hasDescription = !!(description || externalDocs); return ( @@ -48,6 +48,7 @@ export class Operation extends React.Component {

{summary} {deprecated && Deprecated } + {isWebhook && Webhook }

{options.pathInMiddlePanel && } {hasDescription && ( @@ -63,7 +64,7 @@ export class Operation extends React.Component { - {!options.pathInMiddlePanel && } + {!options.pathInMiddlePanel && !isWebhook && } diff --git a/src/components/SideMenu/MenuItem.tsx b/src/components/SideMenu/MenuItem.tsx index 8b418aad..51172742 100644 --- a/src/components/SideMenu/MenuItem.tsx +++ b/src/components/SideMenu/MenuItem.tsx @@ -90,7 +90,11 @@ export class OperationMenuItemContent extends React.Component - {shortenHTTPVerb(item.httpVerb)} + {item.isWebhook ? ( + hook + ) : ( + {shortenHTTPVerb(item.httpVerb)} + )} {item.name} {this.props.children} diff --git a/src/components/SideMenu/styled.elements.ts b/src/components/SideMenu/styled.elements.ts index f400b170..1b14fcb8 100644 --- a/src/components/SideMenu/styled.elements.ts +++ b/src/components/SideMenu/styled.elements.ts @@ -60,6 +60,10 @@ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({ &.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 { diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts index 38f104e5..339ba6cf 100644 --- a/src/services/MenuBuilder.ts +++ b/src/services/MenuBuilder.ts @@ -5,6 +5,7 @@ import { OpenAPITag, Referenced, OpenAPIServer, + OpenAPIPaths, } from '../types'; import { isOperationName, @@ -28,6 +29,7 @@ export type ExtendedOpenAPIOperation = { httpVerb: string; pathParameters: Array>; pathServers: Array | undefined; + isWebhook: boolean; } & OpenAPIOperation; export type TagsInfoMap = Record; @@ -219,43 +221,49 @@ export class MenuBuilder { tags[tag.name] = { ...tag, operations: [] }; } - const paths = spec.paths; - for (const pathName of Object.keys(paths)) { - const path = paths[pathName]; - const operations = Object.keys(path).filter(isOperationName); - for (const operationName of operations) { - const operationInfo = path[operationName]; - let operationTags = operationInfo.tags; + getTags(spec.paths); + if (spec['x-webhooks']) { + getTags(spec['x-webhooks'], true); + } - if (!operationTags || !operationTags.length) { - // empty tag - operationTags = ['']; - } + function getTags(paths: OpenAPIPaths, isWebhook?: boolean) { + for (const pathName of Object.keys(paths)) { + 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) { - let tag = tags[tagName]; - if (tag === undefined) { - tag = { - name: tagName, - operations: [], - }; - tags[tagName] = tag; + if (!operationTags || !operationTags.length) { + // empty tag + operationTags = ['']; } - 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; } } diff --git a/src/services/SpecStore.ts b/src/services/SpecStore.ts index 5a6c835d..270d272b 100644 --- a/src/services/SpecStore.ts +++ b/src/services/SpecStore.ts @@ -2,6 +2,7 @@ import { OpenAPIExternalDocumentation, OpenAPISpec } from '../types'; import { ContentItemModel, MenuBuilder } from './MenuBuilder'; import { ApiInfoModel } from './models/ApiInfo'; +import { WebhookModel } from './models/Webhook'; import { SecuritySchemesModel } from './models/SecuritySchemes'; import { OpenAPIParser } from './OpenAPIParser'; import { RedocNormalizedOptions } from './RedocNormalizedOptions'; @@ -15,6 +16,7 @@ export class SpecStore { externalDocs?: OpenAPIExternalDocumentation; contentItems: ContentItemModel[]; securitySchemes: SecuritySchemesModel; + 'x-webhooks'?: WebhookModel; constructor( spec: OpenAPISpec, @@ -26,5 +28,6 @@ export class SpecStore { this.externalDocs = this.parser.spec.externalDocs; this.contentItems = MenuBuilder.buildStructure(this.parser, this.options); this.securitySchemes = new SecuritySchemesModel(this.parser); + this['x-webhooks'] = new WebhookModel(this.parser, options, this.parser.spec['x-webhooks']); } } diff --git a/src/services/models/Operation.ts b/src/services/models/Operation.ts index 2bb63a01..a22fa4b9 100644 --- a/src/services/models/Operation.ts +++ b/src/services/models/Operation.ts @@ -75,6 +75,7 @@ export class OperationModel implements IMenuItem { security: SecurityRequirementModel[]; extensions: Record; isCallback: boolean; + isWebhook: boolean; constructor( private parser: OpenAPIParser, @@ -95,6 +96,7 @@ export class OperationModel implements IMenuItem { this.operationId = operationSpec.operationId; this.path = operationSpec.pathName; this.isCallback = isCallback; + this.isWebhook = !!operationSpec.isWebhook; this.name = getOperationSummary(operationSpec); diff --git a/src/services/models/Webhook.ts b/src/services/models/Webhook.ts new file mode 100644 index 00000000..7349b8bd --- /dev/null +++ b/src/services/models/Webhook.ts @@ -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, + ) { + const webhooks = parser.deref(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); + } + } + } +} diff --git a/src/types/open-api.d.ts b/src/types/open-api.d.ts index bb050b6e..aefc0b73 100644 --- a/src/types/open-api.d.ts +++ b/src/types/open-api.d.ts @@ -9,6 +9,7 @@ export interface OpenAPISpec { security?: OpenAPISecurityRequirement[]; tags?: OpenAPITag[]; externalDocs?: OpenAPIExternalDocumentation; + 'x-webhooks'?: OpenAPIPaths; } export interface OpenAPIInfo {