Skip to content

Commit

Permalink
feat: support externalValue for examples
Browse files Browse the repository at this point in the history
implements #551, related to #840
  • Loading branch information
RomanHotsiy committed Mar 11, 2019
1 parent 309901b commit 2cdfcd2
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 46 deletions.
8 changes: 4 additions & 4 deletions demo/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
env.playground
? 'playground/hmr-playground.tsx'
: env.bench
? '../benchmark/index.tsx'
: 'index.tsx',
? '../benchmark/index.tsx'
: 'index.tsx',
),
],
output: {
Expand Down Expand Up @@ -141,8 +141,8 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
template: env.playground
? 'demo/playground/index.html'
: env.bench
? 'benchmark/index.html'
: 'demo/index.html',
? 'benchmark/index.html'
: 'demo/index.html',
}),
new ForkTsCheckerWebpackPlugin(),
ignore(/js-yaml\/dumper\.js$/),
Expand Down
10 changes: 10 additions & 0 deletions src/common-elements/samples.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import styled from '../styled-components';
import { PrismDiv } from './PrismDiv';

export const SampleControls = styled.div`
opacity: 0.4;
Expand All @@ -21,3 +22,12 @@ export const SampleControlsWrap = styled.div`
opacity: 1;
}
`;

export const StyledPre = styled(PrismDiv.withComponent('pre'))`
font-family: ${props => props.theme.typography.code.fontFamily};
font-size: ${props => props.theme.typography.code.fontSize};
overflow-x: auto;
margin: 0;
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
`;
52 changes: 52 additions & 0 deletions src/components/PayloadSamples/Example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from 'react';

import { StyledPre } from '../../common-elements/samples';
import { ExampleModel } from '../../services/models';
import { isJsonLike, langFromMime } from '../../utils';
import { JsonViewer } from '../JsonViewer/JsonViewer';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
import { ExampleValue } from './ExampleValue';
import { useExternalExample } from './exernalExampleHook';

export interface ExampleProps {
example: ExampleModel;
mimeType: string;
}

export function Example({ example, mimeType }: ExampleProps) {
if (example.value === undefined && example.externalValueUrl) {
return <ExternalExample example={example} mimeType={mimeType} />;
} else {
return <ExampleValue value={example.value} mimeType={mimeType} />;
}
}

export function ExternalExample({ example, mimeType }: ExampleProps) {
let value = useExternalExample(example, mimeType);

if (value === undefined) {
return <span>Loading...</span>;
}

if (value instanceof Error) {
console.log(value);
return (
<StyledPre>
Error loading external example: <br />
<a className={'token string'} href={example.externalValueUrl} target="_blank">
{example.externalValueUrl}
</a>
</StyledPre>
);
}

if (isJsonLike(mimeType)) {
return <JsonViewer data={value} />;
} else {
if (typeof value === 'object') {
// just in case example was cached as json but used as non-json
value = JSON.stringify(value, null, 2);
}
return <SourceCodeWithCopy lang={langFromMime(mimeType)} source={value} />;
}
}
18 changes: 18 additions & 0 deletions src/components/PayloadSamples/ExampleValue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react';

import { isJsonLike, langFromMime } from '../../utils/openapi';
import { JsonViewer } from '../JsonViewer/JsonViewer';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';

export interface ExampleValueProps {
value: any;
mimeType: string;
}

export function ExampleValue({ value, mimeType }: ExampleValueProps) {
if (isJsonLike(mimeType)) {
return <JsonViewer data={value} />;
} else {
return <SourceCodeWithCopy lang={langFromMime(mimeType)} source={value} />;
}
}
23 changes: 10 additions & 13 deletions src/components/PayloadSamples/MediaTypeSamples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import * as React from 'react';

import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
import { MediaTypeModel } from '../../services/models';
import { JsonViewer } from '../JsonViewer/JsonViewer';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
import { NoSampleLabel } from './styled.elements';

import { isJsonLike, langFromMime } from '../../utils';
import { Example } from './Example';
import { NoSampleLabel } from './styled.elements';

export interface PayloadSamplesProps {
mediaType: MediaTypeModel;
Expand All @@ -18,13 +16,6 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
const mimeType = this.props.mediaType.name;

const noSample = <NoSampleLabel>No sample</NoSampleLabel>;
const sampleView = isJsonLike(mimeType)
? sample => <JsonViewer data={sample} />
: sample =>
(sample !== undefined && (
<SourceCodeWithCopy lang={langFromMime(mimeType)} source={sample} />
)) ||
noSample;

const examplesNames = Object.keys(examples);
if (examplesNames.length === 0) {
Expand All @@ -39,13 +30,19 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
))}
</TabList>
{examplesNames.map(name => (
<TabPanel key={name}>{sampleView(examples[name].value)}</TabPanel>
<TabPanel key={name}>
<Example example={examples[name]} mimeType={mimeType} />
</TabPanel>
))}
</SmallTabs>
);
} else {
const name = examplesNames[0];
return <div>{sampleView(examples[name].value)}</div>;
return (
<div>
<Example example={examples[name]} mimeType={mimeType} />
</div>
);
}
}
}
34 changes: 34 additions & 0 deletions src/components/PayloadSamples/exernalExampleHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect, useRef, useState } from 'react';
import { ExampleModel } from '../../services/models/Example';

export function useExternalExample(example: ExampleModel, mimeType: string) {
const [, setIsLoading] = useState(true); // to trigger component reload

const value = useRef<any>(undefined);
const prevRef = useRef<ExampleModel | undefined>(undefined);

if (prevRef.current !== example) {
value.current = undefined;
}

prevRef.current = example;

useEffect(
() => {
const load = async () => {
setIsLoading(true);
try {
value.current = await example.getExternalValue(mimeType);
} catch (e) {
value.current = e;
}
setIsLoading(false);
};

load();
},
[example, mimeType],
);

return value.current;
}
13 changes: 1 addition & 12 deletions src/components/SourceCode/SourceCode.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import * as React from 'react';
import { highlight } from '../../utils';

import { SampleControls, SampleControlsWrap } from '../../common-elements';
import { SampleControls, SampleControlsWrap, StyledPre } from '../../common-elements';
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
import { PrismDiv } from '../../common-elements/PrismDiv';
import styled from '../../styled-components';

const StyledPre = styled(PrismDiv.withComponent('pre'))`
font-family: ${props => props.theme.typography.code.fontFamily};
font-size: ${props => props.theme.typography.code.fontSize};
overflow-x: auto;
margin: 0;
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
`;

export interface SourceCodeProps {
source: string;
Expand Down
45 changes: 43 additions & 2 deletions src/services/models/Example.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,55 @@
import { resolve as urlResolve } from 'url';

import { OpenAPIExample, Referenced } from '../../types';
import { isJsonLike } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser';

const externalExamplesCache: { [url: string]: Promise<any> } = {};

export class ExampleModel {
value: any;
summary?: string;
description?: string;
externalValue?: string;
externalValueUrl?: string;

constructor(parser: OpenAPIParser, infoOrRef: Referenced<OpenAPIExample>) {
Object.assign(this, parser.deref(infoOrRef));
const example = parser.deref(infoOrRef);
this.value = example.value;
this.summary = example.summary;
this.description = example.description;
if (example.externalValue) {
this.externalValueUrl = urlResolve(parser.specUrl || '', example.externalValue);
}
parser.exitRef(infoOrRef);
}

getExternalValue(mimeType: string): Promise<any> {
if (!this.externalValueUrl) {
return Promise.resolve(undefined);
}

if (externalExamplesCache[this.externalValueUrl]) {
return externalExamplesCache[this.externalValueUrl];
}

externalExamplesCache[this.externalValueUrl] = fetch(this.externalValueUrl).then(res => {
return res.text().then(txt => {
if (!res.ok) {
return Promise.reject(new Error(txt));
}

if (isJsonLike(mimeType)) {
try {
return JSON.parse(txt);
} catch (e) {
return txt;
}
} else {
return txt;
}
});
});

return externalExamplesCache[this.externalValueUrl];
}
}
22 changes: 7 additions & 15 deletions src/services/models/MediaType.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as Sampler from 'openapi-sampler';

import { OpenAPIExample, OpenAPIMediaType } from '../../types';
import { OpenAPIMediaType } from '../../types';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { SchemaModel } from './Schema';

Expand All @@ -9,7 +9,7 @@ import { OpenAPIParser } from '../OpenAPIParser';
import { ExampleModel } from './Example';

export class MediaTypeModel {
examples?: { [name: string]: OpenAPIExample };
examples?: { [name: string]: ExampleModel };
schema?: SchemaModel;
name: string;
isRequestType: boolean;
Expand All @@ -33,7 +33,7 @@ export class MediaTypeModel {
this.examples = mapValues(info.examples, example => new ExampleModel(parser, example));
} else if (info.example !== undefined) {
this.examples = {
default: new ExampleModel(parser, { value: info.example }),
default: new ExampleModel(parser, { value: parser.shalowDeref(info.example) }),
};
} else if (isJsonLike(name)) {
this.generateExample(parser, info);
Expand All @@ -49,28 +49,20 @@ export class MediaTypeModel {
if (this.schema && this.schema.oneOf) {
this.examples = {};
for (const subSchema of this.schema.oneOf) {
const sample = Sampler.sample(
subSchema.rawSchema,
samplerOptions,
parser.spec,
);
const sample = Sampler.sample(subSchema.rawSchema, samplerOptions, parser.spec);

if (this.schema.discriminatorProp && typeof sample === 'object' && sample) {
sample[this.schema.discriminatorProp] = subSchema.title;
}

this.examples[subSchema.title] = {
this.examples[subSchema.title] = new ExampleModel(parser, {
value: sample,
};
});
}
} else if (this.schema) {
this.examples = {
default: new ExampleModel(parser, {
value: Sampler.sample(
info.schema,
samplerOptions,
parser.spec,
),
value: Sampler.sample(info.schema, samplerOptions, parser.spec),
}),
};
}
Expand Down

0 comments on commit 2cdfcd2

Please sign in to comment.