From 0755ac6f04514eb0c08f90afceeda7858206b435 Mon Sep 17 00:00:00 2001 From: Anastasiia Derymarko Date: Thu, 5 May 2022 12:04:43 +0300 Subject: [PATCH] feat: Support OAS 3.1 unevaluatedProperties (#1978) --- demo/openapi-3-1.yaml | 3 + .../fixtures/3.1/unevaluatedProperties.json | 60 +++++++++++++++++++ src/services/__tests__/models/Schema.test.ts | 28 +++++++++ src/services/models/Schema.ts | 2 +- src/types/open-api.ts | 1 + .../loadAndBundleSpec.test.ts.snap | 4 ++ src/utils/__tests__/openapi.test.ts | 42 ++++++++++++- src/utils/openapi.ts | 3 +- 8 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 src/services/__tests__/fixtures/3.1/unevaluatedProperties.json diff --git a/demo/openapi-3-1.yaml b/demo/openapi-3-1.yaml index 707033cf..484fd33d 100644 --- a/demo/openapi-3-1.yaml +++ b/demo/openapi-3-1.yaml @@ -305,6 +305,9 @@ paths: content: application/json: schema: + unevaluatedProperties: + type: integer + format: int32 $ref: '#/components/schemas/ApiResponse' security: - petstore_auth: diff --git a/src/services/__tests__/fixtures/3.1/unevaluatedProperties.json b/src/services/__tests__/fixtures/3.1/unevaluatedProperties.json new file mode 100644 index 00000000..f5332c3b --- /dev/null +++ b/src/services/__tests__/fixtures/3.1/unevaluatedProperties.json @@ -0,0 +1,60 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Schema definition with unevaluatedProperties", + "version": "1.0.0" + }, + "servers": [ + { + "url": "example.com" + } + ], + "components": { + "schemas": { + "Test": { + "type": "object", + "unevaluatedProperties": true, + "properties": { + "$ref": "#/components/schemas/Cat" + } + }, + "Test2": { + "type": "object", + "unevaluatedProperties": true, + "anyOf": [ + { + "$ref": "#/components/schemas/Cat" + }, + { + "$ref": "#/components/schemas/Dog" + } + ] + }, + "Test3": { + "type": "object", + "unevaluatedProperties": { + "type": "boolean" + }, + "properties": { + "$ref": "#/components/schemas/Cat" + } + }, + "Cat": { + "type": "object", + "properties": { + "color": { + "type": "string" + } + } + }, + "Dog": { + "type": "object", + "properties": { + "size": { + "type": "string" + } + } + } + } + } +} diff --git a/src/services/__tests__/models/Schema.test.ts b/src/services/__tests__/models/Schema.test.ts index 9b9cdafc..966de1af 100644 --- a/src/services/__tests__/models/Schema.test.ts +++ b/src/services/__tests__/models/Schema.test.ts @@ -48,5 +48,33 @@ describe('Models', () => { expect(schema.fields).toHaveLength(1); expect(schema.pointer).toBe('#/components/schemas/Child'); }); + + test('schemaDefinition should resolve unevaluatedProperties in properties', () => { + const spec = require('../fixtures/3.1/unevaluatedProperties.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts); + expect(schema.fields).toHaveLength(2); + expect(schema.fields![1].kind).toEqual('additionalProperties'); + expect(schema.fields![1].schema.type).toEqual('any'); + }); + + test('schemaDefinition should resolve unevaluatedProperties in anyOf', () => { + const spec = require('../fixtures/3.1/unevaluatedProperties.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Test2, '', opts); + expect(schema.oneOf![0].fields).toHaveLength(2); + expect(schema.oneOf![0].fields![1].kind).toEqual('additionalProperties'); + expect(schema.oneOf![1].fields).toHaveLength(2); + expect(schema.oneOf![1].fields![1].kind).toEqual('additionalProperties'); + }); + + test('schemaDefinition should resolve unevaluatedProperties type boolean', () => { + const spec = require('../fixtures/3.1/unevaluatedProperties.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Test3, '', opts); + expect(schema.fields).toHaveLength(2); + expect(schema.fields![1].kind).toEqual('additionalProperties'); + expect(schema.fields![1].schema.type).toEqual('boolean'); + }); }); }); diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index b48f232c..7b2720da 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -364,7 +364,7 @@ function buildFields( options: RedocNormalizedOptions, ): FieldModel[] { const props = schema.properties || {}; - const additionalProps = schema.additionalProperties; + const additionalProps = schema.additionalProperties || schema.unevaluatedProperties; const defaults = schema.default; let fields = Object.keys(props || []).map(fieldName => { let field = props[fieldName]; diff --git a/src/types/open-api.ts b/src/types/open-api.ts index 7038aa7a..3d74e380 100644 --- a/src/types/open-api.ts +++ b/src/types/open-api.ts @@ -114,6 +114,7 @@ export interface OpenAPISchema { type?: string | string[]; properties?: { [name: string]: OpenAPISchema }; additionalProperties?: boolean | OpenAPISchema; + unevaluatedProperties?: boolean | OpenAPISchema; description?: string; default?: any; items?: OpenAPISchema; diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index 4ae0cd33..8459b133 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -2776,6 +2776,10 @@ try { "application/json": Object { "schema": Object { "$ref": "#/components/schemas/ApiResponse", + "unevaluatedProperties": Object { + "format": "int32", + "type": "integer", + }, }, }, }, diff --git a/src/utils/__tests__/openapi.test.ts b/src/utils/__tests__/openapi.test.ts index dde44fdd..e7d73b51 100644 --- a/src/utils/__tests__/openapi.test.ts +++ b/src/utils/__tests__/openapi.test.ts @@ -146,7 +146,14 @@ describe('Utils', () => { string: ['pattern', 'minLength', 'maxLength'], array: ['items', 'maxItems', 'minItems', 'uniqueItems'], - object: ['maxProperties', 'minProperties', 'required', 'additionalProperties', 'properties'], + object: [ + 'maxProperties', + 'minProperties', + 'required', + 'additionalProperties', + 'unevaluatedProperties', + 'properties', + ], }; Object.keys(tests).forEach(name => { @@ -212,6 +219,17 @@ describe('Utils', () => { expect(isPrimitiveType(schema)).toEqual(false); }); + it('should return false for array contains array type and schema has items (unevaluatedProperties)', () => { + const schema = { + type: ['array'], + items: { + type: 'object', + unevaluatedProperties: 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'], @@ -223,6 +241,17 @@ describe('Utils', () => { expect(isPrimitiveType(schema)).toEqual(false); }); + it('should return false for array contains object and array types and schema has items (unevaluatedProperties)', () => { + const schema = { + type: ['array', 'object'], + items: { + type: 'object', + unevaluatedProperties: 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'], @@ -281,6 +310,17 @@ describe('Utils', () => { expect(isPrimitiveType(schema)).toEqual(false); }); + it('should return false for object with unevaluatedProperties', () => { + const schema = { + type: 'array', + items: { + type: 'object', + unevaluatedProperties: true, + }, + }; + expect(isPrimitiveType(schema)).toEqual(false); + }); + it('should work with externally provided type', () => { const schema = { properties: { diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index fc3cc3b2..cedb9053 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -97,6 +97,7 @@ const schemaKeywordTypes = { minProperties: 'object', required: 'object', additionalProperties: 'object', + unevaluatedProperties: 'object', properties: 'object', }; @@ -130,7 +131,7 @@ export function isPrimitiveType( isPrimitive = schema.properties !== undefined ? Object.keys(schema.properties).length === 0 - : schema.additionalProperties === undefined; + : schema.additionalProperties === undefined && schema.unevaluatedProperties === undefined; } if (schema.items !== undefined && (type === 'array' || (isArray && type?.includes('array')))) {