diff --git a/demo/webpack.config.ts b/demo/webpack.config.ts index a1f4b5d9e2..0a5be01406 100644 --- a/demo/webpack.config.ts +++ b/demo/webpack.config.ts @@ -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: { @@ -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$/), diff --git a/src/common-elements/samples.tsx b/src/common-elements/samples.tsx index 457657b10c..f4855a433c 100644 --- a/src/common-elements/samples.tsx +++ b/src/common-elements/samples.tsx @@ -1,4 +1,5 @@ import styled from '../styled-components'; +import { PrismDiv } from './PrismDiv'; export const SampleControls = styled.div` opacity: 0.4; @@ -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')}; +`; diff --git a/src/components/PayloadSamples/Example.tsx b/src/components/PayloadSamples/Example.tsx new file mode 100644 index 0000000000..392e213818 --- /dev/null +++ b/src/components/PayloadSamples/Example.tsx @@ -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 ; + } else { + return ; + } +} + +export function ExternalExample({ example, mimeType }: ExampleProps) { + let value = useExternalExample(example, mimeType); + + if (value === undefined) { + return Loading...; + } + + if (value instanceof Error) { + console.log(value); + return ( + + Error loading external example:
+ + {example.externalValueUrl} + +
+ ); + } + + if (isJsonLike(mimeType)) { + return ; + } 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 ; + } +} diff --git a/src/components/PayloadSamples/ExampleValue.tsx b/src/components/PayloadSamples/ExampleValue.tsx new file mode 100644 index 0000000000..fa35e842d1 --- /dev/null +++ b/src/components/PayloadSamples/ExampleValue.tsx @@ -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 ; + } else { + return ; + } +} diff --git a/src/components/PayloadSamples/MediaTypeSamples.tsx b/src/components/PayloadSamples/MediaTypeSamples.tsx index 5c26aac425..251f182f6f 100644 --- a/src/components/PayloadSamples/MediaTypeSamples.tsx +++ b/src/components/PayloadSamples/MediaTypeSamples.tsx @@ -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; @@ -18,13 +16,6 @@ export class MediaTypeSamples extends React.Component { const mimeType = this.props.mediaType.name; const noSample = No sample; - const sampleView = isJsonLike(mimeType) - ? sample => - : sample => - (sample !== undefined && ( - - )) || - noSample; const examplesNames = Object.keys(examples); if (examplesNames.length === 0) { @@ -39,13 +30,19 @@ export class MediaTypeSamples extends React.Component { ))} {examplesNames.map(name => ( - {sampleView(examples[name].value)} + + + ))} ); } else { const name = examplesNames[0]; - return
{sampleView(examples[name].value)}
; + return ( +
+ +
+ ); } } } diff --git a/src/components/PayloadSamples/exernalExampleHook.ts b/src/components/PayloadSamples/exernalExampleHook.ts new file mode 100644 index 0000000000..e3e33f08ba --- /dev/null +++ b/src/components/PayloadSamples/exernalExampleHook.ts @@ -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(undefined); + const prevRef = useRef(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; +} diff --git a/src/components/SourceCode/SourceCode.tsx b/src/components/SourceCode/SourceCode.tsx index b670766314..3b7d4eb225 100644 --- a/src/components/SourceCode/SourceCode.tsx +++ b/src/components/SourceCode/SourceCode.tsx @@ -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; diff --git a/src/services/models/Example.ts b/src/services/models/Example.ts index f043bccd19..a142a956bd 100644 --- a/src/services/models/Example.ts +++ b/src/services/models/Example.ts @@ -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 } = {}; + export class ExampleModel { value: any; summary?: string; description?: string; - externalValue?: string; + externalValueUrl?: string; constructor(parser: OpenAPIParser, infoOrRef: Referenced) { - 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 { + 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]; + } } diff --git a/src/services/models/MediaType.ts b/src/services/models/MediaType.ts index 941a35fbe7..5b1ab5d272 100644 --- a/src/services/models/MediaType.ts +++ b/src/services/models/MediaType.ts @@ -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'; @@ -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; @@ -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); @@ -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), }), }; }