Skip to content

Commit

Permalink
fix: change keywords priority for inferring example and try infer exa…
Browse files Browse the repository at this point in the history
…mple 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
  • Loading branch information
magicmatatjahu authored Jul 2, 2021
1 parent 16268be commit e03e923
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 19 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 36 additions & 15 deletions src/traverse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,7 +65,6 @@ export function traverse(schema, options, spec, context) {
}

const referenced = JsonPointer.get(spec, ref);

let result;

if ($refCache[ref] !== true) {
Expand All @@ -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,
Expand All @@ -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];
Expand Down
119 changes: 117 additions & 2 deletions test/integration.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand All @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit e03e923

Please sign in to comment.