diff --git a/packages/openapi3-parser/lib/parser/oas/parseSchemaObject.js b/packages/openapi3-parser/lib/parser/oas/parseSchemaObject.js index 68bd4359..342dd5fb 100644 --- a/packages/openapi3-parser/lib/parser/oas/parseSchemaObject.js +++ b/packages/openapi3-parser/lib/parser/oas/parseSchemaObject.js @@ -6,7 +6,7 @@ const { } = require('../annotations'); const pipeParseResult = require('../../pipeParseResult'); const { - isString, hasKey, getValue, + isArray, isString, hasKey, getValue, } = require('../../predicates'); const parseObject = require('../parseObject'); const parseArray = require('../parseArray'); @@ -128,10 +128,14 @@ const typeToElementNameMap = { * Normalises result into an ArrayElement of StringElement */ function parseType(context) { + const { namespace } = context; + let types; let isValidType; - if (context.isOpenAPIVersionMoreThanOrEqual(3, 1)) { + const isOpenAPI31OrHigher = R.always(context.isOpenAPIVersionMoreThanOrEqual(3, 1)); + + if (isOpenAPI31OrHigher()) { types = openapi31Types; isValidType = isValidOpenAPI31Type; } else { @@ -141,13 +145,71 @@ function parseType(context) { const ensureValidType = R.unless( element => isValidType(element.toValue()), - createWarning(context.namespace, `'${name}' 'type' must be either ${types.join(', ')}`) + createWarning(namespace, `'${name}' 'type' must be either ${types.join(', ')}`) ); - return pipeParseResult(context.namespace, - R.unless(isString, value => createWarning(context.namespace, `'${name}' 'type' is not a string`, value)), + const parseStringType = pipeParseResult(namespace, ensureValidType, - type => new context.namespace.elements.Array([type])); + type => new namespace.elements.Array([type])); + + const ensureValidTypeInArray = R.unless( + element => isValidType(element.toValue()), + createWarning(namespace, `'${name}' 'type' array must only contain values: ${types.join(', ')}`) + ); + + const parseArrayTypeItem = pipeParseResult(namespace, + R.unless(isString, createWarning(namespace, `'${name}' 'type' array value is not a string`)), + ensureValidTypeInArray); + + const isEmpty = arrayElement => arrayElement.isEmpty; + + // ArrayElement -> ParseResult + const ensureTypesAreUnique = (types) => { + const inspectedTypes = []; + const warnings = []; + const permittedTypes = R.filter((type) => { + if (inspectedTypes.includes(type.toValue())) { + warnings.push(createWarning(namespace, `'${name}' 'type' array must contain unique items, ${type.toValue()} is already present`, type)); + return false; + } + + inspectedTypes.push(type.toValue()); + return true; + }, types); + + const parseResult = new namespace.elements.ParseResult(); + parseResult.push(permittedTypes.elements); + + if (warnings.length > 0) { + parseResult.push(...warnings); + } + return parseResult; + }; + + const parseArrayType = pipeParseResult(namespace, + R.when(isEmpty, createWarning(namespace, `'${name}' 'type' array must contain at least one type`)), + parseArray(context, `${name}' 'type`, parseArrayTypeItem), + ensureTypesAreUnique, + + // FIXME support >1 type + R.when(e => e.length > 1, createWarning(namespace, `'${name}' 'type' more than one type is current unsupported`))); + + return R.cond([ + [isString, parseStringType], + [ + R.both(isArray, isOpenAPI31OrHigher), + parseArrayType, + ], + + [ + isOpenAPI31OrHigher, + value => createWarning(namespace, `'${name}' 'type' is not a string or an array`, value), + ], + [ + R.T, + value => createWarning(namespace, `'${name}' 'type' is not a string`, value), + ], + ]); } // Returns whether the given element value matches the provided schema type diff --git a/packages/openapi3-parser/test/unit/parser/oas/parseSchemaObject-test.js b/packages/openapi3-parser/test/unit/parser/oas/parseSchemaObject-test.js index 42f2d7f7..33180fe5 100644 --- a/packages/openapi3-parser/test/unit/parser/oas/parseSchemaObject-test.js +++ b/packages/openapi3-parser/test/unit/parser/oas/parseSchemaObject-test.js @@ -21,15 +21,25 @@ describe('Schema Object', () => { }); describe('#type', () => { - it('warns when type is not a string', () => { + it('warns when type is unexpected on OpenAPI 3.0', () => { const schema = new namespace.elements.Object({ - type: ['null'], + type: true, }); const parseResult = parse(context, schema); expect(parseResult).to.contain.warning("'Schema Object' 'type' is not a string"); }); + it('warns when type is unexpected on OpenAPI 3.1', () => { + context.openapiVersion = { major: 3, minor: 1 }; + const schema = new namespace.elements.Object({ + type: true, + }); + const parseResult = parse(context, schema); + + expect(parseResult).to.contain.warning("'Schema Object' 'type' is not a string or an array"); + }); + it('warns when type is not a valid type', () => { const schema = new namespace.elements.Object({ type: 'invalid', @@ -82,114 +92,206 @@ describe('Schema Object', () => { expect(element.attributes.getValue('typeAttributes')).to.deep.equal(['nullable']); }); - it('returns a boolean structure for boolean type', () => { - const schema = new namespace.elements.Object({ - type: 'boolean', + describe('when type is a string', () => { + it('returns a boolean structure for boolean type', () => { + const schema = new namespace.elements.Object({ + type: 'boolean', + }); + const parseResult = parse(context, schema); + + expect(parseResult.length).to.equal(1); + expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); + expect(parseResult).to.not.contain.annotations; + + const string = parseResult.get(0).content; + expect(string).to.be.instanceof(namespace.elements.Boolean); }); - const parseResult = parse(context, schema); - expect(parseResult.length).to.equal(1); - expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); - expect(parseResult).to.not.contain.annotations; + it('returns an object structure for object type', () => { + const schema = new namespace.elements.Object({ + type: 'object', + }); + const parseResult = parse(context, schema); - const string = parseResult.get(0).content; - expect(string).to.be.instanceof(namespace.elements.Boolean); - }); + expect(parseResult.length).to.equal(1); + expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); + expect(parseResult).to.not.contain.annotations; - it('returns an object structure for object type', () => { - const schema = new namespace.elements.Object({ - type: 'object', + const object = parseResult.get(0).content; + expect(object).to.be.instanceof(namespace.elements.Object); }); - const parseResult = parse(context, schema); - expect(parseResult.length).to.equal(1); - expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); - expect(parseResult).to.not.contain.annotations; + it('returns an array structure for array type', () => { + const schema = new namespace.elements.Object({ + type: 'array', + }); + const parseResult = parse(context, schema); - const object = parseResult.get(0).content; - expect(object).to.be.instanceof(namespace.elements.Object); - }); + expect(parseResult.length).to.equal(1); + expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); + expect(parseResult).to.not.contain.annotations; - it('returns an array structure for array type', () => { - const schema = new namespace.elements.Object({ - type: 'array', + const array = parseResult.get(0).content; + expect(array).to.be.instanceof(namespace.elements.Array); }); - const parseResult = parse(context, schema); - expect(parseResult.length).to.equal(1); - expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); - expect(parseResult).to.not.contain.annotations; + it('returns a number structure for number type', () => { + const schema = new namespace.elements.Object({ + type: 'number', + }); + const parseResult = parse(context, schema); - const array = parseResult.get(0).content; - expect(array).to.be.instanceof(namespace.elements.Array); - }); + expect(parseResult.length).to.equal(1); + expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); + expect(parseResult).to.not.contain.annotations; - it('returns a number structure for number type', () => { - const schema = new namespace.elements.Object({ - type: 'number', + const string = parseResult.get(0).content; + expect(string).to.be.instanceof(namespace.elements.Number); }); - const parseResult = parse(context, schema); - expect(parseResult.length).to.equal(1); - expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); - expect(parseResult).to.not.contain.annotations; + it('returns a string structure for string type', () => { + const schema = new namespace.elements.Object({ + type: 'string', + }); + const parseResult = parse(context, schema); - const string = parseResult.get(0).content; - expect(string).to.be.instanceof(namespace.elements.Number); - }); + expect(parseResult.length).to.equal(1); + expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); + expect(parseResult).to.not.contain.annotations; - it('returns a string structure for string type', () => { - const schema = new namespace.elements.Object({ - type: 'string', + const string = parseResult.get(0).content; + expect(string).to.be.instanceof(namespace.elements.String); }); - const parseResult = parse(context, schema); - expect(parseResult.length).to.equal(1); - expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); - expect(parseResult).to.not.contain.annotations; + it('returns a number structure for integer type', () => { + const schema = new namespace.elements.Object({ + type: 'integer', + }); + const parseResult = parse(context, schema); - const string = parseResult.get(0).content; - expect(string).to.be.instanceof(namespace.elements.String); - }); + expect(parseResult.length).to.equal(1); + expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); + expect(parseResult).to.not.contain.annotations; - it('returns a number structure for integer type', () => { - const schema = new namespace.elements.Object({ - type: 'integer', + const string = parseResult.get(0).content; + expect(string).to.be.instanceof(namespace.elements.Number); }); - const parseResult = parse(context, schema); - expect(parseResult.length).to.equal(1); - expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); - expect(parseResult).to.not.contain.annotations; + it('returns a warning for null type on OpenAPI prior 3.1', () => { + const schema = new namespace.elements.Object({ + type: 'null', + }); + const parseResult = parse(context, schema); + + expect(parseResult).to.contain.warning( + "'Schema Object' 'type' must be either boolean, object, array, number, string, integer" + ); + }); - const string = parseResult.get(0).content; - expect(string).to.be.instanceof(namespace.elements.Number); + it('returns a null structure for null type on OpenAPI 3.1', () => { + context.openapiVersion = { major: 3, minor: 1 }; + const schema = new namespace.elements.Object({ + type: 'null', + }); + const parseResult = parse(context, schema); + + expect(parseResult.length).to.equal(1); + expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); + expect(parseResult).to.not.contain.annotations; + + const element = parseResult.get(0).content; + expect(element).to.be.instanceof(namespace.elements.Null); + }); }); - it('returns a warning for null type on OpenAPI prior 3.1', () => { - const schema = new namespace.elements.Object({ - type: 'null', + describe('when type is an array', () => { + beforeEach(() => { + context = new Context(namespace); + context.openapiVersion = { major: 3, minor: 1 }; }); - const parseResult = parse(context, schema); - expect(parseResult).to.contain.warning( - "'Schema Object' 'type' must be either boolean, object, array, number, string, integer" - ); - }); + it('warns when using array on OpenAPI 3.0', () => { + context.openapiVersion = { major: 3, minor: 0 }; - it('returns a null structure for null type on OpenAPI 3.1', () => { - context.openapiVersion = { major: 3, minor: 1 }; - const schema = new namespace.elements.Object({ - type: 'null', + const schema = new namespace.elements.Object({ + type: [], + }); + const parseResult = parse(context, schema); + + expect(parseResult).to.contain.warning( + "'Schema Object' 'type' is not a string" + ); }); - const parseResult = parse(context, schema); - expect(parseResult.length).to.equal(1); - expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); - expect(parseResult).to.not.contain.annotations; + it('warns when array items is empty', () => { + const schema = new namespace.elements.Object({ + type: [], + }); + const parseResult = parse(context, schema); - const element = parseResult.get(0).content; - expect(element).to.be.instanceof(namespace.elements.Null); + expect(parseResult).to.contain.warning( + "'Schema Object' 'type' array must contain at least one type" + ); + }); + + it('warns when array items are not a string', () => { + const schema = new namespace.elements.Object({ + type: [true], + }); + const parseResult = parse(context, schema); + + expect(parseResult).to.contain.warning( + "'Schema Object' 'type' array value is not a string" + ); + }); + + it('warns when array items are not valid types', () => { + const schema = new namespace.elements.Object({ + type: ['invalid'], + }); + const parseResult = parse(context, schema); + + expect(parseResult).to.contain.warning( + "'Schema Object' 'type' array must only contain values: boolean, object, array, number, string, integer, null" + ); + }); + + it('warns when array items are duplicated', () => { + const schema = new namespace.elements.Object({ + type: ['string', 'string'], + }); + const parseResult = parse(context, schema); + + expect(parseResult).to.contain.warning( + "'Schema Object' 'type' array must contain unique items, string is already present" + ); + }); + + it('warns when array items contain more than 1 entry', () => { + // FIXME support more than one entry :) + const schema = new namespace.elements.Object({ + type: ['string', 'number'], + }); + const parseResult = parse(context, schema); + + expect(parseResult).to.contain.warning( + "'Schema Object' 'type' more than one type is current unsupported" + ); + }); + + it('when type contains a single value', () => { + const schema = new namespace.elements.Object({ + type: ['string'], + }); + const parseResult = parse(context, schema); + + expect(parseResult.length).to.equal(1); + expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); + expect(parseResult).to.not.contain.annotations; + + const string = parseResult.get(0).content; + expect(string).to.be.instanceof(namespace.elements.String); + }); }); });