Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add webhooks support #1304

Merged
merged 5 commits into from
Aug 14, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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;
}
`;
5 changes: 3 additions & 2 deletions src/components/Operation/Operation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ 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 (
Expand All @@ -48,6 +48,7 @@ export class Operation extends React.Component<OperationProps> {
<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} />}
{hasDescription && (
Expand All @@ -63,7 +64,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
6 changes: 5 additions & 1 deletion src/components/SideMenu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,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">hook</OperationBadge>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why not webhook?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the screenshot above.webhook will be wider than the rest badges and will look ugly.

Copy link
Member

@adamaltman adamaltman Jun 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This label/badge needs to be configurable. "hook" is a pejorative word in various vernacular (can mean to cheat or prostitution).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamaltman Do you have any other ideas? Maybe whook or webh? We use opts for options and del for delete.

) : (
<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
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;
'x-webhooks'?: WebhookModel;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about xWebhooks or xWebHooks?

Copy link
Member

@RomanHotsiy RomanHotsiy Jun 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is by OpenAPI spec, all vendor extensions should be prefixed by x-.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, my bad. This is our code not typing for OAS. @stasiukanya could you update it to xWebhooks? And everywhere else.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not webhooks then if it is our code?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamaltman good catch!


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['x-webhooks'] = new WebhookModel(this.parser, options, this.parser.spec['x-webhooks']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this['x-webhooks'] = new WebhookModel(this.parser, options, this.parser.spec['x-webhooks']);
this.xWebhooks = 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