Skip to content

Commit a141718

Browse files
committed
Support validating cyclic inputs. Added test for validation error with recursive schema
1 parent 226adab commit a141718

File tree

2 files changed

+75
-35
lines changed

2 files changed

+75
-35
lines changed

lib/validator.js

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,14 @@ Validator.prototype.compile = function(schema) {
6868
}
6969

7070
const rules = this.compileSchemaType(schema);
71+
this.cache.clear();
7172
return function(value) {
7273
return self.checkSchemaType(value, rules, undefined, null);
7374
};
7475
}
7576

7677
const rule = this.compileSchemaObject(schema);
78+
this.cache.clear();
7779
return function(value) {
7880
return self.checkSchemaObject(value, rule, undefined, null);
7981
};
@@ -84,26 +86,32 @@ Validator.prototype.compileSchemaObject = function(schemaObject) {
8486
throw new Error("Invalid schema!");
8587
}
8688

87-
const compiledObject = Object.keys(schemaObject).map(name => {
89+
let compiledObject = this.cache.get(schemaObject);
90+
if (compiledObject) {
91+
compiledObject.cycle = true;
92+
return compiledObject;
93+
} else{
94+
compiledObject = { cycle: false, properties: null, compiledObjectFunction: null, objectStack: [] };
95+
this.cache.set(schemaObject, compiledObject);
96+
}
97+
98+
compiledObject.properties = Object.keys(schemaObject).map(name => {
8899
const compiledType = this.compileSchemaType(schemaObject[name]);
89100
return {name: name, compiledType: compiledType};
90101
});
91102

92-
// Uncomment this line to use compiled object validator:
93-
// return compiledObject;
94-
95103
const sourceCode = [];
96104
sourceCode.push("let res;");
97105
sourceCode.push("let propertyPath;");
98106
sourceCode.push("const errors = [];");
99-
for (let i = 0; i < compiledObject.length; i++) {
100-
const property = compiledObject[i];
107+
for (let i = 0; i < compiledObject.properties.length; i++) {
108+
const property = compiledObject.properties[i];
101109
const name = property.name;
102110
sourceCode.push(`propertyPath = (path !== undefined ? path + ".${name}" : "${name}");`);
103111
if (Array.isArray(property.compiledType)) {
104-
sourceCode.push(`res = this.checkSchemaType(value.${name}, compiledObject[${i}].compiledType, propertyPath, value);`);
112+
sourceCode.push(`res = this.checkSchemaType(value.${name}, properties[${i}].compiledType, propertyPath, value);`);
105113
} else {
106-
sourceCode.push(`res = this.checkSchemaRule(value.${name}, compiledObject[${i}].compiledType, propertyPath, value);`);
114+
sourceCode.push(`res = this.checkSchemaRule(value.${name}, properties[${i}].compiledType, propertyPath, value);`);
107115
}
108116
sourceCode.push("if (res !== true) {");
109117
sourceCode.push("\tthis.handleResult(errors, propertyPath, res);");
@@ -112,12 +120,9 @@ Validator.prototype.compileSchemaObject = function(schemaObject) {
112120

113121
sourceCode.push("return errors.length === 0 ? true : errors;");
114122

115-
const compiledObjectFunction = new Function("value", "compiledObject", "path", "parent", sourceCode.join("\n"));
123+
compiledObject.compiledObjectFunction = new Function("value", "properties", "path", "parent", sourceCode.join("\n"));
116124

117-
const self = this;
118-
return function(value, _unused, path, parent) {
119-
return compiledObjectFunction.call(self, value, compiledObject, path, parent);
120-
};
125+
return compiledObject;
121126
};
122127

123128
Validator.prototype.compileSchemaType = function(schemaType) {
@@ -135,14 +140,6 @@ Validator.prototype.compileSchemaType = function(schemaType) {
135140
};
136141

137142
Validator.prototype.compileSchemaRule = function(schemaRule) {
138-
const compiledRule = {};
139-
const cachedResult = this.cache.get(schemaRule);
140-
if (cachedResult) {
141-
return cachedResult;
142-
} else {
143-
this.cache.set(schemaRule, compiledRule);
144-
}
145-
146143
if (typeof schemaRule === "string") {
147144
schemaRule = {
148145
type: schemaRule
@@ -165,32 +162,49 @@ Validator.prototype.compileSchemaRule = function(schemaRule) {
165162
dataFunction = this.checkSchemaArray;
166163
}
167164

168-
return Object.assign(compiledRule, {
165+
return {
169166
schemaRule: schemaRule,
170167
ruleFunction: ruleFunction,
171168
dataFunction: dataFunction,
172169
dataParameter: dataParameter
173-
});
170+
};
174171
};
175172

176173
Validator.prototype.checkSchemaObject = function(value, compiledObject, path, parent) {
177-
if (compiledObject instanceof Function) {
178-
return compiledObject(value, undefined, path, parent);
174+
if (compiledObject.cycle) {
175+
if (compiledObject.objectStack.indexOf(value) !== -1) {
176+
return true;
177+
}
178+
179+
compiledObject.objectStack.push(value);
180+
const result = this.checkSchemaObjectInner(value, compiledObject, path, parent);
181+
compiledObject.objectStack.pop();
182+
return result;
183+
} else {
184+
return this.checkSchemaObjectInner(value, compiledObject, path, parent);
179185
}
186+
};
187+
188+
Validator.prototype.checkSchemaObjectInner = function(value, compiledObject, path, parent) {
189+
return compiledObject.compiledObjectFunction.call(this, value, compiledObject.properties, path, parent);
190+
191+
/*
192+
// Reference implementation of the object checker
180193
181194
const errors = [];
182-
const checksLength = compiledObject.length;
183-
for (let i = 0; i < checksLength; i++) {
184-
const check = compiledObject[i];
185-
const propertyPath = (path !== undefined ? path + "." : "") + check.name;
186-
const res = this.checkSchemaType(value[check.name], check.compiledType, propertyPath, value);
195+
const propertiesLength = compiledObject.properties.length;
196+
for (let i = 0; i < propertiesLength; i++) {
197+
const property = compiledObject.properties[i];
198+
const propertyPath = (path !== undefined ? path + "." : "") + property.name;
199+
const res = this.checkSchemaType(value[property.name], property.compiledType, propertyPath, value);
187200
188201
if (res !== true) {
189202
this.handleResult(errors, propertyPath, res);
190203
}
191204
}
192205
193206
return errors.length === 0 ? true : errors;
207+
*/
194208
};
195209

196210
Validator.prototype.checkSchemaType = function(value, compiledType, path, parent) {

test/validator.spec.js

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,12 +1005,13 @@ describe("Test array without items", () => {
10051005
});
10061006
});
10071007

1008-
describe("Test recursive schema", () => {
1008+
describe("Test recursive/cyclic schema", () => {
10091009
const v = new Validator();
10101010

10111011
let schema = {};
10121012
Object.assign(schema, {
10131013
name: { type: "string" },
1014+
parent: { type: "object", props: schema, optional: true },
10141015
subcategories: {
10151016
type: "array",
10161017
optional: true,
@@ -1019,20 +1020,45 @@ describe("Test recursive schema", () => {
10191020
});
10201021

10211022
it("should compile and validate", () => {
1022-
let category = {
1023+
let category = {};
1024+
Object.assign(category, {
1025+
name: "top",
1026+
subcategories: [
1027+
{
1028+
name: "sub1",
1029+
parent: category
1030+
},
1031+
{
1032+
name: "sub2",
1033+
parent: category
1034+
}
1035+
]
1036+
});
1037+
1038+
const res = v.validate(category, schema);
1039+
1040+
expect(res).toBe(true);
1041+
});
1042+
1043+
it("should give error if nested object is broken", () => {
1044+
const category = {
10231045
name: "top",
10241046
subcategories: [
10251047
{
10261048
name: "sub1"
10271049
},
10281050
{
1029-
name: "sub2"
1051+
name: "sub2",
1052+
subcategories: [ {} ]
10301053
}
10311054
]
10321055
};
10331056

1034-
let res = v.validate(category, schema);
1057+
const res = v.validate(category, schema);
10351058

1036-
expect(res).toBe(true);
1059+
expect(res).toBeInstanceOf(Array);
1060+
expect(res.length).toBe(1);
1061+
expect(res[0].type).toBe("required");
1062+
expect(res[0].field).toBe("subcategories[1].subcategories[0].name");
10371063
});
10381064
});

0 commit comments

Comments
 (0)