From e03e9237ddaae8cc289c0a9e1f73b87cef47b269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Urba=C5=84czyk?= Date: Fri, 2 Jul 2021 07:14:58 +0200 Subject: [PATCH] fix: change keywords priority for inferring example and try infer example from root in combined schemas (#126) * fix: change keywords priority for inferring example and try infer example from root in compound schemas * update Readme.md * revert package-lock * change name of function and enable all tests --- README.md | 2 +- package-lock.json | 2 +- src/traverse.js | 51 ++++++++++++----- test/integration.spec.js | 119 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 155 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index adff43b..7ddf459 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Tool for generation samples based on OpenAPI payload/response schema - Deterministic (given a particular input, will always produce the same output) - Supports compound keywords: `allOf`, `oneOf`, `anyOf`, `if/then/else` - Supports `additionalProperties` -- Uses `default`, `const`, `enum` and `examples` where possible +- Uses `const`, `examples`, `enum` and `default` where possible - in this order - Good array support: supports `contains`, `minItems`, `maxItems`, and tuples (`items` as an array) - Supports `minLength`, `maxLength`, `min`, `max`, `exclusiveMinimum`, `exclusiveMaximum` - Supports the following `string` formats: diff --git a/package-lock.json b/package-lock.json index d355ff3..74c09bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28261,4 +28261,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/src/traverse.js b/src/traverse.js index 1472a07..fcadbc5 100644 --- a/src/traverse.js +++ b/src/traverse.js @@ -13,6 +13,34 @@ export function clearCache() { seenSchemasStack = []; } +function inferExample(schema) { + let example; + if (schema.const !== undefined) { + example = schema.const; + } else if (schema.examples !== undefined && schema.examples.length) { + example = schema.examples[0]; + } else if (schema.enum !== undefined && schema.enum.length) { + example = schema.enum[0]; + } else if (schema.default !== undefined) { + example = schema.default; + } + return example; +} + +function tryInferExample(schema) { + const example = inferExample(schema); + // case when we don't infer example from schema but take from `const`, `examples`, `default` or `enum` keywords + if (example !== undefined) { + return { + value: example, + readOnly: schema.readOnly, + writeOnly: schema.writeOnly, + type: null, + }; + } + return; +} + export function traverse(schema, options, spec, context) { // checking circular JS references by checking context // because context is passed only when traversing through nested objects happens @@ -37,7 +65,6 @@ export function traverse(schema, options, spec, context) { } const referenced = JsonPointer.get(spec, ref); - let result; if ($refCache[ref] !== true) { @@ -64,7 +91,7 @@ export function traverse(schema, options, spec, context) { if (schema.allOf !== undefined) { popSchemaStack(seenSchemasStack, context); - return allOfSample( + return tryInferExample(schema) || allOfSample( { ...schema, allOf: undefined }, schema.allOf, options, @@ -78,29 +105,23 @@ export function traverse(schema, options, spec, context) { if (!options.quiet) console.warn('oneOf and anyOf are not supported on the same level. Skipping anyOf'); } popSchemaStack(seenSchemasStack, context); - return traverse(schema.oneOf[0], options, spec, context); + return tryInferExample(schema) || traverse(schema.oneOf[0], options, spec, context); } if (schema.anyOf && schema.anyOf.length) { popSchemaStack(seenSchemasStack, context); - return traverse(schema.anyOf[0], options, spec, context); + return tryInferExample(schema) || traverse(schema.anyOf[0], options, spec, context); } if (schema.if && schema.then) { - return traverse(mergeDeep(schema.if, schema.then), options, spec, context); + popSchemaStack(seenSchemasStack, context); + return tryInferExample(schema) || traverse(mergeDeep(schema.if, schema.then), options, spec, context); } - let example = null; + let example = inferExample(schema); let type = null; - if (schema.default !== undefined) { - example = schema.default; - } else if (schema.const !== undefined) { - example = schema.const; - } else if (schema.enum !== undefined && schema.enum.length) { - example = schema.enum[0]; - } else if (schema.examples !== undefined && schema.examples.length) { - example = schema.examples[0]; - } else { + if (example === undefined) { + example = null; type = schema.type; if (Array.isArray(type) && schema.type.length > 0) { type = schema.type[0]; diff --git a/test/integration.spec.js b/test/integration.spec.js index babb801..3a3b94d 100644 --- a/test/integration.spec.js +++ b/test/integration.spec.js @@ -505,7 +505,7 @@ describe('Integration', function() { expect(result).to.equal(expected); }); - it('should prefer oneOf if anyOf and oneOf are on the same level ', function () { + it('should prefer oneOf if anyOf and oneOf are on the same level', function () { schema = { anyOf: [ { @@ -523,6 +523,122 @@ describe('Integration', function() { expect(result).to.equal(expected); }); }); + + describe('inferring type from root schema', function() { + const basicSchema = { + oneOf: [ + { + type: 'string' + }, + { + type: 'number' + } + ], + anyOf: [ + { + type: 'string' + }, + { + type: 'number' + } + ], + allOf: [ + { + type: 'object', + properties: { + title: { + type: 'string' + } + } + }, + { + type: 'object', + properties: { + amount: { + type: 'number', + default: 1 + } + } + } + ], + if: {properties: {foo: {type: 'string', format: 'email'}}}, + then: {properties: {bar: {type: 'string'}}}, + else: {properties: {baz: {type: 'number'}}}, + }; + + it('should infer example from root schema which has defined const keyword', function() { + schema = { + ...basicSchema, + const: 'foobar' + }; + result = OpenAPISampler.sample(schema); + expected = 'foobar'; + expect(result).to.equal(expected); + }); + + it('should infer example from root schema which has defined examples keyword', function() { + schema = { + ...basicSchema, + examples: ['foobar'] + }; + result = OpenAPISampler.sample(schema); + expected = 'foobar'; + expect(result).to.equal(expected); + }); + + it('should infer example from root schema which has defined default keyword', function() { + schema = { + ...basicSchema, + const: 'foobar' + }; + result = OpenAPISampler.sample(schema); + expected = 'foobar'; + expect(result).to.equal(expected); + }); + + it('should infer example from root schema which has defined enum keyword', function() { + schema = { + ...basicSchema, + enum: ['foobar'] + }; + result = OpenAPISampler.sample(schema); + expected = 'foobar'; + expect(result).to.equal(expected); + }); + + it('should infer example from root schema which has defined const and examples keyword (const has higher priority)', function() { + schema = { + ...basicSchema, + const: 'foobar', + examples: ['barfoo'] + }; + result = OpenAPISampler.sample(schema); + expected = 'foobar'; + expect(result).to.equal(expected); + }); + + it('should infer example from root schema which has defined examples and enum keyword (examples have higher priority)', function() { + schema = { + ...basicSchema, + enum: ['barfoo'], + examples: ['foobar'] + }; + result = OpenAPISampler.sample(schema); + expected = 'foobar'; + expect(result).to.equal(expected); + }); + + it('should infer example from root schema which has defined enum and default keyword (enum have higher priority)', function() { + schema = { + ...basicSchema, + default: 'barfoo', + enum: ['foobar'] + }; + result = OpenAPISampler.sample(schema); + expected = 'foobar'; + expect(result).to.equal(expected); + }); + }); }); describe('$refs', function() { @@ -653,7 +769,6 @@ describe('Integration', function() { }); describe('circular references in JS object', function() { - let result, schema, expected; it('should not follow circular references in JS object', function() {