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 { useBundledData } from '../hooks/useBundledData';
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 bundledNodeData = useBundledData(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={bundledNodeData} 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={bundledNodeData} />
</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
47 changes: 47 additions & 0 deletions packages/elements/src/hooks/useBundledData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import $RefParser from '@stoplight/json-schema-ref-parser';
import { NodeType } from '@stoplight/types';
import { isObject } from 'lodash';
Copy link
Contributor

@P0lip P0lip Sep 21, 2020

Choose a reason for hiding this comment

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

Suggested change
import { isObject } from 'lodash';
import { isObjectLike } from 'lodash';

isObject returns true for functions.

(or isPlainObject)

We can leave it as well too, as I doubt function will ever be provided.

import * as React from 'react';

import { useParsedData } from './useParsedData';

/**
* @param type branch node snapshot type
* @param data branch node snapshot data
*/

interface Options {
baseUrl?: string;
}

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

const [bundledData, setBundledData] = React.useState(parsedData);

React.useEffect(() => {
if (!isObject(parsedData) || type !== NodeType.HttpOperation) {
setBundledData(parsedData);
return;
}

doBundle(parsedData, options?.baseUrl)
.then(res => setBundledData(res))
.catch(reason => {
console.error(`Could not bundle: ${reason.message}`);
console.error(reason);
marcelltoth marked this conversation as resolved.
Show resolved Hide resolved
setBundledData(parsedData);
});
}, [parsedData, type, options?.baseUrl]);

return bundledData;
}

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