Merge branch 'master' into sample-collapse-level-option

This commit is contained in:
Roman Hotsiy 2019-07-29 13:09:13 +03:00 committed by GitHub
commit cc8964f71c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1032 additions and 1075 deletions

View File

@ -1,3 +1,33 @@
# [2.0.0-rc.10](https://github.com/Redocly/redoc/compare/v2.0.0-rc.9...v2.0.0-rc.10) (2019-07-08)
### Bug Fixes
* broken headings with single quote ([51d3b9b](https://github.com/Redocly/redoc/commit/51d3b9b)), closes [#955](https://github.com/Redocly/redoc/issues/955)
* fix fields table overflow if deeply nested with long title ([12b7057](https://github.com/Redocly/redoc/commit/12b7057))
* hide empty example when it is not defined ([4bd499f](https://github.com/Redocly/redoc/commit/4bd499f))
* markdown in examples descriptions + minor ui tweaks ([f52d9e8](https://github.com/Redocly/redoc/commit/f52d9e8))
* organize response examples in dropdown and display description ([995e557](https://github.com/Redocly/redoc/commit/995e557))
# [2.0.0-rc.9](https://github.com/Redocly/redoc/compare/v2.0.0-rc.8-1...v2.0.0-rc.9) (2019-06-27)
### Bug Fixes
* fix regression double slashes added to full URL display ([f29a4fe](https://github.com/Redocly/redoc/commit/f29a4fe))
* IE11, add missing Object.assign polyfill ([888f04e](https://github.com/Redocly/redoc/commit/888f04e))
* serialize parameter example values according to the spec ([#917](https://github.com/Redocly/redoc/issues/917)) ([3939286](https://github.com/Redocly/redoc/commit/3939286))
* styled-component style error in tabs ([#946](https://github.com/Redocly/redoc/issues/946)) ([c488bbf](https://github.com/Redocly/redoc/commit/c488bbf))
### Features
* add x-additionalPropertiesName ([#622](https://github.com/Redocly/redoc/issues/622)) ([#944](https://github.com/Redocly/redoc/issues/944)) ([0eb1e66](https://github.com/Redocly/redoc/commit/0eb1e66))
# [2.0.0-rc.8-1](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.8...v2.0.0-rc.8-1) (2019-05-13) # [2.0.0-rc.8-1](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.8...v2.0.0-rc.8-1) (2019-05-13)

View File

@ -218,6 +218,7 @@ ReDoc makes use of the following [vendor extensions](https://swagger.io/specific
* [`x-tagGroups`](docs/redoc-vendor-extensions.md#x-tagGroups) - group tags by categories in the side menu * [`x-tagGroups`](docs/redoc-vendor-extensions.md#x-tagGroups) - group tags by categories in the side menu
* [`x-servers`](docs/redoc-vendor-extensions.md#x-servers) - ability to specify different servers for API (backported from OpenAPI 3.0) * [`x-servers`](docs/redoc-vendor-extensions.md#x-servers) - ability to specify different servers for API (backported from OpenAPI 3.0)
* [`x-ignoredHeaderParameters`](docs/redoc-vendor-extensions.md#x-ignoredHeaderParameters) - ability to specify header parameter names to ignore * [`x-ignoredHeaderParameters`](docs/redoc-vendor-extensions.md#x-ignoredHeaderParameters) - ability to specify header parameter names to ignore
* [`x-additionalPropertiesName`](docs/redoc-vendor-extensions.md#x-additionalPropertiesName) - ability to supply a descriptive name for the additional property keys
### `<redoc>` options object ### `<redoc>` options object
You can use all of the following options with standalone version on <redoc> tag by kebab-casing them, e.g. `scrollYOffset` becomes `scroll-y-offset` and `expandResponses` becomes `expand-responses`. You can use all of the following options with standalone version on <redoc> tag by kebab-casing them, e.g. `scrollYOffset` becomes `scroll-y-offset` and `expandResponses` becomes `expand-responses`.

View File

@ -1,25 +1,28 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.8.4", "version": "0.8.5",
"description": "ReDoc's Command Line Interface", "description": "ReDoc's Command Line Interface",
"main": "index.js", "main": "index.js",
"bin": "index.js", "bin": "index.js",
"repository": "https://github.com/Redocly/redoc", "repository": "https://github.com/Redocly/redoc",
"author": "Roman Hotsiy <gotsijroman@gmail.com>", "author": "Roman Hotsiy <gotsijroman@gmail.com>",
"license": "MIT", "license": "MIT",
"engines": {
"node": ">= 8"
},
"dependencies": { "dependencies": {
"chokidar": "^2.0.4", "chokidar": "^3.0.2",
"handlebars": "^4.0.11", "handlebars": "^4.1.2",
"isarray": "^2.0.4", "isarray": "^2.0.4",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mobx": "^4.2.0", "mobx": "^4.2.0",
"node-libs-browser": "^2.2.0", "node-libs-browser": "^2.2.1",
"react": "^16.8.4", "react": "^16.8.6",
"react-dom": "^16.8.4", "react-dom": "^16.8.6",
"redoc": "^2.0.0-rc.8-1", "redoc": "^2.0.0-rc.10",
"styled-components": "^4.1.3", "styled-components": "^4.3.2",
"tslib": "^1.9.3", "tslib": "^1.10.0",
"yargs": "^12.0.5" "yargs": "^13.2.4"
}, },
"scripts": { "scripts": {
"ci-publish": "ci-publish" "ci-publish": "ci-publish"
@ -28,7 +31,7 @@
"access": "public" "access": "public"
}, },
"devDependencies": { "devDependencies": {
"@types/chokidar": "^1.7.5", "@types/chokidar": "^2.1.3",
"@types/handlebars": "^4.0.39", "@types/handlebars": "^4.0.39",
"@types/mkdirp": "^0.5.2", "@types/mkdirp": "^0.5.2",
"ci-publish": "^1.3.1" "ci-publish": "^1.3.1"

File diff suppressed because it is too large Load Diff

View File

@ -278,3 +278,31 @@ PayPalPayment:
In the example above the names of definitions (`PayPalPayment`) are named differently than In the example above the names of definitions (`PayPalPayment`) are named differently than
names in the payload (`paypal`) which is not supported by default `discriminator`. names in the payload (`paypal`) which is not supported by default `discriminator`.
#### x-additionalPropertiesName
**ATTENTION**: This is ReDoc-specific vendor extension. It won't be supported by other tools.
Extends the `additionalProperties` property of the schema object.
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-additionalPropertiesName | string | descriptive name of additional properties keys |
###### Usage in ReDoc
ReDoc uses this extension to display a more descriptive property name in objects with `additionalProperties` when viewing the property list with an `object`.
###### x-additionalPropertiesName example
```yaml
Player:
required:
- name
properties:
name:
type: string
additionalProperties:
x-additionalPropertiesName: attribute-name
type: string
```

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc", "name": "redoc",
"version": "2.0.0-rc.8-1", "version": "2.0.0-rc.10",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",
@ -129,7 +129,8 @@
"mobx": "^4.2.0 || ^5.0.0", "mobx": "^4.2.0 || ^5.0.0",
"react": "^16.8.4", "react": "^16.8.4",
"react-dom": "^16.8.4", "react-dom": "^16.8.4",
"styled-components": "^4.1.1" "styled-components": "^4.1.1",
"core-js": "^2.6.5"
}, },
"dependencies": { "dependencies": {
"classnames": "^2.2.6", "classnames": "^2.2.6",
@ -154,7 +155,8 @@
"slugify": "^1.3.4", "slugify": "^1.3.4",
"stickyfill": "^1.1.1", "stickyfill": "^1.1.1",
"swagger2openapi": "^5.2.3", "swagger2openapi": "^5.2.3",
"tslib": "^1.9.3" "tslib": "^1.9.3",
"uri-template-lite": "^19.4.0"
}, },
"bundlesize": [ "bundlesize": [
{ {

View File

@ -5,11 +5,7 @@ import { ClipboardService } from '../services/ClipboardService';
export interface CopyButtonWrapperProps { export interface CopyButtonWrapperProps {
data: any; data: any;
children: ( children: (props: { renderCopyButton: () => React.ReactNode }) => React.ReactNode;
props: {
renderCopyButton: (() => React.ReactNode);
},
) => React.ReactNode;
} }
export class CopyButtonWrapper extends React.PureComponent< export class CopyButtonWrapper extends React.PureComponent<

View File

@ -55,7 +55,7 @@ export const StyledDropdown = styled(Dropdown)`
display: block; display: block;
height: 0; height: 0;
position: absolute; position: absolute;
right: 0.35em; right: 0.6em;
top: 50%; top: 50%;
margin-top: -0.125em; margin-top: -0.125em;
width: 0; width: 0;

View File

@ -32,6 +32,7 @@ export const TypeName = styled(FieldLabel)`
export const TypeTitle = styled(FieldLabel)` export const TypeTitle = styled(FieldLabel)`
color: ${props => props.theme.schema.typeTitleColor}; color: ${props => props.theme.schema.typeTitleColor};
word-break: break-word;
`; `;
export const TypeFormat = TypeName; export const TypeFormat = TypeName;

View File

@ -98,7 +98,7 @@ export const SmallTabs = styled(Tabs)`
> .react-tabs__tab-panel { > .react-tabs__tab-panel {
& > div, & > div,
& > pre { & > pre {
padding: ${props => props.theme.spacing.unit * 2} 0; padding: ${props => props.theme.spacing.unit * 2}px 0;
} }
} }
`; `;

View File

@ -44,8 +44,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
null; null;
const website = const website =
(info.contact && (info.contact && info.contact.url && (
info.contact.url && (
<InfoSpan> <InfoSpan>
URL: <a href={info.contact.url}>{info.contact.url}</a> URL: <a href={info.contact.url}>{info.contact.url}</a>
</InfoSpan> </InfoSpan>
@ -53,8 +52,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
null; null;
const email = const email =
(info.contact && (info.contact && info.contact.email && (
info.contact.email && (
<InfoSpan> <InfoSpan>
{info.contact.name || 'E-mail'}:{' '} {info.contact.name || 'E-mail'}:{' '}
<a href={'mailto:' + info.contact.email}>{info.contact.email}</a> <a href={'mailto:' + info.contact.email}>{info.contact.email}</a>
@ -70,11 +68,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
)) || )) ||
null; null;
const version = const version = (info.version && <span>({info.version})</span>) || null;
(info.version && (
<span>({info.version})</span>
)) ||
null;
return ( return (
<Section> <Section>

View File

@ -5,7 +5,7 @@ import { Markdown } from '../Markdown/Markdown';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
import { SelectOnClick } from '../SelectOnClick/SelectOnClick'; import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
import { getBasePath } from '../../utils'; import { expandDefaultServerVariables, getBasePath } from '../../utils';
import { import {
EndpointInfo, EndpointInfo,
HttpVerb, HttpVerb,
@ -60,21 +60,24 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
/> />
</EndpointInfo> </EndpointInfo>
<ServersOverlay expanded={expanded}> <ServersOverlay expanded={expanded}>
{operation.servers.map(server => ( {operation.servers.map(server => {
<ServerItem key={server.url}> const normalizedUrl = expandDefaultServerVariables(server.url, server.variables);
return (
<ServerItem key={normalizedUrl}>
<Markdown source={server.description || ''} compact={true} /> <Markdown source={server.description || ''} compact={true} />
<SelectOnClick> <SelectOnClick>
<ServerUrl> <ServerUrl>
<span> <span>
{hideHostname || options.hideHostname {hideHostname || options.hideHostname
? getBasePath(server.url) ? getBasePath(normalizedUrl)
: server.url} : normalizedUrl}
</span> </span>
{operation.path} {operation.path}
</ServerUrl> </ServerUrl>
</SelectOnClick> </SelectOnClick>
</ServerItem> </ServerItem>
))} );
})}
</ServersOverlay> </ServersOverlay>
</OperationEndpointWrap> </OperationEndpointWrap>
)} )}

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import { ExampleValue, FieldLabel } from '../../common-elements/fields'; import { ExampleValue, FieldLabel } from '../../common-elements/fields';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export interface EnumValuesProps { export interface EnumValuesProps {
values: string[]; values: string[];
@ -9,8 +10,10 @@ export interface EnumValuesProps {
} }
export class EnumValues extends React.PureComponent<EnumValuesProps> { export class EnumValues extends React.PureComponent<EnumValuesProps> {
static contextType = OptionsContext;
render() { render() {
const { values, type } = this.props; const { values, type } = this.props;
const { enumSkipQuotes } = this.context;
if (!values.length) { if (!values.length) {
return null; return null;
} }
@ -21,11 +24,14 @@ export class EnumValues extends React.PureComponent<EnumValuesProps> {
{type === 'array' ? l('enumArray') : ''}{' '} {type === 'array' ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}: {values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel> </FieldLabel>
{values.map((value, idx) => ( {values.map((value, idx) => {
const exampleValue = enumSkipQuotes ? value : JSON.stringify(value);
return (
<React.Fragment key={idx}> <React.Fragment key={idx}>
<ExampleValue>{JSON.stringify(value)}</ExampleValue>{' '} <ExampleValue>{exampleValue}</ExampleValue>
</React.Fragment> </React.Fragment>
))} );
})}
</div> </div>
); );
} }

View File

@ -65,8 +65,7 @@ export class Field extends React.Component<FieldProps> {
<FieldDetails {...this.props} /> <FieldDetails {...this.props} />
</PropertyDetailsCell> </PropertyDetailsCell>
</tr> </tr>
{field.expanded && {field.expanded && withSubSchema && (
withSubSchema && (
<tr key={field.name + 'inner'}> <tr key={field.name + 'inner'}>
<PropertyCellWithInner colSpan={2}> <PropertyCellWithInner colSpan={2}>
<InnerPropertiesWrap> <InnerPropertiesWrap>

View File

@ -4,6 +4,7 @@ import { ExampleValue, FieldLabel } from '../../common-elements/fields';
export interface FieldDetailProps { export interface FieldDetailProps {
value?: any; value?: any;
label: string; label: string;
raw?: boolean;
} }
export class FieldDetail extends React.PureComponent<FieldDetailProps> { export class FieldDetail extends React.PureComponent<FieldDetailProps> {
@ -11,12 +12,12 @@ export class FieldDetail extends React.PureComponent<FieldDetailProps> {
if (this.props.value === undefined) { if (this.props.value === undefined) {
return null; return null;
} }
const value = this.props.raw ? this.props.value : JSON.stringify(this.props.value);
return ( return (
<div> <div>
<FieldLabel> {this.props.label} </FieldLabel>{' '} <FieldLabel> {this.props.label} </FieldLabel> <ExampleValue>{value}</ExampleValue>
<ExampleValue>
{JSON.stringify(this.props.value)}
</ExampleValue>
</div> </div>
); );
} }

View File

@ -9,6 +9,7 @@ import {
TypePrefix, TypePrefix,
TypeTitle, TypeTitle,
} from '../../common-elements/fields'; } from '../../common-elements/fields';
import { serializeParameterValue } from '../../utils/openapi';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Markdown } from '../Markdown/Markdown'; import { Markdown } from '../Markdown/Markdown';
import { EnumValues } from './EnumValues'; import { EnumValues } from './EnumValues';
@ -20,13 +21,29 @@ import { FieldDetail } from './FieldDetail';
import { Badge } from '../../common-elements/'; import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export class FieldDetails extends React.PureComponent<FieldProps> { export class FieldDetails extends React.PureComponent<FieldProps> {
static contextType = OptionsContext;
render() { render() {
const { showExamples, field, renderDiscriminatorSwitch } = this.props; const { showExamples, field, renderDiscriminatorSwitch } = this.props;
const { enumSkipQuotes } = this.context;
const { schema, description, example, deprecated } = field; const { schema, description, example, deprecated } = field;
let exampleField: JSX.Element | null = null;
if (showExamples) {
const label = l('example') + ':';
if (field.in && field.style) {
const serializedValue =
example !== undefined ? serializeParameterValue(field, example) : undefined;
exampleField = <FieldDetail label={label} value={serializedValue} raw={true} />;
} else {
exampleField = <FieldDetail label={label} value={example} />;
}
}
return ( return (
<div> <div>
<div> <div>
@ -51,9 +68,9 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
<Badge type="warning"> {l('deprecated')} </Badge> <Badge type="warning"> {l('deprecated')} </Badge>
</div> </div>
)} )}
<FieldDetail label={l('default') + ':'} value={schema.default} /> <FieldDetail raw={enumSkipQuotes} label={l('default') + ':'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '} {!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
{showExamples && <FieldDetail label={l('example') + ':'} value={example} />} {exampleField}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />} {<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}
<div> <div>
<Markdown compact={true} source={description} /> <Markdown compact={true} source={description} />

View File

@ -3,6 +3,7 @@ import * as React from 'react';
import { DropdownProps } from '../../common-elements/dropdown'; import { DropdownProps } from '../../common-elements/dropdown';
import { MediaContentModel, MediaTypeModel, SchemaModel } from '../../services/models'; import { MediaContentModel, MediaTypeModel, SchemaModel } from '../../services/models';
import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
export interface MediaTypeChildProps { export interface MediaTypeChildProps {
schema: SchemaModel; schema: SchemaModel;
@ -11,6 +12,8 @@ export interface MediaTypeChildProps {
export interface MediaTypesSwitchProps { export interface MediaTypesSwitchProps {
content?: MediaContentModel; content?: MediaContentModel;
withLabel?: boolean;
renderDropdown: (props: DropdownProps) => JSX.Element; renderDropdown: (props: DropdownProps) => JSX.Element;
children: (activeMime: MediaTypeModel) => JSX.Element; children: (activeMime: MediaTypeModel) => JSX.Element;
} }
@ -37,13 +40,25 @@ export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> {
}; };
}); });
const Wrapper = ({ children }) =>
this.props.withLabel ? (
<DropdownWrapper>
<DropdownLabel>Content type</DropdownLabel>
{children}
</DropdownWrapper>
) : (
children
);
return ( return (
<> <>
<Wrapper>
{this.props.renderDropdown({ {this.props.renderDropdown({
value: options[activeMimeIdx], value: options[activeMimeIdx],
options, options,
onChange: this.switchMedia, onChange: this.switchMedia,
})} })}
</Wrapper>
{this.props.children(content.active)} {this.props.children(content.active)}
</> </>
); );

View File

@ -1,17 +1,31 @@
import * as React from 'react'; import * as React from 'react';
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements'; import { DropdownProps } from '../../common-elements';
import { MediaTypeModel } from '../../services/models'; import { MediaTypeModel } from '../../services/models';
import { Markdown } from '../Markdown/Markdown';
import { Example } from './Example'; import { Example } from './Example';
import { NoSampleLabel } from './styled.elements'; import { DropdownLabel, DropdownWrapper, NoSampleLabel } from './styled.elements';
export interface PayloadSamplesProps { export interface PayloadSamplesProps {
mediaType: MediaTypeModel; mediaType: MediaTypeModel;
renderDropdown: (props: DropdownProps) => JSX.Element;
} }
export class MediaTypeSamples extends React.Component<PayloadSamplesProps> { interface MediaTypeSamplesState {
activeIdx: number;
}
export class MediaTypeSamples extends React.Component<PayloadSamplesProps, MediaTypeSamplesState> {
state = {
activeIdx: 0,
};
switchMedia = ({ value }) => {
this.setState({
activeIdx: parseInt(value, 10),
});
};
render() { render() {
const { activeIdx } = this.state;
const examples = this.props.mediaType.examples || {}; const examples = this.props.mediaType.examples || {};
const mimeType = this.props.mediaType.name; const mimeType = this.props.mediaType.name;
@ -21,26 +35,40 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
if (examplesNames.length === 0) { if (examplesNames.length === 0) {
return noSample; return noSample;
} }
if (examplesNames.length > 1) { if (examplesNames.length > 1) {
const options = examplesNames.map((name, idx) => {
return {
label: examples[name].summary || name,
value: idx.toString(),
};
});
const example = examples[examplesNames[activeIdx]];
const description = example.description;
return ( return (
<SmallTabs defaultIndex={0}> <>
<TabList> <DropdownWrapper>
{examplesNames.map(name => ( <DropdownLabel>Example</DropdownLabel>
<Tab key={name}> {examples[name].summary || name} </Tab> {this.props.renderDropdown({
))} value: options[activeIdx],
</TabList> options,
{examplesNames.map(name => ( onChange: this.switchMedia,
<TabPanel key={name}> })}
<Example example={examples[name]} mimeType={mimeType} /> </DropdownWrapper>
</TabPanel> <div>
))} {description && <Markdown source={description} />}
</SmallTabs> <Example example={example} mimeType={mimeType} />
</div>
</>
); );
} else { } else {
const name = examplesNames[0]; const example = examples[examplesNames[0]];
return ( return (
<div> <div>
<Example example={examples[name]} mimeType={mimeType} /> {example.description && <Markdown source={example.description} />}
<Example example={example} mimeType={mimeType} />
</div> </div>
); );
} }

View File

@ -4,6 +4,7 @@ import { MediaTypeSamples } from './MediaTypeSamples';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch'; import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import styled from '../../../src/styled-components';
import { MediaContentModel } from '../../services/models'; import { MediaContentModel } from '../../services/models';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel'; import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { InvertedSimpleDropdown, MimeLabel } from './styled.elements'; import { InvertedSimpleDropdown, MimeLabel } from './styled.elements';
@ -21,8 +22,16 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
} }
return ( return (
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown}> <MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown} withLabel={true}>
{mediaType => <MediaTypeSamples key="samples" mediaType={mediaType} />} {mediaType => (
<SamplesWrapper>
<MediaTypeSamples
key="samples"
mediaType={mediaType}
renderDropdown={this.renderDropdown}
/>
</SamplesWrapper>
)}
</MediaTypesSwitch> </MediaTypesSwitch>
); );
} }
@ -31,3 +40,7 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />; return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
}; };
} }
const SamplesWrapper = styled.div`
margin-top: 15px;
`;

View File

@ -13,8 +13,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
prevRef.current = example; prevRef.current = example;
useEffect( useEffect(() => {
() => {
const load = async () => { const load = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
@ -26,9 +25,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
}; };
load(); load();
}, }, [example, mimeType]);
[example, mimeType],
);
return value.current; return value.current;
} }

View File

@ -1,29 +1,48 @@
// @ts-ignore // @ts-ignore
import Dropdown from 'react-dropdown'; import Dropdown from 'react-dropdown';
import { transparentize } from 'polished';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements'; import { StyledDropdown } from '../../common-elements';
export const MimeLabel = styled.div` export const MimeLabel = styled.div`
border-bottom: 1px solid rgba(255, 255, 255, 0.9); padding: 12px;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
margin: 0 0 10px 0; margin: 0 0 10px 0;
display: block; display: block;
color: rgba(255, 255, 255, 0.8); `;
export const DropdownLabel = styled.span`
font-family: ${({ theme }) => theme.typography.headings.fontFamily};
font-size: 12px;
position: absolute;
z-index: 1;
top: -11px;
left: 12px;
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
color: ${({ theme }) => transparentize(0.6, theme.rightPanel.textColor)};
`;
export const DropdownWrapper = styled.div`
position: relative;
`; `;
export const InvertedSimpleDropdown = styled(StyledDropdown)` export const InvertedSimpleDropdown = styled(StyledDropdown)`
margin-left: 10px; margin-left: 10px;
text-transform: none; text-transform: none;
font-size: 0.929em; font-size: 0.929em;
border-bottom: 1px solid ${({ theme }) => theme.rightPanel.textColor};
margin: 0 0 10px 0; margin: 0 0 10px 0;
display: block; display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
.Dropdown-control {
margin-top: 0;
}
.Dropdown-control, .Dropdown-control,
.Dropdown-control:hover { .Dropdown-control:hover {
font-size: 1em; font-size: 1em;
border: none; border: none;
padding: 0 1.2em 0 0; padding: 0.9em 1.6em 0.9em 0.9em;
background: transparent; background: transparent;
color: ${({ theme }) => theme.rightPanel.textColor}; color: ${({ theme }) => theme.rightPanel.textColor};
box-shadow: none; box-shadow: none;
@ -34,6 +53,7 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
} }
.Dropdown-menu { .Dropdown-menu {
margin: 0; margin: 0;
margin-top: 10px;
} }
`; `;

View File

@ -28,8 +28,7 @@ export class ResponseView extends React.Component<{ response: ResponseModel }> {
code={code} code={code}
opened={expanded} opened={expanded}
/> />
{expanded && {expanded && !empty && (
!empty && (
<ResponseDetailsWrap> <ResponseDetailsWrap>
<ResponseDetails response={this.props.response} /> <ResponseDetails response={this.props.response} />
</ResponseDetailsWrap> </ResponseDetailsWrap>

View File

@ -57,16 +57,13 @@ export class MenuItem extends React.Component<MenuItemProps> {
{item.name} {item.name}
{this.props.children} {this.props.children}
</MenuItemTitle> </MenuItemTitle>
{(item.depth > 0 && {(item.depth > 0 && item.items.length > 0 && (
item.items.length > 0 && (
<ShelfIcon float={'right'} direction={item.expanded ? 'down' : 'right'} /> <ShelfIcon float={'right'} direction={item.expanded ? 'down' : 'right'} />
)) || )) ||
null} null}
</MenuItemLabel> </MenuItemLabel>
)} )}
{!withoutChildren && {!withoutChildren && item.items && item.items.length > 0 && (
item.items &&
item.items.length > 0 && (
<MenuItems <MenuItems
expanded={item.expanded} expanded={item.expanded}
items={item.items} items={item.items}
@ -83,7 +80,7 @@ export interface OperationMenuItemContentProps {
} }
@observer @observer
class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> { export class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
render() { render() {
const { item } = this.props; const { item } = this.props;
return ( return (

View File

@ -10,6 +10,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"description": "", "description": "",
"example": undefined, "example": undefined,
"expanded": false, "expanded": false,
"explode": false,
"in": undefined, "in": undefined,
"kind": "field", "kind": "field",
"name": "packSize", "name": "packSize",
@ -59,6 +60,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"description": "", "description": "",
"example": undefined, "example": undefined,
"expanded": false, "expanded": false,
"explode": false,
"in": undefined, "in": undefined,
"kind": "field", "kind": "field",
"name": "type", "name": "type",

View File

@ -3,6 +3,7 @@ import 'core-js/fn/array/find';
import 'core-js/fn/object/assign'; import 'core-js/fn/object/assign';
import 'core-js/fn/string/ends-with'; import 'core-js/fn/string/ends-with';
import 'core-js/fn/string/starts-with'; import 'core-js/fn/string/starts-with';
import 'core-js/fn/object/assign';
import 'core-js/es6/map'; import 'core-js/es6/map';
import 'core-js/es6/symbol'; import 'core-js/es6/symbol';

View File

@ -1,6 +1,6 @@
import * as marked from 'marked'; import * as marked from 'marked';
import { highlight, safeSlugify } from '../utils'; import { highlight, safeSlugify, unescapeHTMLChars } from '../utils';
import { AppStore } from './AppStore'; import { AppStore } from './AppStore';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions } from './RedocNormalizedOptions';
@ -65,6 +65,7 @@ export class MarkdownRenderer {
container: MarkdownHeading[] = this.headings, container: MarkdownHeading[] = this.headings,
parentId?: string, parentId?: string,
): MarkdownHeading { ): MarkdownHeading {
name = unescapeHTMLChars(name);
const item = { const item = {
id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`, id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`,
name, name,
@ -88,7 +89,7 @@ export class MarkdownRenderer {
} }
attachHeadingsDescriptions(rawText: string) { attachHeadingsDescriptions(rawText: string) {
const buildRegexp = heading => { const buildRegexp = (heading: MarkdownHeading) => {
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`); return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`);
}; };

View File

@ -29,6 +29,7 @@ export interface RedocRawOptions {
allowedMdComponents?: Dict<MDXComponentMeta>; allowedMdComponents?: Dict<MDXComponentMeta>;
labels?: LabelsConfigRaw; labels?: LabelsConfigRaw;
enumSkipQuotes?: boolean | string;
} }
function argValueToBoolean(val?: string | boolean): boolean { function argValueToBoolean(val?: string | boolean): boolean {
@ -137,6 +138,7 @@ export class RedocNormalizedOptions {
showExtensions: boolean | string[]; showExtensions: boolean | string[];
hideSingleRequestSampleTab: boolean; hideSingleRequestSampleTab: boolean;
jsonSampleExpandLevel: number; jsonSampleExpandLevel: number;
enumSkipQuotes: boolean;
/* tslint:disable-next-line */ /* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean; unstable_ignoreMimeParameters: boolean;
@ -171,6 +173,7 @@ export class RedocNormalizedOptions {
this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel( this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel(
raw.jsonSampleExpandLevel, raw.jsonSampleExpandLevel,
); );
this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);

View File

@ -10,6 +10,13 @@
"in": "path", "in": "path",
"name": "test_name", "name": "test_name",
"schema": { "type": "string" } "schema": { "type": "string" }
},
"serializationParam": {
"in": "query",
"name": "serialization_test_name",
"schema": { "type": "array" },
"style": "form",
"explode": true
} }
}, },
"headers": { "headers": {

View File

@ -26,6 +26,23 @@ describe('Models', () => {
expect(field.schema.type).toEqual('string'); expect(field.schema.type).toEqual('string');
}); });
test('field details relevant for parameter serialization', () => {
const field = new FieldModel(
parser,
{
$ref: '#/components/parameters/serializationParam',
},
'#/components/parameters/serializationParam',
opts,
);
expect(field.name).toEqual('serialization_test_name');
expect(field.in).toEqual('query');
expect(field.schema.type).toEqual('array');
expect(field.style).toEqual('form');
expect(field.explode).toEqual(true);
});
test('field name should populated from name even if $ref (headers)', () => { test('field name should populated from name even if $ref (headers)', () => {
const field = new FieldModel( const field = new FieldModel(
parser, parser,

View File

@ -1,12 +1,30 @@
import { action, observable } from 'mobx'; import { action, observable } from 'mobx';
import { OpenAPIParameter, Referenced } from '../../types'; import {
OpenAPIParameter,
OpenAPIParameterLocation,
OpenAPIParameterStyle,
Referenced,
} from '../../types';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { extractExtensions } from '../../utils/openapi'; import { extractExtensions } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
import { SchemaModel } from './Schema'; import { SchemaModel } from './Schema';
function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): OpenAPIParameterStyle {
switch (parameterLocation) {
case 'header':
return 'simple';
case 'query':
return 'form';
case 'path':
return 'simple';
default:
return 'form';
}
}
/** /**
* Field or Parameter model ready to be used by components * Field or Parameter model ready to be used by components
*/ */
@ -20,9 +38,11 @@ export class FieldModel {
description: string; description: string;
example?: string; example?: string;
deprecated: boolean; deprecated: boolean;
in?: string; in?: OpenAPIParameterLocation;
kind: string; kind: string;
extensions?: Dict<any>; extensions?: Dict<any>;
explode: boolean;
style?: OpenAPIParameterStyle;
constructor( constructor(
parser: OpenAPIParser, parser: OpenAPIParser,
@ -40,6 +60,14 @@ export class FieldModel {
info.description === undefined ? this.schema.description || '' : info.description; info.description === undefined ? this.schema.description || '' : info.description;
this.example = info.example || this.schema.example; this.example = info.example || this.schema.example;
if (info.style) {
this.style = info.style;
} else if (this.in) {
this.style = getDefaultStyleValue(this.in);
}
this.explode = !!info.explode;
this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated; this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated;
parser.exitRef(infoOrRef); parser.exitRef(infoOrRef);

View File

@ -292,7 +292,10 @@ function buildFields(
new FieldModel( new FieldModel(
parser, parser,
{ {
name: 'property name *', name: (typeof additionalProps === 'object'
? additionalProps['x-additionalPropertiesName'] || 'property name'
: 'property name'
).concat('*'),
required: false, required: false,
schema: additionalProps === true ? {} : additionalProps, schema: additionalProps === true ? {} : additionalProps,
kind: 'additionalProperties', kind: 'additionalProperties',

View File

@ -8,10 +8,11 @@ import {
mergeParams, mergeParams,
normalizeServers, normalizeServers,
pluralizeType, pluralizeType,
serializeParameterValue,
} from '../'; } from '../';
import { OpenAPIParser } from '../../services'; import { OpenAPIParser } from '../../services';
import { OpenAPIParameter } from '../../types'; import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';
describe('Utils', () => { describe('Utils', () => {
describe('openapi getStatusCode', () => { describe('openapi getStatusCode', () => {
@ -377,4 +378,190 @@ describe('Utils', () => {
); );
}); });
}); });
describe('openapi serializeParameter', () => {
interface TestCase {
style: OpenAPIParameterStyle;
explode: boolean;
expected: string;
}
interface TestValueTypeGroup {
value: any;
description: string;
cases: TestCase[];
}
interface TestLocationGroup {
location: OpenAPIParameterLocation;
name: string;
description: string;
cases: TestValueTypeGroup[];
}
const testCases: TestLocationGroup[] = [
{
location: 'path',
name: 'id',
description: 'path parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'simple', explode: false, expected: '5' },
{ style: 'simple', explode: true, expected: '5' },
{ style: 'label', explode: false, expected: '.5' },
{ style: 'label', explode: true, expected: '.5' },
{ style: 'matrix', explode: false, expected: ';id=5' },
{ style: 'matrix', explode: true, expected: ';id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'simple', explode: false, expected: '3,4,5' },
{ style: 'simple', explode: true, expected: '3,4,5' },
{ style: 'label', explode: false, expected: '.3,4,5' },
{ style: 'label', explode: true, expected: '.3.4.5' },
{ style: 'matrix', explode: false, expected: ';id=3,4,5' },
{ style: 'matrix', explode: true, expected: ';id=3;id=4;id=5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
{ style: 'label', explode: false, expected: '.role,admin,firstName,Alex' },
{ style: 'label', explode: true, expected: '.role=admin,firstName=Alex' },
{ style: 'matrix', explode: false, expected: ';id=role,admin,firstName,Alex' },
{ style: 'matrix', explode: true, expected: ';role=admin;firstName=Alex' },
],
},
],
},
{
location: 'query',
name: 'id',
description: 'query parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'form', explode: true, expected: 'id=5' },
{ style: 'form', explode: false, expected: 'id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'form', explode: false, expected: 'id=3,4,5' },
{ style: 'spaceDelimited', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'spaceDelimited', explode: false, expected: 'id=3%204%205' },
{ style: 'pipeDelimited', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'pipeDelimited', explode: false, expected: 'id=3|4|5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
{ style: 'deepObject', explode: true, expected: 'id[role]=admin&id[firstName]=Alex' },
],
},
],
},
{
location: 'cookie',
name: 'id',
description: 'cookie parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'form', explode: true, expected: 'id=5' },
{ style: 'form', explode: false, expected: 'id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'form', explode: false, expected: 'id=3,4,5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
],
},
],
},
{
location: 'header',
name: 'id',
description: 'header parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'simple', explode: false, expected: '5' },
{ style: 'simple', explode: true, expected: '5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'simple', explode: false, expected: '3,4,5' },
{ style: 'simple', explode: true, expected: '3,4,5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
],
},
],
},
];
testCases.forEach(locationTestGroup => {
describe(locationTestGroup.description, () => {
locationTestGroup.cases.forEach(valueTypeTestGroup => {
describe(valueTypeTestGroup.description, () => {
valueTypeTestGroup.cases.forEach(testCase => {
it(`should serialize correctly when style is ${testCase.style} and explode is ${
testCase.explode
}`, () => {
const parameter: OpenAPIParameter = {
name: locationTestGroup.name,
in: locationTestGroup.location,
style: testCase.style,
explode: testCase.explode,
};
const serialized = serializeParameterValue(parameter, valueTypeTestGroup.value);
expect(serialized).toEqual(testCase.expected);
});
});
});
});
});
});
});
}); });

View File

@ -194,3 +194,7 @@ function parseURL(url: string) {
return new URL(url); return new URL(url);
} }
} }
export function unescapeHTMLChars(str: string): string {
return str.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10)));
}

View File

@ -1,4 +1,5 @@
import { dirname } from 'path'; import { dirname } from 'path';
import { URI } from 'uri-template-lite';
import { OpenAPIParser } from '../services/OpenAPIParser'; import { OpenAPIParser } from '../services/OpenAPIParser';
import { import {
@ -6,6 +7,7 @@ import {
OpenAPIMediaType, OpenAPIMediaType,
OpenAPIOperation, OpenAPIOperation,
OpenAPIParameter, OpenAPIParameter,
OpenAPIParameterStyle,
OpenAPISchema, OpenAPISchema,
OpenAPIServer, OpenAPIServer,
Referenced, Referenced,
@ -135,36 +137,6 @@ export function isFormUrlEncoded(contentType: string): boolean {
return contentType === 'application/x-www-form-urlencoded'; return contentType === 'application/x-www-form-urlencoded';
} }
function formEncodeField(fieldVal: any, fieldName: string, explode: boolean): string {
if (!fieldVal || !fieldVal.length) {
return fieldName + '=';
}
if (Array.isArray(fieldVal)) {
if (explode) {
return fieldVal.map(val => `${fieldName}=${val}`).join('&');
} else {
return fieldName + '=' + fieldVal.map(val => val.toString()).join(',');
}
} else if (typeof fieldVal === 'object') {
if (explode) {
return Object.keys(fieldVal)
.map(k => `${k}=${fieldVal[k]}`)
.join('&');
} else {
return (
fieldName +
'=' +
Object.keys(fieldVal)
.map(k => `${k},${fieldVal[k]}`)
.join(',')
);
}
} else {
return fieldName + '=' + fieldVal.toString();
}
}
function delimitedEncodeField(fieldVal: any, fieldName: string, delimeter: string): string { function delimitedEncodeField(fieldVal: any, fieldName: string, delimeter: string): string {
if (Array.isArray(fieldVal)) { if (Array.isArray(fieldVal)) {
return fieldVal.map(v => v.toString()).join(delimeter); return fieldVal.map(v => v.toString()).join(delimeter);
@ -191,6 +163,13 @@ function deepObjectEncodeField(fieldVal: any, fieldName: string): string {
} }
} }
function serializeFormValue(name: string, explode: boolean, value: any) {
const suffix = explode ? '*' : '';
const template = new URI.Template(`{?${name}${suffix}}`);
return template.expand({ [name]: value }).substring(1);
}
/* /*
* Should be used only for url-form-encoded body payloads * Should be used only for url-form-encoded body payloads
* To be used for parmaters should be extended with other style values * To be used for parmaters should be extended with other style values
@ -208,7 +187,7 @@ export function urlFormEncodePayload(
const { style = 'form', explode = true } = encoding[fieldName] || {}; const { style = 'form', explode = true } = encoding[fieldName] || {};
switch (style) { switch (style) {
case 'form': case 'form':
return formEncodeField(fieldVal, fieldName, explode); return serializeFormValue(fieldName, explode, fieldVal);
break; break;
case 'spaceDelimited': case 'spaceDelimited':
return delimitedEncodeField(fieldVal, fieldName, '%20'); return delimitedEncodeField(fieldVal, fieldName, '%20');
@ -226,6 +205,124 @@ export function urlFormEncodePayload(
} }
} }
function serializePathParameter(
name: string,
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
const suffix = explode ? '*' : '';
let prefix = '';
if (style === 'label') {
prefix = '.';
} else if (style === 'matrix') {
prefix = ';';
}
const template = new URI.Template(`{${prefix}${name}${suffix}}`);
return template.expand({ [name]: value });
}
function serializeQueryParameter(
name: string,
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
switch (style) {
case 'form':
return serializeFormValue(name, explode, value);
case 'spaceDelimited':
if (!Array.isArray(value)) {
console.warn('The style spaceDelimited is only applicable to arrays');
return '';
}
if (explode) {
return serializeFormValue(name, explode, value);
}
return `${name}=${value.join('%20')}`;
case 'pipeDelimited':
if (!Array.isArray(value)) {
console.warn('The style pipeDelimited is only applicable to arrays');
return '';
}
if (explode) {
return serializeFormValue(name, explode, value);
}
return `${name}=${value.join('|')}`;
case 'deepObject':
if (!explode || Array.isArray(value) || typeof value !== 'object') {
console.warn('The style deepObject is only applicable for objects with expolde=true');
return '';
}
return deepObjectEncodeField(value, name);
default:
console.warn('Unexpected style for query: ' + style);
return '';
}
}
function serializeHeaderParameter(
name: string,
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
switch (style) {
case 'simple':
const suffix = explode ? '*' : '';
const template = new URI.Template(`{${name}${suffix}}`);
return template.expand({ [name]: value });
default:
console.warn('Unexpected style for header: ' + style);
return '';
}
}
function serializeCookieParameter(
name: string,
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
switch (style) {
case 'form':
return serializeFormValue(name, explode, value);
default:
console.warn('Unexpected style for cookie: ' + style);
return '';
}
}
export function serializeParameterValue(parameter: OpenAPIParameter, value: any): string {
const { name, style, explode = false } = parameter;
if (!style) {
console.warn(`Missing style attribute for parameter ${name}`);
return '';
}
switch (parameter.in) {
case 'path':
return serializePathParameter(name, style, explode, value);
case 'query':
return serializeQueryParameter(name, style, explode, value);
case 'header':
return serializeHeaderParameter(name, style, explode, value);
case 'cookie':
return serializeCookieParameter(name, style, explode, value);
default:
console.warn('Unexpected parameter location: ' + parameter.in);
return '';
}
}
export function langFromMime(contentType: string): string { export function langFromMime(contentType: string): string {
if (contentType.search(/xml/i) !== -1) { if (contentType.search(/xml/i) !== -1) {
return 'xml'; return 'xml';
@ -356,7 +453,7 @@ export function mergeSimilarMediaTypes(types: Dict<OpenAPIMediaType>): Dict<Open
return mergedTypes; return mergedTypes;
} }
function expandVariables(url: string, variables: object = {}) { export function expandDefaultServerVariables(url: string, variables: object = {}) {
return url.replace( return url.replace(
/(?:{)(\w+)(?:})/g, /(?:{)(\w+)(?:})/g,
(match, name) => (variables[name] && variables[name].default) || match, (match, name) => (variables[name] && variables[name].default) || match,
@ -385,15 +482,14 @@ export function normalizeServers(
]; ];
} }
function normalizeUrl(url: string, variables: object | undefined): string { function normalizeUrl(url: string): string {
url = expandVariables(url, variables);
return resolveUrl(baseUrl, url); return resolveUrl(baseUrl, url);
} }
return servers.map(server => { return servers.map(server => {
return { return {
...server, ...server,
url: normalizeUrl(server.url, server.variables), url: normalizeUrl(server.url),
description: server.description || '', description: server.description || '',
}; };
}); });
@ -423,6 +519,7 @@ export function isRedocExtension(key: string): boolean {
'x-servers': true, 'x-servers': true,
'x-tagGroups': true, 'x-tagGroups': true,
'x-traitTag': true, 'x-traitTag': true,
'x-additionalPropertiesName': true,
}; };
return key in redocExtensions; return key in redocExtensions;

View File

@ -9651,6 +9651,11 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
uri-template-lite@^19.4.0:
version "19.4.0"
resolved "https://registry.yarnpkg.com/uri-template-lite/-/uri-template-lite-19.4.0.tgz#cbc2c072cf4931428a2f9d3aea36b8254a33cce5"
integrity sha512-VY8dgwyMwnCztkzhq0cA/YhNmO+YZqow//5FdmgE2fZU/JPi+U0rPL7MRDi0F+Ch4vJ7nYidWzeWAeY7uywe9g==
urix@^0.1.0: urix@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"