diff --git a/packages/ruleset/src/functions/inline-schema-rules.js b/packages/ruleset/src/functions/inline-schema-rules.js index fffa47d22..6599c9510 100644 --- a/packages/ruleset/src/functions/inline-schema-rules.js +++ b/packages/ruleset/src/functions/inline-schema-rules.js @@ -3,7 +3,7 @@ const { isJsonMimeType, isArraySchema, isEmptyObjectSchema, - isPrimitiveType, + isPrimitiveSchema, isRefSiblingSchema, validateSubschemas } = require('../utils'); @@ -35,7 +35,7 @@ function inlineResponseSchema(schema, options, { path }) { if ( !schema.$ref && isJsonMimeType(mimeType) && - !isPrimitiveType(schema) && + !isPrimitiveSchema(schema) && !arrayItemsAreRefOrPrimitive(schema) && !isRefSiblingSchema(schema) ) { @@ -72,7 +72,7 @@ function inlineRequestSchema(schema, options, { path }) { if ( !schema.$ref && isJsonMimeType(mimeType) && - !isPrimitiveType(schema) && + !isPrimitiveSchema(schema) && !arrayItemsAreRefOrPrimitive(schema) && !isRefSiblingSchema(schema) ) { @@ -111,9 +111,11 @@ function inlineRequestSchema(schema, options, { path }) { * @returns an array containing the violations found or [] if no violations */ function inlinePropertySchema(schema, options, { path }) { - // Check each sub-schema that is reachable from "schema" (properties, - // additionalProperties, allOf/anyOf/oneOf, array items, etc.) . - return validateSubschemas(schema, path, checkForInlineNestedObjectSchema); + // If "schema" is not a primitive, then check each sub-schema that is reachable from + // "schema" (properties, additionalProperties, allOf/anyOf/oneOf, array items, etc.). + return isPrimitiveSchema(schema) + ? [] + : validateSubschemas(schema, path, checkForInlineNestedObjectSchema); } /** @@ -129,7 +131,7 @@ function checkForInlineNestedObjectSchema(schema, path) { // then bail out now to avoid a warning. if ( schema.$ref || - isPrimitiveType(schema) || + isPrimitiveSchema(schema) || isRefSiblingSchema(schema) || isEmptyObjectSchema(schema) || isArraySchema(schema) @@ -182,5 +184,5 @@ function checkForInlineNestedObjectSchema(schema, path) { function arrayItemsAreRefOrPrimitive(schema) { const isArray = isArraySchema(schema); const items = isArray && getCompositeSchemaAttribute(schema, 'items'); - return items && (items.$ref || isPrimitiveType(items)); + return items && (items.$ref || isPrimitiveSchema(items)); } diff --git a/packages/ruleset/src/utils/get-schema-type.js b/packages/ruleset/src/utils/get-schema-type.js index 85dbee9d5..1eb9b09eb 100644 --- a/packages/ruleset/src/utils/get-schema-type.js +++ b/packages/ruleset/src/utils/get-schema-type.js @@ -294,6 +294,21 @@ const isStringSchema = schema => { return checkCompositeSchemaForConstraint(schema, s => s.type === 'string'); }; +/** + * Returns `true` for a primitive schema (anything encoded as a JSON string, number, or boolean). + * + * @param {object} schema - Simple or composite OpenAPI 3.0 schema object. + * @returns {boolean} + */ +const isPrimitiveSchema = schema => { + // This implementation should remain stable when additional specific types are added + return ( + !isObjectSchema(schema) && + !isArraySchema(schema) && + getSchemaType(schema) !== SchemaType.UNKNOWN + ); +}; + module.exports = { SchemaType, getSchemaType, @@ -311,5 +326,6 @@ module.exports = { isIntegerSchema, isNumberSchema, isObjectSchema, + isPrimitiveSchema, isStringSchema }; diff --git a/packages/ruleset/src/utils/index.js b/packages/ruleset/src/utils/index.js index 5318c3036..6cad71835 100644 --- a/packages/ruleset/src/utils/index.js +++ b/packages/ruleset/src/utils/index.js @@ -6,7 +6,6 @@ module.exports = { isDeprecated: require('./is-deprecated'), isEmptyObjectSchema: require('./is-empty-object-schema'), isObject: require('./is-object'), - isPrimitiveType: require('./is-primitive-type'), isRefSiblingSchema: require('./is-ref-sibling-schema'), mergeAllOfSchemaProperties: require('./merge-allof-schema-properties'), ...require('./mimetype-utils'), diff --git a/packages/ruleset/src/utils/is-primitive-type.js b/packages/ruleset/src/utils/is-primitive-type.js deleted file mode 100644 index d708e932b..000000000 --- a/packages/ruleset/src/utils/is-primitive-type.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Returns true if "schema" is a primitive schema. - * @param {*} schema the schema to check - * @returns boolean - */ -function isPrimitiveType(schema) { - return ( - schema.type && - ['boolean', 'integer', 'number', 'string'].includes(schema.type) - ); -} - -module.exports = isPrimitiveType; diff --git a/packages/ruleset/test/inline-property-schema.test.js b/packages/ruleset/test/inline-property-schema.test.js index 952f66af2..0af8bddb5 100644 --- a/packages/ruleset/test/inline-property-schema.test.js +++ b/packages/ruleset/test/inline-property-schema.test.js @@ -49,6 +49,24 @@ describe('Spectral rule: inline-property-schema', () => { const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(0); }); + + it('Composed primitive schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.components.schemas.Car.properties['inline_prop'] = { + oneOf: [ + { + type: 'string' + }, + { + type: 'string' + } + ] + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); }); describe('Should yield errors', () => { diff --git a/packages/ruleset/test/inline-request-schema.test.js b/packages/ruleset/test/inline-request-schema.test.js index 7d347384e..74edb41ba 100644 --- a/packages/ruleset/test/inline-request-schema.test.js +++ b/packages/ruleset/test/inline-request-schema.test.js @@ -107,6 +107,26 @@ describe('Spectral rule: inline-request-schema', () => { const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(0); }); + + it('Composed primitive schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.paths['/v1/drinks'].post.requestBody.content[ + 'application/json' + ].schema = { + oneOf: [ + { + type: 'string' + }, + { + type: 'string' + } + ] + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); }); describe('Should yield errors', () => { diff --git a/packages/ruleset/test/inline-response-schema.test.js b/packages/ruleset/test/inline-response-schema.test.js index 2df5286fe..8463a6de4 100644 --- a/packages/ruleset/test/inline-response-schema.test.js +++ b/packages/ruleset/test/inline-response-schema.test.js @@ -137,6 +137,26 @@ describe('Spectral rule: inline-response-schema', () => { const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(0); }); + + it('Composed primitive schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.paths['/v1/movies'].post.responses['201'].content[ + 'application/json' + ].schema = { + oneOf: [ + { + type: 'string' + }, + { + type: 'string' + } + ] + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); }); describe('Should yield errors', () => { diff --git a/packages/ruleset/test/is-object-schema.test.js b/packages/ruleset/test/is-object-schema.test.js index 833a6ae54..b82ddc625 100644 --- a/packages/ruleset/test/is-object-schema.test.js +++ b/packages/ruleset/test/is-object-schema.test.js @@ -58,4 +58,34 @@ describe('Utility function: isObjectSchema()', () => { }) ).toBe(true); }); + + it('should recurse through `oneOf` and `allOf` (implicit object type)', async () => { + expect( + isObjectSchema({ + oneOf: [ + { + allOf: [{ properties: {} }, {}] + }, + { + properties: {} + } + ] + }) + ).toBe(true); + }); + + it('should recurse through `allOf` (implicit object type)', async () => { + expect( + isObjectSchema({ + allOf: [ + { + allOf: [{ properties: {} }, {}] + }, + { + properties: {} + } + ] + }) + ).toBe(true); + }); }); diff --git a/packages/ruleset/test/is-primitive-schema.test.js b/packages/ruleset/test/is-primitive-schema.test.js new file mode 100644 index 000000000..6f552d3de --- /dev/null +++ b/packages/ruleset/test/is-primitive-schema.test.js @@ -0,0 +1,142 @@ +const { isPrimitiveSchema } = require('../src/utils'); + +describe('Utility function: isPrimitiveSchema()', () => { + it('should return `false` for `undefined`', async () => { + expect(isPrimitiveSchema(undefined)).toBe(false); + }); + + it('should return `false` for `null`', async () => { + expect(isPrimitiveSchema(null)).toBe(false); + }); + + it('should return `false` for an array', async () => { + expect(isPrimitiveSchema([])).toBe(false); + }); + + it('should return `false` for an empty object', async () => { + expect(isPrimitiveSchema({})).toBe(false); + }); + + it('should return `true` for a boolean schema', async () => { + expect(isPrimitiveSchema({ type: 'boolean' })).toBe(true); + }); + + it('should return `true` for a byte schema', async () => { + expect(isPrimitiveSchema({ type: 'string', format: 'byte' })).toBe(true); + }); + + it('should return `true` for a double schema', async () => { + expect(isPrimitiveSchema({ type: 'number', format: 'double' })).toBe(true); + }); + + it('should return `true` for an enumeration', async () => { + expect(isPrimitiveSchema({ type: 'string', enum: ['foo', 'bar'] })).toBe( + true + ); + }); + + it('should return `true` for a float schema', async () => { + expect(isPrimitiveSchema({ type: 'number', format: 'float' })).toBe(true); + }); + + it('should return `true` for a int32 schema', async () => { + expect(isPrimitiveSchema({ type: 'integer', format: 'int32' })).toBe(true); + }); + + it('should return `true` for a int64 schema', async () => { + expect(isPrimitiveSchema({ type: 'integer', format: 'int64' })).toBe(true); + }); + + it('should return `true` for a integer schema', async () => { + expect(isPrimitiveSchema({ type: 'integer' })).toBe(true); + }); + + it('should return `true` for a number schema', async () => { + expect(isPrimitiveSchema({ type: 'number' })).toBe(true); + }); + + it('should return `true` for a string schema', async () => { + expect(isPrimitiveSchema({ type: 'string' })).toBe(true); + }); + + it('should return true for a composed schema that resolves to "int32"', async () => { + expect( + isPrimitiveSchema({ + oneOf: [ + { + allOf: [ + { + anyOf: [ + { type: 'integer', format: 'int32' }, + { type: 'integer', format: 'int32' } + ] + }, + {} + ] + }, + { type: 'integer', format: 'int32' } + ] + }) + ).toBe(true); + }); + + it('should return true for a composed schema that resolves to "double"', async () => { + expect( + isPrimitiveSchema({ + oneOf: [ + { + allOf: [ + { + anyOf: [ + { type: 'number', format: 'double' }, + { type: 'number', format: 'double' } + ] + }, + {} + ] + }, + { type: 'number', format: 'double' } + ] + }) + ).toBe(true); + }); + + it('should return true for a composed schema that resolves to "number"', async () => { + expect( + isPrimitiveSchema({ + oneOf: [ + { + allOf: [{ anyOf: [{ type: 'number' }, { type: 'number' }] }, {}] + }, + { type: 'number' } + ] + }) + ).toBe(true); + }); + + it('should return true for a composed schema that resolves to "boolean"', async () => { + expect( + isPrimitiveSchema({ + oneOf: [ + { + allOf: [{ anyOf: [{ type: 'boolean' }, { type: 'boolean' }] }, {}] + }, + { type: 'boolean' } + ] + }) + ).toBe(true); + }); + + it('should return true for a composed schema that resolves to "string"', async () => { + expect( + isPrimitiveSchema({ + oneOf: [ + { + allOf: [{ anyOf: [{ type: 'string' }, { type: 'string' }] }, {}] + }, + { type: 'string' } + ] + }) + ).toBe(true); + }); +});