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:
application/json:
schema:
unevaluatedProperties:
type: integer
format: int32
$ref: '#/components/schemas/ApiResponse'
security:
- 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.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,
): 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];

View File

@ -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;

View File

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

View File

@ -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: {

View File

@ -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')))) {