Skip to content

Commit 70e779d

Browse files
authored
Merge pull request #659 from postmanlabs/feature/fix-validate-schema-example
Fix for conversion taking a long time in case of schema with example
2 parents 85c135e + 70d3727 commit 70e779d

File tree

10 files changed

+79
-59
lines changed

10 files changed

+79
-59
lines changed

OPTIONS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ requestParametersResolution|enum|Example, Schema|Schema|Select whether to genera
88
exampleParametersResolution|enum|Example, Schema|Example|Select whether to generate the response parameters based on the [schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject) or the [example](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#exampleObject) in the schema.|CONVERSION
99
folderStrategy|enum|Paths, Tags|Paths|Select whether to create folders according to the spec’s paths or tags.|CONVERSION
1010
schemaFaker|boolean|-|true|Whether or not schemas should be faked.|CONVERSION
11-
stackLimit|integer|-|8|Number of nesting limit till which schema resolution will happen. Increasing this limit may result in more time to convert collection depending on complexity of specification. (To make sure this option works correctly "optimizeConversion" option needs to be disabled)|CONVERSION
11+
stackLimit|integer|-|10|Number of nesting limit till which schema resolution will happen. Increasing this limit may result in more time to convert collection depending on complexity of specification. (To make sure this option works correctly "optimizeConversion" option needs to be disabled)|CONVERSION
1212
includeAuthInfoInExample|boolean|-|true|Select whether to include authentication parameters in the example request|CONVERSION
1313
shortValidationErrors|boolean|-|false|Whether detailed error messages are required for request <> schema validation operations.|VALIDATION
1414
validationPropertiesToIgnore|array|-|[]|Specific properties (parts of a request/response pair) to ignore during validation. Must be sent as an array of strings. Valid inputs in the array: PATHVARIABLE, QUERYPARAM, HEADER, BODY, RESPONSE_HEADER, RESPONSE_BODY|VALIDATION

assets/json-schema-faker.js

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ var _ = require('lodash'),
1515
{
1616
handleExclusiveMaximum,
1717
handleExclusiveMinimum
18-
} = require('./../lib/common/schemaUtilsCommon');
18+
} = require('./../lib/common/schemaUtilsCommon'),
19+
hash = require('object-hash');
1920

