mirror of
https://github.com/Redocly/redoc.git
synced 2025-01-31 10:04:08 +03:00
feat: support externalValue for examples
implements #551, related to #840
This commit is contained in:
parent
309901bd31
commit
2cdfcd25cd
|
@ -1,4 +1,5 @@
|
||||||
import styled from '../styled-components';
|
import styled from '../styled-components';
|
||||||
|
import { PrismDiv } from './PrismDiv';
|
||||||
|
|
||||||
export const SampleControls = styled.div`
|
export const SampleControls = styled.div`
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
|
@ -21,3 +22,12 @@ export const SampleControlsWrap = styled.div`
|
||||||
opacity: 1;
|
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
src/components/PayloadSamples/Example.tsx
Normal file
52
src/components/PayloadSamples/Example.tsx
Normal file
|
@ -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
src/components/PayloadSamples/ExampleValue.tsx
Normal file
18
src/components/PayloadSamples/ExampleValue.tsx
Normal file
|
@ -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} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,9 @@ import * as React from 'react';
|
||||||
|
|
||||||
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
|
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
|
||||||
import { MediaTypeModel } from '../../services/models';
|
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 {
|
export interface PayloadSamplesProps {
|
||||||
mediaType: MediaTypeModel;
|
mediaType: MediaTypeModel;
|
||||||
|
@ -18,13 +16,6 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
|
||||||
const mimeType = this.props.mediaType.name;
|
const mimeType = this.props.mediaType.name;
|
||||||
|
|
||||||
const noSample = <NoSampleLabel>No sample</NoSampleLabel>;
|
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);
|
const examplesNames = Object.keys(examples);
|
||||||
if (examplesNames.length === 0) {
|
if (examplesNames.length === 0) {
|
||||||
|
@ -39,13 +30,19 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
|
||||||
))}
|
))}
|
||||||
</TabList>
|
</TabList>
|
||||||
{examplesNames.map(name => (
|
{examplesNames.map(name => (
|
||||||
<TabPanel key={name}>{sampleView(examples[name].value)}</TabPanel>
|
<TabPanel key={name}>
|
||||||
|
<Example example={examples[name]} mimeType={mimeType} />
|
||||||
|
</TabPanel>
|
||||||
))}
|
))}
|
||||||
</SmallTabs>
|
</SmallTabs>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const name = examplesNames[0];
|
const name = examplesNames[0];
|
||||||
return <div>{sampleView(examples[name].value)}</div>;
|
return (
|
||||||
|
<div>
|
||||||
|
<Example example={examples[name]} mimeType={mimeType} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
34
src/components/PayloadSamples/exernalExampleHook.ts
Normal file
34
src/components/PayloadSamples/exernalExampleHook.ts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -1,19 +1,8 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { highlight } from '../../utils';
|
import { highlight } from '../../utils';
|
||||||
|
|
||||||
import { SampleControls, SampleControlsWrap } from '../../common-elements';
|
import { SampleControls, SampleControlsWrap, StyledPre } from '../../common-elements';
|
||||||
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
|
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 {
|
export interface SourceCodeProps {
|
||||||
source: string;
|
source: string;
|
||||||
|
|
|
@ -1,14 +1,55 @@
|
||||||
|
import { resolve as urlResolve } from 'url';
|
||||||
|
|
||||||
import { OpenAPIExample, Referenced } from '../../types';
|
import { OpenAPIExample, Referenced } from '../../types';
|
||||||
|
import { isJsonLike } from '../../utils/openapi';
|
||||||
import { OpenAPIParser } from '../OpenAPIParser';
|
import { OpenAPIParser } from '../OpenAPIParser';
|
||||||
|
|
||||||
|
const externalExamplesCache: { [url: string]: Promise<any> } = {};
|
||||||
|
|
||||||
export class ExampleModel {
|
export class ExampleModel {
|
||||||
value: any;
|
value: any;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
externalValue?: string;
|
externalValueUrl?: string;
|
||||||
|
|
||||||
constructor(parser: OpenAPIParser, infoOrRef: Referenced<OpenAPIExample>) {
|
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);
|
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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as Sampler from 'openapi-sampler';
|
import * as Sampler from 'openapi-sampler';
|
||||||
|
|
||||||
import { OpenAPIExample, OpenAPIMediaType } from '../../types';
|
import { OpenAPIMediaType } from '../../types';
|
||||||
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||||
import { SchemaModel } from './Schema';
|
import { SchemaModel } from './Schema';
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import { OpenAPIParser } from '../OpenAPIParser';
|
||||||
import { ExampleModel } from './Example';
|
import { ExampleModel } from './Example';
|
||||||
|
|
||||||
export class MediaTypeModel {
|
export class MediaTypeModel {
|
||||||
examples?: { [name: string]: OpenAPIExample };
|
examples?: { [name: string]: ExampleModel };
|
||||||
schema?: SchemaModel;
|
schema?: SchemaModel;
|
||||||
name: string;
|
name: string;
|
||||||
isRequestType: boolean;
|
isRequestType: boolean;
|
||||||
|
@ -33,7 +33,7 @@ export class MediaTypeModel {
|
||||||
this.examples = mapValues(info.examples, example => new ExampleModel(parser, example));
|
this.examples = mapValues(info.examples, example => new ExampleModel(parser, example));
|
||||||
} else if (info.example !== undefined) {
|
} else if (info.example !== undefined) {
|
||||||
this.examples = {
|
this.examples = {
|
||||||
default: new ExampleModel(parser, { value: info.example }),
|
default: new ExampleModel(parser, { value: parser.shalowDeref(info.example) }),
|
||||||
};
|
};
|
||||||
} else if (isJsonLike(name)) {
|
} else if (isJsonLike(name)) {
|
||||||
this.generateExample(parser, info);
|
this.generateExample(parser, info);
|
||||||
|
@ -49,28 +49,20 @@ export class MediaTypeModel {
|
||||||
if (this.schema && this.schema.oneOf) {
|
if (this.schema && this.schema.oneOf) {
|
||||||
this.examples = {};
|
this.examples = {};
|
||||||
for (const subSchema of this.schema.oneOf) {
|
for (const subSchema of this.schema.oneOf) {
|
||||||
const sample = Sampler.sample(
|
const sample = Sampler.sample(subSchema.rawSchema, samplerOptions, parser.spec);
|
||||||
subSchema.rawSchema,
|
|
||||||
samplerOptions,
|
|
||||||
parser.spec,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.schema.discriminatorProp && typeof sample === 'object' && sample) {
|
if (this.schema.discriminatorProp && typeof sample === 'object' && sample) {
|
||||||
sample[this.schema.discriminatorProp] = subSchema.title;
|
sample[this.schema.discriminatorProp] = subSchema.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.examples[subSchema.title] = {
|
this.examples[subSchema.title] = new ExampleModel(parser, {
|
||||||
value: sample,
|
value: sample,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
} else if (this.schema) {
|
} else if (this.schema) {
|
||||||
this.examples = {
|
this.examples = {
|
||||||
default: new ExampleModel(parser, {
|
default: new ExampleModel(parser, {
|
||||||
value: Sampler.sample(
|
value: Sampler.sample(info.schema, samplerOptions, parser.spec),
|
||||||
info.schema,
|
|
||||||
samplerOptions,
|
|
||||||
parser.spec,
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user