Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Don't auto generate sample for non-required object properties #123

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ Available options:
Don't include `readOnly` object properties
- **skipWriteOnly** - `boolean`
Don't include `writeOnly` object properties
- **disableNonRequiredAutoGen** - `boolean`
Don't auto generate sample for non-required object properties when the schema hasn't explicit example nor default value
- **quiet** - `boolean`
Don't log console warning messages
- **spec** - whole specification where the schema is taken from. Required only when schema may contain `$ref`. **spec** must not contain any external references
Expand Down
12 changes: 8 additions & 4 deletions src/samplers/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function sampleArray(schema, options = {}, spec, context) {
arrayLength = Math.max(arrayLength, items.length);
}

let itemSchemaGetter = itemNumber => {
const itemSchemaGetter = itemNumber => {
if (Array.isArray(schema.items)) {
return items[itemNumber] || {};
}
Expand All @@ -20,9 +20,13 @@ export function sampleArray(schema, options = {}, spec, context) {
if (!items) return res;

for (let i = 0; i < arrayLength; i++) {
let itemSchema = itemSchemaGetter(i);
let { value: sample } = traverse(itemSchema, options, spec, {depth: depth + 1});
let { value: sample } = traverse(itemSchemaGetter(i), options, spec, {depth: depth + 1});
res.push(sample);
}
return res;

if (!options.omissible || res.some(item => item !== null)) {
return res;
}

return null;
}
5 changes: 4 additions & 1 deletion src/samplers/boolean.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export function sampleBoolean(schema) {
export function sampleBoolean(schema, options={}) {
if (options.omissible) {
return null;
}
return true; // let be optimistic :)
}
5 changes: 4 additions & 1 deletion src/samplers/number.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export function sampleNumber(schema) {
export function sampleNumber(schema, options={}) {
if (options.omissible) {
return null;
}
let res = 0;
if (typeof schema.exclusiveMinimum === 'boolean' || typeof schema.exclusiveMaximum === 'boolean') { //legacy support for jsonschema draft 4 of exclusiveMaximum/exclusiveMinimum as booleans
if (schema.maximum && schema.minimum) {
Expand Down
31 changes: 24 additions & 7 deletions src/samplers/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,50 @@ export function sampleObject(schema, options = {}, spec, context) {
const depth = (context && context.depth || 1);

if (schema && typeof schema.properties === 'object') {
let requiredKeys = (Array.isArray(schema.required) ? schema.required : []);
let requiredKeyDict = requiredKeys.reduce((dict, key) => {
const requiredKeys = (Array.isArray(schema.required) ? schema.required : []);
const requiredKeyDict = requiredKeys.reduce((dict, key) => {
dict[key] = true;
return dict;
}, {});

Object.keys(schema.properties).forEach(propertyName => {
const isRequired = requiredKeyDict.hasOwnProperty(propertyName);
// skip before traverse that could be costly
if (options.skipNonRequired && !requiredKeyDict.hasOwnProperty(propertyName)) {
if (options.skipNonRequired && !isRequired) {
return;
}

const sample = traverse(schema.properties[propertyName], options, spec, { propertyName, depth: depth + 1 });
const propertyOmissible = options.disableNonRequiredAutoGen && !isRequired;

const sample = traverse(
schema.properties[propertyName],
Object.assign({}, options, { omissible: propertyOmissible }),
spec,
{ propertyName, depth: depth + 1 }
);

if (options.skipReadOnly && sample.readOnly) {
return;
}

if (options.skipWriteOnly && sample.writeOnly) {
return;
}
res[propertyName] = sample.value;

if (sample.value || !propertyOmissible) {
res[propertyName] = sample.value;
}
});
}

if (schema && typeof schema.additionalProperties === 'object') {
if (!options.disableNonRequiredAutoGen && schema && typeof schema.additionalProperties === 'object') {
res.property1 = traverse(schema.additionalProperties, options, spec, {depth: depth + 1 }).value;
res.property2 = traverse(schema.additionalProperties, options, spec, {depth: depth + 1 }).value;
}
return res;

if (Object.keys(res).length > 0 || !options.omissible) {
return res;
}

return null;
}
3 changes: 3 additions & 0 deletions src/samplers/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ const stringFormats = {
};

export function sampleString(schema, options, spec, context) {
if (options && options.omissible) {
return null;
}
let format = schema.format || 'default';
let sampler = stringFormats[format] || defaultSample;
let propertyName = context && context.propertyName;
Expand Down
6 changes: 3 additions & 3 deletions src/traverse.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ export function clearCache() {
seenSchemasStack = [];
}

function inferExample(schema) {
function inferExample(schema, omissible=false) {
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) {
} else if (schema.enum !== undefined && schema.enum.length && !omissible) {
example = schema.enum[0];
} else if (schema.default !== undefined) {
example = schema.default;
Expand Down Expand Up @@ -127,7 +127,7 @@ export function traverse(schema, options, spec, context) {
return tryInferExample(schema) || traverse(mergeDeep(schema.if, schema.then), options, spec, context);
}

let example = inferExample(schema);
let example = inferExample(schema, options.omissible);
let type = null;
if (example === undefined) {
example = null;
Expand Down
56 changes: 56 additions & 0 deletions test/integration.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,62 @@ describe('Integration', function() {
expected = 'test1';
expect(result).to.equal(expected);
});

it('should skip non-required properties without example if disableNonRequiredAutoGen=true', () => {
var obj = {
withExample: 'Example',
withExampleArray: [3],
};
schema = {
type: 'object',
properties: {
withoutExampleString: {
type: 'string'
},
withoutExampleNumber: {
type: 'number'
},
withoutExampleBoolean: {
type: 'boolean'
},
withoutExampleObject: {
type: 'object',
},
withoutExampleArray: {
type: 'array',
items: { type: 'string'}
},
withExample: {
type: 'string',
example: 'Example'
},
withExampleArray: {
type: 'array',
items: { type: 'number', default: 3 }
}
},
additionalProperties: {
type: 'number'
}
};
result = OpenAPISampler.sample(schema, { disableNonRequiredAutoGen: true });
expected = obj;
expect(result).to.deep.equal(obj);
});

it('should return empty object if disableNonRequiredAutoGen=true and no explicit example', () => {
var obj = {};
schema = {
type: 'object',
properties: {
withoutExampleString: { type: 'string' },
}
};
result = OpenAPISampler.sample(schema, { disableNonRequiredAutoGen: true });
expected = obj;
expect(result).to.deep.equal(obj);
});

});

describe('Detection', function() {
Expand Down
41 changes: 41 additions & 0 deletions test/unit/array.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,45 @@ describe('sampleArray', () => {
res = sampleArray({items: [{type: 'number'}, {type: 'string'}, {}]});
expect(res).to.deep.equal([0, 'string', null]);
});

describe('disableNonRequiredAutoGen', () => {

it('should return null if omissible=true and primitive type item has no example', () => {
res = sampleArray({ items: { type: 'string' } }, { omissible: true, disableNonRequiredAutoGen: true });
expect(res).to.be.null;
});

it('should return null if omssible=true and object type item has no example', () => {
res = sampleArray({
items: {
type: 'object',
properties: {
a: { type: 'string' },
},
}
}, { omissible: true, disableNonRequiredAutoGen: true });
expect(res).to.be.null;
});

it('should return valid array samples if omissible=false and primitive type item has no example', () => {
// the sample must be valid to schema and show the array item type when the array is not omitted
res = sampleArray({ items: { type: 'string' }, minItems: 2 }, { disableNonRequiredAutoGen: true });
expect(res).to.deep.equal(['string', 'string']);
});

it('should return array of empty object if omssible=false and object type item has no example', () => {
// the sample must be valid to schema and show the array item type when the array is not omitted
res = sampleArray({
items: {
type: 'object',
properties: {
a: { type: 'string' },
},
}
}, { disableNonRequiredAutoGen: true });
expect(res).to.deep.equal([{}]);
});

});

});
42 changes: 23 additions & 19 deletions test/unit/number.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,26 +57,30 @@ describe('sampleNumber', () => {
expect(res).to.equal(-4);
});

// (2, 3) -> 2.5
it('should return middle point if boundary integer is not possible for draft v7', () => {
res = sampleNumber({exclusiveMinimum: 2, exclusiveMaximum: 3});
expect(res).to.equal(2.5);
});
// (2, 3) -> 2.5
it('should return middle point if boundary integer is not possible for draft v7', () => {
res = sampleNumber({exclusiveMinimum: 2, exclusiveMaximum: 3});
expect(res).to.equal(2.5);
});

// [2, 3] -> 2
// (8, 13) -> 9
it('should return closer to minimum possible int for draft v7', () => {
res = sampleNumber({minimum: 2, maximum: 3});
expect(res).to.equal(2);
res = sampleNumber({exclusiveMinimum: 8, exclusiveMaximum: 13});
expect(res).to.equal(9);
});
// [2, 3] -> 2
// (8, 13) -> 9
it('should return closer to minimum possible int for draft v7', () => {
res = sampleNumber({minimum: 2, maximum: 3});
expect(res).to.equal(2);
res = sampleNumber({exclusiveMinimum: 8, exclusiveMaximum: 13});
expect(res).to.equal(9);
});

it('should return closer to minimum possible int for draft v7', () => {
res = sampleNumber({minimum: 2, maximum: 3});
expect(res).to.equal(2);
res = sampleNumber({exclusiveMinimum: 8, exclusiveMaximum: 13});
expect(res).to.equal(9);
});
it('should return closer to minimum possible int for draft v7', () => {
res = sampleNumber({minimum: 2, maximum: 3});
expect(res).to.equal(2);
res = sampleNumber({exclusiveMinimum: 8, exclusiveMaximum: 13});
expect(res).to.equal(9);
});

it('should return null if it is omissible', () => {
res = sampleNumber({}, { omissible: true });
expect(res).to.be.null;
});
});
46 changes: 45 additions & 1 deletion test/unit/object.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,49 @@ describe('sampleObject', () => {
fooId: 'fb4274c7-4fcd-4035-8958-a680548957ff',
barId: '3c966637-4898-4972-9a9d-baefa6cd6c89'
});
})
});

describe('disableNonRequiredAutoGen', () => {

it('should skip non-required properties without explicit example value', () => {
res = sampleObject({
required: ['e'],
properties: {
a: { type: 'string', enum: ['foo', 'bar'] },
b: { type: 'integer', default: 100 },
c: { type: 'string' },
d: { type: 'string', example: 'Example' },
e: { type: 'string', enum: ['foo', 'bar'] },
},
}, { disableNonRequiredAutoGen: true });
expect(res).to.deep.equal({
b: 100,
d: 'Example',
e: 'foo',
});
});

it('should skip additional properties', () => {
res = sampleObject({
properties: {
a: { type: 'string', example: 'Example' },
},
additionalProperties: { type: 'string' }
}, { disableNonRequiredAutoGen: true });
expect(res).to.deep.equal({
a: 'Example'
});
});

it('should return null if omissible=true and no property has example', () => {
res = sampleObject({
properties: {
a: { type: 'string' },
b: { type: 'integer' }
},
}, { disableNonRequiredAutoGen: true, omissible: true });
expect(res).to.be.null;
});

});
});
5 changes: 5 additions & 0 deletions test/unit/string.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ describe('sampleString', () => {
expect(res).to.equal('fb4274c7-4fcd-4035-8958-a680548957ff');
});

it('should return null if it is omissible', () => {
res = sampleString({}, { omissible: true });
expect(res).to.be.null;
});

it.each([
'email',
// 'idn-email', // unsupported by ajv-formats
Expand Down