2021
(function (global, factory) {
2122
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
@@ -24100,7 +24101,7 @@ function extend() {
2410024101
var nullType = nullGenerator;
2410124102

2410224103
// TODO provide types
24103-
function unique(path, items, value, sample, resolve, traverseCallback) {
24104+
function unique(path, items, value, sample, resolve, traverseCallback, seenSchemaCache) {
2410424105
var tmp = [], seen = [];
2410524106
function walk(obj) {
2410624107
var json = JSON.stringify(obj);
@@ -24113,15 +24114,15 @@ function extend() {
2411324114
// TODO: find a better solution?
2411424115
var limit = 10;
2411524116
while (tmp.length !== items.length) {
24116-
walk(traverseCallback(value.items || sample, path, resolve));
24117+
walk(traverseCallback(value.items || sample, path, resolve, null, seenSchemaCache));
2411724118
if (!limit--) {
2411824119
break;
2411924120
}
2412024121
}
2412124122
return tmp;
2412224123
}
2412324124
// TODO provide types
24124-
var arrayType = function arrayType(value, path, resolve, traverseCallback) {
24125+
var arrayType = function arrayType(value, path, resolve, traverseCallback, seenSchemaCache) {
2412524126
var items = [];
2412624127
if (!(value.items || value.additionalItems)) {
2412724128
if (utils.hasProperties(value, 'minItems', 'maxItems', 'uniqueItems')) {
@@ -24136,7 +24137,7 @@ function extend() {
2413624137
if (tmpItems instanceof Array) {
2413724138
return Array.prototype.concat.call(items, tmpItems.map(function (item, key) {
2413824139
var itemSubpath = path.concat(['items', key + '']);
24139-
return traverseCallback(item, itemSubpath, resolve);
24140+
return traverseCallback(item, itemSubpath, resolve, null, seenSchemaCache);
2414024141
}));
2414124142
}
2414224143
var minItems = value.minItems;
@@ -24165,11 +24166,11 @@ function extend() {
2416524166
sample = typeof value.additionalItems === 'object' ? value.additionalItems : {};
2416624167
for (var current = items.length; current < length; current++) {
2416724168
var itemSubpath = path.concat(['items', current + '']);
24168-
var element = traverseCallback(value.items || sample, itemSubpath, resolve);
24169+
var element = traverseCallback(value.items || sample, itemSubpath, resolve, null, seenSchemaCache);
2416924170
items.push(element);
2417024171
}
2417124172
if (value.uniqueItems) {
24172-
return unique(path.concat(['items']), items, value, sample, resolve, traverseCallback);
24173+
return unique(path.concat(['items']), items, value, sample, resolve, traverseCallback, seenSchemaCache);
2417324174
}
2417424175
return items;
2417524176
};
@@ -24233,7 +24234,7 @@ function extend() {
2423324234
var anyType = { type: ['string', 'number', 'integer', 'boolean'] };
2423424235
// TODO provide types
2423524236
// Updated objectType definition to latest version (0.5.0-rcv.41)
24236-
var objectType = function objectType(value, path, resolve, traverseCallback) {
24237+
var objectType = function objectType(value, path, resolve, traverseCallback, seenSchemaCache) {
2423724238
const props = {};
2423824239

2423924240
const properties = value.properties || {};
@@ -24269,7 +24270,7 @@ function extend() {
2426924270
}
2427024271
});
2427124272

24272-
return traverseCallback(props, path.concat(['properties']), resolve, value);
24273+
return traverseCallback(props, path.concat(['properties']), resolve, value, seenSchemaCache);
2427324274
}
2427424275

2427524276
const optionalsProbability = optionAPI('alwaysFakeOptionals') === true ? 1.0 : optionAPI('optionalsProbability');
@@ -24325,7 +24326,7 @@ function extend() {
2432524326

2432624327
return traverseCallback({
2432724328
allOf: _defns.concat(value),
24328-
}, path.concat(['properties']), resolve, value);
24329+
}, path.concat(['properties']), resolve, value, seenSchemaCache);
2432924330
}
2433024331
}
2433124332

@@ -24478,7 +24479,7 @@ function extend() {
2447824479
}
2447924480
}
2448024481

24481-
return traverseCallback(props, path.concat(['properties']), resolve, value);
24482+
return traverseCallback(props, path.concat(['properties']), resolve, value, seenSchemaCache);
2448224483
};
2448324484

2448424485
/**
@@ -24664,31 +24665,42 @@ function extend() {
2466424665
};
2466524666

2466624667
// TODO provide types
24667-
function traverse(schema, path, resolve, rootSchema) {
24668+
function traverse(schema, path, resolve, rootSchema, seenSchemaCache) {
2466824669
schema = resolve(schema);
2466924670
if (!schema) {
24670-
return;
24671+
return;
2467124672
}
2467224673
if (optionAPI('useExamplesValue') && 'example' in schema) {
2467324674
var clonedSchema,
24674-
result;
24675-
24676-
// avoid minItems and maxItems while checking for valid examples
24677-
if (optionAPI('avoidExampleItemsLength') && _.get(schema, 'type') === 'array') {
24678-
clonedSchema = _.clone(schema);
24679-
_.unset(clonedSchema, 'minItems');
24680-
_.unset(clonedSchema, 'maxItems');
24675+
result,
24676+
isExampleValid,
24677+
hashSchema = hash(schema);
2468124678

24682-
// avoid validation of values that are in pm variable format (i.e. '{{userId}}')
24683-
result = validateSchema(clonedSchema, schema.example, { ignoreUnresolvedVariables: true });
24679+
if(seenSchemaCache && seenSchemaCache.has(hashSchema)) {
24680+
isExampleValid = seenSchemaCache.get(hashSchema);
2468424681
}
2468524682
else {
24686-
// avoid validation of values that are in pm variable format (i.e. '{{userId}}')
24687-
result = validateSchema(schema, schema.example, { ignoreUnresolvedVariables: true });
24683+
// avoid minItems and maxItems while checking for valid examples
24684+
if (optionAPI('avoidExampleItemsLength') && _.get(schema, 'type') === 'array') {
24685+
clonedSchema = _.clone(schema);
24686+
_.unset(clonedSchema, 'minItems');
24687+
_.unset(clonedSchema, 'maxItems');
24688+
24689+
// avoid validation of values that are in pm variable format (i.e. '{{userId}}')
24690+
result = validateSchema(clonedSchema, schema.example, { ignoreUnresolvedVariables: true });
24691+
}
24692+
else {
24693+
// avoid validation of values that are in pm variable format (i.e. '{{userId}}')
24694+
result = validateSchema(schema, schema.example, { ignoreUnresolvedVariables: true });
24695+
}
24696+
24697+
// Store the final result that needs to be used in the seen map
24698+
isExampleValid = result && result.length === 0;
24699+
seenSchemaCache && seenSchemaCache.set(hashSchema, isExampleValid);
2468824700
}
2468924701

2469024702
// Use example only if valid
24691-
if (result && result.length === 0) {
24703+
if (isExampleValid) {
2469224704
return schema.example;
2469324705
}
2469424706
}
@@ -24707,7 +24719,7 @@ function extend() {
2470724719
}
2470824720
// thunks can return sub-schemas
2470924721
if (typeof schema.thunk === 'function') {
24710-
return traverse(schema.thunk(), path, resolve);
24722+
return traverse(schema.thunk(), path, resolve, null, seenSchemaCache);
2471124723
}
2471224724
if (typeof schema.generate === 'function') {
2471324725
return utils.typecast(schema, function () { return schema.generate(rootSchema); });
@@ -24735,7 +24747,7 @@ function extend() {
2473524747
}
2473624748
else {
2473724749
try {
24738-
var result = typeMap[type](schema, path, resolve, traverse);
24750+
var result = typeMap[type](schema, path, resolve, traverse, seenSchemaCache);
2473924751
var required = schema.items
2474024752
? schema.items.required
2474124753
: schema.required;
@@ -24755,7 +24767,7 @@ function extend() {
2475524767
}
2475624768
for (var prop in schema) {
2475724769
if (typeof schema[prop] === 'object' && prop !== 'definitions') {
24758-
copy[prop] = traverse(schema[prop], path.concat([prop]), resolve, copy);
24770+
copy[prop] = traverse(schema[prop], path.concat([prop]), resolve, copy, seenSchemaCache);
2475924771
}
2476024772
else {
2476124773
copy[prop] = schema[prop];
@@ -24825,7 +24837,7 @@ function extend() {
2482524837
return obj;
2482624838
}
2482724839
// TODO provide types
24828-
function run(refs, schema, container) {
24840+
function run(refs, schema, container, seenSchemaCache) {
2482924841
try {
2483024842
var result = traverse(schema, [], function reduce(sub, maxReduceDepth) {
2483124843
if (typeof maxReduceDepth === 'undefined') {
@@ -24894,7 +24906,7 @@ function extend() {
2489424906
}
2489524907
}
2489624908
return container.wrap(sub);
24897-
});
24909+
}, null, seenSchemaCache);
2489824910
if (optionAPI('resolveJsonPath')) {
2489924911
return resolve(result);
2490024912
}
@@ -24936,7 +24948,7 @@ function extend() {
2493624948
}
2493724949
}
2493824950
}
24939-
var jsf = function (schema, refs) {
24951+
var jsf = function (schema, refs, seenSchemaCache) {
2494024952
var ignore = optionAPI('ignoreMissingRefs');
2494124953
var $ = deref(function (id, refs) {
2494224954
// FIXME: allow custom callback?
@@ -24945,7 +24957,7 @@ function extend() {
2494524957
}
2494624958
});
2494724959
var $refs = getRefs(refs);
24948-
return run($refs, $(schema, $refs, true), container);
24960+
return run($refs, $(schema, $refs, true), container, seenSchemaCache);
2494924961
};
2495024962
jsf.resolve = function (schema, refs, cwd) {
2495124963
if (typeof refs === 'string') {

lib/deref.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ module.exports = {
149149
*/
150150

151151
resolveRefs: function (schema, parameterSourceOption, components, schemaResolutionCache,
152-
resolveFor = 'CONVERSION', resolveTo = 'schema', stack = 0, seenRef = {}, stackLimit = 8, isAllOf = false) {
152+
resolveFor = 'CONVERSION', resolveTo = 'schema', stack = 0, seenRef = {}, stackLimit = 10, isAllOf = false) {
153153
var resolvedSchema, prop, splitRef,
154154
ERR_TOO_MANY_LEVELS = '<Error: Too many levels of nesting to fake this schema>';
155155
let concreteUtils = components && components.hasOwnProperty('concreteUtils') ?

lib/options.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ module.exports = {
155155
name: 'Schema resolution nesting limit',
156156
id: 'stackLimit',
157157
type: 'integer',
158-
default: 8,
158+
default: 10,
159159
description: 'Number of nesting limit till which schema resolution will happen. Increasing this limit may' +
160160
' result in more time to convert collection depending on complexity of specification. (To make sure this' +
161161
' option works correctly "optimizeConversion" option needs to be disabled)',

lib/schemaUtils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ function safeSchemaFaker(oldSchema, resolveTo, resolveFor, parameterSourceOption
218218
return fakedSchema;
219219
}
220220
// for JSON, the indentCharacter will be applied in the JSON.stringify step later on
221-
fakedSchema = schemaFaker(resolvedSchema);
221+
fakedSchema = schemaFaker(resolvedSchema, null, _.get(schemaCache, 'schemaValidationCache'));
222222
schemaFakerCache[key] = fakedSchema;
223223
return fakedSchema;
224224
}
@@ -310,7 +310,7 @@ module.exports = {
310310

311311
var computedOptions = _.clone(options);
312312

313-
computedOptions.stackLimit = 8;
313+
computedOptions.stackLimit = 10;
314314
// This is the score that is given to each spec on the basis of the
315315
// number of references present in spec and the number of requests that will be generated.
316316
// This ranges from 0-10.

lib/schemapack.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class SchemaPack {
4646
this.computedOptions = null;
4747
this.schemaFakerCache = {};
4848
this.schemaResolutionCache = {};
49+
this.schemaValidationCache = new Map();
4950

5051
this.computedOptions = utils.mergeOptions(
5152
// predefined options
@@ -260,7 +261,8 @@ class SchemaPack {
260261
authHelper,
261262
schemaCache = {
262263
schemaResolutionCache: this.schemaResolutionCache,
263-
schemaFakerCache: this.schemaFakerCache
264+
schemaFakerCache: this.schemaFakerCache,
265+
schemaValidationCache: this.schemaValidationCache
264266
};
265267

266268
if (!this.validated) {

0 commit comments

Comments
 (0)