From 2f85d42360b5ab8f6ee893448396d17703a6439e Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Tue, 18 May 2021 12:09:04 +0300 Subject: [PATCH 01/21] feat: add support array in field type --- src/services/models/Schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index dd4a4d6f35..674afcbe61 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -114,7 +114,7 @@ export class SchemaModel { this.externalDocs = schema.externalDocs; this.constraints = humanizeConstraints(schema); - this.displayType = this.type; + this.displayType = Array.isArray(this.type) ? this.type.join(' or ') : this.type; this.displayFormat = this.format; this.isPrimitive = isPrimitiveType(schema, this.type); this.default = schema.default; From e03c4e86d096851175f6975f7e7646a99d873d07 Mon Sep 17 00:00:00 2001 From: Anton_Kozachuk Date: Tue, 18 May 2021 14:06:30 +0300 Subject: [PATCH 02/21] add nullable lable for OAS 3.1 JSON schema --- src/services/models/Schema.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index 674afcbe61..e6fbe4345d 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -76,6 +76,12 @@ export class SchemaModel { this.pointer = schemaOrRef.$ref || pointer || ''; this.rawSchema = parser.deref(schemaOrRef); + + if (Array.isArray(this.rawSchema.type)) { + this.rawSchema.oneOf = this.rawSchema.type.map( type => ({type})); + delete this.rawSchema.type; + } + this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild); this.init(parser, isChild); @@ -138,7 +144,8 @@ export class SchemaModel { } if (schema.oneOf !== undefined) { - this.initOneOf(schema.oneOf, parser); + this.nullable = this.nullable || schema.oneOf.some(s => s.type === 'null'); + this.initOneOf(schema.oneOf.filter(s => s.type !== 'null'), parser); this.oneOfType = 'One of'; if (schema.anyOf !== undefined) { console.warn( @@ -149,7 +156,8 @@ export class SchemaModel { } if (schema.anyOf !== undefined) { - this.initOneOf(schema.anyOf, parser); + this.nullable = this.nullable || schema.anyOf.some(s => s.type === 'null'); + this.initOneOf(schema.anyOf.filter(s => s.type !== 'null'), parser); this.oneOfType = 'Any of'; return; } From c342f6f0f5a019742af1459124026d52b7996645 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Wed, 19 May 2021 12:07:53 +0300 Subject: [PATCH 03/21] feat: add basic support openApi 3.1 --- e2e/integration/menu.e2e.ts | 2 +- package-lock.json | 12 +++++----- package.json | 2 +- src/common-elements/fields.ts | 5 ---- src/components/Fields/EnumValues.tsx | 2 +- src/components/Fields/FieldDetails.tsx | 2 -- src/components/Redoc/Redoc.tsx | 2 +- .../DiscriminatorDropdown.test.tsx.snap | 2 -- src/services/Labels.ts | 2 -- src/services/MenuBuilder.ts | 9 +++---- src/services/models/Schema.ts | 24 +++++++++---------- src/types/open-api.d.ts | 3 ++- src/utils/__tests__/openapi.test.ts | 14 +++++++++++ src/utils/openapi.ts | 6 +++-- 14 files changed, 46 insertions(+), 41 deletions(-) diff --git a/e2e/integration/menu.e2e.ts b/e2e/integration/menu.e2e.ts index e1b053d125..fc703b6c12 100644 --- a/e2e/integration/menu.e2e.ts +++ b/e2e/integration/menu.e2e.ts @@ -6,7 +6,7 @@ describe('Menu', () => { it('should have valid items count', () => { cy.get('.menu-content') .find('li') - .should('have.length', 34); + .should('have.length', 33); }); it('should sync active menu items while scroll', () => { diff --git a/package-lock.json b/package-lock.json index 35abf30297..7183d16929 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1222,9 +1222,9 @@ } }, "@redocly/openapi-core": { - "version": "1.0.0-beta.44", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.44.tgz", - "integrity": "sha512-9HNnh1MzvMsLK1liuidFBqWiAsZ2Yg3RY58fcEsy0QruSMdDbn7SoeI1qnXe6O+BkBS+vAP4oVzZDMHCMKGsOQ==", + "version": "1.0.0-beta.46", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.46.tgz", + "integrity": "sha512-w4VG2KNLFDuZgN7fBmbzbI0GJDiPnJ0SYszj4uuSkMW35SVTvDWyTaeWjW8ggQJ3gluDnTgUvm9tjLdR2tLqUg==", "requires": { "@redocly/ajv": "^6.12.3", "@types/node": "^14.11.8", @@ -1238,9 +1238,9 @@ }, "dependencies": { "@types/node": { - "version": "14.14.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz", - "integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==" + "version": "14.17.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.0.tgz", + "integrity": "sha512-w8VZUN/f7SSbvVReb9SWp6cJFevxb4/nkG65yLAya//98WgocKm5PLDAtSs5CtJJJM+kHmJjO/6mmYW4MHShZA==" } } }, diff --git a/package.json b/package.json index 7f80c432f0..d8fe29a448 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "styled-components": "^4.1.1 || ^5.1.1" }, "dependencies": { - "@redocly/openapi-core": "^1.0.0-beta.44", + "@redocly/openapi-core": "^1.0.0-beta.45", "@redocly/react-dropdown-aria": "^2.0.11", "@types/node": "^13.11.1", "classnames": "^2.2.6", diff --git a/src/common-elements/fields.ts b/src/common-elements/fields.ts index de7c651329..c8d0df1b0d 100644 --- a/src/common-elements/fields.ts +++ b/src/common-elements/fields.ts @@ -61,11 +61,6 @@ export const RecursiveLabel = styled(FieldLabel)` font-size: 13px; `; -export const NullableLabel = styled(FieldLabel)` - color: #0e7c86; - font-size: 13px; -`; - export const PatternLabel = styled(FieldLabel)` color: #0e7c86; &::before, diff --git a/src/components/Fields/EnumValues.tsx b/src/components/Fields/EnumValues.tsx index 978426f665..d10f9f692a 100644 --- a/src/components/Fields/EnumValues.tsx +++ b/src/components/Fields/EnumValues.tsx @@ -8,7 +8,7 @@ import { RedocRawOptions } from '../../services/RedocNormalizedOptions'; export interface EnumValuesProps { values: string[]; - type: string; + type: string | string[]; } export interface EnumValuesState { diff --git a/src/components/Fields/FieldDetails.tsx b/src/components/Fields/FieldDetails.tsx index 980eff4fb3..ac362a4059 100644 --- a/src/components/Fields/FieldDetails.tsx +++ b/src/components/Fields/FieldDetails.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { - NullableLabel, PatternLabel, RecursiveLabel, TypeFormat, @@ -79,7 +78,6 @@ export class FieldDetails extends React.PureComponent ({schema.title}) } - {schema.nullable && {l('nullable')} } {schema.pattern && !hideSchemaPattern && ( <> diff --git a/src/components/Redoc/Redoc.tsx b/src/components/Redoc/Redoc.tsx index 2b43e77d0a..a3d0eef7cd 100644 --- a/src/components/Redoc/Redoc.tsx +++ b/src/components/Redoc/Redoc.tsx @@ -39,7 +39,7 @@ export class Redoc extends React.Component { const store = this.props.store; return ( - + diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap index 745c4cad4c..f660e397d3 100644 --- a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap @@ -29,7 +29,6 @@ exports[`Components SchemaView discriminator should correctly render discriminat "format": undefined, "isCircular": undefined, "isPrimitive": true, - "nullable": false, "options": "<<>>", "pattern": undefined, "pointer": "#/components/schemas/Dog/properties/packSize", @@ -79,7 +78,6 @@ exports[`Components SchemaView discriminator should correctly render discriminat "format": undefined, "isCircular": undefined, "isPrimitive": true, - "nullable": false, "options": "<<>>", "pattern": undefined, "pointer": "#/components/schemas/Dog/properties/type", diff --git a/src/services/Labels.ts b/src/services/Labels.ts index c3568eb1ef..f5fa1fd14a 100644 --- a/src/services/Labels.ts +++ b/src/services/Labels.ts @@ -6,7 +6,6 @@ export interface LabelsConfig { deprecated: string; example: string; examples: string; - nullable: string; recursive: string; arrayOf: string; webhook: string; @@ -22,7 +21,6 @@ const labels: LabelsConfig = { deprecated: 'Deprecated', example: 'Example', examples: 'Examples', - nullable: 'Nullable', recursive: 'Recursive', arrayOf: 'Array of ', webhook: 'Event', diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts index 339ba6cf18..907a025613 100644 --- a/src/services/MenuBuilder.ts +++ b/src/services/MenuBuilder.ts @@ -220,10 +220,11 @@ export class MenuBuilder { for (const tag of spec.tags || []) { tags[tag.name] = { ...tag, operations: [] }; } - - getTags(spec.paths); - if (spec['x-webhooks']) { - getTags(spec['x-webhooks'], true); + if (spec.paths){ + getTags(spec.paths); + } + if (spec.webhooks) { + getTags(spec.webhooks, true); } function getTags(paths: OpenAPIPaths, isWebhook?: boolean) { diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index e6fbe4345d..fb0c516442 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; @@ -77,11 +77,6 @@ export class SchemaModel { this.pointer = schemaOrRef.$ref || pointer || ''; this.rawSchema = parser.deref(schemaOrRef); - if (Array.isArray(this.rawSchema.type)) { - this.rawSchema.oneOf = this.rawSchema.type.map( type => ({type})); - delete this.rawSchema.type; - } - this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild); this.init(parser, isChild); @@ -110,9 +105,8 @@ export class SchemaModel { this.title = schema.title || (isNamedDefinition(this.pointer) && JsonPointer.baseName(this.pointer)) || ''; this.description = schema.description || ''; - this.type = schema.type || detectType(schema); + this.type = (Array.isArray(schema.type) && schema.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; @@ -120,13 +114,19 @@ export class SchemaModel { this.externalDocs = schema.externalDocs; this.constraints = humanizeConstraints(schema); - this.displayType = Array.isArray(this.type) ? this.type.join(' or ') : this.type; this.displayFormat = this.format; this.isPrimitive = isPrimitiveType(schema, this.type); this.default = schema.default; this.readOnly = !!schema.readOnly; this.writeOnly = !!schema.writeOnly; + if (!!schema.nullable) { + if (Array.isArray(this.type)) this.type.push('null'); + else this.type = [this.type, 'null']; + } + + this.displayType = Array.isArray(this.type) ? this.type.join(' or ') : this.type; + if (this.isCircular) { return; } @@ -144,8 +144,7 @@ export class SchemaModel { } if (schema.oneOf !== undefined) { - this.nullable = this.nullable || schema.oneOf.some(s => s.type === 'null'); - this.initOneOf(schema.oneOf.filter(s => s.type !== 'null'), parser); + this.initOneOf(schema.oneOf, parser); this.oneOfType = 'One of'; if (schema.anyOf !== undefined) { console.warn( @@ -156,8 +155,7 @@ export class SchemaModel { } if (schema.anyOf !== undefined) { - this.nullable = this.nullable || schema.anyOf.some(s => s.type === 'null'); - this.initOneOf(schema.anyOf.filter(s => s.type !== 'null'), parser); + this.initOneOf(schema.anyOf, parser); this.oneOfType = 'Any of'; return; } diff --git a/src/types/open-api.d.ts b/src/types/open-api.d.ts index 7134890ea0..f235632672 100644 --- a/src/types/open-api.d.ts +++ b/src/types/open-api.d.ts @@ -10,6 +10,7 @@ export interface OpenAPISpec { tags?: OpenAPITag[]; externalDocs?: OpenAPIExternalDocumentation; 'x-webhooks'?: OpenAPIPaths; + webhooks?: OpenAPIPaths; } export interface OpenAPIInfo { @@ -107,7 +108,7 @@ export interface OpenAPIExample { export interface OpenAPISchema { $ref?: string; - type?: string; + type?: string | string[]; properties?: { [name: string]: OpenAPISchema }; additionalProperties?: boolean | OpenAPISchema; description?: string; diff --git a/src/utils/__tests__/openapi.test.ts b/src/utils/__tests__/openapi.test.ts index 7050f36f16..2045c49e49 100644 --- a/src/utils/__tests__/openapi.test.ts +++ b/src/utils/__tests__/openapi.test.ts @@ -174,6 +174,20 @@ describe('Utils', () => { expect(isPrimitiveType(schema)).toEqual(false); }); + it('Should return false for array of strings', () => { + const schema = { + type: ['object', 'string'], + }; + expect(isPrimitiveType(schema)).toEqual(false); + }); + + it('Should return false for array of string which include the null value', () => { + const schema = { + type: ['object', 'string', 'null'], + }; + expect(isPrimitiveType(schema)).toEqual(false); + }); + it('Should return false for array with non-empty objects', () => { const schema = { type: 'array', diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index 71af2572be..ff9c6afd25 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -96,7 +96,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); @@ -110,7 +110,7 @@ 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; } @@ -128,6 +128,8 @@ export function isPrimitiveType(schema: OpenAPISchema, type: string | undefined return false; } + if (Array.isArray(type)) return false + return true; } From 17d05667afda400c21c77aa5c6c4ba2f46a4517f Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Fri, 21 May 2021 16:37:03 +0300 Subject: [PATCH 04/21] feat: add webhooks supporting --- e2e/integration/menu.e2e.ts | 2 +- src/services/MenuBuilder.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/e2e/integration/menu.e2e.ts b/e2e/integration/menu.e2e.ts index fc703b6c12..e1b053d125 100644 --- a/e2e/integration/menu.e2e.ts +++ b/e2e/integration/menu.e2e.ts @@ -6,7 +6,7 @@ describe('Menu', () => { it('should have valid items count', () => { cy.get('.menu-content') .find('li') - .should('have.length', 33); + .should('have.length', 34); }); it('should sync active menu items while scroll', () => { diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts index 907a025613..8a45bb9b9c 100644 --- a/src/services/MenuBuilder.ts +++ b/src/services/MenuBuilder.ts @@ -217,15 +217,18 @@ export class MenuBuilder { */ static getTagsWithOperations(spec: OpenAPISpec): TagsInfoMap { const tags: TagsInfoMap = {}; + const webhooks = spec['x-webhooks'] || spec.webhooks; for (const tag of spec.tags || []) { tags[tag.name] = { ...tag, operations: [] }; } + + if (webhooks) { + getTags(webhooks, true); + } + if (spec.paths){ getTags(spec.paths); } - if (spec.webhooks) { - getTags(spec.webhooks, true); - } function getTags(paths: OpenAPIPaths, isWebhook?: boolean) { for (const pathName of Object.keys(paths)) { From 33ddbbd9f0f39c7da0855c0e211c7de6cb1564ea Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Fri, 21 May 2021 17:39:15 +0300 Subject: [PATCH 05/21] feat: add summary and indentifier to license --- src/components/ApiInfo/ApiInfo.tsx | 5 +++-- src/services/AppStore.ts | 3 +++ src/services/__tests__/models/ApiInfo.test.ts | 12 ++++++++++++ src/services/models/ApiInfo.ts | 2 ++ src/types/open-api.d.ts | 2 ++ 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components/ApiInfo/ApiInfo.tsx b/src/components/ApiInfo/ApiInfo.tsx index 91a1de2259..26819db0c7 100644 --- a/src/components/ApiInfo/ApiInfo.tsx +++ b/src/components/ApiInfo/ApiInfo.tsx @@ -38,7 +38,7 @@ export class ApiInfo extends React.Component { const license = (info.license && ( - License: {info.license.name} + License: {info.license.identifier ? info.license.identifier : ({info.license.name})} )) || null; @@ -100,7 +100,8 @@ export class ApiInfo extends React.Component { )) || null} - + + {externalDocs && } diff --git a/src/services/AppStore.ts b/src/services/AppStore.ts index 9387c521f0..7680541910 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/__tests__/models/ApiInfo.test.ts b/src/services/__tests__/models/ApiInfo.test.ts index 5adb6036b7..457937ecfd 100644 --- a/src/services/__tests__/models/ApiInfo.test.ts +++ b/src/services/__tests__/models/ApiInfo.test.ts @@ -34,5 +34,17 @@ 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'); + }); }); }); diff --git a/src/services/models/ApiInfo.ts b/src/services/models/ApiInfo.ts index fea480aa16..517538db0b 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/types/open-api.d.ts b/src/types/open-api.d.ts index f235632672..96f98f36ca 100644 --- a/src/types/open-api.d.ts +++ b/src/types/open-api.d.ts @@ -18,6 +18,7 @@ export interface OpenAPIInfo { version: string; description?: string; + summary?: string; termsOfService?: string; contact?: OpenAPIContact; license?: OpenAPILicense; @@ -272,4 +273,5 @@ export interface OpenAPIContact { export interface OpenAPILicense { name: string; url?: string; + identifier?: string; } From 84b944939c0ad3ab07b4cc9da60be1c08b35a33c Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Mon, 24 May 2021 16:49:46 +0300 Subject: [PATCH 06/21] chore: upgrage openapi-sampler --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7183d16929..cadcbdb32b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1459,8 +1459,7 @@ "@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", - "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", - "dev": true + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" }, "@types/json5": { "version": "0.0.29", @@ -11693,11 +11692,12 @@ "dev": true }, "openapi-sampler": { - "version": "1.0.0-beta.18", - "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.0-beta.18.tgz", - "integrity": "sha512-nG/0kvvSY5FbrU5A+Dbp1xTQN++7pKIh87/atryZlxrzDuok5Y6TCbpxO1jYqpUKLycE4ReKGHCywezngG6xtQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.0.tgz", + "integrity": "sha512-HysKj4ZuLk0RpZkopao5SIupUX8LMOTsEDTw9dSzcRv6BBW6Ep1IdbKwYsCrYM9tnw4VZtebR/N5sJHY6qqRew==", "requires": { - "json-pointer": "^0.6.0" + "@types/json-schema": "^7.0.7", + "json-pointer": "^0.6.1" } }, "opn": { diff --git a/package.json b/package.json index d8fe29a448..3c87d54877 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "marked": "^0.7.0", "memoize-one": "~5.1.1", "mobx-react": "^7.0.5", - "openapi-sampler": "^1.0.0-beta.18", + "openapi-sampler": "^1.0.0", "perfect-scrollbar": "^1.4.0", "polished": "^3.6.5", "prismjs": "^1.22.0", From e627583da8fb7df7378f31a8a2d17a4e360d6296 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Mon, 24 May 2021 17:37:58 +0300 Subject: [PATCH 07/21] feat: add resolve pathItems for webhooks --- src/services/MenuBuilder.ts | 24 ++++++++++++++++-------- src/services/SpecStore.ts | 5 +++-- src/services/models/Webhook.ts | 16 ++++++++++++---- src/utils/openapi.ts | 4 ++-- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts index 8a45bb9b9c..7aacf19802 100644 --- a/src/services/MenuBuilder.ts +++ b/src/services/MenuBuilder.ts @@ -6,6 +6,7 @@ import { Referenced, OpenAPIServer, OpenAPIPaths, + OpenAPIPath, } from '../types'; import { isOperationName, @@ -53,7 +54,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,7 +216,7 @@ 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 || []) { @@ -223,19 +224,26 @@ export class MenuBuilder { } if (webhooks) { - getTags(webhooks, true); + getTags(parser, webhooks, true); } if (spec.paths){ - getTags(spec.paths); + getTags(parser, spec.paths); } - function getTags(paths: OpenAPIPaths, isWebhook?: boolean) { + 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]; + const operations = Object.keys(path); + for (let operationName of operations) { + let operationInfo = isOperationName(operationName) && path[operationName]; + + if (!isOperationName(operationName) && path[operationName].$ref) { + const resolvedOperationInfo = parser.deref(path[operationName] || {}) + operationInfo = resolvedOperationInfo + delete operationInfo.put + operationName = resolvedOperationInfo[Object.keys(resolvedOperationInfo)[0]] + } let operationTags = operationInfo.tags; if (!operationTags || !operationTags.length) { diff --git a/src/services/SpecStore.ts b/src/services/SpecStore.ts index 1f39f9033e..277e2c0399 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/models/Webhook.ts b/src/services/models/Webhook.ts index 7349b8bd17..6e8760f0c5 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[] = []; @@ -17,9 +17,17 @@ export class WebhookModel { 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]; + const operations = Object.keys(webhook); + for (let operationName of operations) { + let operationInfo = isOperationName(operationName) && webhook[operationName]; + + if (!isOperationName(operationName) && webhook[operationName].$ref) { + const resolvedOperationInfo = parser.deref(webhook[operationName] || {}) + operationInfo = resolvedOperationInfo + operationName = resolvedOperationInfo[Object.keys(resolvedOperationInfo)[0]] + } + + if (!operationInfo) continue; const operation = new OperationModel( parser, { diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index ff9c6afd25..bd7db6b70a 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -369,12 +369,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] } From f6aea16e451894225815096946c8c3c185dfb9ff Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Mon, 24 May 2021 19:20:45 +0300 Subject: [PATCH 08/21] feat: support for type of number in exclusiveMin/Max --- src/types/open-api.d.ts | 4 ++-- src/utils/openapi.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/types/open-api.d.ts b/src/types/open-api.d.ts index 96f98f36ca..78b1b1aa03 100644 --- a/src/types/open-api.d.ts +++ b/src/types/open-api.d.ts @@ -131,9 +131,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; diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index bd7db6b70a..ae70b112d6 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -447,6 +447,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); } From e4383ae40bd4642db650c4388ac9137d98470dc3 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Tue, 25 May 2021 14:18:55 +0300 Subject: [PATCH 09/21] fix: resolve webhook --- src/services/MenuBuilder.ts | 16 +++++++--------- src/services/models/Webhook.ts | 13 ++++++------- src/types/open-api.d.ts | 1 + src/utils/openapi.ts | 1 + 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts index 7aacf19802..7a750d65d8 100644 --- a/src/services/MenuBuilder.ts +++ b/src/services/MenuBuilder.ts @@ -234,17 +234,15 @@ export class MenuBuilder { function getTags(parser: OpenAPIParser, paths: OpenAPIPaths, isWebhook?: boolean) { for (const pathName of Object.keys(paths)) { const path = paths[pathName]; - const operations = Object.keys(path); + const operations = Object.keys(path).filter(isOperationName); for (let operationName of operations) { - let operationInfo = isOperationName(operationName) && path[operationName]; - - if (!isOperationName(operationName) && path[operationName].$ref) { - const resolvedOperationInfo = parser.deref(path[operationName] || {}) - operationInfo = resolvedOperationInfo - delete operationInfo.put - operationName = resolvedOperationInfo[Object.keys(resolvedOperationInfo)[0]] + let operationInfo = path[operationName]; + if (path.$ref) { + const resolvedPath = parser.deref(path || {}) + operationName = Object.keys(resolvedPath)[0] + operationInfo = resolvedPath[operationName] } - let operationTags = operationInfo.tags; + let operationTags = operationInfo?.tags; if (!operationTags || !operationTags.length) { // empty tag diff --git a/src/services/models/Webhook.ts b/src/services/models/Webhook.ts index 6e8760f0c5..a1be7d3911 100644 --- a/src/services/models/Webhook.ts +++ b/src/services/models/Webhook.ts @@ -17,14 +17,13 @@ export class WebhookModel { for (const webhookName of Object.keys(webhooks)) { const webhook = webhooks[webhookName]; - const operations = Object.keys(webhook); + const operations = Object.keys(webhook).filter(isOperationName); for (let operationName of operations) { - let operationInfo = isOperationName(operationName) && webhook[operationName]; - - if (!isOperationName(operationName) && webhook[operationName].$ref) { - const resolvedOperationInfo = parser.deref(webhook[operationName] || {}) - operationInfo = resolvedOperationInfo - operationName = resolvedOperationInfo[Object.keys(resolvedOperationInfo)[0]] + let operationInfo = webhook[operationName]; + if (webhook.$ref) { + const resolvedWebhook = parser.deref(webhook || {}); + operationName = Object.keys(resolvedWebhook)[0]; + operationInfo = resolvedWebhook[operationName]; } if (!operationInfo) continue; diff --git a/src/types/open-api.d.ts b/src/types/open-api.d.ts index 78b1b1aa03..90ed467bbe 100644 --- a/src/types/open-api.d.ts +++ b/src/types/open-api.d.ts @@ -58,6 +58,7 @@ export interface OpenAPIPath { trace?: OpenAPIOperation; servers?: OpenAPIServer[]; parameters?: Array>; + $ref?: string; } export interface OpenAPIXCodeSample { diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index ae70b112d6..37dba6797b 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -56,6 +56,7 @@ const operationNames = { patch: true, delete: true, options: true, + $ref: true, }; export function isOperationName(key: string): boolean { From 24c02e3c5218e24599377f2fdecb496e2dc8f49f Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Wed, 26 May 2021 11:26:14 +0300 Subject: [PATCH 10/21] feat: add placeholder if no paths or webhooks --- src/components/ContentItems/ContentItems.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx index 2766887af7..646108276a 100644 --- a/src/components/ContentItems/ContentItems.tsx +++ b/src/components/ContentItems/ContentItems.tsx @@ -15,7 +15,7 @@ export class ContentItems extends React.Component<{ render() { const items = this.props.items; if (items.length === 0) { - return null; + return Cannot find path or webhooks items; } return items.map(item => { return ; From aef48a18c8d4c3510a6dd94f225c5ec77651585f Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Wed, 26 May 2021 11:50:47 +0300 Subject: [PATCH 11/21] feat: add const keyword --- src/components/Fields/FieldDetails.tsx | 1 + src/services/Labels.ts | 2 ++ src/services/models/Field.ts | 3 +++ src/services/models/Schema.ts | 2 ++ src/types/open-api.d.ts | 2 ++ 5 files changed, 10 insertions(+) diff --git a/src/components/Fields/FieldDetails.tsx b/src/components/Fields/FieldDetails.tsx index ac362a4059..be102622ca 100644 --- a/src/components/Fields/FieldDetails.tsx +++ b/src/components/Fields/FieldDetails.tsx @@ -110,6 +110,7 @@ export class FieldDetails extends React.PureComponent )} {(renderDiscriminatorSwitch && renderDiscriminatorSwitch(this.props)) || null} + {field.const && () || null} ); } diff --git a/src/services/Labels.ts b/src/services/Labels.ts index f5fa1fd14a..8e89bc2fec 100644 --- a/src/services/Labels.ts +++ b/src/services/Labels.ts @@ -9,6 +9,7 @@ export interface LabelsConfig { recursive: string; arrayOf: string; webhook: string; + const: string; } export type LabelsConfigRaw = Partial; @@ -24,6 +25,7 @@ const labels: LabelsConfig = { recursive: 'Recursive', arrayOf: 'Array of ', webhook: 'Event', + const: 'Value', }; export function setRedocLabels(_labels?: LabelsConfigRaw) { diff --git a/src/services/models/Field.ts b/src/services/models/Field.ts index 1154c9a894..ef031ab894 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/Schema.ts b/src/services/models/Schema.ts index fb0c516442..8749625e42 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -60,6 +60,7 @@ export class SchemaModel { rawSchema: OpenAPISchema; schema: MergedOpenAPISchema; extensions?: Record; + const: any; /** * @param isChild if schema discriminator Child @@ -119,6 +120,7 @@ export class SchemaModel { this.default = schema.default; this.readOnly = !!schema.readOnly; this.writeOnly = !!schema.writeOnly; + this.const = schema.const || ''; if (!!schema.nullable) { if (Array.isArray(this.type)) this.type.push('null'); diff --git a/src/types/open-api.d.ts b/src/types/open-api.d.ts index 90ed467bbe..d55e44b13b 100644 --- a/src/types/open-api.d.ts +++ b/src/types/open-api.d.ts @@ -99,6 +99,7 @@ export interface OpenAPIParameter { examples?: { [media: string]: Referenced }; content?: { [media: string]: OpenAPIMediaType }; encoding?: Record; + const?: any; } export interface OpenAPIExample { @@ -145,6 +146,7 @@ export interface OpenAPISchema { minProperties?: number; enum?: any[]; example?: any; + const?: string; } export interface OpenAPIDiscriminator { From 83ed6f353814d1bbac59df4ef6915661bd03c482 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Wed, 26 May 2021 13:11:48 +0300 Subject: [PATCH 12/21] fix: fix test for const keyword --- .../__snapshots__/DiscriminatorDropdown.test.tsx.snap | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap index f660e397d3..f293088f37 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 Date: Wed, 26 May 2021 18:40:35 +0300 Subject: [PATCH 13/21] feat: add test and improve code --- demo/openapi.yaml | 53 +++++++- e2e/integration/menu.e2e.ts | 2 +- src/components/Fields/FieldDetails.tsx | 2 +- src/services/MenuBuilder.ts | 10 +- .../__tests__/fixtures/3.1/pathItems.json | 66 ++++++++++ src/services/__tests__/models/ApiInfo.test.ts | 16 +++ .../__tests__/models/MenuBuilder.test.ts | 25 ++++ src/services/models/Webhook.ts | 10 +- .../loadAndBundleSpec.test.ts.snap | 115 ++++++++++++++---- 9 files changed, 256 insertions(+), 43 deletions(-) create mode 100644 src/services/__tests__/fixtures/3.1/pathItems.json create mode 100644 src/services/__tests__/models/MenuBuilder.test.ts diff --git a/demo/openapi.yaml b/demo/openapi.yaml index 74c16b70f2..e486189008 100644 --- a/demo/openapi.yaml +++ b/demo/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.0 +openapi: 3.1.0 servers: - url: //petstore.swagger.io/v2 description: Default server @@ -42,6 +42,7 @@ info: version: 1.0.0 title: Swagger Petstore + summary: My lovely API termsOfService: 'http://swagger.io/terms/' contact: name: API Support @@ -53,6 +54,7 @@ info: license: name: Apache 2.0 url: 'http://www.apache.org/licenses/LICENSE-2.0.html' + identifier: Apache 2.0 externalDocs: description: Find out how to create Github repo for your OpenAPI spec. url: 'https://github.com/Rebilly/generator-openapi-repo' @@ -893,6 +895,38 @@ paths: default: description: successful operation 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/Cat" + 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/Cat" + responses: + '200': + description: create Cat details schemas: ApiResponse: type: object @@ -1040,7 +1074,8 @@ components: example: Guru photoUrls: description: The list of URL to a cute photos featuring pet - type: array + type: [string, integer, 'null', array] + minItems: 1 maxItems: 20 xml: name: photoUrl @@ -1054,7 +1089,8 @@ components: tags: description: Tags attached to the pet type: array - minItems: 1 + exclusiveMaximum: 100 + exclusiveMinimum: 0 xml: name: tag wrapped: true @@ -1067,6 +1103,7 @@ components: - available - pending - sold + default: pending petType: description: Type of a pet type: string @@ -1187,13 +1224,13 @@ components: shipDate: '2018-10-19T16:46:45Z' status: placed complete: false -x-webhooks: +webhooks: newPet: post: summary: New pet description: Information about a new pet in the systems operationId: newPet - tags: + tags: - pet requestBody: content: @@ -1202,4 +1239,8 @@ x-webhooks: $ref: "#/components/schemas/Pet" responses: "200": - description: Return a 200 status to indicate that the data was received successfully \ No newline at end of file + description: Return a 200 status to indicate that the data was received successfully + myWebhook: + $ref: '#/components/pathItems/catsWebhook' + description: Overriding description + summary: Overriding summary \ No newline at end of file diff --git a/e2e/integration/menu.e2e.ts b/e2e/integration/menu.e2e.ts index e1b053d125..41ecd79cea 100644 --- a/e2e/integration/menu.e2e.ts +++ b/e2e/integration/menu.e2e.ts @@ -6,7 +6,7 @@ describe('Menu', () => { it('should have valid items count', () => { cy.get('.menu-content') .find('li') - .should('have.length', 34); + .should('have.length', 36); }); it('should sync active menu items while scroll', () => { diff --git a/src/components/Fields/FieldDetails.tsx b/src/components/Fields/FieldDetails.tsx index be102622ca..9ac6cea235 100644 --- a/src/components/Fields/FieldDetails.tsx +++ b/src/components/Fields/FieldDetails.tsx @@ -110,7 +110,7 @@ export class FieldDetails extends React.PureComponent )} {(renderDiscriminatorSwitch && renderDiscriminatorSwitch(this.props)) || null} - {field.const && () || null} + {field.const && () || null} ); } diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts index 7a750d65d8..7322dab00d 100644 --- a/src/services/MenuBuilder.ts +++ b/src/services/MenuBuilder.ts @@ -6,7 +6,6 @@ import { Referenced, OpenAPIServer, OpenAPIPaths, - OpenAPIPath, } from '../types'; import { isOperationName, @@ -235,12 +234,11 @@ export class MenuBuilder { for (const pathName of Object.keys(paths)) { const path = paths[pathName]; const operations = Object.keys(path).filter(isOperationName); - for (let operationName of operations) { - let operationInfo = path[operationName]; + for (const operationName of operations) { + const operationInfo = path[operationName]; if (path.$ref) { - const resolvedPath = parser.deref(path || {}) - operationName = Object.keys(resolvedPath)[0] - operationInfo = resolvedPath[operationName] + const resolvedPaths = parser.deref(path as OpenAPIPaths); + getTags(parser, { [operationName]: resolvedPaths }, isWebhook); } let operationTags = operationInfo?.tags; 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 0000000000..87888a1b91 --- /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" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/services/__tests__/models/ApiInfo.test.ts b/src/services/__tests__/models/ApiInfo.test.ts index 457937ecfd..5db4252751 100644 --- a/src/services/__tests__/models/ApiInfo.test.ts +++ b/src/services/__tests__/models/ApiInfo.test.ts @@ -46,5 +46,21 @@ describe('Models', () => { 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 0000000000..a6ab4776c7 --- /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('discriminator with one field', () => { + const spec = require('../fixtures/3.1/pathItems.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const contentItems = MenuBuilder.buildStructure(parser, opts); + expect(contentItems).toHaveLength(2); + 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/Webhook.ts b/src/services/models/Webhook.ts index a1be7d3911..5512293518 100644 --- a/src/services/models/Webhook.ts +++ b/src/services/models/Webhook.ts @@ -14,16 +14,18 @@ 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 (let operationName of operations) { - let operationInfo = webhook[operationName]; + for (const operationName of operations) { + const operationInfo = webhook[operationName]; if (webhook.$ref) { const resolvedWebhook = parser.deref(webhook || {}); - operationName = Object.keys(resolvedWebhook)[0]; - operationInfo = resolvedWebhook[operationName]; + this.initWebhooks(parser, { [operationName]: resolvedWebhook }, options); } if (!operationInfo) continue; diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index e7c967e6e8..ebf6792ded 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -13,6 +13,56 @@ Object { }, }, }, + "pathItems": Object { + "catsWebhook": 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 [ + "pet", + ], + }, + "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 [ + "pet", + ], + }, + }, + }, "requestBodies": Object { "Pet": Object { "content": Object { @@ -292,13 +342,20 @@ Object { "type": "string", }, "maxItems": 20, - "type": "array", + "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", @@ -309,10 +366,11 @@ Object { }, "tags": Object { "description": "Tags attached to the pet", + "exclusiveMaximum": 100, + "exclusiveMinimum": 0, "items": Object { "$ref": "#/components/schemas/Tag", }, - "minItems": 1, "type": "array", "xml": Object { "name": "tag", @@ -485,9 +543,11 @@ 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", @@ -496,7 +556,7 @@ and standard method from web, mobile and desktop applications. "url": "https://redocly.github.io/redoc/petstore-logo.png", }, }, - "openapi": "3.0.0", + "openapi": "3.1.0", "paths": Object { "/pet": Object { "parameters": Array [ @@ -1745,29 +1805,12 @@ culpa qui officia deserunt mollit anim id est laborum. "x-displayName": "The Order Model", }, ], - "x-tagGroups": Array [ - Object { - "name": "General", - "tags": Array [ - "pet", - "store", - ], - }, - Object { - "name": "User Management", - "tags": Array [ - "user", - ], + "webhooks": Object { + "myWebhook": Object { + "$ref": "#/components/pathItems/catsWebhook", + "description": "Overriding description", + "summary": "Overriding summary", }, - Object { - "name": "Models", - "tags": Array [ - "pet_model", - "store_model", - ], - }, - ], - "x-webhooks": Object { "newPet": Object { "post": Object { "description": "Information about a new pet in the systems", @@ -1793,6 +1836,28 @@ culpa qui officia deserunt mollit anim id est laborum. }, }, }, + "x-tagGroups": Array [ + Object { + "name": "General", + "tags": Array [ + "pet", + "store", + ], + }, + Object { + "name": "User Management", + "tags": Array [ + "user", + ], + }, + Object { + "name": "Models", + "tags": Array [ + "pet_model", + "store_model", + ], + }, + ], } `; From d525e6d289f68cd567fa4e7a53ea6381e2f9bea4 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Wed, 26 May 2021 19:33:47 +0300 Subject: [PATCH 14/21] chore: improve demo --- demo/openapi.yaml | 13 ++++++++----- e2e/integration/menu.e2e.ts | 2 +- .../__snapshots__/loadAndBundleSpec.test.ts.snap | 15 ++++++++++----- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/demo/openapi.yaml b/demo/openapi.yaml index e486189008..1b363c5566 100644 --- a/demo/openapi.yaml +++ b/demo/openapi.yaml @@ -61,6 +61,8 @@ externalDocs: tags: - name: pet description: Everything about your Pets + - name: webhooks + description: Everything about your Webhooks - name: store description: Access to Petstore orders - name: user @@ -78,6 +80,7 @@ x-tagGroups: tags: - pet - store + - webhooks - name: User Management tags: - user @@ -896,13 +899,13 @@ paths: description: successful operation components: pathItems: - catsWebhook: + webhooks: put: summary: Get a cat details after update description: Get a cat details after update operationId: updatedCat tags: - - pet + - webhooks requestBody: description: Information about cat in the system content: @@ -917,7 +920,7 @@ components: description: Info about new cat operationId: createdCat tags: - - pet + - webhooks requestBody: description: Information about cat in the system content: @@ -1231,7 +1234,7 @@ webhooks: description: Information about a new pet in the systems operationId: newPet tags: - - pet + - webhooks requestBody: content: application/json: @@ -1241,6 +1244,6 @@ webhooks: "200": description: Return a 200 status to indicate that the data was received successfully myWebhook: - $ref: '#/components/pathItems/catsWebhook' + $ref: '#/components/pathItems/webhooks' description: Overriding description summary: Overriding summary \ No newline at end of file diff --git a/e2e/integration/menu.e2e.ts b/e2e/integration/menu.e2e.ts index 41ecd79cea..89ac53d9ce 100644 --- a/e2e/integration/menu.e2e.ts +++ b/e2e/integration/menu.e2e.ts @@ -6,7 +6,7 @@ describe('Menu', () => { it('should have valid items count', () => { cy.get('.menu-content') .find('li') - .should('have.length', 36); + .should('have.length', 37); }); it('should sync active menu items while scroll', () => { diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index ebf6792ded..ecd32e05cb 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -14,7 +14,7 @@ Object { }, }, "pathItems": Object { - "catsWebhook": Object { + "webhooks": Object { "post": Object { "description": "Info about new cat", "operationId": "createdCat", @@ -35,7 +35,7 @@ Object { }, "summary": "Create new cat", "tags": Array [ - "pet", + "webhooks", ], }, "put": Object { @@ -58,7 +58,7 @@ Object { }, "summary": "Get a cat details after update", "tags": Array [ - "pet", + "webhooks", ], }, }, @@ -1784,6 +1784,10 @@ culpa qui officia deserunt mollit anim id est laborum. "description": "Everything about your Pets", "name": "pet", }, + Object { + "description": "Everything about your Webhooks", + "name": "webhooks", + }, Object { "description": "Access to Petstore orders", "name": "store", @@ -1807,7 +1811,7 @@ culpa qui officia deserunt mollit anim id est laborum. ], "webhooks": Object { "myWebhook": Object { - "$ref": "#/components/pathItems/catsWebhook", + "$ref": "#/components/pathItems/webhooks", "description": "Overriding description", "summary": "Overriding summary", }, @@ -1831,7 +1835,7 @@ culpa qui officia deserunt mollit anim id est laborum. }, "summary": "New pet", "tags": Array [ - "pet", + "webhooks", ], }, }, @@ -1842,6 +1846,7 @@ culpa qui officia deserunt mollit anim id est laborum. "tags": Array [ "pet", "store", + "webhooks", ], }, Object { From 456666eaa2551615700ac0d17c16cf1c06989088 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Thu, 27 May 2021 12:44:55 +0300 Subject: [PATCH 15/21] feat: add new demo file and improve test --- demo/index.tsx | 9 +- demo/openapi-3-1.yaml | 1249 ++++++++++++ demo/openapi.yaml | 58 +- e2e/integration/menu.e2e.ts | 2 +- e2e/integration/standalone.e2e.ts | 1 + e2e/standalone-3-1.html | 8 + package-lock.json | 12 +- package.json | 2 +- .../__tests__/fixtures/3.1/pathItems.json | 2 +- src/services/models/Schema.ts | 1 - .../loadAndBundleSpec.test.ts.snap | 1804 ++++++++++++++++- src/utils/__tests__/loadAndBundleSpec.test.ts | 6 + 12 files changed, 3087 insertions(+), 67 deletions(-) create mode 100644 demo/openapi-3-1.yaml create mode 100644 e2e/standalone-3-1.html diff --git a/demo/index.tsx b/demo/index.tsx index b6f1fe0571..e5e48c00e2 100644 --- a/demo/index.tsx +++ b/demo/index.tsx @@ -5,7 +5,11 @@ import { resolve as urlResolve } from 'url'; import { RedocStandalone } from '../src'; import ComboBox from './ComboBox'; +const DEFAULT_SPEC = 'openapi.yaml'; +const NEW_VERSION_SPEC = 'openapi-3-1.yaml'; + const demos = [ + { value: NEW_VERSION_SPEC, label: 'OpenApi 3.1' }, { value: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml', label: 'Instagram' }, { value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/openapi.yaml', @@ -16,8 +20,6 @@ const demos = [ { value: 'https://docs.graphhopper.com/openapi.json', label: 'GraphHopper' }, ]; -const DEFAULT_SPEC = 'openapi.yaml'; - class DemoApp extends React.Component< {}, { specUrl: string; dropdownOpen: boolean; cors: boolean } @@ -45,6 +47,9 @@ class DemoApp extends React.Component< } handleChange = (url: string) => { + if (url === NEW_VERSION_SPEC) { + this.setState({ cors: false }) + } this.setState({ specUrl: url, }); diff --git a/demo/openapi-3-1.yaml b/demo/openapi-3-1.yaml new file mode 100644 index 0000000000..6f6496aa8e --- /dev/null +++ b/demo/openapi-3-1.yaml @@ -0,0 +1,1249 @@ +openapi: 3.1.0 +servers: + - url: //petstore.swagger.io/v2 + description: Default server + - url: //petstore.swagger.io/sandbox + description: Sandbox server +info: + 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. + + + + version: 1.0.0 + title: Swagger Petstore + summary: My lovely API + termsOfService: 'http://swagger.io/terms/' + contact: + name: API Support + email: apiteam@swagger.io + url: https://github.com/Redocly/redoc + x-logo: + url: 'https://redocly.github.io/redoc/petstore-logo.png' + altText: Petstore logo + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' + identifier: Apache 2.0 +externalDocs: + description: Find out how to create Github repo for your OpenAPI spec. + url: 'https://github.com/Rebilly/generator-openapi-repo' +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + - name: webhooks + description: Everything about your Webhooks + - name: pet_model + x-displayName: The Pet Model + description: | + + - name: store_model + x-displayName: The Order Model + description: | + +x-tagGroups: + - name: General + tags: + - pet + - store + - webhooks + - name: User Management + tags: + - user + - name: Models + tags: + - pet_model + - store_model +paths: + /pet: + parameters: + - name: Accept-Language + in: header + description: "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US" + example: en-US + required: false + schema: + type: string + default: en-AU + - name: cookieParam + in: cookie + description: Some cookie + required: true + schema: + type: integer + format: int64 + post: + tags: + - pet + summary: Add a new pet to the store + description: Add new pet to the store inventory. + operationId: addPet + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + x-codeSamples: + - 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()); + } + - 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()); + } + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + x-codeSamples: + - 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()); + } + requestBody: + $ref: '#/components/requestBodies/Pet' + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + deprecated: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + example: "Bearer " + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + schema: + type: array + minItems: 1 + maxItems: 3 + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + deprecated: true + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + content: + application/json: + example: + status: 400 + message: "Invalid Order" + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value <= 5 or > 10. Other values + will generated exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 5 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with value < 1000. Anything above + 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: string + minimum: 1 + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /store/subscribe: + post: + tags: + - store + summary: Subscribe to the Store events + description: Add subscription for a store events + requestBody: + content: + application/json: + schema: + type: object + properties: + callbackUrl: + type: string + format: uri + description: This URL will be called by the server when the desired event will occur + example: https://myserver.com/send/callback/here + eventName: + type: string + description: Event name for the subscription + enum: + - orderInProgress + - orderShipped + - orderDelivered + example: orderInProgress + required: + - callbackUrl + - eventName + responses: + '201': + description: Subscription added + content: + application/json: + schema: + type: object + properties: + subscriptionId: + type: string + example: AAA-123-BBB-456 + callbacks: + orderInProgress: + '{$request.body#/callbackUrl}?event={$request.body#/eventName}': + servers: + - url: //callback-url.path-level/v1 + description: Path level server 1 + - url: //callback-url.path-level/v2 + description: Path level server 2 + post: + summary: Order in Progress (Summary) + description: A callback triggered every time an Order is updated status to "inProgress" (Description) + externalDocs: + description: Find out more + url: 'https://more-details.com/demo' + requestBody: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: '123' + timestamp: + type: string + format: date-time + example: '2018-10-19T16:46:45Z' + status: + type: string + example: 'inProgress' + application/xml: + schema: + type: object + properties: + orderId: + type: string + example: '123' + example: | + + + 123 + inProgress + 2018-10-19T16:46:45Z + + responses: + '200': + description: Callback successfully processed and no retries will be performed + content: + application/json: + schema: + type: object + properties: + someProp: + type: string + example: '123' + '299': + description: Response for cancelling subscription + '500': + description: Callback processing failed and retries will be performed + x-codeSamples: + - 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()); + } + - 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: + description: Order in Progress (Only Description) + servers: + - url: //callback-url.operation-level/v1 + description: Operation level server 1 (Operation override) + - url: //callback-url.operation-level/v2 + description: Operation level server 2 (Operation override) + requestBody: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: '123' + timestamp: + type: string + format: date-time + example: '2018-10-19T16:46:45Z' + status: + type: string + example: 'inProgress' + application/xml: + schema: + type: object + properties: + orderId: + type: string + example: '123' + example: | + + + 123 + inProgress + 2018-10-19T16:46:45Z + + responses: + '200': + description: Callback successfully processed and no retries will be performed + content: + application/json: + schema: + type: object + properties: + someProp: + type: string + example: '123' + orderShipped: + '{$request.body#/callbackUrl}?event={$request.body#/eventName}': + post: + 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: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: '123' + timestamp: + type: string + format: date-time + example: '2018-10-19T16:46:45Z' + estimatedDeliveryDate: + type: string + format: date-time + example: '2018-11-11T16:00:00Z' + responses: + '200': + description: Callback successfully processed and no retries will be performed + orderDelivered: + 'http://notificationServer.com?url={$request.body#/callbackUrl}&event={$request.body#/eventName}': + post: + deprecated: true + summary: Order delivered + description: A callback triggered every time an Order is delivered to the recipient + requestBody: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: '123' + timestamp: + type: string + format: date-time + example: '2018-10-19T16:46:45Z' + responses: + '200': + description: Callback successfully processed and no retries will be performed + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/json: + schema: + type: string + examples: + response: + value: OK + application/xml: + schema: + type: string + examples: + response: + value: OK + text/plain: + examples: + response: + value: OK + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + responses: + default: + description: successful operation +components: + pathItems: + webhooks: + put: + summary: Get a cat details after update + description: Get a cat details after update + operationId: updatedCat + tags: + - webhooks + requestBody: + description: Information about cat in the system + content: + multipart/form-data: + schema: + $ref: "#/components/schemas/Cat" + responses: + '200': + description: update Cat details + post: + summary: Create new cat + description: Info about new cat + operationId: createdCat + tags: + - webhooks + requestBody: + description: Information about cat in the system + content: + multipart/form-data: + schema: + $ref: "#/components/schemas/Cat" + responses: + '200': + description: create Cat details + schemas: + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + Cat: + description: A representation of a cat + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + huntingSkill: + type: string + description: The measured skill for hunting + default: lazy + example: adventurous + enum: + - clueless + - lazy + - adventurous + - aggressive + required: + - huntingSkill + Category: + type: object + properties: + id: + description: Category ID + allOf: + - $ref: '#/components/schemas/Id' + name: + description: Category name + type: string + minLength: 1 + sub: + description: Test Sub Category + type: object + properties: + prop1: + type: string + description: Dumb Property + xml: + name: Category + Dog: + description: A representation of a dog + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + packSize: + type: integer + format: int32 + description: The size of the pack the dog is from + default: 1 + minimum: 1 + required: + - packSize + HoneyBee: + description: A representation of a honey bee + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + honeyPerDay: + type: number + description: Average amount of honey produced per day in ounces + example: 3.14 + multipleOf: .01 + required: + - honeyPerDay + Id: + type: integer + format: int64 + readOnly: true + Order: + type: object + properties: + id: + description: Order ID + allOf: + - $ref: '#/components/schemas/Id' + petId: + description: Pet ID + allOf: + - $ref: '#/components/schemas/Id' + quantity: + type: integer + format: int32 + minimum: 1 + default: 1 + shipDate: + description: Estimated ship date + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + description: Indicates whenever order was completed or not + type: boolean + default: false + readOnly: true + requestId: + description: Unique Request Id + type: string + writeOnly: true + xml: + name: Order + Pet: + type: object + required: + - name + - photoUrls + discriminator: + propertyName: petType + mapping: + cat: '#/components/schemas/Cat' + dog: '#/components/schemas/Dog' + bee: '#/components/schemas/HoneyBee' + properties: + id: + externalDocs: + description: "Find more info here" + url: "https://example.com" + description: Pet ID + allOf: + - $ref: '#/components/schemas/Id' + category: + description: Categories this pet belongs to + allOf: + - $ref: '#/components/schemas/Category' + name: + description: The name given to a pet + type: string + example: Guru + photoUrls: + description: The list of URL to a cute photos featuring pet + type: [string, integer, 'null', array] + minItems: 1 + maxItems: 20 + xml: + name: photoUrl + wrapped: true + items: + type: string + format: url + friend: + allOf: + - $ref: '#/components/schemas/Pet' + tags: + description: Tags attached to the pet + type: array + exclusiveMaximum: 100 + exclusiveMinimum: 0 + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: Pet status in the store + enum: + - available + - pending + - sold + default: pending + petType: + description: Type of a pet + type: string + xml: + name: Pet + Tag: + type: object + properties: + id: + description: Tag ID + allOf: + - $ref: '#/components/schemas/Id' + name: + description: Tag name + type: string + minLength: 1 + xml: + name: Tag + User: + type: object + properties: + id: + $ref: '#/components/schemas/Id' + pet: + oneOf: + - $ref: '#/components/schemas/Pet' + - $ref: '#/components/schemas/Tag' + username: + description: User supplied username + type: string + minLength: 4 + example: John78 + firstName: + description: User first name + type: string + minLength: 1 + example: John + lastName: + description: User last name + type: string + minLength: 1 + example: Smith + email: + description: User email address + type: string + format: email + example: john.smith@example.com + password: + type: string + description: >- + User password, MUST contain a mix of upper and lower case letters, + as well as digits + format: password + minLength: 8 + pattern: '/(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/' + example: drowssaP123 + phone: + description: User phone number in international format + type: string + pattern: '/^\+(?:[0-9]-?){6,14}[0-9]$/' + example: +1-202-555-0192 + userStatus: + description: User status + type: integer + format: int32 + xml: + name: User + requestBodies: + Pet: + content: + application/json: + schema: + allOf: + - description: My Pet + title: Pettie + - $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: 'object' + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + securitySchemes: + petstore_auth: + description: | + Get access to data while protecting your account credentials. + OAuth2 is also a safer and more secure way to give you access. + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + description: > + For this sample, you can use the api key `special-key` to test the + authorization filters. + type: apiKey + name: api_key + in: header + examples: + Order: + value: + quantity: 1 + shipDate: '2018-10-19T16:46:45Z' + status: placed + complete: false +webhooks: + newPet: + post: + summary: New pet + description: Information about a new pet in the systems + operationId: newPet + tags: + - webhooks + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + responses: + "200": + description: Return a 200 status to indicate that the data was received successfully + myWebhook: + $ref: '#/components/pathItems/webhooks' + description: Overriding description + summary: Overriding summary diff --git a/demo/openapi.yaml b/demo/openapi.yaml index 1b363c5566..74c16b70f2 100644 --- a/demo/openapi.yaml +++ b/demo/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.1.0 +openapi: 3.0.0 servers: - url: //petstore.swagger.io/v2 description: Default server @@ -42,7 +42,6 @@ info: version: 1.0.0 title: Swagger Petstore - summary: My lovely API termsOfService: 'http://swagger.io/terms/' contact: name: API Support @@ -54,15 +53,12 @@ info: license: name: Apache 2.0 url: 'http://www.apache.org/licenses/LICENSE-2.0.html' - identifier: Apache 2.0 externalDocs: description: Find out how to create Github repo for your OpenAPI spec. url: 'https://github.com/Rebilly/generator-openapi-repo' tags: - name: pet description: Everything about your Pets - - name: webhooks - description: Everything about your Webhooks - name: store description: Access to Petstore orders - name: user @@ -80,7 +76,6 @@ x-tagGroups: tags: - pet - store - - webhooks - name: User Management tags: - user @@ -898,38 +893,6 @@ paths: default: description: successful operation components: - pathItems: - webhooks: - put: - summary: Get a cat details after update - description: Get a cat details after update - operationId: updatedCat - tags: - - webhooks - requestBody: - description: Information about cat in the system - content: - multipart/form-data: - schema: - $ref: "#/components/schemas/Cat" - responses: - '200': - description: update Cat details - post: - summary: Create new cat - description: Info about new cat - operationId: createdCat - tags: - - webhooks - requestBody: - description: Information about cat in the system - content: - multipart/form-data: - schema: - $ref: "#/components/schemas/Cat" - responses: - '200': - description: create Cat details schemas: ApiResponse: type: object @@ -1077,8 +1040,7 @@ components: example: Guru photoUrls: description: The list of URL to a cute photos featuring pet - type: [string, integer, 'null', array] - minItems: 1 + type: array maxItems: 20 xml: name: photoUrl @@ -1092,8 +1054,7 @@ components: tags: description: Tags attached to the pet type: array - exclusiveMaximum: 100 - exclusiveMinimum: 0 + minItems: 1 xml: name: tag wrapped: true @@ -1106,7 +1067,6 @@ components: - available - pending - sold - default: pending petType: description: Type of a pet type: string @@ -1227,14 +1187,14 @@ components: shipDate: '2018-10-19T16:46:45Z' status: placed complete: false -webhooks: +x-webhooks: newPet: post: summary: New pet description: Information about a new pet in the systems operationId: newPet - tags: - - webhooks + tags: + - pet requestBody: content: application/json: @@ -1242,8 +1202,4 @@ webhooks: $ref: "#/components/schemas/Pet" responses: "200": - description: Return a 200 status to indicate that the data was received successfully - myWebhook: - $ref: '#/components/pathItems/webhooks' - description: Overriding description - summary: Overriding summary \ No newline at end of file + description: Return a 200 status to indicate that the data was received successfully \ No newline at end of file diff --git a/e2e/integration/menu.e2e.ts b/e2e/integration/menu.e2e.ts index 89ac53d9ce..e1b053d125 100644 --- a/e2e/integration/menu.e2e.ts +++ b/e2e/integration/menu.e2e.ts @@ -6,7 +6,7 @@ describe('Menu', () => { it('should have valid items count', () => { cy.get('.menu-content') .find('li') - .should('have.length', 37); + .should('have.length', 34); }); it('should sync active menu items while scroll', () => { diff --git a/e2e/integration/standalone.e2e.ts b/e2e/integration/standalone.e2e.ts index 587c73b4a1..918ef51f8c 100644 --- a/e2e/integration/standalone.e2e.ts +++ b/e2e/integration/standalone.e2e.ts @@ -16,5 +16,6 @@ describe('Standalone bundle test', () => { } baseCheck('OAS3 mode', 'e2e/standalone.html'); + baseCheck('OAS3.1 mode', 'e2e/standalone-3-1.html'); baseCheck('OAS2 compatibility mode', 'e2e/standalone-compatibility.html'); }); diff --git a/e2e/standalone-3-1.html b/e2e/standalone-3-1.html new file mode 100644 index 0000000000..9d26bbe6b5 --- /dev/null +++ b/e2e/standalone-3-1.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/package-lock.json b/package-lock.json index cadcbdb32b..dd56e6dbb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1222,9 +1222,9 @@ } }, "@redocly/openapi-core": { - "version": "1.0.0-beta.46", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.46.tgz", - "integrity": "sha512-w4VG2KNLFDuZgN7fBmbzbI0GJDiPnJ0SYszj4uuSkMW35SVTvDWyTaeWjW8ggQJ3gluDnTgUvm9tjLdR2tLqUg==", + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.48.tgz", + "integrity": "sha512-rlus9qQC4Pkzz2Ljcv+jQjFdKOYSWnsYXWN6zNik9iiiQtMmGEdszsERCbSAYw/CZ5DRCAHMeKrh8f4LBCpx1A==", "requires": { "@redocly/ajv": "^6.12.3", "@types/node": "^14.11.8", @@ -1238,9 +1238,9 @@ }, "dependencies": { "@types/node": { - "version": "14.17.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.0.tgz", - "integrity": "sha512-w8VZUN/f7SSbvVReb9SWp6cJFevxb4/nkG65yLAya//98WgocKm5PLDAtSs5CtJJJM+kHmJjO/6mmYW4MHShZA==" + "version": "14.17.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.1.tgz", + "integrity": "sha512-/tpUyFD7meeooTRwl3sYlihx2BrJE7q9XF71EguPFIySj9B7qgnRtHsHTho+0AUm4m1SvWGm6uSncrR94q6Vtw==" } } }, diff --git a/package.json b/package.json index 3c87d54877..9296a29f79 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "styled-components": "^4.1.1 || ^5.1.1" }, "dependencies": { - "@redocly/openapi-core": "^1.0.0-beta.45", + "@redocly/openapi-core": "^1.0.0-beta.48", "@redocly/react-dropdown-aria": "^2.0.11", "@types/node": "^13.11.1", "classnames": "^2.2.6", diff --git a/src/services/__tests__/fixtures/3.1/pathItems.json b/src/services/__tests__/fixtures/3.1/pathItems.json index 87888a1b91..0bd88b05d8 100644 --- a/src/services/__tests__/fixtures/3.1/pathItems.json +++ b/src/services/__tests__/fixtures/3.1/pathItems.json @@ -63,4 +63,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index 8749625e42..33d9f65030 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -77,7 +77,6 @@ export class SchemaModel { this.pointer = schemaOrRef.$ref || pointer || ''; this.rawSchema = parser.deref(schemaOrRef); - this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild); this.init(parser, isChild); diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index ecd32e05cb..c878803796 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -1,6 +1,1802 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`#loadAndBundleSpec should load And Bundle Spec demo/openapi.yaml 1`] = ` +Object { + "components": Object { + "examples": Object { + "Order": Object { + "value": Object { + "complete": false, + "quantity": 1, + "shipDate": "2018-10-19T16:46:45Z", + "status": "placed", + }, + }, + }, + "requestBodies": Object { + "Pet": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "allOf": Array [ + Object { + "description": "My Pet", + "title": "Pettie", + }, + Object { + "$ref": "#/components/schemas/Pet", + }, + ], + }, + }, + "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 { + "allOf": Array [ + 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 { + "allOf": Array [ + Object { + "$ref": "#/components/schemas/Id", + }, + ], + "description": "Order ID", + }, + "petId": Object { + "allOf": Array [ + 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 { + "allOf": Array [ + Object { + "$ref": "#/components/schemas/Category", + }, + ], + "description": "Categories this pet belongs to", + }, + "friend": Object { + "allOf": Array [ + Object { + "$ref": "#/components/schemas/Pet", + }, + ], + }, + "id": Object { + "allOf": Array [ + 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, + "type": "array", + "xml": Object { + "name": "photoUrl", + "wrapped": true, + }, + }, + "status": Object { + "description": "Pet status in the store", + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "description": "Tags attached to the pet", + "items": Object { + "$ref": "#/components/schemas/Tag", + }, + "minItems": 1, + "type": "array", + "xml": Object { + "name": "tag", + "wrapped": true, + }, + }, + }, + "required": Array [ + "name", + "photoUrls", + ], + "type": "object", + "xml": Object { + "name": "Pet", + }, + }, + "Tag": Object { + "properties": Object { + "id": Object { + "allOf": Array [ + 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", + }, + "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", + }, + 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 { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html", + }, + "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.0.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": " +", + "name": "pet_model", + "x-displayName": "The Pet Model", + }, + Object { + "description": " +", + "name": "store_model", + "x-displayName": "The Order Model", + }, + ], + "x-tagGroups": Array [ + Object { + "name": "General", + "tags": Array [ + "pet", + "store", + ], + }, + Object { + "name": "User Management", + "tags": Array [ + "user", + ], + }, + Object { + "name": "Models", + "tags": Array [ + "pet_model", + "store_model", + ], + }, + ], + "x-webhooks": Object { + "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 [ + "pet", + ], + }, + }, + }, +} +`; + +exports[`#loadAndBundleSpec should load And Bundle Spec demo/openapi-3-1.yaml 1`] = ` Object { "components": Object { "examples": Object { @@ -1784,10 +3580,6 @@ culpa qui officia deserunt mollit anim id est laborum. "description": "Everything about your Pets", "name": "pet", }, - Object { - "description": "Everything about your Webhooks", - "name": "webhooks", - }, Object { "description": "Access to Petstore orders", "name": "store", @@ -1796,6 +3588,10 @@ culpa qui officia deserunt mollit anim id est laborum. "description": "Operations about user", "name": "user", }, + Object { + "description": "Everything about your Webhooks", + "name": "webhooks", + }, Object { "description": " ", diff --git a/src/utils/__tests__/loadAndBundleSpec.test.ts b/src/utils/__tests__/loadAndBundleSpec.test.ts index 74dd2c881d..62f4c6fee8 100644 --- a/src/utils/__tests__/loadAndBundleSpec.test.ts +++ b/src/utils/__tests__/loadAndBundleSpec.test.ts @@ -10,6 +10,12 @@ describe('#loadAndBundleSpec', () => { 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'))); + 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 bundledSpec = await loadAndBundleSpec(spec); From 0c91eef92fbfd4ec6f459ed01cd4cd13a7b79f43 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Thu, 27 May 2021 15:43:08 +0300 Subject: [PATCH 16/21] fix: resolve webhooks and content items problem --- src/components/ContentItems/ContentItems.tsx | 2 +- src/services/MenuBuilder.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx index 646108276a..2766887af7 100644 --- a/src/components/ContentItems/ContentItems.tsx +++ b/src/components/ContentItems/ContentItems.tsx @@ -15,7 +15,7 @@ export class ContentItems extends React.Component<{ render() { const items = this.props.items; if (items.length === 0) { - return Cannot find path or webhooks items; + return null; } return items.map(item => { return ; diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts index 7322dab00d..7cb875edac 100644 --- a/src/services/MenuBuilder.ts +++ b/src/services/MenuBuilder.ts @@ -238,7 +238,8 @@ export class MenuBuilder { const operationInfo = path[operationName]; if (path.$ref) { const resolvedPaths = parser.deref(path as OpenAPIPaths); - getTags(parser, { [operationName]: resolvedPaths }, isWebhook); + getTags(parser, { [pathName]: resolvedPaths }, isWebhook); + continue; } let operationTags = operationInfo?.tags; From 68fb9e937b456c50827342a6b550d9012d9dc321 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Thu, 27 May 2021 16:00:14 +0300 Subject: [PATCH 17/21] fix: test in menu bulder --- src/services/__tests__/models/MenuBuilder.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/__tests__/models/MenuBuilder.test.ts b/src/services/__tests__/models/MenuBuilder.test.ts index a6ab4776c7..ed9306394d 100644 --- a/src/services/__tests__/models/MenuBuilder.test.ts +++ b/src/services/__tests__/models/MenuBuilder.test.ts @@ -10,11 +10,11 @@ describe('Models', () => { describe('MenuBuilder', () => { let parser; - test('discriminator with one field', () => { + 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(2); + expect(contentItems).toHaveLength(1); expect(contentItems[0].items).toHaveLength(2); expect(contentItems[0].id).toEqual('tag/pet'); expect(contentItems[0].name).toEqual('pet'); From 532c30e88c4005fd16a4b5f0c6a451cbbbe83f5e Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Fri, 28 May 2021 12:23:10 +0300 Subject: [PATCH 18/21] fix: improve code after review --- demo/index.tsx | 2 +- src/components/ApiInfo/ApiInfo.tsx | 2 +- src/services/models/Schema.ts | 13 +++++--- src/utils/__tests__/openapi.test.ts | 50 +++++++++++++++++++++++++++-- src/utils/openapi.ts | 28 ++++++++-------- 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/demo/index.tsx b/demo/index.tsx index e5e48c00e2..10abdd9cac 100644 --- a/demo/index.tsx +++ b/demo/index.tsx @@ -9,7 +9,7 @@ const DEFAULT_SPEC = 'openapi.yaml'; const NEW_VERSION_SPEC = 'openapi-3-1.yaml'; const demos = [ - { value: NEW_VERSION_SPEC, label: 'OpenApi 3.1' }, + { value: NEW_VERSION_SPEC, label: 'Petstore OpenAPI 3.1' }, { value: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml', label: 'Instagram' }, { value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/openapi.yaml', diff --git a/src/components/ApiInfo/ApiInfo.tsx b/src/components/ApiInfo/ApiInfo.tsx index 26819db0c7..ae4b4b20c6 100644 --- a/src/components/ApiInfo/ApiInfo.tsx +++ b/src/components/ApiInfo/ApiInfo.tsx @@ -100,8 +100,8 @@ export class ApiInfo extends React.Component { )) || null} - + {externalDocs && } diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index 33d9f65030..4d8a9665e4 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -105,7 +105,7 @@ export class SchemaModel { this.title = schema.title || (isNamedDefinition(this.pointer) && JsonPointer.baseName(this.pointer)) || ''; this.description = schema.description || ''; - this.type = (Array.isArray(schema.type) && schema.type) || (schema.type || detectType(schema)); + this.type = schema.type || detectType(schema); this.format = schema.format; this.enum = schema.enum || []; this.example = schema.example; @@ -122,11 +122,14 @@ export class SchemaModel { this.const = schema.const || ''; if (!!schema.nullable) { - if (Array.isArray(this.type)) this.type.push('null'); - else this.type = [this.type, 'null']; + if (Array.isArray(this.type) && !this.type.includes('null')) { + this.type = [...this.type, 'null']; + } } - this.displayType = Array.isArray(this.type) ? this.type.join(' or ') : this.type; + this.displayType = Array.isArray(this.type) + ? this.type.map(item => item === null ? 'null' : item).join(' or ') + : this.type; if (this.isCircular) { return; @@ -193,7 +196,7 @@ export class SchemaModel { const title = isNamedDefinition(variant.$ref) && !merged.title ? JsonPointer.baseName(variant.$ref) - : merged.title; + : (merged.const && JSON.stringify(merged.const)) || merged.title; const schema = new SchemaModel( parser, diff --git a/src/utils/__tests__/openapi.test.ts b/src/utils/__tests__/openapi.test.ts index 2045c49e49..670ac42c99 100644 --- a/src/utils/__tests__/openapi.test.ts +++ b/src/utils/__tests__/openapi.test.ts @@ -174,10 +174,56 @@ describe('Utils', () => { expect(isPrimitiveType(schema)).toEqual(false); }); - it('Should return false for array of strings', () => { + 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); }); @@ -185,7 +231,7 @@ describe('Utils', () => { const schema = { type: ['object', 'string', 'null'], }; - expect(isPrimitiveType(schema)).toEqual(false); + expect(isPrimitiveType(schema)).toEqual(true); }); it('Should return false for array with non-empty objects', () => { diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index 37dba6797b..f6d80f3148 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -69,7 +69,7 @@ export function getOperationSummary(operation: ExtendedOpenAPIOperation): string operation.operationId || (operation.description && operation.description.substring(0, 50)) || operation.pathName || - '' + '' ); } @@ -116,22 +116,20 @@ export function isPrimitiveType(schema: OpenAPISchema, type: string | string[] | 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 = false; } - if (Array.isArray(type)) return false - - return true; + return isPrimitive; } export function isJsonLike(contentType: string): boolean { @@ -589,10 +587,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 = { From 9fdcdfb049e2af9145810a85ae5df90fc92b496d Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Mon, 31 May 2021 13:53:15 +0300 Subject: [PATCH 19/21] fix: improve code --- src/services/models/Schema.ts | 2 +- src/utils/__tests__/openapi.test.ts | 13 +++++++++++++ src/utils/openapi.ts | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index 4d8a9665e4..2de8740ae0 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -196,7 +196,7 @@ export class SchemaModel { const title = isNamedDefinition(variant.$ref) && !merged.title ? JsonPointer.baseName(variant.$ref) - : (merged.const && JSON.stringify(merged.const)) || merged.title; + : `${(merged.title || '')}${(merged.const && JSON.stringify(merged.const)) || ''}`; const schema = new SchemaModel( parser, diff --git a/src/utils/__tests__/openapi.test.ts b/src/utils/__tests__/openapi.test.ts index 670ac42c99..d4f23e0219 100644 --- a/src/utils/__tests__/openapi.test.ts +++ b/src/utils/__tests__/openapi.test.ts @@ -227,6 +227,19 @@ describe('Utils', () => { 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'], diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index f6d80f3148..c31e3cd21c 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -126,7 +126,7 @@ export function isPrimitiveType(schema: OpenAPISchema, type: string | string[] | } if (schema.items !== undefined && (type === 'array' || (isArray && type?.includes('array')))) { - isPrimitive = false; + isPrimitive = isPrimitiveType(schema.items, schema.items.type); } return isPrimitive; From ce264112f2c148f266ca4b5ad2fe2d00896c970f Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Mon, 31 May 2021 18:30:15 +0300 Subject: [PATCH 20/21] fix: dropdown if type is array --- src/components/Schema/Schema.tsx | 4 ++++ src/services/models/Schema.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Schema/Schema.tsx b/src/components/Schema/Schema.tsx index ef774004ce..8948e2b47e 100644 --- a/src/components/Schema/Schema.tsx +++ b/src/components/Schema/Schema.tsx @@ -63,6 +63,10 @@ export class Schema extends React.Component> { return ; } + if (type && Array.isArray(type)) { + return ; + } + switch (type) { case 'object': if (schema.fields?.length) { diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index 2de8740ae0..1f329285ff 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -166,7 +166,7 @@ export class SchemaModel { if (this.type === 'object') { this.fields = buildFields(parser, schema, this.pointer, this.options); - } else if (this.type === 'array' && schema.items) { + } else if ((this.type === 'array' || Array.isArray(this.type)) && 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; From fd1a2c2537466beb16bc42571f7c9d6ae364d97f Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Mon, 31 May 2021 19:03:00 +0300 Subject: [PATCH 21/21] fix: improve label for array type --- src/services/models/Schema.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index 1f329285ff..f8971ac448 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -179,6 +179,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) {