Skip to content

Commit

Permalink
feat: add webhooks support (#1304)
Browse files Browse the repository at this point in the history
  • Loading branch information
stasiukanya authored Aug 14, 2020
1 parent 171711f commit 41f81b4
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 38 deletions.
16 changes: 16 additions & 0 deletions demo/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion e2e/integration/menu.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('Menu', () => {
it('should have valid items count', () => {
cy.get('.menu-content')
.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', () => {
Expand Down
11 changes: 9 additions & 2 deletions src/common-elements/shelfs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,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;
}
`;
11 changes: 7 additions & 4 deletions src/components/Operation/Operation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,22 @@ export class Operation extends React.Component<OperationProps> {
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 (
<OptionsContext.Consumer>
{options => (
{(options) => (
<OperationRow>
<MiddlePanel>
<H2>
<ShareLink to={operation.id} />
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
{isWebhook && <Badge type="primary"> Webhook </Badge>}
</H2>
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />}
{options.pathInMiddlePanel && !isWebhook && (
<Endpoint operation={operation} inverted={true} />
)}
{hasDescription && (
<Description>
{description !== undefined && <Markdown source={description} />}
Expand All @@ -63,7 +66,7 @@ export class Operation extends React.Component<OperationProps> {
<CallbacksList callbacks={operation.callbacks} />
</MiddlePanel>
<DarkRightPanel>
{!options.pathInMiddlePanel && <Endpoint operation={operation} />}
{!options.pathInMiddlePanel && !isWebhook && <Endpoint operation={operation} />}
<RequestSamples operation={operation} />
<ResponseSamples operation={operation} />
<CallbackSamples callbacks={operation.callbacks} />
Expand Down
7 changes: 6 additions & 1 deletion src/components/SideMenu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IMenuItem, OperationModel } from '../../services';
import { shortenHTTPVerb } from '../../utils/openapi';
import { MenuItems } from './MenuItems';
import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements';
import { l } from '../../services/Labels';

export interface MenuItemProps {
item: IMenuItem;
Expand Down Expand Up @@ -90,7 +91,11 @@ export class OperationMenuItemContent extends React.Component<OperationMenuItemC
deprecated={item.deprecated}
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)">
{item.name}
{this.props.children}
Expand Down
4 changes: 4 additions & 0 deletions src/components/SideMenu/styled.elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/services/Labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface LabelsConfig {
nullable: string;
recursive: string;
arrayOf: string;
webhook: string;
}

export type LabelsConfigRaw = Partial<LabelsConfig>;
Expand All @@ -22,6 +23,7 @@ const labels: LabelsConfig = {
nullable: 'Nullable',
recursive: 'Recursive',
arrayOf: 'Array of ',
webhook: 'Event',
};

export function setRedocLabels(_labels?: LabelsConfigRaw) {
Expand Down
68 changes: 38 additions & 30 deletions src/services/MenuBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
OpenAPITag,
Referenced,
OpenAPIServer,
OpenAPIPaths,
} from '../types';
import {
isOperationName,
Expand All @@ -28,6 +29,7 @@ export type ExtendedOpenAPIOperation = {
httpVerb: string;
pathParameters: Array<Referenced<OpenAPIParameter>>;
pathServers: Array<OpenAPIServer> | undefined;
isWebhook: boolean;
} & OpenAPIOperation;

export type TagsInfoMap = Record<string, TagInfo>;
Expand Down Expand Up @@ -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;
}
}
3 changes: 3 additions & 0 deletions src/services/SpecStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -15,6 +16,7 @@ export class SpecStore {
externalDocs?: OpenAPIExternalDocumentation;
contentItems: ContentItemModel[];
securitySchemes: SecuritySchemesModel;
webhooks?: WebhookModel;

constructor(
spec: OpenAPISpec,
Expand All @@ -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.webhooks = new WebhookModel(this.parser, options, this.parser.spec['x-webhooks']);
}
}
2 changes: 2 additions & 0 deletions src/services/models/Operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export class OperationModel implements IMenuItem {
security: SecurityRequirementModel[];
extensions: Record<string, any>;
isCallback: boolean;
isWebhook: boolean;

constructor(
private parser: OpenAPIParser,
Expand All @@ -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);

Expand Down
38 changes: 38 additions & 0 deletions src/services/models/Webhook.ts
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions src/types/open-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface OpenAPISpec {
security?: OpenAPISecurityRequirement[];
tags?: OpenAPITag[];
externalDocs?: OpenAPIExternalDocumentation;
'x-webhooks'?: OpenAPIPaths;
}

export interface OpenAPIInfo {
Expand Down

0 comments on commit 41f81b4

Please sign in to comment.