-
{isCallback ? 'Callback responses' : 'Responses'}
+
{isCallback ? l('callbackResponses') : l('responses')}
{responses.map(response => {
return
;
})}
diff --git a/src/components/Schema/ArraySchema.tsx b/src/components/Schema/ArraySchema.tsx
index 48e7dc41..eab37595 100644
--- a/src/components/Schema/ArraySchema.tsx
+++ b/src/components/Schema/ArraySchema.tsx
@@ -4,7 +4,8 @@ import { Schema, SchemaProps } from './Schema';
import { ArrayClosingLabel, ArrayOpenningLabel } from '../../common-elements';
import styled from '../../styled-components';
-import {humanizeConstraints} from "../../utils";
+import { humanizeConstraints } from '../../utils';
+import { TypeName } from '../../common-elements/fields';
const PaddedSchema = styled.div`
padding-left: ${({ theme }) => theme.spacing.unit * 2}px;
@@ -13,12 +14,20 @@ const PaddedSchema = styled.div`
export class ArraySchema extends React.PureComponent
{
render() {
const itemsSchema = this.props.schema.items!;
+ const schema = this.props.schema;
+
const itemConstraintSchema = (
min: number | undefined = undefined,
max: number | undefined = undefined,
) => ({ type: 'array', minItems: min, maxItems: max });
- const minMaxItems = humanizeConstraints(itemConstraintSchema(itemsSchema.schema.minItems, itemsSchema.schema.maxItems));
+ const minMaxItems = humanizeConstraints(itemConstraintSchema(itemsSchema?.schema?.minItems, itemsSchema?.schema?.maxItems));
+
+ if (schema.displayType && !itemsSchema && !minMaxItems.length) {
+ return (
+ {schema.displayType}
+
);
+ }
return (
diff --git a/src/components/Schema/Schema.tsx b/src/components/Schema/Schema.tsx
index ef774004..392a0f23 100644
--- a/src/components/Schema/Schema.tsx
+++ b/src/components/Schema/Schema.tsx
@@ -63,14 +63,13 @@ export class Schema extends React.Component
> {
return ;
}
- switch (type) {
- case 'object':
- if (schema.fields?.length) {
- return ;
- }
- break;
- case 'array':
- return ;
+ const types = Array.isArray(type) ? type : [type];
+ if (types.includes('object')) {
+ if (schema.fields?.length) {
+ return ;
+ }
+ } else if (types.includes('array')) {
+ return ;
}
// TODO: maybe adjust FieldDetails to accept schema
diff --git a/src/components/SecurityRequirement/SecurityRequirement.tsx b/src/components/SecurityRequirement/SecurityRequirement.tsx
index 3a52ad81..91bf9f43 100644
--- a/src/components/SecurityRequirement/SecurityRequirement.tsx
+++ b/src/components/SecurityRequirement/SecurityRequirement.tsx
@@ -8,8 +8,8 @@ import { SecurityRequirementModel } from '../../services/models/SecurityRequirem
import { linksCss } from '../Markdown/styled.elements';
const ScopeName = styled.code`
- font-size: ${props => props.theme.typography.code.fontSize};
- font-family: ${props => props.theme.typography.code.fontFamily};
+ font-size: ${(props) => props.theme.typography.code.fontSize};
+ font-family: ${(props) => props.theme.typography.code.fontFamily};
border: 1px solid ${({ theme }) => theme.colors.border.dark};
margin: 0 3px;
padding: 0.2em;
@@ -67,18 +67,22 @@ export class SecurityRequirement extends React.PureComponent
- {security.schemes.map(scheme => {
- return (
-
- {scheme.id}
- {scheme.scopes.length > 0 && ' ('}
- {scheme.scopes.map(scope => (
- {scope}
- ))}
- {scheme.scopes.length > 0 && ') '}
-
- );
- })}
+ {security.schemes.length ? (
+ security.schemes.map((scheme) => {
+ return (
+
+ {scheme.id}
+ {scheme.scopes.length > 0 && ' ('}
+ {scheme.scopes.map((scope) => (
+ {scope}
+ ))}
+ {scheme.scopes.length > 0 && ') '}
+
+ );
+ })
+ ) : (
+ None
+ )}
);
}
@@ -89,7 +93,7 @@ const AuthHeaderColumn = styled.div`
`;
const SecuritiesColumn = styled.div`
- width: ${props => props.theme.schema.defaultDetailsWidth};
+ width: ${(props) => props.theme.schema.defaultDetailsWidth};
${media.lessThan('small')`
margin-top: 10px;
`}
diff --git a/src/components/StoreBuilder.ts b/src/components/StoreBuilder.ts
index d675ca5e..527d3700 100644
--- a/src/components/StoreBuilder.ts
+++ b/src/components/StoreBuilder.ts
@@ -1,5 +1,5 @@
-import * as memoize from 'memoize-one/dist/memoize-one.cjs'; // fixme: https://github.com/alexreardon/memoize-one/issues/37
-import { Component, createContext } from 'react';
+import * as React from 'react';
+import { createContext } from 'react';
import { AppStore } from '../services/';
import { RedocRawOptions } from '../services/RedocNormalizedOptions';
@@ -14,7 +14,7 @@ export interface StoreBuilderProps {
onLoaded?: (e?: Error) => void;
- children: (props: { loading: boolean; store?: AppStore }) => any;
+ children: (props: { loading: boolean; store: AppStore | null }) => any;
}
export interface StoreBuilderState {
@@ -25,79 +25,47 @@ export interface StoreBuilderState {
prevSpecUrl?: string;
}
-const { Provider, Consumer } = createContext(undefined);
-export { Provider as StoreProvider, Consumer as StoreConsumer };
+const StoreContext = createContext(undefined);
+const { Provider, Consumer } = StoreContext;
+export { Provider as StoreProvider, Consumer as StoreConsumer, StoreContext };
-export class StoreBuilder extends Component {
- static getDerivedStateFromProps(nextProps: StoreBuilderProps, prevState: StoreBuilderState) {
- if (nextProps.specUrl !== prevState.prevSpecUrl || nextProps.spec !== prevState.prevSpec) {
- return {
- loading: true,
- resolvedSpec: null,
- prevSpec: nextProps.spec,
- prevSpecUrl: nextProps.specUrl,
- };
+export function StoreBuilder(props: StoreBuilderProps) {
+ const {spec, specUrl, options, onLoaded, children } = props;
+
+ const [resolvedSpec, setResolvedSpec] = React.useState(null);
+
+ React.useEffect(() => {
+ async function load() {
+ if (!spec && !specUrl) {
+ return undefined;
+ }
+ setResolvedSpec(null);
+ const resolved = await loadAndBundleSpec(spec || specUrl!);
+ setResolvedSpec(resolved);
}
+ load();
+ }, [spec, specUrl])
- return null;
- }
-
- state: StoreBuilderState = {
- loading: true,
- resolvedSpec: null,
- };
-
- @memoize
- makeStore(spec, specUrl, options) {
- if (!spec) {
- return undefined;
- }
+ const store = React.useMemo(() => {
+ if (!resolvedSpec) return null;
try {
- return new AppStore(spec, specUrl, options);
+ return new AppStore(resolvedSpec, specUrl, options);
} catch (e) {
- if (this.props.onLoaded) {
- this.props.onLoaded(e);
+ if (onLoaded) {
+ onLoaded(e);
}
throw e;
}
- }
+ }, [resolvedSpec, specUrl, options]);
- componentDidMount() {
- this.load();
- }
-
- componentDidUpdate() {
- if (this.state.resolvedSpec === null) {
- this.load();
- } else if (!this.state.loading && this.props.onLoaded) {
- // may run multiple time
- this.props.onLoaded();
+ React.useEffect(() => {
+ if (store && onLoaded) {
+ onLoaded();
}
- }
+ }, [store, onLoaded])
- async load() {
- const { specUrl, spec } = this.props;
- try {
- const resolvedSpec = await loadAndBundleSpec(spec || specUrl!);
- this.setState({ resolvedSpec, loading: false });
- } catch (e) {
- if (this.props.onLoaded) {
- this.props.onLoaded(e);
- }
- this.setState({ error: e });
- }
- }
-
- render() {
- if (this.state.error) {
- throw this.state.error;
- }
-
- const { specUrl, options } = this.props;
- const { loading, resolvedSpec } = this.state;
- return this.props.children({
- loading,
- store: this.makeStore(resolvedSpec, specUrl, options),
- });
- }
+ return children({
+ loading: !store,
+ store,
+ });
}
diff --git a/src/components/__tests__/SecurityRequirement.test.tsx b/src/components/__tests__/SecurityRequirement.test.tsx
new file mode 100644
index 00000000..723c0505
--- /dev/null
+++ b/src/components/__tests__/SecurityRequirement.test.tsx
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import { shallow } from 'enzyme';
+
+import { OpenAPIParser } from '../../services';
+import { SecurityRequirementModel } from '../../services/models/SecurityRequirement';
+import { SecurityRequirement } from '../SecurityRequirement/SecurityRequirement';
+import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions';
+
+const options = new RedocNormalizedOptions({});
+describe('Components', () => {
+ describe('SecurityRequirement', () => {
+ describe('SecurityRequirement', () => {
+ it('should render \'None\' when empty object in security open api', () => {
+ const parser = new OpenAPIParser({ openapi: '3.0', info: { title: 'test', version: '0' }, paths: {} },
+ undefined,
+ options,
+ );
+ const securityRequirement = new SecurityRequirementModel({}, parser);
+ const securityElement = shallow(
+
+ ).getElement();
+ expect(securityElement.props.children.type.target).toEqual('span');
+ expect(securityElement.props.children.props.children).toEqual('None');
+ });
+ });
+ });
+});
diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap
index 745c4cad..fe5617ed 100644
--- a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap
@@ -6,6 +6,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
>>",
"pattern": undefined,
"pointer": "#/components/schemas/Dog/properties/packSize",
@@ -56,6 +59,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
>>",
"pattern": undefined,
"pointer": "#/components/schemas/Dog/properties/type",
diff --git a/src/index.ts b/src/index.ts
index b74e150e..e8be304c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,9 +6,9 @@ export {
Section,
StyledDropdown,
SimpleDropdown,
- DropdownOption,
} from './common-elements/';
-export { OpenAPIEncoding } from './types';
+export type { DropdownOption } from './common-elements';
+export type { OpenAPIEncoding } from './types';
export * from './services';
export * from './utils';
diff --git a/src/polyfills.ts b/src/polyfills.ts
index 279ed79e..555e69aa 100644
--- a/src/polyfills.ts
+++ b/src/polyfills.ts
@@ -1,15 +1,2 @@
-import 'core-js/es/promise';
-
-import 'core-js/es/array/find';
-import 'core-js/es/array/includes';
-import 'core-js/es/object/assign';
-import 'core-js/es/object/entries';
-import 'core-js/es/object/is';
-import 'core-js/es/string/ends-with';
-import 'core-js/es/string/starts-with';
-
-import 'core-js/es/map';
-import 'core-js/es/symbol';
-
import 'unfetch/polyfill/index';
-import 'url-polyfill';
+import 'core-js/es/symbol';
diff --git a/src/services/AppStore.ts b/src/services/AppStore.ts
index 9387c521..76805419 100644
--- a/src/services/AppStore.ts
+++ b/src/services/AppStore.ts
@@ -145,7 +145,10 @@ export class AppStore {
if (idx === -1 && IS_BROWSER) {
const $description = document.querySelector('[data-role="redoc-description"]');
+ const $summary = document.querySelector('[data-role="redoc-summary"]');
+
if ($description) elements.push($description);
+ if ($summary) elements.push($summary);
}
this.marker.addOnly(elements);
diff --git a/src/services/Labels.ts b/src/services/Labels.ts
index c3568eb1..71b83e58 100644
--- a/src/services/Labels.ts
+++ b/src/services/Labels.ts
@@ -6,10 +6,16 @@ export interface LabelsConfig {
deprecated: string;
example: string;
examples: string;
- nullable: string;
recursive: string;
arrayOf: string;
webhook: string;
+ const: string;
+ download: string;
+ downloadSpecification: string;
+ responses: string;
+ callbackResponses: string;
+ requestSamples: string;
+ responseSamples: string;
}
export type LabelsConfigRaw = Partial;
@@ -22,10 +28,16 @@ const labels: LabelsConfig = {
deprecated: 'Deprecated',
example: 'Example',
examples: 'Examples',
- nullable: 'Nullable',
recursive: 'Recursive',
arrayOf: 'Array of ',
webhook: 'Event',
+ const: 'Value',
+ download: 'Download',
+ downloadSpecification: 'Download OpenAPI specification',
+ responses: 'Responses',
+ callbackResponses: 'Callback responses',
+ requestSamples: 'Request samples',
+ responseSamples: 'Response samples',
};
export function setRedocLabels(_labels?: LabelsConfigRaw) {
diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts
index 339ba6cf..7cb875ed 100644
--- a/src/services/MenuBuilder.ts
+++ b/src/services/MenuBuilder.ts
@@ -53,7 +53,7 @@ export class MenuBuilder {
const spec = parser.spec;
const items: ContentItemModel[] = [];
- const tagsMap = MenuBuilder.getTagsWithOperations(spec);
+ const tagsMap = MenuBuilder.getTagsWithOperations(parser, spec);
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options));
if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
items.push(
@@ -215,24 +215,33 @@ export class MenuBuilder {
/**
* collects tags and maps each tag to list of operations belonging to this tag
*/
- static getTagsWithOperations(spec: OpenAPISpec): TagsInfoMap {
+ static getTagsWithOperations(parser: OpenAPIParser, spec: OpenAPISpec): TagsInfoMap {
const tags: TagsInfoMap = {};
+ const webhooks = spec['x-webhooks'] || spec.webhooks;
for (const tag of spec.tags || []) {
tags[tag.name] = { ...tag, operations: [] };
}
- getTags(spec.paths);
- if (spec['x-webhooks']) {
- getTags(spec['x-webhooks'], true);
+ if (webhooks) {
+ getTags(parser, webhooks, true);
}
- function getTags(paths: OpenAPIPaths, isWebhook?: boolean) {
+ if (spec.paths){
+ getTags(parser, spec.paths);
+ }
+
+ function getTags(parser: OpenAPIParser, paths: OpenAPIPaths, isWebhook?: boolean) {
for (const pathName of Object.keys(paths)) {
const path = paths[pathName];
const operations = Object.keys(path).filter(isOperationName);
for (const operationName of operations) {
const operationInfo = path[operationName];
- let operationTags = operationInfo.tags;
+ if (path.$ref) {
+ const resolvedPaths = parser.deref(path as OpenAPIPaths);
+ getTags(parser, { [pathName]: resolvedPaths }, isWebhook);
+ continue;
+ }
+ let operationTags = operationInfo?.tags;
if (!operationTags || !operationTags.length) {
// empty tag
diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts
index b0d3a8f4..11a001aa 100644
--- a/src/services/OpenAPIParser.ts
+++ b/src/services/OpenAPIParser.ts
@@ -45,9 +45,9 @@ class RefCounter {
export class OpenAPIParser {
specUrl?: string;
spec: OpenAPISpec;
- mergeRefs: Set;
private _refCounter: RefCounter = new RefCounter();
+ private allowMergeRefs: boolean = false;
constructor(
spec: OpenAPISpec,
@@ -58,8 +58,7 @@ export class OpenAPIParser {
this.preprocess(spec);
this.spec = spec;
-
- this.mergeRefs = new Set();
+ this.allowMergeRefs = spec.openapi.startsWith('3.1');
const href = IS_BROWSER ? window.location.href : '';
if (typeof specUrl === 'string') {
@@ -149,7 +148,7 @@ export class OpenAPIParser {
* @param obj object to dereference
* @param forceCircular whether to dereference even if it is circular ref
*/
- deref(obj: OpenAPIRef | T, forceCircular = false): T {
+ deref(obj: OpenAPIRef | T, forceCircular = false, mergeAsAllOf = false): T {
if (this.isRef(obj)) {
const schemaName = getDefinitionName(obj.$ref);
if (schemaName && this.options.ignoreNamedSchemas.has(schemaName)) {
@@ -165,16 +164,36 @@ export class OpenAPIParser {
return Object.assign({}, resolved, { 'x-circular-ref': true });
}
// deref again in case one more $ref is here
+ let result = resolved;
if (this.isRef(resolved)) {
- const res = this.deref(resolved);
+ result = this.deref(resolved, false, mergeAsAllOf);
this.exitRef(resolved);
- return res;
}
- return resolved;
+ return this.allowMergeRefs ? this.mergeRefs(obj, resolved, mergeAsAllOf) : result;
}
return obj;
}
+ mergeRefs(ref, resolved, mergeAsAllOf: boolean) {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { $ref, ...rest } = ref;
+ const keys = Object.keys(rest);
+ if (keys.length === 0) {
+ return resolved;
+ }
+ if (mergeAsAllOf && keys.some((k) => k !== 'description' && k !== 'title' && k !== 'externalDocs')) {
+ return {
+ allOf: [resolved, rest],
+ };
+ } else {
+ // small optimization
+ return {
+ ...resolved,
+ ...rest,
+ };
+ }
+ }
+
shalowDeref(obj: OpenAPIRef | T): T {
if (this.isRef(obj)) {
return this.byRef(obj.$ref)!;
@@ -225,7 +244,7 @@ export class OpenAPIParser {
return undefined;
}
- const resolved = this.deref(subSchema, forceCircular);
+ const resolved = this.deref(subSchema, forceCircular, true);
const subRef = subSchema.$ref || undefined;
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs);
receiver.parentRefs!.push(...(subMerged.parentRefs || []));
@@ -234,7 +253,7 @@ export class OpenAPIParser {
schema: subMerged,
};
})
- .filter(child => child !== undefined) as Array<{
+ .filter((child) => child !== undefined) as Array<{
$ref: string | undefined;
schema: MergedOpenAPISchema;
}>;
@@ -265,7 +284,7 @@ export class OpenAPIParser {
{ allOf: [receiver.properties[prop], subSchema.properties[prop]] },
$ref + '/properties/' + prop,
);
- receiver.properties[prop] = mergedProp
+ receiver.properties[prop] = mergedProp;
this.exitParents(mergedProp); // every prop resolution should have separate recursive stack
}
}
@@ -313,7 +332,7 @@ export class OpenAPIParser {
const def = this.deref(schemas[defName]);
if (
def.allOf !== undefined &&
- def.allOf.find(obj => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1)
+ def.allOf.find((obj) => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1)
) {
res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName];
}
@@ -339,7 +358,7 @@ export class OpenAPIParser {
const beforeAllOf = allOf.slice(0, i);
const afterAllOf = allOf.slice(i + 1);
return {
- oneOf: sub.oneOf.map(part => {
+ oneOf: sub.oneOf.map((part) => {
const merged = this.mergeAllOf({
allOf: [...beforeAllOf, part, ...afterAllOf],
});
diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts
index 58d4b8b0..e310a9ad 100644
--- a/src/services/RedocNormalizedOptions.ts
+++ b/src/services/RedocNormalizedOptions.ts
@@ -44,7 +44,7 @@ export interface RedocRawOptions {
hideSchemaPattern?: boolean;
}
-function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
+export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
if (val === undefined) {
return defaultValue || false;
}
diff --git a/src/services/SpecStore.ts b/src/services/SpecStore.ts
index 1f39f903..277e2c03 100644
--- a/src/services/SpecStore.ts
+++ b/src/services/SpecStore.ts
@@ -1,4 +1,4 @@
-import { OpenAPIExternalDocumentation, OpenAPISpec } from '../types';
+import { OpenAPIExternalDocumentation, OpenAPIPath, OpenAPISpec, Referenced } from '../types';
import { ContentItemModel, MenuBuilder } from './MenuBuilder';
import { ApiInfoModel } from './models/ApiInfo';
@@ -28,6 +28,7 @@ export class SpecStore {
this.externalDocs = this.parser.spec.externalDocs;
this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
this.securitySchemes = new SecuritySchemesModel(this.parser);
- this.webhooks = new WebhookModel(this.parser, options, this.parser.spec['x-webhooks']);
+ const webhookPath: Referenced = {...this.parser?.spec?.['x-webhooks'], ...this.parser?.spec.webhooks};
+ this.webhooks = new WebhookModel(this.parser, options, webhookPath);
}
}
diff --git a/src/services/__tests__/fixtures/3.1/pathItems.json b/src/services/__tests__/fixtures/3.1/pathItems.json
new file mode 100644
index 00000000..0bd88b05
--- /dev/null
+++ b/src/services/__tests__/fixtures/3.1/pathItems.json
@@ -0,0 +1,66 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "version": "1.0.0",
+ "title": "Swagger Petstore"
+ },
+ "webhooks": {
+ "myWebhook": {
+ "$ref": "#/components/pathItems/catsWebhook",
+ "description": "Overriding description",
+ "summary": "Overriding summary"
+ }
+ },
+ "components": {
+ "pathItems": {
+ "catsWebhook": {
+ "put": {
+ "summary": "Get a cat details after update",
+ "description": "Get a cat details after update",
+ "operationId": "updatedCat",
+ "tags": [
+ "pet"
+ ],
+ "requestBody": {
+ "description": "Information about cat in the system",
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "update Cat details"
+ }
+ }
+ },
+ "post": {
+ "summary": "Create new cat",
+ "description": "Info about new cat",
+ "operationId": "createdCat",
+ "tags": [
+ "pet"
+ ],
+ "requestBody": {
+ "description": "Information about cat in the system",
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "create Cat details"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/services/__tests__/models/ApiInfo.test.ts b/src/services/__tests__/models/ApiInfo.test.ts
index 5adb6036..5db42527 100644
--- a/src/services/__tests__/models/ApiInfo.test.ts
+++ b/src/services/__tests__/models/ApiInfo.test.ts
@@ -34,5 +34,33 @@ describe('Models', () => {
const info = new ApiInfoModel(parser);
expect(info.description).toEqual('Test description\nsome text\n');
});
+
+ test('should correctly populate summary up to the first md heading', () => {
+ parser.spec = {
+ openapi: '3.1.0',
+ info: {
+ summary: 'Test summary\nsome text\n## Heading\n test',
+ },
+ } as any;
+
+ const info = new ApiInfoModel(parser);
+ expect(info.summary).toEqual('Test summary\nsome text\n## Heading\n test');
+ });
+
+ test('should correctly populate license identifier', () => {
+ parser.spec = {
+ openapi: '3.1.0',
+ info: {
+ license: {
+ name: 'MIT',
+ identifier: 'MIT',
+ url: 'https://opensource.org/licenses/MIT'
+ }
+ },
+ } as any;
+
+ const { license = { identifier: null } } = new ApiInfoModel(parser);
+ expect(license.identifier).toEqual('MIT');
+ });
});
});
diff --git a/src/services/__tests__/models/MenuBuilder.test.ts b/src/services/__tests__/models/MenuBuilder.test.ts
new file mode 100644
index 00000000..ed930639
--- /dev/null
+++ b/src/services/__tests__/models/MenuBuilder.test.ts
@@ -0,0 +1,25 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+import { MenuBuilder } from '../../MenuBuilder';
+import { OpenAPIParser } from '../../OpenAPIParser';
+
+import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
+
+const opts = new RedocNormalizedOptions({});
+
+describe('Models', () => {
+ describe('MenuBuilder', () => {
+ let parser;
+
+ test('should resolve pathItems', () => {
+ const spec = require('../fixtures/3.1/pathItems.json');
+ parser = new OpenAPIParser(spec, undefined, opts);
+ const contentItems = MenuBuilder.buildStructure(parser, opts);
+ expect(contentItems).toHaveLength(1);
+ expect(contentItems[0].items).toHaveLength(2);
+ expect(contentItems[0].id).toEqual('tag/pet');
+ expect(contentItems[0].name).toEqual('pet');
+ expect(contentItems[0].type).toEqual('tag');
+
+ });
+ });
+});
diff --git a/src/services/models/ApiInfo.ts b/src/services/models/ApiInfo.ts
index fea480aa..517538db 100644
--- a/src/services/models/ApiInfo.ts
+++ b/src/services/models/ApiInfo.ts
@@ -7,6 +7,7 @@ export class ApiInfoModel implements OpenAPIInfo {
version: string;
description: string;
+ summary: string;
termsOfService?: string;
contact?: OpenAPIContact;
license?: OpenAPILicense;
@@ -17,6 +18,7 @@ export class ApiInfoModel implements OpenAPIInfo {
constructor(private parser: OpenAPIParser) {
Object.assign(this, parser.spec.info);
this.description = parser.spec.info.description || '';
+ this.summary = parser.spec.info.summary || '';
const firstHeadingLinePos = this.description.search(/^##?\s+/m);
if (firstHeadingLinePos > -1) {
diff --git a/src/services/models/Field.ts b/src/services/models/Field.ts
index 1154c9a8..ef031ab8 100644
--- a/src/services/models/Field.ts
+++ b/src/services/models/Field.ts
@@ -55,6 +55,7 @@ export class FieldModel {
extensions?: Record;
explode: boolean;
style?: OpenAPIParameterStyle;
+ const?: any;
serializationMime?: string;
@@ -111,6 +112,8 @@ export class FieldModel {
if (options.showExtensions) {
this.extensions = extractExtensions(info, options.showExtensions);
}
+
+ this.const = this.schema?.const || info?.const || '';
}
@action
diff --git a/src/services/models/MediaType.ts b/src/services/models/MediaType.ts
index 7807c358..9283fbae 100644
--- a/src/services/models/MediaType.ts
+++ b/src/services/models/MediaType.ts
@@ -58,7 +58,7 @@ 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 as any, samplerOptions, parser.spec);
if (this.schema.discriminatorProp && typeof sample === 'object' && sample) {
sample[this.schema.discriminatorProp] = subSchema.title;
@@ -78,7 +78,7 @@ export class MediaTypeModel {
default: new ExampleModel(
parser,
{
- value: Sampler.sample(info.schema, samplerOptions, parser.spec),
+ value: Sampler.sample(info.schema as any, samplerOptions, parser.spec),
},
this.name,
info.encoding,
diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts
index dd4a4d6f..d79d738b 100644
--- a/src/services/models/Schema.ts
+++ b/src/services/models/Schema.ts
@@ -25,7 +25,7 @@ import { l } from '../Labels';
export class SchemaModel {
pointer: string;
- type: string;
+ type: string | string[];
displayType: string;
typePrefix: string = '';
title: string;
@@ -60,6 +60,9 @@ export class SchemaModel {
rawSchema: OpenAPISchema;
schema: MergedOpenAPISchema;
extensions?: Record;
+ const: any;
+ contentEncoding?: string;
+ contentMediaType?: string;
/**
* @param isChild if schema discriminator Child
@@ -75,7 +78,7 @@ export class SchemaModel {
makeObservable(this);
this.pointer = schemaOrRef.$ref || pointer || '';
- this.rawSchema = parser.deref(schemaOrRef);
+ this.rawSchema = parser.deref(schemaOrRef, false, true);
this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild);
this.init(parser, isChild);
@@ -97,6 +100,10 @@ export class SchemaModel {
this.activeOneOf = idx;
}
+ hasType(type: string) {
+ return this.type === type || (Array.isArray(this.type) && this.type.includes(type));
+ }
+
init(parser: OpenAPIParser, isChild: boolean) {
const schema = this.schema;
this.isCircular = schema['x-circular-ref'];
@@ -106,7 +113,6 @@ export class SchemaModel {
this.description = schema.description || '';
this.type = schema.type || detectType(schema);
this.format = schema.format;
- this.nullable = !!schema.nullable;
this.enum = schema.enum || [];
this.example = schema.example;
this.deprecated = !!schema.deprecated;
@@ -114,12 +120,26 @@ export class SchemaModel {
this.externalDocs = schema.externalDocs;
this.constraints = humanizeConstraints(schema);
- this.displayType = this.type;
this.displayFormat = this.format;
this.isPrimitive = isPrimitiveType(schema, this.type);
this.default = schema.default;
this.readOnly = !!schema.readOnly;
this.writeOnly = !!schema.writeOnly;
+ this.const = schema.const || '';
+ this.contentEncoding = schema.contentEncoding;
+ this.contentMediaType = schema.contentMediaType;
+
+ if (!!schema.nullable || schema['x-nullable']) {
+ if (Array.isArray(this.type) && !this.type.some((value) => value === null || value === 'null')) {
+ this.type = [...this.type, 'null'];
+ } else if (!Array.isArray(this.type) && (this.type !== null || this.type !== 'null')) {
+ this.type = [this.type, 'null'];
+ }
+ }
+
+ this.displayType = Array.isArray(this.type)
+ ? this.type.map(item => item === null ? 'null' : item).join(' or ')
+ : this.type;
if (this.isCircular) {
return;
@@ -154,9 +174,9 @@ export class SchemaModel {
return;
}
- if (this.type === 'object') {
+ if (this.hasType('object')) {
this.fields = buildFields(parser, schema, this.pointer, this.options);
- } else if (this.type === 'array' && schema.items) {
+ } else if (this.hasType('array') && schema.items) {
this.items = new SchemaModel(parser, schema.items, this.pointer + '/items', this.options);
this.displayType = pluralizeType(this.items.displayType);
this.displayFormat = this.items.format;
@@ -169,6 +189,11 @@ export class SchemaModel {
if (this.items.isPrimitive) {
this.enum = this.items.enum;
}
+ if (Array.isArray(this.type)) {
+ const filteredType = this.type.filter(item => item !== 'array');
+ if (filteredType.length)
+ this.displayType += ` or ${filteredType.join(' or ')}`;
+ }
}
if (this.enum.length && this.options.sortEnumValuesAlphabetically) {
@@ -178,7 +203,7 @@ export class SchemaModel {
private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) {
this.oneOf = oneOf!.map((variant, idx) => {
- const derefVariant = parser.deref(variant);
+ const derefVariant = parser.deref(variant, false, true);
const merged = parser.mergeAllOf(derefVariant, this.pointer + '/oneOf/' + idx);
@@ -186,7 +211,7 @@ export class SchemaModel {
const title =
isNamedDefinition(variant.$ref) && !merged.title
? JsonPointer.baseName(variant.$ref)
- : merged.title;
+ : `${(merged.title || '')}${(merged.const && JSON.stringify(merged.const)) || ''}`;
const schema = new SchemaModel(
parser,
diff --git a/src/services/models/Webhook.ts b/src/services/models/Webhook.ts
index 7349b8bd..55122935 100644
--- a/src/services/models/Webhook.ts
+++ b/src/services/models/Webhook.ts
@@ -1,8 +1,8 @@
import { OpenAPIPath, Referenced } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser';
import { OperationModel } from './Operation';
-import { isOperationName } from '../..';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
+import { isOperationName } from '../..';
export class WebhookModel {
operations: OperationModel[] = [];
@@ -14,12 +14,21 @@ export class WebhookModel {
) {
const webhooks = parser.deref(infoOrRef || {});
parser.exitRef(infoOrRef);
+ this.initWebhooks(parser, webhooks, options);
+ }
+ initWebhooks(parser: OpenAPIParser, webhooks: OpenAPIPath, options: RedocNormalizedOptions) {
for (const webhookName of Object.keys(webhooks)) {
const webhook = webhooks[webhookName];
const operations = Object.keys(webhook).filter(isOperationName);
for (const operationName of operations) {
const operationInfo = webhook[operationName];
+ if (webhook.$ref) {
+ const resolvedWebhook = parser.deref(webhook || {});
+ this.initWebhooks(parser, { [operationName]: resolvedWebhook }, options);
+ }
+
+ if (!operationInfo) continue;
const operation = new OperationModel(
parser,
{
diff --git a/src/setupTests.ts b/src/setupTests.ts
index 5a599cc2..da5b397c 100644
--- a/src/setupTests.ts
+++ b/src/setupTests.ts
@@ -1,5 +1,6 @@
import * as Enzyme from 'enzyme';
-import * as Adapter from 'enzyme-adapter-react-16';
+import * as Adapter from '@wojtekmaj/enzyme-adapter-react-17';
+
import 'raf/polyfill';
Enzyme.configure({ adapter: new Adapter() });
diff --git a/src/styled-components.ts b/src/styled-components.ts
index 8acd13d4..689ad538 100644
--- a/src/styled-components.ts
+++ b/src/styled-components.ts
@@ -1,8 +1,8 @@
import * as styledComponents from 'styled-components';
-import { ResolvedThemeInterface } from './theme';
+import type { ResolvedThemeInterface } from './theme';
-export { ResolvedThemeInterface };
+export type { ResolvedThemeInterface };
const {
default: styled,
@@ -10,7 +10,7 @@ const {
createGlobalStyle,
keyframes,
ThemeProvider,
-} = styledComponents as styledComponents.ThemedStyledComponentsModule;
+} = styledComponents as unknown as styledComponents.ThemedStyledComponentsModule;
export const media = {
lessThan(breakpoint, print?: boolean, extra?: string) {
diff --git a/src/types/index.d.ts b/src/types/index.ts
similarity index 100%
rename from src/types/index.d.ts
rename to src/types/index.ts
diff --git a/src/types/open-api.d.ts b/src/types/open-api.ts
similarity index 95%
rename from src/types/open-api.d.ts
rename to src/types/open-api.ts
index 7134890e..03898457 100644
--- a/src/types/open-api.d.ts
+++ b/src/types/open-api.ts
@@ -10,6 +10,7 @@ export interface OpenAPISpec {
tags?: OpenAPITag[];
externalDocs?: OpenAPIExternalDocumentation;
'x-webhooks'?: OpenAPIPaths;
+ webhooks?: OpenAPIPaths;
}
export interface OpenAPIInfo {
@@ -17,6 +18,7 @@ export interface OpenAPIInfo {
version: string;
description?: string;
+ summary?: string;
termsOfService?: string;
contact?: OpenAPIContact;
license?: OpenAPILicense;
@@ -56,6 +58,7 @@ export interface OpenAPIPath {
trace?: OpenAPIOperation;
servers?: OpenAPIServer[];
parameters?: Array>;
+ $ref?: string;
}
export interface OpenAPIXCodeSample {
@@ -96,6 +99,7 @@ export interface OpenAPIParameter {
examples?: { [media: string]: Referenced };
content?: { [media: string]: OpenAPIMediaType };
encoding?: Record;
+ const?: any;
}
export interface OpenAPIExample {
@@ -107,7 +111,7 @@ export interface OpenAPIExample {
export interface OpenAPISchema {
$ref?: string;
- type?: string;
+ type?: string | string[];
properties?: { [name: string]: OpenAPISchema };
additionalProperties?: boolean | OpenAPISchema;
description?: string;
@@ -129,9 +133,9 @@ export interface OpenAPISchema {
title?: string;
multipleOf?: number;
maximum?: number;
- exclusiveMaximum?: boolean;
+ exclusiveMaximum?: boolean | number;
minimum?: number;
- exclusiveMinimum?: boolean;
+ exclusiveMinimum?: boolean | number;
maxLength?: number;
minLength?: number;
pattern?: string;
@@ -142,6 +146,9 @@ export interface OpenAPISchema {
minProperties?: number;
enum?: any[];
example?: any;
+ const?: string;
+ contentEncoding?: string;
+ contentMediaType?: string;
}
export interface OpenAPIDiscriminator {
@@ -271,4 +278,5 @@ export interface OpenAPIContact {
export interface OpenAPILicense {
name: string;
url?: string;
+ identifier?: string;
}
diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap
index e7c967e6..7807e39e 100644
--- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap
+++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap
@@ -1796,6 +1796,1845 @@ culpa qui officia deserunt mollit anim id est laborum.
}
`;
+exports[`#loadAndBundleSpec should load And Bundle Spec demo/openapi-3-1.yaml 1`] = `
+Object {
+ "components": Object {
+ "examples": Object {
+ "Order": Object {
+ "value": Object {
+ "complete": false,
+ "quantity": 1,
+ "shipDate": "2018-10-19T16:46:45Z",
+ "status": "placed",
+ },
+ },
+ },
+ "pathItems": Object {
+ "webhooks": Object {
+ "post": Object {
+ "description": "Info about new cat",
+ "operationId": "createdCat",
+ "requestBody": Object {
+ "content": Object {
+ "multipart/form-data": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/Cat",
+ },
+ },
+ },
+ "description": "Information about cat in the system",
+ },
+ "responses": Object {
+ "200": Object {
+ "description": "create Cat details",
+ },
+ },
+ "summary": "Create new cat",
+ "tags": Array [
+ "webhooks",
+ ],
+ },
+ "put": Object {
+ "description": "Get a cat details after update",
+ "operationId": "updatedCat",
+ "requestBody": Object {
+ "content": Object {
+ "multipart/form-data": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/Cat",
+ },
+ },
+ },
+ "description": "Information about cat in the system",
+ },
+ "responses": Object {
+ "200": Object {
+ "description": "update Cat details",
+ },
+ },
+ "summary": "Get a cat details after update",
+ "tags": Array [
+ "webhooks",
+ ],
+ },
+ },
+ },
+ "requestBodies": Object {
+ "Pet": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/Pet",
+ "description": "My Pet",
+ "title": "Pettie",
+ },
+ },
+ "application/xml": Object {
+ "schema": Object {
+ "properties": Object {
+ "name": Object {
+ "description": "hooray",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ },
+ "description": "Pet object that needs to be added to the store",
+ "required": true,
+ },
+ "UserArray": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "items": Object {
+ "$ref": "#/components/schemas/User",
+ },
+ "type": "array",
+ },
+ },
+ },
+ "description": "List of user object",
+ "required": true,
+ },
+ },
+ "schemas": Object {
+ "ApiResponse": Object {
+ "properties": Object {
+ "code": Object {
+ "format": "int32",
+ "type": "integer",
+ },
+ "message": Object {
+ "type": "string",
+ },
+ "type": Object {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "Cat": Object {
+ "allOf": Array [
+ Object {
+ "$ref": "#/components/schemas/Pet",
+ },
+ Object {
+ "properties": Object {
+ "huntingSkill": Object {
+ "default": "lazy",
+ "description": "The measured skill for hunting",
+ "enum": Array [
+ "clueless",
+ "lazy",
+ "adventurous",
+ "aggressive",
+ ],
+ "example": "adventurous",
+ "type": "string",
+ },
+ },
+ "required": Array [
+ "huntingSkill",
+ ],
+ "type": "object",
+ },
+ ],
+ "description": "A representation of a cat",
+ },
+ "Category": Object {
+ "properties": Object {
+ "id": Object {
+ "$ref": "#/components/schemas/Id",
+ "description": "Category ID",
+ },
+ "name": Object {
+ "description": "Category name",
+ "minLength": 1,
+ "type": "string",
+ },
+ "sub": Object {
+ "description": "Test Sub Category",
+ "properties": Object {
+ "prop1": Object {
+ "description": "Dumb Property",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ "type": "object",
+ "xml": Object {
+ "name": "Category",
+ },
+ },
+ "Dog": Object {
+ "allOf": Array [
+ Object {
+ "$ref": "#/components/schemas/Pet",
+ },
+ Object {
+ "properties": Object {
+ "packSize": Object {
+ "default": 1,
+ "description": "The size of the pack the dog is from",
+ "format": "int32",
+ "minimum": 1,
+ "type": "integer",
+ },
+ },
+ "required": Array [
+ "packSize",
+ ],
+ "type": "object",
+ },
+ ],
+ "description": "A representation of a dog",
+ },
+ "HoneyBee": Object {
+ "allOf": Array [
+ Object {
+ "$ref": "#/components/schemas/Pet",
+ },
+ Object {
+ "properties": Object {
+ "honeyPerDay": Object {
+ "description": "Average amount of honey produced per day in ounces",
+ "example": 3.14,
+ "multipleOf": 0.01,
+ "type": "number",
+ },
+ },
+ "required": Array [
+ "honeyPerDay",
+ ],
+ "type": "object",
+ },
+ ],
+ "description": "A representation of a honey bee",
+ },
+ "Id": Object {
+ "format": "int64",
+ "readOnly": true,
+ "type": "integer",
+ },
+ "Order": Object {
+ "properties": Object {
+ "complete": Object {
+ "default": false,
+ "description": "Indicates whenever order was completed or not",
+ "readOnly": true,
+ "type": "boolean",
+ },
+ "id": Object {
+ "$ref": "#/components/schemas/Id",
+ "description": "Order ID",
+ },
+ "petId": Object {
+ "$ref": "#/components/schemas/Id",
+ "description": "Pet ID",
+ },
+ "quantity": Object {
+ "default": 1,
+ "format": "int32",
+ "minimum": 1,
+ "type": "integer",
+ },
+ "requestId": Object {
+ "description": "Unique Request Id",
+ "type": "string",
+ "writeOnly": true,
+ },
+ "shipDate": Object {
+ "description": "Estimated ship date",
+ "format": "date-time",
+ "type": "string",
+ },
+ "status": Object {
+ "description": "Order Status",
+ "enum": Array [
+ "placed",
+ "approved",
+ "delivered",
+ ],
+ "type": "string",
+ },
+ },
+ "type": "object",
+ "xml": Object {
+ "name": "Order",
+ },
+ },
+ "Pet": Object {
+ "discriminator": Object {
+ "mapping": Object {
+ "bee": "#/components/schemas/HoneyBee",
+ "cat": "#/components/schemas/Cat",
+ "dog": "#/components/schemas/Dog",
+ },
+ "propertyName": "petType",
+ },
+ "properties": Object {
+ "category": Object {
+ "$ref": "#/components/schemas/Category",
+ "description": "Categories this pet belongs to",
+ },
+ "friend": Object {
+ "$ref": "#/components/schemas/Pet",
+ },
+ "id": Object {
+ "$ref": "#/components/schemas/Id",
+ "description": "Pet ID",
+ "externalDocs": Object {
+ "description": "Find more info here",
+ "url": "https://example.com",
+ },
+ },
+ "name": Object {
+ "description": "The name given to a pet",
+ "example": "Guru",
+ "type": "string",
+ },
+ "petType": Object {
+ "description": "Type of a pet",
+ "type": "string",
+ },
+ "photoUrls": Object {
+ "description": "The list of URL to a cute photos featuring pet",
+ "items": Object {
+ "format": "url",
+ "type": "string",
+ },
+ "maxItems": 20,
+ "minItems": 1,
+ "type": Array [
+ "string",
+ "integer",
+ "null",
+ "array",
+ ],
+ "xml": Object {
+ "name": "photoUrl",
+ "wrapped": true,
+ },
+ },
+ "status": Object {
+ "default": "pending",
+ "description": "Pet status in the store",
+ "enum": Array [
+ "available",
+ "pending",
+ "sold",
+ ],
+ "type": "string",
+ },
+ "tags": Object {
+ "description": "Tags attached to the pet",
+ "exclusiveMaximum": 100,
+ "exclusiveMinimum": 0,
+ "items": Object {
+ "$ref": "#/components/schemas/Tag",
+ },
+ "type": "array",
+ "xml": Object {
+ "name": "tag",
+ "wrapped": true,
+ },
+ },
+ },
+ "required": Array [
+ "name",
+ "photoUrls",
+ ],
+ "type": "object",
+ "xml": Object {
+ "name": "Pet",
+ },
+ },
+ "Tag": Object {
+ "properties": Object {
+ "id": Object {
+ "$ref": "#/components/schemas/Id",
+ "description": "Tag ID",
+ },
+ "name": Object {
+ "description": "Tag name",
+ "minLength": 1,
+ "type": "string",
+ },
+ },
+ "type": "object",
+ "xml": Object {
+ "name": "Tag",
+ },
+ },
+ "User": Object {
+ "properties": Object {
+ "email": Object {
+ "description": "User email address",
+ "example": "john.smith@example.com",
+ "format": "email",
+ "type": "string",
+ },
+ "firstName": Object {
+ "description": "User first name",
+ "example": "John",
+ "minLength": 1,
+ "type": "string",
+ },
+ "id": Object {
+ "$ref": "#/components/schemas/Id",
+ },
+ "image": Object {
+ "contentEncoding": "base64",
+ "contentMediaType": "image/png",
+ "description": "User image",
+ "type": "string",
+ },
+ "lastName": Object {
+ "description": "User last name",
+ "example": "Smith",
+ "minLength": 1,
+ "type": "string",
+ },
+ "password": Object {
+ "description": "User password, MUST contain a mix of upper and lower case letters, as well as digits",
+ "example": "drowssaP123",
+ "format": "password",
+ "minLength": 8,
+ "pattern": "/(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/",
+ "type": "string",
+ },
+ "pet": Object {
+ "oneOf": Array [
+ Object {
+ "$ref": "#/components/schemas/Pet",
+ "title": "Pettie",
+ },
+ Object {
+ "$ref": "#/components/schemas/Tag",
+ },
+ ],
+ },
+ "phone": Object {
+ "description": "User phone number in international format",
+ "example": "+1-202-555-0192",
+ "pattern": "/^\\\\+(?:[0-9]-?){6,14}[0-9]$/",
+ "type": "string",
+ },
+ "userStatus": Object {
+ "description": "User status",
+ "format": "int32",
+ "type": "integer",
+ },
+ "username": Object {
+ "description": "User supplied username",
+ "example": "John78",
+ "minLength": 4,
+ "type": "string",
+ },
+ },
+ "type": "object",
+ "xml": Object {
+ "name": "User",
+ },
+ },
+ },
+ "securitySchemes": Object {
+ "api_key": Object {
+ "description": "For this sample, you can use the api key \`special-key\` to test the authorization filters.
+",
+ "in": "header",
+ "name": "api_key",
+ "type": "apiKey",
+ },
+ "petstore_auth": Object {
+ "description": "Get access to data while protecting your account credentials.
+OAuth2 is also a safer and more secure way to give you access.
+",
+ "flows": Object {
+ "implicit": Object {
+ "authorizationUrl": "http://petstore.swagger.io/api/oauth/dialog",
+ "scopes": Object {
+ "read:pets": "read your pets",
+ "write:pets": "modify pets in your account",
+ },
+ },
+ },
+ "type": "oauth2",
+ },
+ },
+ },
+ "externalDocs": Object {
+ "description": "Find out how to create Github repo for your OpenAPI spec.",
+ "url": "https://github.com/Rebilly/generator-openapi-repo",
+ },
+ "info": Object {
+ "contact": Object {
+ "email": "apiteam@swagger.io",
+ "name": "API Support",
+ "url": "https://github.com/Redocly/redoc",
+ },
+ "description": "This is a sample server Petstore server.
+You can find out more about Swagger at
+[http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).
+For this sample, you can use the api key \`special-key\` to test the authorization filters.
+
+# Introduction
+This API is documented in **OpenAPI format** and is based on
+[Petstore sample](http://petstore.swagger.io/) provided by [swagger.io](http://swagger.io) team.
+It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo)
+tool and [ReDoc](https://github.com/Redocly/redoc) documentation. In addition to standard
+OpenAPI syntax we use a few [vendor extensions](https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md).
+
+# OpenAPI Specification
+This API is documented in **OpenAPI format** and is based on
+[Petstore sample](http://petstore.swagger.io/) provided by [swagger.io](http://swagger.io) team.
+It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo)
+tool and [ReDoc](https://github.com/Redocly/redoc) documentation. In addition to standard
+OpenAPI syntax we use a few [vendor extensions](https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md).
+
+# Cross-Origin Resource Sharing
+This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/).
+And that allows cross-domain communication from the browser.
+All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site.
+
+# Authentication
+
+Petstore offers two forms of authentication:
+ - API Key
+ - OAuth2
+OAuth2 - an open protocol to allow secure authorization in a simple
+and standard method from web, mobile and desktop applications.
+
+
+",
+ "license": Object {
+ "identifier": "Apache 2.0",
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html",
+ },
+ "summary": "My lovely API",
+ "termsOfService": "http://swagger.io/terms/",
+ "title": "Swagger Petstore",
+ "version": "1.0.0",
+ "x-logo": Object {
+ "altText": "Petstore logo",
+ "url": "https://redocly.github.io/redoc/petstore-logo.png",
+ },
+ },
+ "openapi": "3.1.0",
+ "paths": Object {
+ "/pet": Object {
+ "parameters": Array [
+ Object {
+ "description": "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US",
+ "example": "en-US",
+ "in": "header",
+ "name": "Accept-Language",
+ "required": false,
+ "schema": Object {
+ "default": "en-AU",
+ "type": "string",
+ },
+ },
+ Object {
+ "description": "Some cookie",
+ "in": "cookie",
+ "name": "cookieParam",
+ "required": true,
+ "schema": Object {
+ "format": "int64",
+ "type": "integer",
+ },
+ },
+ ],
+ "post": Object {
+ "description": "Add new pet to the store inventory.",
+ "operationId": "addPet",
+ "requestBody": Object {
+ "$ref": "#/components/requestBodies/Pet",
+ },
+ "responses": Object {
+ "405": Object {
+ "description": "Invalid input",
+ },
+ },
+ "security": Array [
+ Object {
+ "petstore_auth": Array [
+ "write:pets",
+ "read:pets",
+ ],
+ },
+ ],
+ "summary": "Add a new pet to the store",
+ "tags": Array [
+ "pet",
+ ],
+ "x-codeSamples": Array [
+ Object {
+ "lang": "C#",
+ "source": "PetStore.v1.Pet pet = new PetStore.v1.Pet();
+pet.setApiKey(\\"your api key\\");
+pet.petType = PetStore.v1.Pet.TYPE_DOG;
+pet.name = \\"Rex\\";
+// set other fields
+PetStoreResponse response = pet.create();
+if (response.statusCode == HttpStatusCode.Created)
+{
+ // Successfully created
+}
+else
+{
+ // Something wrong -- check response for errors
+ Console.WriteLine(response.getRawResponse());
+}
+",
+ },
+ Object {
+ "lang": "PHP",
+ "source": "$form = new \\\\PetStore\\\\Entities\\\\Pet();
+$form->setPetType(\\"Dog\\");
+$form->setName(\\"Rex\\");
+// set other fields
+try {
+ $pet = $client->pets()->create($form);
+} catch (UnprocessableEntityException $e) {
+ var_dump($e->getErrors());
+}
+",
+ },
+ ],
+ },
+ "put": Object {
+ "description": "",
+ "operationId": "updatePet",
+ "requestBody": Object {
+ "$ref": "#/components/requestBodies/Pet",
+ },
+ "responses": Object {
+ "400": Object {
+ "description": "Invalid ID supplied",
+ },
+ "404": Object {
+ "description": "Pet not found",
+ },
+ "405": Object {
+ "description": "Validation exception",
+ },
+ },
+ "security": Array [
+ Object {
+ "petstore_auth": Array [
+ "write:pets",
+ "read:pets",
+ ],
+ },
+ ],
+ "summary": "Update an existing pet",
+ "tags": Array [
+ "pet",
+ ],
+ "x-codeSamples": Array [
+ Object {
+ "lang": "PHP",
+ "source": "$form = new \\\\PetStore\\\\Entities\\\\Pet();
+$form->setPetId(1);
+$form->setPetType(\\"Dog\\");
+$form->setName(\\"Rex\\");
+// set other fields
+try {
+ $pet = $client->pets()->update($form);
+} catch (UnprocessableEntityException $e) {
+ var_dump($e->getErrors());
+}
+",
+ },
+ ],
+ },
+ },
+ "/pet/findByStatus": Object {
+ "get": Object {
+ "description": "Multiple status values can be provided with comma separated strings",
+ "operationId": "findPetsByStatus",
+ "parameters": Array [
+ Object {
+ "description": "Status values that need to be considered for filter",
+ "in": "query",
+ "name": "status",
+ "required": true,
+ "schema": Object {
+ "items": Object {
+ "default": "available",
+ "enum": Array [
+ "available",
+ "pending",
+ "sold",
+ ],
+ "type": "string",
+ },
+ "maxItems": 3,
+ "minItems": 1,
+ "type": "array",
+ },
+ "style": "form",
+ },
+ ],
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "items": Object {
+ "$ref": "#/components/schemas/Pet",
+ },
+ "type": "array",
+ },
+ },
+ "application/xml": Object {
+ "schema": Object {
+ "items": Object {
+ "$ref": "#/components/schemas/Pet",
+ },
+ "type": "array",
+ },
+ },
+ },
+ "description": "successful operation",
+ },
+ "400": Object {
+ "description": "Invalid status value",
+ },
+ },
+ "security": Array [
+ Object {
+ "petstore_auth": Array [
+ "write:pets",
+ "read:pets",
+ ],
+ },
+ ],
+ "summary": "Finds Pets by status",
+ "tags": Array [
+ "pet",
+ ],
+ },
+ },
+ "/pet/findByTags": Object {
+ "get": Object {
+ "deprecated": true,
+ "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
+ "operationId": "findPetsByTags",
+ "parameters": Array [
+ Object {
+ "description": "Tags to filter by",
+ "in": "query",
+ "name": "tags",
+ "required": true,
+ "schema": Object {
+ "items": Object {
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "style": "form",
+ },
+ ],
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "items": Object {
+ "$ref": "#/components/schemas/Pet",
+ },
+ "type": "array",
+ },
+ },
+ "application/xml": Object {
+ "schema": Object {
+ "items": Object {
+ "$ref": "#/components/schemas/Pet",
+ },
+ "type": "array",
+ },
+ },
+ },
+ "description": "successful operation",
+ },
+ "400": Object {
+ "description": "Invalid tag value",
+ },
+ },
+ "security": Array [
+ Object {
+ "petstore_auth": Array [
+ "write:pets",
+ "read:pets",
+ ],
+ },
+ ],
+ "summary": "Finds Pets by tags",
+ "tags": Array [
+ "pet",
+ ],
+ },
+ },
+ "/pet/{petId}": Object {
+ "delete": Object {
+ "description": "",
+ "operationId": "deletePet",
+ "parameters": Array [
+ Object {
+ "example": "Bearer ",
+ "in": "header",
+ "name": "api_key",
+ "required": false,
+ "schema": Object {
+ "type": "string",
+ },
+ },
+ Object {
+ "description": "Pet id to delete",
+ "in": "path",
+ "name": "petId",
+ "required": true,
+ "schema": Object {
+ "format": "int64",
+ "type": "integer",
+ },
+ },
+ ],
+ "responses": Object {
+ "400": Object {
+ "description": "Invalid pet value",
+ },
+ },
+ "security": Array [
+ Object {
+ "petstore_auth": Array [
+ "write:pets",
+ "read:pets",
+ ],
+ },
+ ],
+ "summary": "Deletes a pet",
+ "tags": Array [
+ "pet",
+ ],
+ },
+ "get": Object {
+ "description": "Returns a single pet",
+ "operationId": "getPetById",
+ "parameters": Array [
+ Object {
+ "deprecated": true,
+ "description": "ID of pet to return",
+ "in": "path",
+ "name": "petId",
+ "required": true,
+ "schema": Object {
+ "format": "int64",
+ "type": "integer",
+ },
+ },
+ ],
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/Pet",
+ },
+ },
+ "application/xml": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/Pet",
+ },
+ },
+ },
+ "description": "successful operation",
+ },
+ "400": Object {
+ "description": "Invalid ID supplied",
+ },
+ "404": Object {
+ "description": "Pet not found",
+ },
+ },
+ "security": Array [
+ Object {
+ "api_key": Array [],
+ },
+ ],
+ "summary": "Find pet by ID",
+ "tags": Array [
+ "pet",
+ ],
+ },
+ "post": Object {
+ "description": "",
+ "operationId": "updatePetWithForm",
+ "parameters": Array [
+ Object {
+ "description": "ID of pet that needs to be updated",
+ "in": "path",
+ "name": "petId",
+ "required": true,
+ "schema": Object {
+ "format": "int64",
+ "type": "integer",
+ },
+ },
+ ],
+ "requestBody": Object {
+ "content": Object {
+ "application/x-www-form-urlencoded": Object {
+ "schema": Object {
+ "properties": Object {
+ "name": Object {
+ "description": "Updated name of the pet",
+ "type": "string",
+ },
+ "status": Object {
+ "description": "Updated status of the pet",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "405": Object {
+ "description": "Invalid input",
+ },
+ },
+ "security": Array [
+ Object {
+ "petstore_auth": Array [
+ "write:pets",
+ "read:pets",
+ ],
+ },
+ ],
+ "summary": "Updates a pet in the store with form data",
+ "tags": Array [
+ "pet",
+ ],
+ },
+ },
+ "/pet/{petId}/uploadImage": Object {
+ "post": Object {
+ "description": "",
+ "operationId": "uploadFile",
+ "parameters": Array [
+ Object {
+ "description": "ID of pet to update",
+ "in": "path",
+ "name": "petId",
+ "required": true,
+ "schema": Object {
+ "format": "int64",
+ "type": "integer",
+ },
+ },
+ ],
+ "requestBody": Object {
+ "content": Object {
+ "application/octet-stream": Object {
+ "schema": Object {
+ "format": "binary",
+ "type": "string",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/ApiResponse",
+ },
+ },
+ },
+ "description": "successful operation",
+ },
+ },
+ "security": Array [
+ Object {
+ "petstore_auth": Array [
+ "write:pets",
+ "read:pets",
+ ],
+ },
+ ],
+ "summary": "uploads an image",
+ "tags": Array [
+ "pet",
+ ],
+ },
+ },
+ "/store/inventory": Object {
+ "get": Object {
+ "description": "Returns a map of status codes to quantities",
+ "operationId": "getInventory",
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "additionalProperties": Object {
+ "format": "int32",
+ "type": "integer",
+ },
+ "type": "object",
+ },
+ },
+ },
+ "description": "successful operation",
+ },
+ },
+ "security": Array [
+ Object {
+ "api_key": Array [],
+ },
+ ],
+ "summary": "Returns pet inventories by status",
+ "tags": Array [
+ "store",
+ ],
+ },
+ },
+ "/store/order": Object {
+ "post": Object {
+ "description": "",
+ "operationId": "placeOrder",
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/Order",
+ },
+ },
+ },
+ "description": "order placed for purchasing the pet",
+ "required": true,
+ },
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/Order",
+ },
+ },
+ "application/xml": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/Order",
+ },
+ },
+ },
+ "description": "successful operation",
+ },
+ "400": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Invalid Order",
+ "status": 400,
+ },
+ },
+ },
+ "description": "Invalid Order",
+ },
+ },
+ "summary": "Place an order for a pet",
+ "tags": Array [
+ "store",
+ ],
+ },
+ },
+ "/store/order/{orderId}": Object {
+ "delete": Object {
+ "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors",
+ "operationId": "deleteOrder",
+ "parameters": Array [
+ Object {
+ "description": "ID of the order that needs to be deleted",
+ "in": "path",
+ "name": "orderId",
+ "required": true,
+ "schema": Object {
+ "minimum": 1,
+ "type": "string",
+ },
+ },
+ ],
+ "responses": Object {
+ "400": Object {
+ "description": "Invalid ID supplied",
+ },
+ "404": Object {
+ "description": "Order not found",
+ },
+ },
+ "summary": "Delete purchase order by ID",
+ "tags": Array [
+ "store",
+ ],
+ },
+ "get": Object {
+ "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions",
+ "operationId": "getOrderById",
+ "parameters": Array [
+ Object {
+ "description": "ID of pet that needs to be fetched",
+ "in": "path",
+ "name": "orderId",
+ "required": true,
+ "schema": Object {
+ "format": "int64",
+ "maximum": 5,
+ "minimum": 1,
+ "type": "integer",
+ },
+ },
+ ],
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/Order",
+ },
+ },
+ "application/xml": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/Order",
+ },
+ },
+ },
+ "description": "successful operation",
+ },
+ "400": Object {
+ "description": "Invalid ID supplied",
+ },
+ "404": Object {
+ "description": "Order not found",
+ },
+ },
+ "summary": "Find purchase order by ID",
+ "tags": Array [
+ "store",
+ ],
+ },
+ },
+ "/store/subscribe": Object {
+ "post": Object {
+ "callbacks": Object {
+ "orderDelivered": Object {
+ "http://notificationServer.com?url={$request.body#/callbackUrl}&event={$request.body#/eventName}": Object {
+ "post": Object {
+ "deprecated": true,
+ "description": "A callback triggered every time an Order is delivered to the recipient",
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "orderId": Object {
+ "example": "123",
+ "type": "string",
+ },
+ "timestamp": Object {
+ "example": "2018-10-19T16:46:45Z",
+ "format": "date-time",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "200": Object {
+ "description": "Callback successfully processed and no retries will be performed",
+ },
+ },
+ "summary": "Order delivered",
+ },
+ },
+ },
+ "orderInProgress": Object {
+ "{$request.body#/callbackUrl}?event={$request.body#/eventName}": Object {
+ "post": Object {
+ "description": "A callback triggered every time an Order is updated status to \\"inProgress\\" (Description)",
+ "externalDocs": Object {
+ "description": "Find out more",
+ "url": "https://more-details.com/demo",
+ },
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "orderId": Object {
+ "example": "123",
+ "type": "string",
+ },
+ "status": Object {
+ "example": "inProgress",
+ "type": "string",
+ },
+ "timestamp": Object {
+ "example": "2018-10-19T16:46:45Z",
+ "format": "date-time",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ "application/xml": Object {
+ "example": "
+
+ 123
+ inProgress
+ 2018-10-19T16:46:45Z
+
+",
+ "schema": Object {
+ "properties": Object {
+ "orderId": Object {
+ "example": "123",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "someProp": Object {
+ "example": "123",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ },
+ "description": "Callback successfully processed and no retries will be performed",
+ },
+ "299": Object {
+ "description": "Response for cancelling subscription",
+ },
+ "500": Object {
+ "description": "Callback processing failed and retries will be performed",
+ },
+ },
+ "summary": "Order in Progress (Summary)",
+ "x-codeSamples": Array [
+ Object {
+ "lang": "C#",
+ "source": "PetStore.v1.Pet pet = new PetStore.v1.Pet();
+pet.setApiKey(\\"your api key\\");
+pet.petType = PetStore.v1.Pet.TYPE_DOG;
+pet.name = \\"Rex\\";
+// set other fields
+PetStoreResponse response = pet.create();
+if (response.statusCode == HttpStatusCode.Created)
+{
+ // Successfully created
+}
+else
+{
+ // Something wrong -- check response for errors
+ Console.WriteLine(response.getRawResponse());
+}
+",
+ },
+ Object {
+ "lang": "PHP",
+ "source": "$form = new \\\\PetStore\\\\Entities\\\\Pet();
+$form->setPetType(\\"Dog\\");
+$form->setName(\\"Rex\\");
+// set other fields
+try {
+ $pet = $client->pets()->create($form);
+} catch (UnprocessableEntityException $e) {
+ var_dump($e->getErrors());
+}
+",
+ },
+ ],
+ },
+ "put": Object {
+ "description": "Order in Progress (Only Description)",
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "orderId": Object {
+ "example": "123",
+ "type": "string",
+ },
+ "status": Object {
+ "example": "inProgress",
+ "type": "string",
+ },
+ "timestamp": Object {
+ "example": "2018-10-19T16:46:45Z",
+ "format": "date-time",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ "application/xml": Object {
+ "example": "
+
+ 123
+ inProgress
+ 2018-10-19T16:46:45Z
+
+",
+ "schema": Object {
+ "properties": Object {
+ "orderId": Object {
+ "example": "123",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "someProp": Object {
+ "example": "123",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ },
+ "description": "Callback successfully processed and no retries will be performed",
+ },
+ },
+ "servers": Array [
+ Object {
+ "description": "Operation level server 1 (Operation override)",
+ "url": "//callback-url.operation-level/v1",
+ },
+ Object {
+ "description": "Operation level server 2 (Operation override)",
+ "url": "//callback-url.operation-level/v2",
+ },
+ ],
+ },
+ "servers": Array [
+ Object {
+ "description": "Path level server 1",
+ "url": "//callback-url.path-level/v1",
+ },
+ Object {
+ "description": "Path level server 2",
+ "url": "//callback-url.path-level/v2",
+ },
+ ],
+ },
+ },
+ "orderShipped": Object {
+ "{$request.body#/callbackUrl}?event={$request.body#/eventName}": Object {
+ "post": Object {
+ "description": "Very long description
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
+incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
+nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
+fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
+culpa qui officia deserunt mollit anim id est laborum.
+",
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "estimatedDeliveryDate": Object {
+ "example": "2018-11-11T16:00:00Z",
+ "format": "date-time",
+ "type": "string",
+ },
+ "orderId": Object {
+ "example": "123",
+ "type": "string",
+ },
+ "timestamp": Object {
+ "example": "2018-10-19T16:46:45Z",
+ "format": "date-time",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "200": Object {
+ "description": "Callback successfully processed and no retries will be performed",
+ },
+ },
+ },
+ },
+ },
+ },
+ "description": "Add subscription for a store events",
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "callbackUrl": Object {
+ "description": "This URL will be called by the server when the desired event will occur",
+ "example": "https://myserver.com/send/callback/here",
+ "format": "uri",
+ "type": "string",
+ },
+ "eventName": Object {
+ "description": "Event name for the subscription",
+ "enum": Array [
+ "orderInProgress",
+ "orderShipped",
+ "orderDelivered",
+ ],
+ "example": "orderInProgress",
+ "type": "string",
+ },
+ },
+ "required": Array [
+ "callbackUrl",
+ "eventName",
+ ],
+ "type": "object",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "201": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "subscriptionId": Object {
+ "example": "AAA-123-BBB-456",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ },
+ },
+ "description": "Subscription added",
+ },
+ },
+ "summary": "Subscribe to the Store events",
+ "tags": Array [
+ "store",
+ ],
+ },
+ },
+ "/user": Object {
+ "post": Object {
+ "description": "This can only be done by the logged in user.",
+ "operationId": "createUser",
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/User",
+ },
+ },
+ },
+ "description": "Created user object",
+ "required": true,
+ },
+ "responses": Object {
+ "default": Object {
+ "description": "successful operation",
+ },
+ },
+ "summary": "Create user",
+ "tags": Array [
+ "user",
+ ],
+ },
+ },
+ "/user/createWithArray": Object {
+ "post": Object {
+ "description": "",
+ "operationId": "createUsersWithArrayInput",
+ "requestBody": Object {
+ "$ref": "#/components/requestBodies/UserArray",
+ },
+ "responses": Object {
+ "default": Object {
+ "description": "successful operation",
+ },
+ },
+ "summary": "Creates list of users with given input array",
+ "tags": Array [
+ "user",
+ ],
+ },
+ },
+ "/user/createWithList": Object {
+ "post": Object {
+ "description": "",
+ "operationId": "createUsersWithListInput",
+ "requestBody": Object {
+ "$ref": "#/components/requestBodies/UserArray",
+ },
+ "responses": Object {
+ "default": Object {
+ "description": "successful operation",
+ },
+ },
+ "summary": "Creates list of users with given input array",
+ "tags": Array [
+ "user",
+ ],
+ },
+ },
+ "/user/login": Object {
+ "get": Object {
+ "description": "",
+ "operationId": "loginUser",
+ "parameters": Array [
+ Object {
+ "description": "The user name for login",
+ "in": "query",
+ "name": "username",
+ "required": true,
+ "schema": Object {
+ "type": "string",
+ },
+ },
+ Object {
+ "description": "The password for login in clear text",
+ "in": "query",
+ "name": "password",
+ "required": true,
+ "schema": Object {
+ "type": "string",
+ },
+ },
+ ],
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "examples": Object {
+ "response": Object {
+ "value": "OK",
+ },
+ },
+ "schema": Object {
+ "type": "string",
+ },
+ },
+ "application/xml": Object {
+ "examples": Object {
+ "response": Object {
+ "value": " OK ",
+ },
+ },
+ "schema": Object {
+ "type": "string",
+ },
+ },
+ "text/plain": Object {
+ "examples": Object {
+ "response": Object {
+ "value": "OK",
+ },
+ },
+ },
+ },
+ "description": "successful operation",
+ "headers": Object {
+ "X-Expires-After": Object {
+ "description": "date in UTC when token expires",
+ "schema": Object {
+ "format": "date-time",
+ "type": "string",
+ },
+ },
+ "X-Rate-Limit": Object {
+ "description": "calls per hour allowed by the user",
+ "schema": Object {
+ "format": "int32",
+ "type": "integer",
+ },
+ },
+ },
+ },
+ "400": Object {
+ "description": "Invalid username/password supplied",
+ },
+ },
+ "summary": "Logs user into the system",
+ "tags": Array [
+ "user",
+ ],
+ },
+ },
+ "/user/logout": Object {
+ "get": Object {
+ "description": "",
+ "operationId": "logoutUser",
+ "responses": Object {
+ "default": Object {
+ "description": "successful operation",
+ },
+ },
+ "summary": "Logs out current logged in user session",
+ "tags": Array [
+ "user",
+ ],
+ },
+ },
+ "/user/{username}": Object {
+ "delete": Object {
+ "description": "This can only be done by the logged in user.",
+ "operationId": "deleteUser",
+ "parameters": Array [
+ Object {
+ "description": "The name that needs to be deleted",
+ "in": "path",
+ "name": "username",
+ "required": true,
+ "schema": Object {
+ "type": "string",
+ },
+ },
+ ],
+ "responses": Object {
+ "400": Object {
+ "description": "Invalid username supplied",
+ },
+ "404": Object {
+ "description": "User not found",
+ },
+ },
+ "summary": "Delete user",
+ "tags": Array [
+ "user",
+ ],
+ },
+ "get": Object {
+ "description": "",
+ "operationId": "getUserByName",
+ "parameters": Array [
+ Object {
+ "description": "The name that needs to be fetched. Use user1 for testing. ",
+ "in": "path",
+ "name": "username",
+ "required": true,
+ "schema": Object {
+ "type": "string",
+ },
+ },
+ ],
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/User",
+ },
+ },
+ "application/xml": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/User",
+ },
+ },
+ },
+ "description": "successful operation",
+ },
+ "400": Object {
+ "description": "Invalid username supplied",
+ },
+ "404": Object {
+ "description": "User not found",
+ },
+ },
+ "summary": "Get user by user name",
+ "tags": Array [
+ "user",
+ ],
+ },
+ "put": Object {
+ "description": "This can only be done by the logged in user.",
+ "operationId": "updateUser",
+ "parameters": Array [
+ Object {
+ "description": "name that need to be deleted",
+ "in": "path",
+ "name": "username",
+ "required": true,
+ "schema": Object {
+ "type": "string",
+ },
+ },
+ ],
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/User",
+ },
+ },
+ },
+ "description": "Updated user object",
+ "required": true,
+ },
+ "responses": Object {
+ "400": Object {
+ "description": "Invalid user supplied",
+ },
+ "404": Object {
+ "description": "User not found",
+ },
+ },
+ "summary": "Updated user",
+ "tags": Array [
+ "user",
+ ],
+ },
+ },
+ },
+ "servers": Array [
+ Object {
+ "description": "Default server",
+ "url": "//petstore.swagger.io/v2",
+ },
+ Object {
+ "description": "Sandbox server",
+ "url": "//petstore.swagger.io/sandbox",
+ },
+ ],
+ "tags": Array [
+ Object {
+ "description": "Everything about your Pets",
+ "name": "pet",
+ },
+ Object {
+ "description": "Access to Petstore orders",
+ "name": "store",
+ },
+ Object {
+ "description": "Operations about user",
+ "name": "user",
+ },
+ Object {
+ "description": "Everything about your Webhooks",
+ "name": "webhooks",
+ },
+ Object {
+ "description": "
+",
+ "name": "pet_model",
+ "x-displayName": "The Pet Model",
+ },
+ Object {
+ "description": "
+",
+ "name": "store_model",
+ "x-displayName": "The Order Model",
+ },
+ ],
+ "webhooks": Object {
+ "myWebhook": Object {
+ "$ref": "#/components/pathItems/webhooks",
+ "description": "Overriding description",
+ "summary": "Overriding summary",
+ },
+ "newPet": Object {
+ "post": Object {
+ "description": "Information about a new pet in the systems",
+ "operationId": "newPet",
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/Pet",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "200": Object {
+ "description": "Return a 200 status to indicate that the data was received successfully",
+ },
+ },
+ "summary": "New pet",
+ "tags": Array [
+ "webhooks",
+ ],
+ },
+ },
+ },
+ "x-tagGroups": Array [
+ Object {
+ "name": "General",
+ "tags": Array [
+ "pet",
+ "store",
+ "webhooks",
+ ],
+ },
+ Object {
+ "name": "User Management",
+ "tags": Array [
+ "user",
+ ],
+ },
+ Object {
+ "name": "Models",
+ "tags": Array [
+ "pet_model",
+ "store_model",
+ ],
+ },
+ ],
+}
+`;
+
exports[`#loadAndBundleSpec should load And Bundle Spec demo/swagger.yaml 1`] = `
Object {
"components": Object {
diff --git a/src/utils/__tests__/loadAndBundleSpec.test.ts b/src/utils/__tests__/loadAndBundleSpec.test.ts
index 74dd2c88..460a1a8b 100644
--- a/src/utils/__tests__/loadAndBundleSpec.test.ts
+++ b/src/utils/__tests__/loadAndBundleSpec.test.ts
@@ -1,17 +1,23 @@
-import * as yaml from 'yaml-js';
+import * as yaml from 'js-yaml';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { loadAndBundleSpec } from '../loadAndBundleSpec';
describe('#loadAndBundleSpec', () => {
it('should load And Bundle Spec demo/openapi.yaml', async () => {
- const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/openapi.yaml')));
+ const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/openapi.yaml'), 'utf-8'));
+ const bundledSpec = await loadAndBundleSpec(spec);
+ expect(bundledSpec).toMatchSnapshot();
+ });
+
+ it('should load And Bundle Spec demo/openapi-3-1.yaml', async () => {
+ const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/openapi-3-1.yaml'), 'utf-8'));
const bundledSpec = await loadAndBundleSpec(spec);
expect(bundledSpec).toMatchSnapshot();
});
it('should load And Bundle Spec demo/swagger.yaml', async () => {
- const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/swagger.yaml')));
+ const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/swagger.yaml'), 'utf-8'));
const bundledSpec = await loadAndBundleSpec(spec);
expect(bundledSpec).toMatchSnapshot();
});
diff --git a/src/utils/__tests__/openapi.test.ts b/src/utils/__tests__/openapi.test.ts
index 9e511259..d4f23e02 100644
--- a/src/utils/__tests__/openapi.test.ts
+++ b/src/utils/__tests__/openapi.test.ts
@@ -101,6 +101,13 @@ describe('Utils', () => {
expect(getOperationSummary(operation as any).length).toBe(50);
});
+ it('Should return pathName if no summary, operationId, description', () => {
+ const operation = {
+ pathName: '/sandbox/test'
+ };
+ expect(getOperationSummary(operation as any)).toBe('/sandbox/test');
+ });
+
it('Should return if no info', () => {
const operation = {
description: undefined,
@@ -167,6 +174,79 @@ describe('Utils', () => {
expect(isPrimitiveType(schema)).toEqual(false);
});
+ it('should return true for array contains object and schema hasn\'t properties', () => {
+ const schema = {
+ type: ['object', 'string'],
+ };
+ expect(isPrimitiveType(schema)).toEqual(true);
+ });
+
+ it('should return false for array contains object and schema has properties', () => {
+ const schema = {
+ type: ['object', 'string'],
+ properties: {
+ a: {
+ type: 'string',
+ },
+ },
+ };
+ expect(isPrimitiveType(schema)).toEqual(false);
+ });
+
+ it('should return false for array contains array type and schema has items', () => {
+ const schema = {
+ type: ['array'],
+ items: {
+ type: 'object',
+ additionalProperties: true,
+ },
+ };
+ expect(isPrimitiveType(schema)).toEqual(false);
+ });
+
+ it('should return false for array contains object and array types and schema has items', () => {
+ const schema = {
+ type: ['array', 'object'],
+ items: {
+ type: 'object',
+ additionalProperties: true,
+ },
+ };
+ expect(isPrimitiveType(schema)).toEqual(false);
+ });
+
+ it('should return false for array contains object and array types and schema has properties', () => {
+ const schema = {
+ type: ['array', 'object'],
+ properties: {
+ a: {
+ type: 'string',
+ },
+ },
+ };
+ expect(isPrimitiveType(schema)).toEqual(false);
+ });
+
+ it('should return true for array contains array of strings', () => {
+ const schema = {
+ type: 'array',
+ items: {
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ },
+ };
+ expect(isPrimitiveType(schema)).toEqual(true);
+ });
+
+ it('Should return false for array of string which include the null value', () => {
+ const schema = {
+ type: ['object', 'string', 'null'],
+ };
+ expect(isPrimitiveType(schema)).toEqual(true);
+ });
+
it('Should return false for array with non-empty objects', () => {
const schema = {
type: 'array',
diff --git a/src/utils/highlight.ts b/src/utils/highlight.ts
index d86ee374..036b9129 100644
--- a/src/utils/highlight.ts
+++ b/src/utils/highlight.ts
@@ -19,6 +19,7 @@ import 'prismjs/components/prism-ruby.js';
import 'prismjs/components/prism-scala.js';
import 'prismjs/components/prism-sql.js';
import 'prismjs/components/prism-swift.js';
+import 'prismjs/components/prism-yaml.js';
const DEFAULT_LANG = 'clike';
diff --git a/src/utils/loadAndBundleSpec.ts b/src/utils/loadAndBundleSpec.ts
index 092e5771..e51aad5c 100644
--- a/src/utils/loadAndBundleSpec.ts
+++ b/src/utils/loadAndBundleSpec.ts
@@ -1,4 +1,10 @@
-import { Source, Document, bundle, Config } from '@redocly/openapi-core';
+import type { Source, Document } from '@redocly/openapi-core';
+
+// eslint-disable-next-line import/no-internal-modules
+import { bundle } from '@redocly/openapi-core/lib/bundle';
+// eslint-disable-next-line import/no-internal-modules
+import { Config } from '@redocly/openapi-core/lib/config/config';
+
/* tslint:disable-next-line:no-implicit-dependencies */
import { convertObj } from 'swagger2openapi';
import { OpenAPISpec } from '../types';
diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts
index e20dbef9..ad267722 100644
--- a/src/utils/openapi.ts
+++ b/src/utils/openapi.ts
@@ -1,12 +1,12 @@
import { dirname } from 'path';
import * as URLtemplate from 'url-template';
+import { ExtendedOpenAPIOperation } from '../services';
import { FieldModel } from '../services/models';
import { OpenAPIParser } from '../services/OpenAPIParser';
import {
OpenAPIEncoding,
OpenAPIMediaType,
- OpenAPIOperation,
OpenAPIParameter,
OpenAPIParameterStyle,
OpenAPISchema,
@@ -56,17 +56,19 @@ const operationNames = {
patch: true,
delete: true,
options: true,
+ $ref: true,
};
export function isOperationName(key: string): boolean {
return key in operationNames;
}
-export function getOperationSummary(operation: OpenAPIOperation): string {
+export function getOperationSummary(operation: ExtendedOpenAPIOperation): string {
return (
operation.summary ||
operation.operationId ||
(operation.description && operation.description.substring(0, 50)) ||
+ operation.pathName ||
''
);
}
@@ -81,6 +83,8 @@ const schemaKeywordTypes = {
maxLength: 'string',
minLength: 'string',
pattern: 'string',
+ contentEncoding: 'string',
+ contentMediaType: 'string',
items: 'array',
maxItems: 'array',
@@ -95,7 +99,7 @@ const schemaKeywordTypes = {
};
export function detectType(schema: OpenAPISchema): string {
- if (schema.type !== undefined) {
+ if (schema.type !== undefined && !Array.isArray(schema.type)) {
return schema.type;
}
const keywords = Object.keys(schemaKeywordTypes);
@@ -109,25 +113,25 @@ export function detectType(schema: OpenAPISchema): string {
return 'any';
}
-export function isPrimitiveType(schema: OpenAPISchema, type: string | undefined = schema.type) {
+export function isPrimitiveType(schema: OpenAPISchema, type: string | string[] | undefined = schema.type) {
if (schema.oneOf !== undefined || schema.anyOf !== undefined) {
return false;
}
- if (type === 'object') {
- return schema.properties !== undefined
+ let isPrimitive = true;
+ const isArray = Array.isArray(type);
+
+ if (type === 'object' || (isArray && type?.includes('object'))) {
+ isPrimitive = schema.properties !== undefined
? Object.keys(schema.properties).length === 0
: schema.additionalProperties === undefined;
}
- if (type === 'array') {
- if (schema.items === undefined) {
- return true;
- }
- return false;
+ if (schema.items !== undefined && (type === 'array' || (isArray && type?.includes('array')))) {
+ isPrimitive = isPrimitiveType(schema.items, schema.items.type);
}
- return true;
+ return isPrimitive;
}
export function isJsonLike(contentType: string): boolean {
@@ -366,12 +370,12 @@ export function langFromMime(contentType: string): string {
}
export function isNamedDefinition(pointer?: string): boolean {
- return /^#\/components\/schemas\/[^\/]+$/.test(pointer || '');
+ return /^#\/components\/(schemas|pathItems)\/[^\/]+$/.test(pointer || '');
}
export function getDefinitionName(pointer?: string): string | undefined {
if (!pointer) return undefined;
- const match = pointer.match(/^#\/components\/schemas\/([^\/]+)$/);
+ const match = pointer.match(/^#\/components\/(schemas|pathItems)\/([^\/]+)$/);
return match === null ? undefined : match[1]
}
@@ -444,6 +448,18 @@ export function humanizeConstraints(schema: OpenAPISchema): string[] {
numberRange += schema.minimum;
}
+ if (typeof schema.exclusiveMinimum === 'number' || typeof schema.exclusiveMaximum === 'number') {
+ let minimum = 0;
+ let maximum = 0;
+ if (schema.minimum) minimum = schema.minimum;
+ if (typeof schema.exclusiveMinimum === 'number') minimum = minimum <= schema.exclusiveMinimum ? minimum : schema.exclusiveMinimum;
+
+ if (schema.maximum) maximum = schema.maximum;
+ if (typeof schema.exclusiveMaximum === 'number') maximum = maximum > schema.exclusiveMaximum ? maximum : schema.exclusiveMaximum;
+
+ numberRange = `[${minimum} .. ${maximum}]`
+ }
+
if (numberRange !== undefined) {
res.push(numberRange);
}
@@ -573,10 +589,10 @@ export function setSecuritySchemePrefix(prefix: string) {
}
export const shortenHTTPVerb = verb =>
- ({
- delete: 'del',
- options: 'opts',
- }[verb] || verb);
+({
+ delete: 'del',
+ options: 'opts',
+}[verb] || verb);
export function isRedocExtension(key: string): boolean {
const redocExtensions = {
diff --git a/webpack.config.ts b/webpack.config.ts
index 8e452b67..dee1ada4 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -2,6 +2,7 @@
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import * as webpack from 'webpack';
import * as path from 'path';
+import { getBabelLoader, webpackIgnore } from './config/webpack-utils';
const nodeExternals = require('webpack-node-externals')({
// bundle in modules that need transpiling + non-js (e.g. css)
@@ -31,7 +32,7 @@ const BANNER = `ReDoc - OpenAPI/Swagger-generated API Reference Documentation
Version: ${VERSION}
Repo: https://github.com/Redocly/redoc`;
-export default (env: { standalone?: boolean } = {}, { mode }) => ({
+export default (env: { standalone?: boolean } = {}) => ({
entry: env.standalone ? ['./src/polyfills.ts', './src/standalone.tsx'] : './src/index.ts',
output: {
filename: env.standalone ? 'redoc.standalone.js' : 'redoc.lib.js',
@@ -42,18 +43,20 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
},
devtool: 'source-map',
resolve: {
- extensions: ['.ts', '.tsx', '.js', '.json'],
- },
- node: {
- fs: 'empty',
+ extensions: ['.ts', '.tsx', '.js', '.mjs', '.json'],
+ fallback: {
+ path: require.resolve('path-browserify'),
+ http: false,
+ fs: false,
+ os: false,
+ }
},
performance: false,
- optimization: {
- minimize: !!env.standalone,
- },
+ // target: 'node',
+ externalsPresets: env.standalone ? {} : { node: true },
externals: env.standalone
? {
- esprima: 'esprima',
+ esprima: 'null',
'node-fetch': 'null',
'node-fetch-h2': 'null',
yaml: 'null',
@@ -61,7 +64,7 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
}
: (context, request, callback) => {
// ignore node-fetch dep of swagger2openapi as it is not used
- if (/esprima|node-fetch|node-fetch-h2|yaml|safe-json-stringify$/i.test(request)) {
+ if (/esprima|node-fetch|node-fetch-h2|\/yaml|safe-json-stringify$/i.test(request)) {
return callback(null, 'var undefined');
}
return nodeExternals(context, request, callback);
@@ -70,51 +73,22 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
module: {
rules: [
{
- test: /\.tsx?$/,
- use: [
- {
- loader: 'ts-loader',
- options: {
- compilerOptions: {
- module: 'es2015',
- declaration: false,
- },
- },
- },
- {
- loader: 'babel-loader',
- options: {
- generatorOpts: {
- decoratorsBeforeExport: true,
- },
- plugins: [
- ['@babel/plugin-syntax-typescript', { isTSX: true }],
- ['@babel/plugin-syntax-decorators', { legacy: true }],
- '@babel/plugin-syntax-jsx',
- [
- 'babel-plugin-styled-components',
- {
- minify: true,
- displayName: mode !== 'production',
- },
- ],
- ],
- },
- },
- ],
- exclude: [/node_modules/],
- },
- {
- test: /node_modules\/(swagger2openapi|reftools|oas-resolver|oas-kit-common|oas-schema-walker)\/.*\.js$/,
- use: {
- loader: 'ts-loader',
- options: {
- instance: 'ts2js-transpiler-only',
- transpileOnly: true,
- compilerOptions: {
- allowJs: true,
- declaration: false,
- },
+ test: /\.(tsx?|[cm]?js)$/,
+ use: [getBabelLoader({useBuiltIns: !!env.standalone})],
+ exclude: {
+ and: [/node_modules/],
+ not: {
+ or: [
+ /swagger2openapi/,
+ /reftools/,
+ /openapi-sampler/,
+ /mobx/,
+ /oas-resolver/,
+ /oas-kit-common/,
+ /oas-schema-walker/,
+ /\@redocly\/openapi-core/,
+ /colorette/,
+ ],
},
},
},
@@ -127,22 +101,19 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
},
},
},
- { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
],
},
plugins: [
new webpack.DefinePlugin({
__REDOC_VERSION__: VERSION,
__REDOC_REVISION__: REVISION,
+ 'process.env': '{}',
+ 'process.platform': '"browser"',
+ 'process.stdout': 'null',
}),
new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }),
new webpack.BannerPlugin(BANNER),
- ignore(/js-yaml\/dumper\.js$/),
- ignore(/json-schema-ref-parser\/lib\/dereference\.js/),
- env.standalone ? ignore(/^\.\/SearchWorker\.worker$/) : ignore(/$non-existing^/),
- ],
+ webpackIgnore(/js-yaml\/dumper\.js$/),
+ env.standalone ? webpackIgnore(/^\.\/SearchWorker\.worker$/) : undefined,
+ ].filter(Boolean),
});
-
-function ignore(regexp) {
- return new webpack.NormalModuleReplacementPlugin(regexp, require.resolve('lodash/noop.js'));
-}