Skip to content

Commit

Permalink
feat(oas): support webhooks from oas3.1
Browse files Browse the repository at this point in the history
BREAKING CHANGE
  • Loading branch information
Daniel A. White committed Jan 9, 2024
1 parent 19e1ae9 commit 45ede7c
Show file tree
Hide file tree
Showing 32 changed files with 1,130 additions and 885 deletions.
5 changes: 4 additions & 1 deletion demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ export function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const isStacked = searchParams.get('layout') === 'stacked';

const [globalState, setGlobalState] = useState<GlobalContext>({
apiDescriptionUrl: searchParams.get('spec') || DEFAULT_API_URL,
setDescriptionUrl: _value => {
const value = _value.trim() || DEFAULT_API_URL;

let nextUrl = '/';
if (value && value !== DEFAULT_API_URL) {
nextUrl = `?spec=${value}`;
nextUrl = `?spec=${value}${isStacked ? '&layout=stacked' : undefined}`;
}

window.history.pushState(undefined, '', nextUrl);
Expand All @@ -35,6 +37,7 @@ export function App() {
});
}, 0);
},
layout: isStacked ? 'stacked' : undefined,
});

return (
Expand Down
6 changes: 3 additions & 3 deletions demo/src/components/ElementsAPI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import React, { useContext } from 'react';
import { GlobalContext } from '../context';

export const ElementsAPI: React.FC = () => {
const { apiDescriptionUrl } = useContext(GlobalContext);
const { apiDescriptionUrl, layout } = useContext(GlobalContext);

const specUrlWithProxy =
apiDescriptionUrl && window.location.origin === 'https://elements-demo.stoplight.io'
? `https://stoplight.io/cors-proxy/${apiDescriptionUrl}`
: apiDescriptionUrl;

return (
<Box flex={1} overflowY="hidden">
<API apiDescriptionUrl={specUrlWithProxy} router="hash" />
<Box flex={1} overflowY={layout !== 'stacked' ? 'hidden' : undefined}>
<API apiDescriptionUrl={specUrlWithProxy} router="hash" layout={layout} />
</Box>
);
};
1 change: 1 addition & 0 deletions demo/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DEFAULT_API_URL } from './constants';
export type GlobalContext = {
apiDescriptionUrl: string;
setDescriptionUrl: (value: string) => void;
layout?: 'stacked' | 'sidebar';
};

