Skip to content

Commit ccab373

Browse files
committed
feat: disable auto sample for non-required properties
1 parent 8c8a820 commit ccab373

File tree

12 files changed

+218
-36
lines changed

12 files changed

+218
-36
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ Available options:
6464
Don't include `readOnly` object properties
6565
- **skipWriteOnly** - `boolean`
6666
Don't include `writeOnly` object properties
67+
- **disableNonRequiredAutoGen** - `boolean`
68+
Don't auto generate sample for non-required object properties when the schema hasn't explicit example nor default value
6769
- **quiet** - `boolean`
6870
Don't log console warning messages
6971
- **spec** - whole specification where the schema is taken from. Required only when schema may contain `$ref`. **spec** must not contain any external references

src/samplers/array.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function sampleArray(schema, options = {}, spec, context) {
99
arrayLength = Math.max(arrayLength, items.length);
1010
}
1111

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

2222
for (let i = 0; i < arrayLength; i++) {
23-
let itemSchema = itemSchemaGetter(i);
24-
let { value: sample } = traverse(itemSchema, options, spec, {depth: depth + 1});
23+
let { value: sample } = traverse(itemSchemaGetter(i), options, spec, {depth: depth + 1});
2524
res.push(sample);
2625
}
27-
return res;
26+
27+
if (!options.omissible || res.some(item => item !== null)) {
28+
return res;
29+
}
30+
31+
return null;
2832
}

