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(api): make API component resolve inline and remote refs #575

Merged
merged 13 commits into from
Sep 29, 2020
3 changes: 1 addition & 2 deletions packages/elements/src/__stories__/components/Api.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { text, withKnobs } from '@storybook/addon-knobs';
import { boolean } from '@storybook/addon-knobs/react';
import { boolean, text, withKnobs } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
import cn from 'classnames';
import * as React from 'react';
Expand Down
24 changes: 14 additions & 10 deletions packages/elements/src/containers/API.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ import * as React from 'react';
import { useLocation } from 'react-router-dom';
import useSwr from 'swr';

import { Docs } from '../components/Docs';
import { DocsSkeleton } from '../components/Docs/Skeleton';
import { Docs, DocsSkeleton } from '../components/Docs';
import { Row } from '../components/TableOfContents/Row';
import { TryIt } from '../components/TryIt';
import { TryItHeader } from '../components/TryIt/header';
import { withRouter } from '../hoc/withRouter';
import { useDereferencedData } from '../hooks/useDereferencedData';
import { useParsedValue } from '../hooks/useParsedValue';
import { useTocContents } from '../hooks/useTocContents';
import { withStyles } from '../styled';
import { IAPI } from '../types';
import { computeTocTree, isOas2, isOas3, isOperation, IUriMap, MODEL_REGEXP, OPERATION_REGEXP } from '../utils/oas';
import { computeOas2UriMap } from '../utils/oas/oas2';
import { computeOas3UriMap } from '../utils/oas/oas3';
import { InlineRefResolverProvider } from './Provider';

const fetcher = (url: string) => axios.get(url).then(res => res.data);

Expand Down Expand Up @@ -62,6 +63,7 @@ const APIImpl = withRouter<IAPI>(({ apiDescriptionUrl, linkComponent: LinkCompon
? NodeType.HttpOperation
: NodeType.HttpService;
const nodeData = uriMap[pathname] || uriMap['/'];
const dereferencedNodeData = useDereferencedData(nodeType, nodeData, { baseUrl: apiDescriptionUrl });

if (error) {
return (
Expand All @@ -88,15 +90,17 @@ const APIImpl = withRouter<IAPI>(({ apiDescriptionUrl, linkComponent: LinkCompon
/>
<div className="flex-grow p-5">
<div className="flex">
<Docs className="px-10" nodeData={nodeData} nodeType={nodeType} />
{showTryIt && (
<div className="w-2/5 border-l relative">
<div className="absolute inset-0 overflow-auto px-10">
<TryItHeader />
<TryIt nodeType={nodeType} nodeData={nodeData} />
<InlineRefResolverProvider document={document}>
marcelltoth marked this conversation as resolved.
Show resolved Hide resolved
<Docs className="px-10" nodeData={dereferencedNodeData} nodeType={nodeType} />
{showTryIt && (
<div className="w-2/5 border-l relative">
<div className="absolute inset-0 overflow-auto px-10">
<TryItHeader />
<TryIt nodeType={nodeType} nodeData={dereferencedNodeData} />
</div>
</div>
</div>
)}
)}
</InlineRefResolverProvider>
</div>
</div>
</div>
Expand Down
14 changes: 3 additions & 11 deletions packages/elements/src/containers/Docs.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { pointerToPath } from '@stoplight/json';
import { SchemaTreeRefDereferenceFn } from '@stoplight/json-schema-viewer';
import { NodeType } from '@stoplight/types';
import { FAIcon, NonIdealState } from '@stoplight/ui-kit';
import { get, isObject } from 'lodash';
import * as React from 'react';
import { useQuery } from 'urql';

import { DocsSkeleton, ParsedDocs } from '../components/Docs';
import { bundledBranchNode } from '../graphql/BranchNodeBySlug';
import { useParsedData } from '../hooks/useParsedData';
import { ActiveInfoContext, InlineRefResolverContext, IProvider, Provider } from './Provider';
import { ActiveInfoContext, InlineRefResolverProvider, IProvider, Provider } from './Provider';

export interface IDocsProps {
className?: string;
Expand All @@ -23,15 +20,10 @@ interface IDocsProvider extends IProvider {
const DocsPopup = React.memo<{ nodeType: NodeType; nodeData: unknown; className?: string }>(
({ nodeType, nodeData, className }) => {
const document = useParsedData(nodeType, nodeData);
const inlineRefResolver = React.useCallback<SchemaTreeRefDereferenceFn>(
({ pointer }, _, schema) =>
pointer === null ? null : get(isObject(document) ? document : schema, pointerToPath(pointer)),
[document],
);
return (
<InlineRefResolverContext.Provider value={inlineRefResolver}>
<InlineRefResolverProvider document={document}>
<ParsedDocs className={className} nodeType={nodeType} nodeData={document} />
</InlineRefResolverContext.Provider>
</InlineRefResolverProvider>
);
},
);
Expand Down
18 changes: 18 additions & 0 deletions packages/elements/src/containers/Provider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { pointerToPath } from '@stoplight/json';
import { SchemaTreeRefDereferenceFn } from '@stoplight/json-schema-viewer';
import { IComponentMapping } from '@stoplight/markdown-viewer';
import { get, isObject } from 'lodash';
import * as React from 'react';
import { Client, Provider as UrqlProvider } from 'urql';

Expand Down Expand Up @@ -27,6 +29,22 @@ export const ComponentsContext = createNamedContext<IComponentMapping | undefine

export const InlineRefResolverContext = React.createContext<SchemaTreeRefDereferenceFn | undefined>(void 0);

interface InlineRefResolverProviderTypes {
document: unknown;
}

/**
* Populates `InlineRefResolverContext` with a standard inline ref resolver based on `document`.
*/
export const InlineRefResolverProvider: React.FC<InlineRefResolverProviderTypes> = ({ document, children }) => {
const inlineRefResolver = React.useCallback<SchemaTreeRefDereferenceFn>(
({ pointer }, _, schema) =>
pointer === null ? null : get(isObject(document) ? document : schema, pointerToPath(pointer)),
[document],
);
return <InlineRefResolverContext.Provider value={inlineRefResolver}>{children}</InlineRefResolverContext.Provider>;
};

const defaultIcons: NodeIconMapping = {};
export const IconsContext = createNamedContext<NodeIconMapping>('IconsContext', defaultIcons);

Expand Down
21 changes: 17 additions & 4 deletions packages/elements/src/hooks/useDereferencedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { useParsedData } from './useParsedData';
* @param type branch node snapshot type
* @param data branch node snapshot data
*/
export function useDereferencedData(type: NodeType, data: string) {

interface Options {
baseUrl?: string;
}

export function useDereferencedData(type: NodeType, data: unknown, options?: Options) {
const parsedData = useParsedData(type, data);

const [dereferencedData, setDereferencedData] = React.useState(parsedData);
Expand All @@ -22,15 +27,23 @@ export function useDereferencedData(type: NodeType, data: string) {
return;
}

$RefParser
.dereference(parsedData, { continueOnError: true })
doDereference(parsedData, options?.baseUrl)
.then(res => setDereferencedData(res))
.catch(reason => {
console.error(`Could not dereference operation: ${reason.message}`);
console.error(reason);
setDereferencedData(parsedData);
});
}, [parsedData, type]);
}, [parsedData, type, options?.baseUrl]);

return dereferencedData;
}

const commonDereferenceOptions = { continueOnError: true };
const doDereference = (data: object, baseUrl?: string) => {
if (!baseUrl) {
return $RefParser.dereference(data, commonDereferenceOptions);
} else {
return $RefParser.dereference(baseUrl, data, commonDereferenceOptions);
}
};