From 31ae8d210bb4d3180984cb82ba44a41a35b1f50f Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Wed, 23 Mar 2022 15:37:31 +0200 Subject: [PATCH 1/8] feat: add support conditional operators --- demo/openapi-3-1.yaml | 52 ++++++--- src/components/Fields/Field.tsx | 21 +++- src/components/Fields/FieldDetails.tsx | 14 ++- .../fixtures/3.1/conditionalField.json | 40 +++++++ .../fixtures/3.1/conditionalSchema.json | 40 +++++++ src/services/__tests__/models/Schema.test.ts | 27 +++++ .../models/__snapshots__/Schema.test.ts.snap | 100 ++++++++++++++++++ src/services/models/Schema.ts | 44 ++++++++ src/types/open-api.ts | 4 + .../loadAndBundleSpec.test.ts.snap | 53 +++++++++- src/utils/__tests__/openapi.test.ts | 2 +- 11 files changed, 374 insertions(+), 23 deletions(-) create mode 100644 src/services/__tests__/fixtures/3.1/conditionalField.json create mode 100644 src/services/__tests__/fixtures/3.1/conditionalSchema.json create mode 100644 src/services/__tests__/models/__snapshots__/Schema.test.ts.snap diff --git a/demo/openapi-3-1.yaml b/demo/openapi-3-1.yaml index ac89619db1..637ae94c05 100644 --- a/demo/openapi-3-1.yaml +++ b/demo/openapi-3-1.yaml @@ -93,7 +93,7 @@ paths: parameters: - name: Accept-Language in: header - description: "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US" + description: 'The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US' example: en-US required: false schema: @@ -259,7 +259,7 @@ paths: required: false schema: type: string - example: "Bearer " + example: 'Bearer ' - name: petId in: path description: Pet id to delete @@ -432,7 +432,7 @@ paths: application/json: example: status: 400 - message: "Invalid Order" + message: 'Invalid Order' requestBody: content: application/json: @@ -894,11 +894,11 @@ paths: type: string examples: response: - value: OK + value: OK text/plain: examples: response: - value: OK + value: OK '400': description: Invalid username/password supplied /user/logout: @@ -925,7 +925,7 @@ components: content: multipart/form-data: schema: - $ref: "#/components/schemas/Cat" + $ref: '#/components/schemas/Cat' responses: '200': description: update Cat details @@ -940,7 +940,7 @@ components: content: multipart/form-data: schema: - $ref: "#/components/schemas/Cat" + $ref: '#/components/schemas/Cat' responses: '200': description: create Cat details @@ -962,7 +962,7 @@ components: - type: object properties: huntingSkill: - type: string + type: [string, boolean] description: The measured skill for hunting default: lazy example: adventurous @@ -1073,8 +1073,8 @@ components: properties: id: externalDocs: - description: "Find more info here" - url: "https://example.com" + description: 'Find more info here' + url: 'https://example.com' description: Pet ID $ref: '#/components/schemas/Id' category: @@ -1087,14 +1087,23 @@ components: 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 + if: + x-displayName: isString + type: string + then: + minItems: 1 + maxItems: 10 + else: + x-displayName: notString + type: [integer, 'null', array] + minItems: 1 + maxItems: 20 friend: $ref: '#/components/schemas/Pet' tags: @@ -1118,6 +1127,12 @@ components: petType: description: Type of a pet type: string + huntingSkill: + type: [integer] + enum: + - 0 + - 1 + - 2 xml: name: Pet Tag: @@ -1185,6 +1200,15 @@ components: type: string contentEncoding: base64 contentMediaType: image/png + if: + title: userStatus === 10 + properties: + userStatus: + enum: [10] + then: + required: ['phone'] + else: + required: [] xml: name: User requestBodies: @@ -1251,9 +1275,9 @@ webhooks: content: application/json: schema: - $ref: "#/components/schemas/Pet" + $ref: '#/components/schemas/Pet' responses: - "200": + '200': description: Return a 200 status to indicate that the data was received successfully myWebhook: $ref: '#/components/pathItems/webhooks' diff --git a/src/components/Fields/Field.tsx b/src/components/Fields/Field.tsx index 2fb5865fdc..8c10620980 100644 --- a/src/components/Fields/Field.tsx +++ b/src/components/Fields/Field.tsx @@ -13,9 +13,11 @@ import { } from '../../common-elements/fields-layout'; import { ShelfIcon } from '../../common-elements/'; +import styled from '../../styled-components'; import { FieldModel } from '../../services/models'; import { Schema, SchemaOptions } from '../Schema/Schema'; +import { DiscriminatorDropdown } from '../Schema'; export interface FieldProps extends SchemaOptions { className?: string; @@ -26,6 +28,7 @@ export interface FieldProps extends SchemaOptions { expandByDefault?: boolean; renderDiscriminatorSwitch?: (opts: FieldProps) => JSX.Element; + renderConditionalSwitch?: (opts: FieldProps) => JSX.Element | undefined; } @observer @@ -77,12 +80,24 @@ export class Field extends React.Component { ); + const renderConditionalSwitch = field.schema?.oneOf + ? () => + field.schema.conditionalEnum && ( + + + + ) + : undefined; + return ( <> {paramName} - + {expanded && withSubSchema && ( @@ -104,3 +119,7 @@ export class Field extends React.Component { ); } } + +const ConditionalWrapper = styled.div` + display: block; +`; diff --git a/src/components/Fields/FieldDetails.tsx b/src/components/Fields/FieldDetails.tsx index c04a5b0fc4..08e30c41ff 100644 --- a/src/components/Fields/FieldDetails.tsx +++ b/src/components/Fields/FieldDetails.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { observer } from 'mobx-react'; import { RecursiveLabel, @@ -24,11 +25,15 @@ import { OptionsContext } from '../OptionsProvider'; import { Pattern } from './Pattern'; import { ArrayItemDetails } from './ArrayItemDetails'; -function FieldDetailsComponent(props: FieldProps) { +export const FieldDetailsComponent = observer((props: FieldProps) => { const { enumSkipQuotes, hideSchemaTitles } = React.useContext(OptionsContext); - const { showExamples, field, renderDiscriminatorSwitch } = props; - const { schema, description, deprecated, extensions, in: _in, const: _const } = field; + const { showExamples, field, renderDiscriminatorSwitch, renderConditionalSwitch } = props; + const { description, deprecated, extensions, in: _in, const: _const } = field; + const schema = + field.schema?.oneOf && renderConditionalSwitch + ? field.schema.oneOf[field.schema?.activeOneOf] + : field.schema; const isArrayType = schema.type === 'array'; const rawDefault = enumSkipQuotes || _in === 'header'; // having quotes around header field default values is confusing and inappropriate @@ -54,6 +59,7 @@ function FieldDetailsComponent(props: FieldProps) { return (
+ {(renderConditionalSwitch && renderConditionalSwitch(props)) || null} {schema.typePrefix} {schema.displayType} {schema.displayFormat && ( @@ -107,6 +113,6 @@ function FieldDetailsComponent(props: FieldProps) { {(_const && ) || null}
); -} +}); export const FieldDetails = React.memo(FieldDetailsComponent); diff --git a/src/services/__tests__/fixtures/3.1/conditionalField.json b/src/services/__tests__/fixtures/3.1/conditionalField.json new file mode 100644 index 0000000000..c48074023b --- /dev/null +++ b/src/services/__tests__/fixtures/3.1/conditionalField.json @@ -0,0 +1,40 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Schema definition field with conditional operators", + "version": "1.0.0" + }, + "components": { + "schemas": { + "Test": { + "type": "object", + "properties": { + "test": { + "type": ["string", "integer", "null", "array"], + "minItems": 1, + "maxItems": 20, + "items": { + "type": "string", + "format": "url" + }, + "if": { + "x-displayName": "isString", + "type": "string" + }, + "then": { + "type": "string", + "minItems": 1, + "maxItems": 20 + }, + "else": { + "x-displayName": "notString", + "minItems": 1, + "maxItems": 10, + "pattern": "\\d+" + } + } + } + } + } + } +} diff --git a/src/services/__tests__/fixtures/3.1/conditionalSchema.json b/src/services/__tests__/fixtures/3.1/conditionalSchema.json new file mode 100644 index 0000000000..035971233f --- /dev/null +++ b/src/services/__tests__/fixtures/3.1/conditionalSchema.json @@ -0,0 +1,40 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Schema definition with conditional operators", + "version": "1.0.0" + }, + "components": { + "schemas": { + "Test": { + "type": "object", + "properties": { + "test": { + "description": "The list of URL to a cute photos featuring pet", + "type": ["string", "integer", "null", "array"], + "minItems": 1, + "maxItems": 20, + "items": { + "type": "string", + "format": "url" + } + } + }, + "if": { + "title": "=== 10", + "properties": { + "test": { + "enum": [10] + } + } + }, + "then": { + "maxItems": 2 + }, + "else": { + "maxItems": 20 + } + } + } + } +} diff --git a/src/services/__tests__/models/Schema.test.ts b/src/services/__tests__/models/Schema.test.ts index 9b9cdafcc0..e887892a1e 100644 --- a/src/services/__tests__/models/Schema.test.ts +++ b/src/services/__tests__/models/Schema.test.ts @@ -48,5 +48,32 @@ describe('Models', () => { expect(schema.fields).toHaveLength(1); expect(schema.pointer).toBe('#/components/schemas/Child'); }); + + test('schemaDefinition should resolve schema with conditional operators', () => { + const spec = require('../fixtures/3.1/conditionalSchema.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts); + expect(schema.conditionalEnum).toHaveLength(2); + expect(schema.oneOf).toHaveLength(2); + + expect(schema.oneOf![0].schema.title).toBe('=== 10'); + expect(schema.oneOf![1].schema.title).toBe('else'); + + expect(schema.oneOf![0].schema).toMatchSnapshot(); + expect(schema.oneOf![1].schema).toMatchSnapshot(); + }); + + test('schemaDefinition should resolve field with conditional operators', () => { + const spec = require('../fixtures/3.1/conditionalField.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts); + expect(schema.fields).toHaveLength(1); + expect(schema.fields && schema.fields[0].schema.oneOf).toHaveLength(2); + expect(schema.fields && schema.fields[0].schema.oneOf![0].schema.title).toBe('isString'); + expect(schema.fields && schema.fields[0].schema.oneOf![1].schema.title).toBe('notString'); + + expect(schema.fields && schema.fields[0].schema.oneOf![0].schema).toMatchSnapshot(); + expect(schema.fields && schema.fields[0].schema.oneOf![1].schema).toMatchSnapshot(); + }); }); }); diff --git a/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap b/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap new file mode 100644 index 0000000000..492d91133a --- /dev/null +++ b/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Models Schema schemaDefinition should resolve field with conditional operators 1`] = ` +Object { + "allOf": undefined, + "default": undefined, + "items": Object { + "format": "url", + "type": "string", + }, + "maxItems": 20, + "minItems": 1, + "parentRefs": Array [], + "title": "isString", + "type": "string", + "x-displayName": "isString", +} +`; + +exports[`Models Schema schemaDefinition should resolve field with conditional operators 2`] = ` +Object { + "default": undefined, + "items": Object { + "format": "url", + "type": "string", + }, + "maxItems": 10, + "minItems": 1, + "pattern": "\\\\d+", + "title": "notString", + "type": Array [ + "string", + "integer", + "null", + "array", + ], + "x-displayName": "notString", +} +`; + +exports[`Models Schema schemaDefinition should resolve schema with conditional operators 1`] = ` +Object { + "allOf": undefined, + "maxItems": 2, + "parentRefs": Array [], + "properties": Object { + "test": Object { + "allOf": undefined, + "description": "The list of URL to a cute photos featuring pet", + "enum": Array [ + 10, + ], + "items": Object { + "allOf": undefined, + "format": "url", + "parentRefs": Array [], + "title": undefined, + "type": "string", + }, + "maxItems": 20, + "minItems": 1, + "parentRefs": Array [], + "title": undefined, + "type": Array [ + "string", + "integer", + "null", + "array", + ], + }, + }, + "title": "=== 10", + "type": "object", +} +`; + +exports[`Models Schema schemaDefinition should resolve schema with conditional operators 2`] = ` +Object { + "maxItems": 20, + "properties": Object { + "test": Object { + "description": "The list of URL to a cute photos featuring pet", + "items": Object { + "format": "url", + "type": "string", + }, + "maxItems": 20, + "minItems": 1, + "type": Array [ + "string", + "integer", + "null", + "array", + ], + }, + }, + "title": "else", + "type": "object", +} +`; diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index b48f232c57..75cde921dd 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -17,6 +17,7 @@ import { pluralizeType, sortByField, sortByRequired, + mergeObjects, } from '../../utils/'; import { l } from '../Labels'; @@ -65,6 +66,7 @@ export class SchemaModel { contentMediaType?: string; minItems?: number; maxItems?: number; + conditionalEnum?: string[]; /** * @param isChild if schema discriminator Child @@ -152,6 +154,10 @@ export class SchemaModel { return; } + if ((schema.if && schema.then) || (schema.if && schema.else)) { + this.initConditionalOperators(schema, parser); + } + if (!isChild && getDiscriminator(schema) !== undefined) { this.initDiscriminator(schema, parser); return; @@ -355,6 +361,44 @@ export class SchemaModel { return innerSchema; }); } + + private initConditionalOperators(schema: OpenAPISchema, parser: OpenAPIParser) { + const { + if: ifOperator, + else: elseOperator = {}, + then: thenOperator = {}, + ...clearSchema + } = schema; + const ifOperatorTitle = + (ifOperator && ifOperator['x-displayName']) || ifOperator?.title || 'if'; + const elseOperatorTitle = + (elseOperator && elseOperator['x-displayName']) || elseOperator?.title || 'else'; + const groupedOperators = [ + mergeObjects( + {}, + clearSchema, + { allOf: [ifOperator, thenOperator] }, + { title: ifOperatorTitle }, + ), + mergeObjects({}, clearSchema, elseOperator, { title: elseOperatorTitle }), + ]; + this.conditionalEnum = [ifOperatorTitle, elseOperatorTitle]; + + this.oneOf = groupedOperators.map((variant, idx) => { + const merged = parser.mergeAllOf(parser.deref(variant || {}), this.pointer + '/oneOf/' + idx); + const title = merged.title || this.title; + const result = new SchemaModel( + parser, + { + ...merged, + title, + } as OpenAPISchema, + this.pointer + '/oneOf/' + idx, + this.options, + ); + return result; + }); + } } function buildFields( diff --git a/src/types/open-api.ts b/src/types/open-api.ts index 7038aa7ab6..001b60dee4 100644 --- a/src/types/open-api.ts +++ b/src/types/open-api.ts @@ -146,6 +146,10 @@ export interface OpenAPISchema { minProperties?: number; enum?: any[]; example?: any; + + if?: OpenAPISchema; + else?: OpenAPISchema; + then?: OpenAPISchema; const?: string; contentEncoding?: string; contentMediaType?: string; diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index 6cd0a034df..86b23f33e3 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -1934,7 +1934,10 @@ Object { "aggressive", ], "example": "adventurous", - "type": "string", + "type": Array [ + "string", + "boolean", + ], }, }, "required": Array [ @@ -2086,6 +2089,16 @@ Object { "friend": Object { "$ref": "#/components/schemas/Pet", }, + "huntingSkill": Object { + "enum": Array [ + 0, + 1, + 2, + ], + "type": Array [ + "integer", + ], + }, "id": Object { "$ref": "#/components/schemas/Id", "description": "Pet ID", @@ -2105,12 +2118,28 @@ Object { }, "photoUrls": Object { "description": "The list of URL to a cute photos featuring pet", + "else": Object { + "maxItems": 20, + "minItems": 1, + "type": Array [ + "integer", + "null", + "array", + ], + "x-displayName": "notString", + }, + "if": Object { + "type": "string", + "x-displayName": "isString", + }, "items": Object { "format": "url", "type": "string", }, - "maxItems": 20, - "minItems": 1, + "then": Object { + "maxItems": 10, + "minItems": 1, + }, "type": Array [ "string", "integer", @@ -2173,6 +2202,19 @@ Object { }, }, "User": Object { + "else": Object { + "required": Array [], + }, + "if": Object { + "properties": Object { + "userStatus": Object { + "enum": Array [ + 10, + ], + }, + }, + "title": "userStatus === 10", + }, "properties": Object { "email": Object { "description": "User email address", @@ -2238,6 +2280,11 @@ Object { "type": "string", }, }, + "then": Object { + "required": Array [ + "phone", + ], + }, "type": "object", "xml": Object { "name": "User", diff --git a/src/utils/__tests__/openapi.test.ts b/src/utils/__tests__/openapi.test.ts index dde44fdd11..dc50fbb598 100644 --- a/src/utils/__tests__/openapi.test.ts +++ b/src/utils/__tests__/openapi.test.ts @@ -248,7 +248,7 @@ describe('Utils', () => { expect(isPrimitiveType(schema)).toEqual(true); }); - it('Should return false for array of string which include the null value', () => { + it('Should return true for array of string which include the null value', () => { const schema = { type: ['object', 'string', 'null'], }; From 3f70679e89f2fc35599a98e65931d42ef3a37d6c Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Wed, 23 Mar 2022 15:38:40 +0200 Subject: [PATCH 2/8] fix: merge type and enum in allOf for 3.1 --- src/services/OpenAPIParser.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts index bdc86bbebb..d98971e46c 100644 --- a/src/services/OpenAPIParser.ts +++ b/src/services/OpenAPIParser.ts @@ -281,7 +281,19 @@ export class OpenAPIParser { } if (subSchema.type !== undefined) { - receiver.type = subSchema.type; + if (Array.isArray(subSchema.type) && Array.isArray(receiver.type)) { + receiver.type = subSchema.type.concat(...receiver.type); + } else { + receiver.type = subSchema.type; + } + } + + if (subSchema.enum !== undefined) { + if (Array.isArray(subSchema.enum) && Array.isArray(receiver.enum)) { + receiver.enum = subSchema.enum.concat(...receiver.enum); + } else { + receiver.enum = subSchema.enum; + } } if (subSchema.properties !== undefined) { From b0f07face56cfc976a521ea65a59c50932aa8ae1 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Fri, 13 May 2022 18:52:24 +0300 Subject: [PATCH 3/8] chore: add example for OpenApi 3.1 --- demo/openapi-3-1.yaml | 27 +++++++++++++ src/components/Fields/Field.tsx | 2 +- .../loadAndBundleSpec.test.ts.snap | 40 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/demo/openapi-3-1.yaml b/demo/openapi-3-1.yaml index e5fa6f0a79..894fa03b7a 100644 --- a/demo/openapi-3-1.yaml +++ b/demo/openapi-3-1.yaml @@ -960,6 +960,33 @@ components: schemas: ApiResponse: type: object + patternProperties: + ^S_\\w+\\.[1-9]{2,4}$: + description: The measured skill for hunting + if: + x-displayName: fieldName === 'status' + else: + minLength: 1 + maxLength: 10 + then: + format: url + type: string + enum: + - success + - failed + ^O_\\w+\\.[1-9]{2,4}$: + type: object + properties: + nestedProperty: + type: [string, boolean] + description: The measured skill for hunting + default: lazy + example: adventurous + enum: + - clueless + - lazy + - adventurous + - aggressive properties: code: type: integer diff --git a/src/components/Fields/Field.tsx b/src/components/Fields/Field.tsx index 6bf1663c09..cd3238d5b5 100644 --- a/src/components/Fields/Field.tsx +++ b/src/components/Fields/Field.tsx @@ -52,7 +52,7 @@ export class Field extends React.Component { }; render() { - const { className, field, isLast, expandByDefault } = this.props; + const { className = '', field, isLast, expandByDefault } = this.props; const { name, deprecated, required, kind } = field; const withSubSchema = !field.schema.isPrimitive && !field.schema.isCircular; diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index 30ae4d55d0..5ba63f39ae 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -1903,6 +1903,46 @@ Object { }, "schemas": Object { "ApiResponse": Object { + "patternProperties": Object { + "^O_\\\\\\\\w+\\\\\\\\.[1-9]{2,4}$": Object { + "properties": Object { + "nestedProperty": Object { + "default": "lazy", + "description": "The measured skill for hunting", + "enum": Array [ + "clueless", + "lazy", + "adventurous", + "aggressive", + ], + "example": "adventurous", + "type": Array [ + "string", + "boolean", + ], + }, + }, + "type": "object", + }, + "^S_\\\\\\\\w+\\\\\\\\.[1-9]{2,4}$": Object { + "description": "The measured skill for hunting", + "else": Object { + "maxLength": 10, + "minLength": 1, + }, + "if": Object { + "x-displayName": "fieldName === 'status'", + }, + "then": Object { + "enum": Array [ + "success", + "failed", + ], + "format": "url", + "type": "string", + }, + }, + }, "properties": Object { "code": Object { "format": "int32", From 2291a12c6b0525ddb5899a301cba5cf044ab301f Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Mon, 16 May 2022 16:00:49 +0300 Subject: [PATCH 4/8] fix: improve code after review --- src/components/Fields/Field.tsx | 21 +---------- src/components/Fields/FieldDetails.tsx | 9 ++--- src/services/models/Schema.ts | 50 +++++++++++--------------- src/utils/openapi.ts | 4 +++ 4 files changed, 28 insertions(+), 56 deletions(-) diff --git a/src/components/Fields/Field.tsx b/src/components/Fields/Field.tsx index cd3238d5b5..5c35b04b92 100644 --- a/src/components/Fields/Field.tsx +++ b/src/components/Fields/Field.tsx @@ -15,8 +15,6 @@ import { PropertyNameCell, } from '../../common-elements/fields-layout'; import { ShelfIcon } from '../../common-elements/'; -import styled from '../../styled-components'; -import { DiscriminatorDropdown } from '../Schema'; import { Schema } from '../Schema/Schema'; import type { SchemaOptions } from '../Schema/Schema'; @@ -31,7 +29,6 @@ export interface FieldProps extends SchemaOptions { expandByDefault?: boolean; renderDiscriminatorSwitch?: (opts: FieldProps) => JSX.Element; - renderConditionalSwitch?: (opts: FieldProps) => JSX.Element | undefined; } @observer @@ -91,24 +88,12 @@ export class Field extends React.Component { ); - const renderConditionalSwitch = field.schema?.oneOf - ? () => - field.schema.conditionalEnum && ( - - - - ) - : undefined; - return ( <> {paramName} - + {expanded && withSubSchema && ( @@ -130,7 +115,3 @@ export class Field extends React.Component { ); } } - -const ConditionalWrapper = styled.div` - display: block; -`; diff --git a/src/components/Fields/FieldDetails.tsx b/src/components/Fields/FieldDetails.tsx index 08e30c41ff..36a43d3e92 100644 --- a/src/components/Fields/FieldDetails.tsx +++ b/src/components/Fields/FieldDetails.tsx @@ -28,12 +28,8 @@ import { ArrayItemDetails } from './ArrayItemDetails'; export const FieldDetailsComponent = observer((props: FieldProps) => { const { enumSkipQuotes, hideSchemaTitles } = React.useContext(OptionsContext); - const { showExamples, field, renderDiscriminatorSwitch, renderConditionalSwitch } = props; - const { description, deprecated, extensions, in: _in, const: _const } = field; - const schema = - field.schema?.oneOf && renderConditionalSwitch - ? field.schema.oneOf[field.schema?.activeOneOf] - : field.schema; + const { showExamples, field, renderDiscriminatorSwitch } = props; + const { schema, description, deprecated, extensions, in: _in, const: _const } = field; const isArrayType = schema.type === 'array'; const rawDefault = enumSkipQuotes || _in === 'header'; // having quotes around header field default values is confusing and inappropriate @@ -59,7 +55,6 @@ export const FieldDetailsComponent = observer((props: FieldProps) => { return (
- {(renderConditionalSwitch && renderConditionalSwitch(props)) || null} {schema.typePrefix} {schema.displayType} {schema.displayFormat && ( diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index 0f05d7f491..e00682bd3f 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -18,7 +18,6 @@ import { pluralizeType, sortByField, sortByRequired, - mergeObjects, } from '../../utils/'; import { l } from '../Labels'; @@ -68,7 +67,6 @@ export class SchemaModel { contentMediaType?: string; minItems?: number; maxItems?: number; - conditionalEnum?: string[]; /** * @param isChild if schema discriminator Child @@ -156,6 +154,7 @@ export class SchemaModel { if ((schema.if && schema.then) || (schema.if && schema.else)) { this.initConditionalOperators(schema, parser); + return; } if (!isChild && getDiscriminator(schema) !== undefined) { @@ -367,37 +366,30 @@ export class SchemaModel { if: ifOperator, else: elseOperator = {}, then: thenOperator = {}, - ...clearSchema + ...restSchema } = schema; - const ifOperatorTitle = - (ifOperator && ifOperator['x-displayName']) || ifOperator?.title || 'if'; - const elseOperatorTitle = - (elseOperator && elseOperator['x-displayName']) || elseOperator?.title || 'else'; const groupedOperators = [ - mergeObjects( - {}, - clearSchema, - { allOf: [ifOperator, thenOperator] }, - { title: ifOperatorTitle }, - ), - mergeObjects({}, clearSchema, elseOperator, { title: elseOperatorTitle }), + { + allOf: [restSchema, thenOperator, ifOperator], + title: (ifOperator && ifOperator['x-displayName']) || ifOperator?.title || 'if', + }, + { + allOf: [restSchema, elseOperator], + title: (elseOperator && elseOperator['x-displayName']) || elseOperator?.title || 'else', + }, ]; - this.conditionalEnum = [ifOperatorTitle, elseOperatorTitle]; - this.oneOf = groupedOperators.map((variant, idx) => { - const merged = parser.mergeAllOf(parser.deref(variant || {}), this.pointer + '/oneOf/' + idx); - const title = merged.title || this.title; - const result = new SchemaModel( - parser, - { - ...merged, - title, - } as OpenAPISchema, - this.pointer + '/oneOf/' + idx, - this.options, - ); - return result; - }); + this.oneOf = groupedOperators.map( + (variant, idx) => + new SchemaModel( + parser, + { + ...variant, + } as OpenAPISchema, + this.pointer + '/oneOf/' + idx, + this.options, + ), + ); } } diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index 0540cecca3..78690dc264 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -125,6 +125,10 @@ export function isPrimitiveType( return false; } + if (schema.if || schema.then || schema.else) { + return false; + } + let isPrimitive = true; const isArrayType = isArray(type); From 633cc3458405bd0784b56c8ee3ab259dd0c421ad Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Mon, 16 May 2022 16:01:21 +0300 Subject: [PATCH 5/8] chore: update test --- demo/openapi-3-1.yaml | 8 +++++--- .../__tests__/fixtures/3.1/conditionalField.json | 2 +- .../__tests__/fixtures/3.1/conditionalSchema.json | 2 +- src/services/__tests__/models/Schema.test.ts | 1 - .../models/__snapshots__/Schema.test.ts.snap | 13 ++++++++++--- .../__snapshots__/loadAndBundleSpec.test.ts.snap | 6 +++--- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/demo/openapi-3-1.yaml b/demo/openapi-3-1.yaml index 894fa03b7a..9c8d0f07ce 100644 --- a/demo/openapi-3-1.yaml +++ b/demo/openapi-3-1.yaml @@ -1126,7 +1126,9 @@ components: example: Guru photoUrls: description: The list of URL to a cute photos featuring pet - type: [string, integer, 'null', array] + type: [string, integer, 'null'] + minItems: 1 + maxItems: 10 xml: name: photoUrl wrapped: true @@ -1138,10 +1140,10 @@ components: type: string then: minItems: 1 - maxItems: 10 + maxItems: 15 else: x-displayName: notString - type: [integer, 'null', array] + type: [integer, 'null'] minItems: 1 maxItems: 20 friend: diff --git a/src/services/__tests__/fixtures/3.1/conditionalField.json b/src/services/__tests__/fixtures/3.1/conditionalField.json index c48074023b..3c010f5d45 100644 --- a/src/services/__tests__/fixtures/3.1/conditionalField.json +++ b/src/services/__tests__/fixtures/3.1/conditionalField.json @@ -10,7 +10,7 @@ "type": "object", "properties": { "test": { - "type": ["string", "integer", "null", "array"], + "type": ["string", "integer", "null"], "minItems": 1, "maxItems": 20, "items": { diff --git a/src/services/__tests__/fixtures/3.1/conditionalSchema.json b/src/services/__tests__/fixtures/3.1/conditionalSchema.json index 035971233f..452747aea4 100644 --- a/src/services/__tests__/fixtures/3.1/conditionalSchema.json +++ b/src/services/__tests__/fixtures/3.1/conditionalSchema.json @@ -11,7 +11,7 @@ "properties": { "test": { "description": "The list of URL to a cute photos featuring pet", - "type": ["string", "integer", "null", "array"], + "type": ["string", "integer", "null"], "minItems": 1, "maxItems": 20, "items": { diff --git a/src/services/__tests__/models/Schema.test.ts b/src/services/__tests__/models/Schema.test.ts index ee8d5c96d3..eab4573b3f 100644 --- a/src/services/__tests__/models/Schema.test.ts +++ b/src/services/__tests__/models/Schema.test.ts @@ -53,7 +53,6 @@ describe('Models', () => { const spec = require('../fixtures/3.1/conditionalSchema.json'); parser = new OpenAPIParser(spec, undefined, opts); const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts); - expect(schema.conditionalEnum).toHaveLength(2); expect(schema.oneOf).toHaveLength(2); expect(schema.oneOf![0].schema.title).toBe('=== 10'); diff --git a/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap b/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap index 492d91133a..d5abbf0316 100644 --- a/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap +++ b/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap @@ -5,7 +5,10 @@ Object { "allOf": undefined, "default": undefined, "items": Object { + "allOf": undefined, "format": "url", + "parentRefs": Array [], + "title": undefined, "type": "string", }, "maxItems": 20, @@ -19,20 +22,24 @@ Object { exports[`Models Schema schemaDefinition should resolve field with conditional operators 2`] = ` Object { + "allOf": undefined, "default": undefined, "items": Object { + "allOf": undefined, "format": "url", + "parentRefs": Array [], + "title": undefined, "type": "string", }, "maxItems": 10, "minItems": 1, + "parentRefs": Array [], "pattern": "\\\\d+", "title": "notString", "type": Array [ "string", "integer", "null", - "array", ], "x-displayName": "notString", } @@ -65,7 +72,6 @@ Object { "string", "integer", "null", - "array", ], }, }, @@ -76,7 +82,9 @@ Object { exports[`Models Schema schemaDefinition should resolve schema with conditional operators 2`] = ` Object { + "allOf": undefined, "maxItems": 20, + "parentRefs": Array [], "properties": Object { "test": Object { "description": "The list of URL to a cute photos featuring pet", @@ -90,7 +98,6 @@ Object { "string", "integer", "null", - "array", ], }, }, diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index 5ba63f39ae..a2c8c1896c 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -2164,7 +2164,6 @@ Object { "type": Array [ "integer", "null", - "array", ], "x-displayName": "notString", }, @@ -2176,15 +2175,16 @@ Object { "format": "url", "type": "string", }, + "maxItems": 10, + "minItems": 1, "then": Object { - "maxItems": 10, + "maxItems": 15, "minItems": 1, }, "type": Array [ "string", "integer", "null", - "array", ], "xml": Object { "name": "photoUrl", From b2c4c11da609acc4f6a75b0205aab8e34b3be622 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Mon, 16 May 2022 16:02:54 +0300 Subject: [PATCH 6/8] fix: correct merge constraints in allOf --- src/services/OpenAPIParser.ts | 56 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts index 1d1324039a..6cf3604d51 100644 --- a/src/services/OpenAPIParser.ts +++ b/src/services/OpenAPIParser.ts @@ -268,41 +268,44 @@ export class OpenAPIParser { }>; for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) { - if ( - receiver.type !== subSchema.type && - receiver.type !== undefined && - subSchema.type !== undefined - ) { - console.warn( - `Incompatible types in allOf at "${$ref}": "${receiver.type}" and "${subSchema.type}"`, - ); + const { + type, + enum: enumProperty, + properties, + items, + required, + ...otherConstraints + } = subSchema; + + if (receiver.type !== type && receiver.type !== undefined && type !== undefined) { + console.warn(`Incompatible types in allOf at "${$ref}": "${receiver.type}" and "${type}"`); } - if (subSchema.type !== undefined) { - if (Array.isArray(subSchema.type) && Array.isArray(receiver.type)) { - receiver.type = subSchema.type.concat(...receiver.type); + if (type !== undefined) { + if (Array.isArray(type) && Array.isArray(receiver.type)) { + receiver.type = [...type, ...receiver.type]; } else { - receiver.type = subSchema.type; + receiver.type = type; } } - if (subSchema.enum !== undefined) { - if (Array.isArray(subSchema.enum) && Array.isArray(receiver.enum)) { - receiver.enum = subSchema.enum.concat(...receiver.enum); + if (enumProperty !== undefined) { + if (Array.isArray(enumProperty) && Array.isArray(receiver.enum)) { + receiver.enum = [...enumProperty, ...receiver.enum]; } else { - receiver.enum = subSchema.enum; + receiver.enum = enumProperty; } } - if (subSchema.properties !== undefined) { + if (properties !== undefined) { receiver.properties = receiver.properties || {}; - for (const prop in subSchema.properties) { + for (const prop in properties) { if (!receiver.properties[prop]) { - receiver.properties[prop] = subSchema.properties[prop]; + receiver.properties[prop] = properties[prop]; } else { // merge inner properties const mergedProp = this.mergeAllOf( - { allOf: [receiver.properties[prop], subSchema.properties[prop]] }, + { allOf: [receiver.properties[prop], properties[prop]] }, $ref + '/properties/' + prop, ); receiver.properties[prop] = mergedProp; @@ -311,22 +314,19 @@ export class OpenAPIParser { } } - if (subSchema.items !== undefined) { + if (items !== undefined) { receiver.items = receiver.items || {}; // merge inner properties - receiver.items = this.mergeAllOf( - { allOf: [receiver.items, subSchema.items] }, - $ref + '/items', - ); + receiver.items = this.mergeAllOf({ allOf: [receiver.items, items] }, $ref + '/items'); } - if (subSchema.required !== undefined) { - receiver.required = (receiver.required || []).concat(subSchema.required); + if (required !== undefined) { + receiver.required = (receiver.required || []).concat(required); } // merge rest of constraints // TODO: do more intelligent merge - receiver = { ...subSchema, ...receiver }; + receiver = { ...receiver, ...otherConstraints }; if (subSchemaRef) { receiver.parentRefs!.push(subSchemaRef); From a9c3599c59230b0afab29a2620e35496a795feeb Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Mon, 16 May 2022 18:41:15 +0300 Subject: [PATCH 7/8] chore: rename default if/else title --- src/services/__tests__/models/Schema.test.ts | 2 +- .../__tests__/models/__snapshots__/Schema.test.ts.snap | 2 +- src/services/models/Schema.ts | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/services/__tests__/models/Schema.test.ts b/src/services/__tests__/models/Schema.test.ts index eab4573b3f..d6eb67381b 100644 --- a/src/services/__tests__/models/Schema.test.ts +++ b/src/services/__tests__/models/Schema.test.ts @@ -56,7 +56,7 @@ describe('Models', () => { expect(schema.oneOf).toHaveLength(2); expect(schema.oneOf![0].schema.title).toBe('=== 10'); - expect(schema.oneOf![1].schema.title).toBe('else'); + expect(schema.oneOf![1].schema.title).toBe('case 2'); expect(schema.oneOf![0].schema).toMatchSnapshot(); expect(schema.oneOf![1].schema).toMatchSnapshot(); diff --git a/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap b/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap index d5abbf0316..b0f6c73ecc 100644 --- a/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap +++ b/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap @@ -101,7 +101,7 @@ Object { ], }, }, - "title": "else", + "title": "case 2", "type": "object", } `; diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index e00682bd3f..71a3a48084 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -371,11 +371,11 @@ export class SchemaModel { const groupedOperators = [ { allOf: [restSchema, thenOperator, ifOperator], - title: (ifOperator && ifOperator['x-displayName']) || ifOperator?.title || 'if', + title: (ifOperator && ifOperator['x-displayName']) || ifOperator?.title || 'case 1', }, { allOf: [restSchema, elseOperator], - title: (elseOperator && elseOperator['x-displayName']) || elseOperator?.title || 'else', + title: (elseOperator && elseOperator['x-displayName']) || elseOperator?.title || 'case 2', }, ]; @@ -390,6 +390,7 @@ export class SchemaModel { this.options, ), ); + this.oneOfType = 'One of'; } } From 6ac9f8e78840780f16710955677078e732677eb0 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Tue, 17 May 2022 10:01:56 +0300 Subject: [PATCH 8/8] fix: conditional for primitive value in if/else --- src/utils/openapi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index 78690dc264..accf169f1c 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -125,7 +125,7 @@ export function isPrimitiveType( return false; } - if (schema.if || schema.then || schema.else) { + if ((schema.if && schema.then) || (schema.if && schema.else)) { return false; }