Skip to content

Commit ab78b45

Browse files
authored
Merge pull request #34 from andersnm/recurse
Support recursive schemas
2 parents 22ff631 + a141718 commit ab78b45

File tree

2 files changed

+101
-20
lines changed

2 files changed

+101
-20
lines changed

lib/validator.js

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function Validator(opts) {
3939

4040
// Load rules
4141
this.rules = loadRules();
42+
this.cache = new Map();
4243
}
4344

4445
/**
@@ -67,12 +68,14 @@ Validator.prototype.compile = function(schema) {
6768
}
6869

6970
const rules = this.compileSchemaType(schema);
71+
this.cache.clear();
7072
return function(value) {
7173
return self.checkSchemaType(value, rules, undefined, null);
7274
};
7375
}
7476

7577
const rule = this.compileSchemaObject(schema);
78+
this.cache.clear();
7679
return function(value) {
7780
return self.checkSchemaObject(value, rule, undefined, null);
7881
};
@@ -83,26 +86,32 @@ Validator.prototype.compileSchemaObject = function(schemaObject) {
8386
throw new Error("Invalid schema!");
8487
}
8588

86-
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 => {
8799
const compiledType = this.compileSchemaType(schemaObject[name]);
88100
return {name: name, compiledType: compiledType};
89101
});
90102

91-
// Uncomment this line to use compiled object validator:
92-
// return compiledObject;
93-
94103
const sourceCode = [];
95104
sourceCode.push("let res;");
96105
sourceCode.push("let propertyPath;");
97106
sourceCode.push("const errors = [];");
98-
for (let i = 0; i < compiledObject.length; i++) {
99-
const property = compiledObject[i];
107+
for (let i = 0; i < compiledObject.properties.length; i++) {
108+
const property = compiledObject.properties[i];
100109
const name = property.name;
101110
sourceCode.push(`propertyPath = (path !== undefined ? path + ".${name}" : "${name}");`);
102111
if (Array.isArray(property.compiledType)) {
103-
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);`);
104113
} else {
105-
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);`);
106115
}
107116
sourceCode.push("if (res !== true) {");
108117
sourceCode.push("\tthis.handleResult(errors, propertyPath, res);");
@@ -111,12 +120,9 @@ Validator.prototype.compileSchemaObject = function(schemaObject) {
111120

112121
sourceCode.push("return errors.length === 0 ? true : errors;");
113122

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

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

122128
Validator.prototype.compileSchemaType = function(schemaType) {
@@ -165,23 +171,40 @@ Validator.prototype.compileSchemaRule = function(schemaRule) {
165171
};
166172

167173
Validator.prototype.checkSchemaObject = function(value, compiledObject, path, parent) {
168-
if (compiledObject instanceof Function) {
169-
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);
170185
}
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
171193
172194
const errors = [];
173-
const checksLength = compiledObject.length;
174-
for (let i = 0; i < checksLength; i++) {
175-
const check = compiledObject[i];
176-
const propertyPath = (path !== undefined ? path + "." : "") + check.name;
177-
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);
178200
179201
if (res !== true) {
180202
this.handleResult(errors, propertyPath, res);
181203
}
182204
}
183205
184206
return errors.length === 0 ? true : errors;
207+
*/
185208
};
186209

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

test/validator.spec.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,3 +1004,61 @@ describe("Test array without items", () => {
10041004
expect(res).toBe(true);
10051005
});
10061006
});
1007+
1008+
describe("Test recursive/cyclic schema", () => {
1009+
const v = new Validator();
1010+
1011+
let schema = {};
1012+
Object.assign(schema, {
1013+
name: { type: "string" },
1014+
parent: { type: "object", props: schema, optional: true },
1015+
subcategories: {
1016+
type: "array",
1017+
optional: true,
1018+
items: { type: "object", props: schema}
1019+
}
1020+
});
1021+
1022+
it("should compile and validate", () => {
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 = {
1045+
name: "top",
1046+
subcategories: [
1047+
{
1048+
name: "sub1"
1049+
},
1050+
{
1051+
name: "sub2",
1052+
subcategories: [ {} ]
1053+
}
1054+
]
1055+
};
1056+
1057+
const res = v.validate(category, schema);
1058+
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");
1063+
});
1064+
});

0 commit comments

Comments
 (0)