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 { 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')};
|
||||
`;
|
||||
|
|
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 { 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<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) {
|
||||
|
@ -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
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 { 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;
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user