feat: Support OAS 3.1 unevaluatedProperties (#1978)

This commit is contained in:
Anastasiia Derymarko 2022-05-05 12:04:43 +03:00 committed by GitHub
parent 60bc603e9b
commit 0755ac6f04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 140 additions and 3 deletions

View File

@ -305,6 +305,9 @@ paths:
content: content:
application/json: application/json:
schema: schema:
unevaluatedProperties:
type: integer
format: int32
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
security: security:
- petstore_auth: - petstore_auth:

View File

@ -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"
}
}
}
}
}
}

View File

@ -48,5 +48,33 @@ describe('Models', () => {
expect(schema.fields).toHaveLength(1); expect(schema.fields).toHaveLength(1);
expect(schema.pointer).toBe('#/components/schemas/Child'); 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');
});
}); });
}); });

View File

@ -364,7 +364,7 @@ function buildFields(
options: RedocNormalizedOptions, options: RedocNormalizedOptions,
): FieldModel[] { ): FieldModel[] {
const props = schema.properties || {}; const props = schema.properties || {};
const additionalProps = schema.additionalProperties; const additionalProps = schema.additionalProperties || schema.unevaluatedProperties;
const defaults = schema.default; const defaults = schema.default;
let fields = Object.keys(props || []).map(fieldName => { let fields = Object.keys(props || []).map(fieldName => {
let field = props[fieldName]; let field = props[fieldName];

View File

@ -114,6 +114,7 @@ export interface OpenAPISchema {
type?: string | string[]; type?: string | string[];
properties?: { [name: string]: OpenAPISchema }; properties?: { [name: string]: OpenAPISchema };
additionalProperties?: boolean | OpenAPISchema; additionalProperties?: boolean | OpenAPISchema;
unevaluatedProperties?: boolean | OpenAPISchema;
description?: string; description?: string;
default?: any; default?: any;
items?: OpenAPISchema; items?: OpenAPISchema;

View File

@ -2776,6 +2776,10 @@ try {
"application/json": Object { "application/json": Object {
"schema": Object { "schema": Object {
"$ref": "#/components/schemas/ApiResponse", "$ref": "#/components/schemas/ApiResponse",
"unevaluatedProperties": Object {
"format": "int32",
"type": "integer",
},
}, },
}, },
}, },

View File

@ -146,7 +146,14 @@ describe('Utils', () => {
string: ['pattern', 'minLength', 'maxLength'], string: ['pattern', 'minLength', 'maxLength'],
array: ['items', 'maxItems', 'minItems', 'uniqueItems'], array: ['items', 'maxItems', 'minItems', 'uniqueItems'],
object: ['maxProperties', 'minProperties', 'required', 'additionalProperties', 'properties'], object: [
'maxProperties',
'minProperties',
'required',
'additionalProperties',
'unevaluatedProperties',
'properties',
],
}; };
Object.keys(tests).forEach(name => { Object.keys(tests).forEach(name => {
@ -212,6 +219,17 @@ describe('Utils', () => {
expect(isPrimitiveType(schema)).toEqual(false); 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', () => { it('should return false for array contains object and array types and schema has items', () => {
const schema = { const schema = {
type: ['array', 'object'], type: ['array', 'object'],
@ -223,6 +241,17 @@ describe('Utils', () => {
expect(isPrimitiveType(schema)).toEqual(false); 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', () => { it('should return false for array contains object and array types and schema has properties', () => {
const schema = { const schema = {
type: ['array', 'object'], type: ['array', 'object'],
@ -281,6 +310,17 @@ describe('Utils', () => {
expect(isPrimitiveType(schema)).toEqual(false); 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', () => { it('should work with externally provided type', () => {
const schema = { const schema = {
properties: { properties: {

View File

@ -97,6 +97,7 @@ const schemaKeywordTypes = {
minProperties: 'object', minProperties: 'object',
required: 'object', required: 'object',
additionalProperties: 'object', additionalProperties: 'object',
unevaluatedProperties: 'object',
properties: 'object', properties: 'object',
}; };
@ -130,7 +131,7 @@ export function isPrimitiveType(
isPrimitive = isPrimitive =
schema.properties !== undefined schema.properties !== undefined
? Object.keys(schema.properties).length === 0 ? 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')))) { if (schema.items !== undefined && (type === 'array' || (isArray && type?.includes('array')))) {