export const defaultGlobalContext: GlobalContext = {
Expand Down
4 changes: 2 additions & 2 deletions packages/elements-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
]
},
"dependencies": {
"@stoplight/http-spec": "^6.0.0",
"@stoplight/http-spec": "^7.0.1",
"@stoplight/json": "^3.18.1",
"@stoplight/json-schema-ref-parser": "^9.0.5",
"@stoplight/json-schema-sampler": "0.2.3",
Expand All @@ -69,7 +69,7 @@
"@stoplight/mosaic-code-viewer": "^1.46.1",
"@stoplight/path": "^1.3.2",
"@stoplight/react-error-boundary": "^3.0.0",
"@stoplight/types": "^14.0.0",
"@stoplight/types": "^14.1.1",
"@stoplight/yaml": "^4.2.3",
"classnames": "^2.2.6",
"httpsnippet-lite": "^3.0.5",
Expand Down
1 change: 1 addition & 0 deletions packages/elements-core/src/components/Docs/Docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export const ParsedDocs = ({ node, nodeUnsupported, ...commonProps }: ParsedDocs
case 'article':
return <Article data={node.data} {...commonProps} />;
case 'http_operation':
case 'http_webhook':
return <HttpOperation data={node.data} {...commonProps} />;
case 'http_service':
return <HttpService data={node.data} {...commonProps} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Flex, Heading, HStack, NodeAnnotation, useThemeIsDark, VStack } from '@stoplight/mosaic';
import { withErrorBoundary } from '@stoplight/react-error-boundary';
import { IHttpOperation } from '@stoplight/types';
import { IHttpEndpointOperation, IHttpOperation } from '@stoplight/types';
import cn from 'classnames';
import { useAtomValue } from 'jotai/utils';
import * as React from 'react';
Expand All @@ -10,6 +10,7 @@ import { MockingContext } from '../../../containers/MockingProvider';
import { useResolvedObject } from '../../../context/InlineRefResolver';
import { useOptionsCtx } from '../../../context/Options';
import { useIsCompact } from '../../../hooks/useIsCompact';
import { isHttpOperation, isHttpWebhookOperation } from '../../../utils/guards';
import { MarkdownViewer } from '../../MarkdownViewer';
import { chosenServerAtom, TryItWithRequestSamples } from '../../TryIt';
import { DocsComponentProps } from '..';
Expand All @@ -19,12 +20,12 @@ import { Callbacks } from './Callbacks';
import { Request } from './Request';
import { Responses } from './Responses';

export type HttpOperationProps = DocsComponentProps<IHttpOperation>;
export type HttpOperationProps = DocsComponentProps<IHttpEndpointOperation>;

const HttpOperationComponent = React.memo<HttpOperationProps>(
({ className, data: unresolvedData, layoutOptions, tryItCredentialsPolicy, tryItCorsProxy }) => {
const { nodeHasChanged } = useOptionsCtx();
const data = useResolvedObject(unresolvedData) as IHttpOperation;
const data = useResolvedObject(unresolvedData) as IHttpEndpointOperation;
const { ref: layoutRef, isCompact } = useIsCompact(layoutOptions);

const mocking = React.useContext(MockingContext);
Expand All @@ -38,11 +39,20 @@ const HttpOperationComponent = React.memo<HttpOperationProps>(
const prettyName = (data.summary || data.iid || '').trim();
const hasBadges = isDeprecated || isInternal;

let path: string;
if (isHttpOperation(data)) {
path = data.path;
} else if (isHttpWebhookOperation(data)) {
path = data.name;
} else {
throw new RangeError('unsupported node type');
}

const header = (
<OperationHeader
id={data.id}
method={data.method}
path={data.path}
path={path}
noHeading={layoutOptions?.noHeading}
hasBadges={hasBadges}
name={prettyName}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, Callout, NodeAnnotation, VStack } from '@stoplight/mosaic';
import { HttpSecurityScheme, IHttpOperation } from '@stoplight/types';
import { HttpSecurityScheme, IHttpEndpointOperation } from '@stoplight/types';
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import * as React from 'react';
Expand All @@ -14,7 +14,7 @@ import { Body, isBodyEmpty } from './Body';
import { Parameters } from './Parameters';

interface IRequestProps {
operation: IHttpOperation;
operation: IHttpEndpointOperation;
onChange?: (requestBodyIndex: number) => void;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { CopyButton, Panel, Select, Text } from '@stoplight/mosaic';
import { CodeViewer } from '@stoplight/mosaic-code-viewer';
import { IHttpOperation, IMediaTypeContent } from '@stoplight/types';
import { IHttpEndpointOperation, IMediaTypeContent } from '@stoplight/types';
import * as React from 'react';

import { exceedsSize, useGenerateExampleFromMediaTypeContent } from '../../utils/exampleGeneration/exampleGeneration';
import { LoadMore } from '../LoadMore';

export interface ResponseExamplesProps {
httpOperation: IHttpOperation;
httpOperation: IHttpEndpointOperation;
responseStatusCode?: string;
responseMediaType?: string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { TableOfContents } from './TableOfContents';
import { TableOfContentsProps } from './types';

export default {
title: 'Internal/MosaicTableOfContents',
title: 'Internal/TableOfContents',
component: TableOfContents,
argTypes: {
tree: { table: { category: 'Input' } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { useRouterType } from '../../context/RouterType';
import { useFirstRender } from '../../hooks/useFirstRender';
import { VersionBadge } from '../Docs/HttpOperation/Badges';
import {
NODE_GROUP_ICON,
NODE_GROUP_ICON_COLOR,
NODE_META_COLOR,
NODE_TITLE_ICON,
NODE_TITLE_ICON_COLOR,
NODE_TYPE_ICON_COLOR,
NODE_TYPE_META_ICON,
NODE_TYPE_TITLE_ICON,
Expand All @@ -34,6 +34,7 @@ import {

const ActiveIdContext = React.createContext<string | undefined>(undefined);
const LinkContext = React.createContext<CustomLinkComponent | undefined>(undefined);
LinkContext.displayName = 'LinkContext';

export const TableOfContents = React.memo<TableOfContentsProps>(
({
Expand Down Expand Up @@ -103,6 +104,7 @@ export const TableOfContents = React.memo<TableOfContentsProps>(
);
},
);
TableOfContents.displayName = 'TableOfContents';

const Divider = React.memo<{
item: TableOfContentsDivider;
Expand All @@ -123,6 +125,7 @@ const Divider = React.memo<{
</Box>
);
});
Divider.displayName = 'Divider';

const GroupItem = React.memo<{
depth: number;
Expand Down Expand Up @@ -184,6 +187,7 @@ const GroupItem = React.memo<{

return null;
});
GroupItem.displayName = 'GroupItem';

const Group = React.memo<{
depth: number;
Expand Down Expand Up @@ -261,8 +265,9 @@ const Group = React.memo<{
depth={depth}
isActive={showAsActive}
icon={
NODE_TITLE_ICON[item.title] && (
<Box as={Icon} color={NODE_TITLE_ICON_COLOR[item.title]} icon={NODE_TITLE_ICON[item.title]} />
item.itemsType &&
NODE_GROUP_ICON[item.itemsType] && (
<Box as={Icon} color={NODE_GROUP_ICON_COLOR[item.itemsType]} icon={NODE_GROUP_ICON[item.itemsType]} />
)
}
/>
Expand All @@ -289,6 +294,7 @@ const Group = React.memo<{
</>
);
});
Group.displayName = 'Group';

const Item = React.memo<{
depth: number;
Expand Down Expand Up @@ -336,6 +342,7 @@ const Item = React.memo<{
</Flex>
);
});
Item.displayName = 'Item';

const Node = React.memo<{
item: TableOfContentsNode | TableOfContentsNodeGroup;
Expand Down Expand Up @@ -393,6 +400,7 @@ const Node = React.memo<{
);
},
);
Node.displayName = 'Node';

const Version: React.FC<{ value: string }> = ({ value }) => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
import { faBullseye, faCloud, faCube, faCubes } from '@fortawesome/free-solid-svg-icons';
import { faBullseye, faCloud, faCube, faCubes, faEnvelope, faEnvelopesBulk } from '@fortawesome/free-solid-svg-icons';
import { IIconProps } from '@stoplight/mosaic';

// Icons appear left of the node title
export const NODE_TYPE_TITLE_ICON: { [nodeType: string]: IIconProps['icon'] } = {
http_service: faCloud,
http_operation: faBullseye,
http_webhook: faEnvelope,
model: faCube,
};

export const NODE_TITLE_ICON: { [nodeTitle: string]: IIconProps['icon'] } = {
Schemas: faCubes,
export const NODE_GROUP_ICON: { [itemType: string]: IIconProps['icon'] } = {
http_webhook: faEnvelopesBulk,
model: faCubes,
};

// Icons appear in the right meta
export const NODE_TYPE_META_ICON: { [nodeType: string]: IIconProps['icon'] } = {
webhook: faEnvelope,
model: faCube,
};

export const NODE_TYPE_ICON_COLOR = {
model: 'warning',
http_service: '#D812EA',
http_operation: '#9747FF',
http_webhook: 'primary',
};

export const NODE_TITLE_ICON_COLOR = {
Schemas: 'warning',
export const NODE_GROUP_ICON_COLOR = {
webhook: 'warning',
http_webhook: 'primary',
};

export const NODE_META_COLOR = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ export type TableOfContentsGroupItem =
export type TableOfContentsGroup = {
title: string;
items: TableOfContentsGroupItem[];
itemsType?: 'article' | 'http_operation' | 'http_webhook' | 'model';
};

export type TableOfContentsExternalLink = {
title: string;
url: string;
};

export type TableOfContentsNode<T = 'http_service' | 'http_operation' | 'model' | 'article' | 'overview'> = {
export type TableOfContentsNode<
T = 'http_service' | 'http_operation' | 'http_webhook' | 'model' | 'article' | 'overview',
> = {
id: string;
slug: string;
title: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function isDivider(item: TableOfContentsItem): item is TableOfContentsDiv
return Object.keys(item).length === 1 && 'title' in item;
}
export function isGroup(item: TableOfContentsItem): item is TableOfContentsGroup {
return Object.keys(item).length === 2 && 'title' in item && 'items' in item;
return Object.keys(item).length >= 2 && 'title' in item && 'items' in item;
}
export function isNodeGroup(item: TableOfContentsItem): item is TableOfContentsNodeGroup {
return 'title' in item && 'items' in item && 'slug' in item && 'id' in item && 'meta' in item && 'type' in item;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IHttpOperation } from '@stoplight/types';
import { IHttpEndpointOperation } from '@stoplight/types';
import { atom, useAtom } from 'jotai';
import { orderBy, uniqBy } from 'lodash';
import * as React from 'react';
Expand All @@ -7,7 +7,7 @@ import { filterOutAuthorizationParams } from '../Auth/authentication-utils';
import { initialParameterValues, ParameterSpec } from './parameter-utils';

const persistedParameterValuesAtom = atom({});
export const useRequestParameters = (httpOperation: IHttpOperation) => {
export const useRequestParameters = (httpOperation: IHttpEndpointOperation) => {
const [persistedParameterValues, setPersistedParameterValues] = useAtom(persistedParameterValuesAtom);

const allParameters = React.useMemo(() => extractAllParameters(httpOperation), [httpOperation]);
Expand Down Expand Up @@ -44,7 +44,7 @@ export const useRequestParameters = (httpOperation: IHttpOperation) => {
};
};

function extractAllParameters(httpOperation: IHttpOperation): ParameterSpec[] {
function extractAllParameters(httpOperation: IHttpEndpointOperation): ParameterSpec[] {
const getRequired = (obj: { required?: boolean }) => obj.required ?? false;

const pathParameters = orderBy(httpOperation.request?.path ?? [], [getRequired, 'name'], ['desc', 'asc']);
Expand Down
Loading

0 comments on commit 45ede7c

Please sign in to comment.