src/samplers/boolean.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1-
export function sampleBoolean(schema) {
1+
export function sampleBoolean(schema, options={}) {
2+
if (options.omissible) {
3+
return null;
4+
}
25
return true; // let be optimistic :)
36
}

src/samplers/number.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
export function sampleNumber(schema) {
1+
export function sampleNumber(schema, options={}) {
2+
if (options.omissible) {
3+
return null;
4+
}
25
let res = 0;
36
if (typeof schema.exclusiveMinimum === 'boolean' || typeof schema.exclusiveMaximum === 'boolean') { //legacy support for jsonschema draft 4 of exclusiveMaximum/exclusiveMinimum as booleans
47
if (schema.maximum && schema.minimum) {

src/samplers/object.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,50 @@ export function sampleObject(schema, options = {}, spec, context) {
44
const depth = (context && context.depth || 1);
55

66
if (schema && typeof schema.properties === 'object') {
7-
let requiredKeys = (Array.isArray(schema.required) ? schema.required : []);
8-
let requiredKeyDict = requiredKeys.reduce((dict, key) => {
7+
const requiredKeys = (Array.isArray(schema.required) ? schema.required : []);
8+
const requiredKeyDict = requiredKeys.reduce((dict, key) => {
99
dict[key] = true;
1010
return dict;
1111
}, {});
1212

1313
Object.keys(schema.properties).forEach(propertyName => {
14+
const isRequired = requiredKeyDict.hasOwnProperty(propertyName);
1415
// skip before traverse that could be costly
15-
if (options.skipNonRequired && !requiredKeyDict.hasOwnProperty(propertyName)) {
16+
if (options.skipNonRequired && !isRequired) {
1617
return;
1718
}
1819

19-
const sample = traverse(schema.properties[propertyName], options, spec, { propertyName, depth: depth + 1 });
20+
const propertyOmissible = options.disableNonRequiredAutoGen && !isRequired;
21+
22+
const sample = traverse(
23+
schema.properties[propertyName],
24+
Object.assign({}, options, { omissible: propertyOmissible }),
25+
spec,
26+
{ propertyName, depth: depth + 1 }
27+
);
28+
2029
if (options.skipReadOnly && sample.readOnly) {
2130
return;
2231
}
2332

2433
if (options.skipWriteOnly && sample.writeOnly) {
2534
return;
2635
}
27-
res[propertyName] = sample.value;
36+
37+
if (sample.value || !propertyOmissible) {
38+
res[propertyName] = sample.value;
39+
}
2840
});
2941
}
3042

31-
if (schema && typeof schema.additionalProperties === 'object') {
43+
if (!options.disableNonRequiredAutoGen && schema && typeof schema.additionalProperties === 'object') {
3244
res.property1 = traverse(schema.additionalProperties, options, spec, {depth: depth + 1 }).value;
3345
res.property2 = traverse(schema.additionalProperties, options, spec, {depth: depth + 1 }).value;
3446
}
35-
return res;
47+
48+
if (Object.keys(res).length > 0 || !options.omissible) {
49+
return res;
50+
}
51+
52+
return null;
3653
}

src/samplers/string.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ const stringFormats = {
124124
};
125125

126126
export function sampleString(schema, options, spec, context) {
127+
if (options && options.omissible) {
128+
return null;
129+
}
127130
let format = schema.format || 'default';
128131
let sampler = stringFormats[format] || defaultSample;
129132
let propertyName = context && context.propertyName;

src/traverse.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ export function clearCache() {
1313
seenSchemasStack = [];
1414
}
1515

16-
function inferExample(schema) {
16+
function inferExample(schema, omissible=false) {
1717
let example;
1818
if (schema.const !== undefined) {
1919
example = schema.const;
2020
} else if (schema.examples !== undefined && schema.examples.length) {
2121
example = schema.examples[0];
22-
} else if (schema.enum !== undefined && schema.enum.length) {
22+
} else if (schema.enum !== undefined && schema.enum.length && !omissible) {
2323
example = schema.enum[0];
2424
} else if (schema.default !== undefined) {
2525
example = schema.default;
@@ -127,7 +127,7 @@ export function traverse(schema, options, spec, context) {
127127
return tryInferExample(schema) || traverse(mergeDeep(schema.if, schema.then), options, spec, context);
128128
}
129129

130-
let example = inferExample(schema);
130+
let example = inferExample(schema, options.omissible);
131131
let type = null;
132132
if (example === undefined) {
133133
example = null;

test/integration.spec.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,62 @@ describe('Integration', function() {
432432
expected = 'test1';
433433
expect(result).to.equal(expected);
434434
});
435+
436+
it('should skip non-required properties without example if disableNonRequiredAutoGen=true', () => {
437+
var obj = {
438+
withExample: 'Example',
439+
withExampleArray: [3],
440+
};
441+
schema = {
442+
type: 'object',
443+
properties: {
444+
withoutExampleString: {
445+
type: 'string'
446+
},
447+
withoutExampleNumber: {
448+
type: 'number'
449+
},
450+
withoutExampleBoolean: {
451+
type: 'boolean'
452+
},
453+
withoutExampleObject: {
454+
type: 'object',
455+
},
456+
withoutExampleArray: {
457+
type: 'array',
458+
items: { type: 'string'}
459+
},
460+
withExample: {
461+
type: 'string',
462+
example: 'Example'
463+
},
464+
withExampleArray: {
465+
type: 'array',
466+
items: { type: 'number', default: 3 }
467+
}
468+
},
469+
additionalProperties: {
470+
type: 'number'
471+
}
472+
};
473+
result = OpenAPISampler.sample(schema, { disableNonRequiredAutoGen: true });
474+
expected = obj;
475+
expect(result).to.deep.equal(obj);
476+
});
477+
478+
it('should return empty object if disableNonRequiredAutoGen=true and no explicit example', () => {
479+
var obj = {};
480+
schema = {
481+
type: 'object',
482+
properties: {
483+
withoutExampleString: { type: 'string' },
484+
}
485+
};
486+
result = OpenAPISampler.sample(schema, { disableNonRequiredAutoGen: true });
487+
expected = obj;
488+
expect(result).to.deep.equal(obj);
489+
});
490+
435491
});
436492

437493
describe('Detection', function() {

test/unit/array.spec.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,45 @@ describe('sampleArray', () => {
2929
res = sampleArray({items: [{type: 'number'}, {type: 'string'}, {}]});
3030
expect(res).to.deep.equal([0, 'string', null]);
3131
});
32+
33+
describe('disableNonRequiredAutoGen', () => {
34+
35+
it('should return null if omissible=true and primitive type item has no example', () => {
36+
res = sampleArray({ items: { type: 'string' } }, { omissible: true, disableNonRequiredAutoGen: true });
37+
expect(res).to.be.null;
38+
});
39+
40+
it('should return null if omssible=true and object type item has no example', () => {
41+
res = sampleArray({
42+
items: {
43+
type: 'object',
44+
properties: {
45+
a: { type: 'string' },
46+
},
47+
}
48+
}, { omissible: true, disableNonRequiredAutoGen: true });
49+
expect(res).to.be.null;
50+
});
51+
52+
it('should return valid array samples if omissible=false and primitive type item has no example', () => {
53+
// the sample must be valid to schema and show the array item type when the array is not omitted
54+
res = sampleArray({ items: { type: 'string' }, minItems: 2 }, { disableNonRequiredAutoGen: true });
55+
expect(res).to.deep.equal(['string', 'string']);
56+
});
57+
58+
it('should return array of empty object if omssible=false and object type item has no example', () => {
59+
// the sample must be valid to schema and show the array item type when the array is not omitted
60+
res = sampleArray({
61+
items: {
62+
type: 'object',
63+
properties: {
64+
a: { type: 'string' },
65+
},
66+
}
67+
}, { disableNonRequiredAutoGen: true });
68+
expect(res).to.deep.equal([{}]);
69+
});
70+
71+
});
72+
3273
});

test/unit/number.spec.js

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,26 +57,30 @@ describe('sampleNumber', () => {
5757
expect(res).to.equal(-4);
5858
});
5959

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

66-
// [2, 3] -> 2
67-
// (8, 13) -> 9
68-
it('should return closer to minimum possible int for draft v7', () => {
69-
res = sampleNumber({minimum: 2, maximum: 3});
70-
expect(res).to.equal(2);
71-
res = sampleNumber({exclusiveMinimum: 8, exclusiveMaximum: 13});
72-
expect(res).to.equal(9);
73-
});
66+
// [2, 3] -> 2
67+
// (8, 13) -> 9
68+
it('should return closer to minimum possible int for draft v7', () => {
69+
res = sampleNumber({minimum: 2, maximum: 3});
70+
expect(res).to.equal(2);
71+
res = sampleNumber({exclusiveMinimum: 8, exclusiveMaximum: 13});
72+
expect(res).to.equal(9);
73+
});
7474

75-
it('should return closer to minimum possible int for draft v7', () => {
76-
res = sampleNumber({minimum: 2, maximum: 3});
77-
expect(res).to.equal(2);
78-
res = sampleNumber({exclusiveMinimum: 8, exclusiveMaximum: 13});
79-
expect(res).to.equal(9);
80-
});
75+
it('should return closer to minimum possible int for draft v7', () => {
76+
res = sampleNumber({minimum: 2, maximum: 3});
77+
expect(res).to.equal(2);
78+
res = sampleNumber({exclusiveMinimum: 8, exclusiveMaximum: 13});
79+
expect(res).to.equal(9);
80+
});
8181

82+
it('should return null if it is omissible', () => {
83+
res = sampleNumber({}, { omissible: true });
84+
expect(res).to.be.null;
85+
});
8286
});

0 commit comments

Comments
 (0)