Skip to content

Commit 9ac40b3

Browse files
committed
Reorganized the compiler
1 parent 64c237b commit 9ac40b3

File tree

2 files changed

+115
-166
lines changed

2 files changed

+115
-166
lines changed

lib/validator.js

Lines changed: 100 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -65,184 +65,133 @@ Validator.prototype.compile = function(schema) {
6565
throw new Error("If the schema is an Array, must contain at least one element!");
6666
}
6767

68-
const rules = flatten(schema.map(r => this._processRule(r, null, false)));
69-
return this._checkWrapper(rules, true);
68+
return this.compileSchemaType(schema);
69+
}
7070

71-
} else if (schema != null && typeof schema === "object") {
72-
const rules = flatten(Object.keys(schema).map(name => this._processRule(schema[name], name, false)));
73-
return this._checkWrapper(rules);
71+
return this.compileSchemaObject(schema);
72+
};
7473

74+
Validator.prototype.compileSchemaObject = function(schemaObject) {
75+
if (schemaObject == null || typeof schemaObject !== "object" || Array.isArray(schemaObject)) {
76+
throw new Error("Invalid schema!");
77+
}
78+
79+
const self = this;
80+
const checks = {};
81+
for (let name in schemaObject) {
82+
checks[name] = this.compileSchemaType(schemaObject[name]);
7583
}
7684

77-
throw new Error("Invalid schema!");
85+
return function(value, _, path, parent) {
86+
const errors = [];
87+
for (let name in checks) {
88+
const propertyPath = (path ? path + "." : "") + name;
89+
const res = checks[name](value[name], schemaObject[name], propertyPath, value);
90+
91+
if (res !== true) {
92+
self.handleResult(errors, propertyPath, res);
93+
}
94+
}
95+
96+
return errors.length === 0 ? true : errors;
97+
};
7898
};
7999

80-
/**
81-
* Process a rule item & return checker functions
82-
*
83-
* @param {Object} rule
84-
* @param {String} name
85-
* @param {Boolean} iterate
86-
*/
87-
Validator.prototype._processRule = function(rule, name, iterate) {
88-
const checks = [];
89-
90-
if (Array.isArray(rule)) {
91-
// Compile the multiple schemas
92-
checks.push({
93-
fn: this.compile(rule),
94-
type: "_multi",
95-
name: name,
96-
schema: rule,
97-
iterate: iterate
98-
});
99-
100-
return checks;
100+
Validator.prototype.compileSchemaType = function(schemaType) {
101+
if (Array.isArray(schemaType)) {
102+
// Multiple rules
103+
const self = this;
104+
const checks = schemaType.map(r => this.compileSchemaType(r));
105+
106+
return function(value, _, path, parent) {
107+
const errors = [];
108+
let validated = false;
109+
for (let i = 0; i < checks.length; i++) {
110+
const res = checks[i](value, schemaType[i], path, parent);
111+
112+
if (res !== true) {
113+
self.handleResult(errors, path, res);
114+
} else {
115+
validated = true;
116+
}
117+
}
118+
119+
return validated ? true : errors;
120+
};
101121
}
102122

103-
if (typeof rule === "string") {
104-
rule = {
105-
type: rule
123+
return this.compileSchemaRule(schemaType);
124+
};
125+
126+
Validator.prototype.compileSchemaRule = function(schemaRule) {
127+
if (typeof schemaRule === "string") {
128+
schemaRule = {
129+
type: schemaRule
106130
};
107131
}
108132

109-
if (!this.rules[rule.type]) {
110-
throw new Error("Invalid '" + rule.type + "' type in validator schema!");
133+
if (!this.rules[schemaRule.type]) {
134+
throw new Error("Invalid '" + schemaRule.type + "' type in validator schema!");
111135
}
112136

137+
const self = this;
138+
let checkContents = null;
139+
140+
if (schemaRule.type === "object") {
141+
if (schemaRule.props) {
142+
checkContents = this.compileSchemaObject(schemaRule.props);
143+
}
144+
} else if (schemaRule.type === "array") {
145+
if (schemaRule.items) {
146+
checkContents = this.compileSchemaArray(schemaRule.items);
147+
}
148+
}
113149

114-
/**
115-
* !IMPORTANT!: For the functioning of multiRule cases it is important that
116-
* pushing of object and array special rules is done in directly after this
117-
* simple rule.
118-
* If you which to push other checks, do it before the simple ones or after
119-
* the array special case.
120-
* See the comments in _checkWrapper for further explanation.
121-
*/
122-
checks.push({
123-
fn: this.rules[rule.type],
124-
type: rule.type,
125-
name: name,
126-
schema: rule,
127-
iterate: iterate
128-
});
150+
return function(value, _, path, parent) {
151+
const res = self.checkRule(value, schemaRule, path, parent);
152+
if (res !== true) {
153+
return res;
154+
}
129155

130-
// Nested schema
131-
if (rule.type === "object" && rule.props) {
132-
// Compile the child schema
133-
checks.push({
134-
fn: this.compile(rule.props),
135-
type: rule.type,
136-
name: name,
137-
schema: rule,
138-
iterate: iterate,
139-
secondPart: true //first part is the "primitive" typeof check above
140-
});
141-
}
156+
if (checkContents) {
157+
return checkContents(value, schemaRule, path, parent);
158+
}
142159

143-
// Array schema
144-
if (rule.type === "array" && rule.items) {
145-
// Compile the array schema
146-
checks.push({
147-
fn: this._checkWrapper(this._processRule(rule.items, null, false)),
148-
type: rule.type,
149-
name: name,
150-
schema: rule,
151-
iterate: true,
152-
secondPart: true //first part is the "primitive" typeof check above
153-
});
160+
return true;
154161
}
155-
156-
return checks;
157162
};
158163

159-
/**
160-
* Create a wrapper function for compiled schema.
161-
*
162-
* @param {Array} rules
163-
* @param {Boolean} isMultipleRules
164-
*/
165-
Validator.prototype._checkWrapper = function(rules, isMultipleRules) {
164+
Validator.prototype.compileSchemaArray = function(schemaType) {
166165
const self = this;
166+
const checkArrayItem = this.compileSchemaType(schemaType);
167167

168-
// Compiled validator function
169-
return function(obj, _schema, pathStack) {
170-
let errors = [];
171-
const count = rules.length;
172-
for (let i = 0; i < count; i++) {
173-
const check = rules[i];
174-
const schema = check.schema;
175-
176-
let value;
177-
let stack;
178-
if (check.name) {
179-
value = obj[check.name];
180-
stack = (pathStack ? pathStack + "." : "") + check.name;
181-
} else {
182-
value = obj;
183-
stack = pathStack ? pathStack : "";
184-
}
168+
return function (value, _, path, parent) {
169+
const errors = [];
170+
for (let i = 0; i < value.length; i++) {
171+
const arrayItemPath = (path ? path : "") + "[" + i + "]";
172+
const res = checkArrayItem(value[i], schemaType, arrayItemPath, value);
185173

186-
// Check required fields
187-
if ((value === undefined || value === null)) {
188-
if (check.type === "forbidden")
189-
continue;
174+
if (res !== true) {
175+
self.handleResult(errors, arrayItemPath, res);
176+
}
177+
}
190178

191-
if (schema.optional === true)
192-
continue;
179+
return errors.length === 0 ? true : errors;
180+
}
181+
};
193182

194-
if (!Array.isArray(schema)) {
195-
self.handleResult(errors, stack, self.makeError("required"));
196-
continue;
197-
}
183+
Validator.prototype.checkRule = function(value, schemaRule, path, parent) {
184+
if (value === undefined || value === null) {
185+
if (schemaRule.type === "forbidden")
186+
return true;
198187

199-
} // else {
200-
// Call the checker function
201-
if (check.iterate) {
202-
let errorInCurrentArray = false;
203-
const l = value.length;
204-
for (let i = 0; i < l; i++) {
205-
let _stack = stack + "[" + i + "]";
206-
let res = check.fn.call(self, value[i], schema, _stack, obj);
207-
if (res !== true) {
208-
errorInCurrentArray = true;
209-
self.handleResult(errors, _stack, res);
210-
}
211-
}
212-
/**
213-
* If this is second part of a multiRule array check and the array
214-
* is valid, then the rule is valid.
215-
*/
216-
if (!errorInCurrentArray && isMultipleRules && check.secondPart) {
217-
return true;
218-
}
219-
} else {
220-
let res = check.fn.call(self, value, schema, stack, obj);
221-
222-
if (isMultipleRules) {
223-
if (res === true) {
224-
/**
225-
* Object and array checks are divided into two internal checks. In case of a multiRule
226-
* we have to make sure to check both parts. Thus we we continue to also do the second
227-
* check if their is one.
228-
*/
229-
const nextRule = rules[i + 1];
230-
if (nextRule && nextRule.secondPart){
231-
continue;
232-
}
233-
// Jump out after first success and clear previous errors
234-
return true;
235-
}
236-
}
188+
if (schemaRule.optional === true)
189+
return true;
237190

238-
if (res !== true)
239-
self.handleResult(errors, stack, res);
240-
}
241-
//}
242-
}
191+
return this.makeError("required");
192+
}
243193

244-
return errors.length === 0 ? true : errors;
245-
};
194+
return this.rules[schemaRule.type].call(this, value, schemaRule, path, parent);
246195
};
247196

248197
/**

test/validator.spec.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -112,33 +112,33 @@ describe("Test resolveMessage", () => {
112112
describe("Test compile (unit test)", () => {
113113

114114
const v = new Validator();
115-
v._processRule = jest.fn(v._processRule.bind(v));
115+
v.compileSchemaRule = jest.fn(v.compileSchemaRule.bind(v));
116116

117-
it("should call processRule", () => {
117+
it("should call compileSchemaRule", () => {
118118
v.compile({
119119
id: { type: "number" },
120120
name: { type: "string", min: 5},
121121
status: "boolean"
122122
});
123123

124-
expect(v._processRule).toHaveBeenCalledTimes(3);
125-
expect(v._processRule).toHaveBeenCalledWith({"type": "number"}, "id", false);
126-
expect(v._processRule).toHaveBeenCalledWith({"type": "string", "min": 5}, "name", false);
127-
expect(v._processRule).toHaveBeenCalledWith("boolean", "status", false);
124+
expect(v.compileSchemaRule).toHaveBeenCalledTimes(3);
125+
expect(v.compileSchemaRule).toHaveBeenCalledWith({"type": "number"});
126+
expect(v.compileSchemaRule).toHaveBeenCalledWith({"type": "string", "min": 5});
127+
expect(v.compileSchemaRule).toHaveBeenCalledWith("boolean");
128128
});
129129

130-
it("should call processRule for root-level array", () => {
131-
v._processRule.mockClear();
130+
it("should call compileSchemaRule for root-level array", () => {
131+
v.compileSchemaRule.mockClear();
132132

133133
v.compile([
134134
{ type: "array", items: "number" },
135135
{ type: "string", min: 2 }
136136
]);
137137

138-
expect(v._processRule).toHaveBeenCalledTimes(3);
139-
expect(v._processRule).toHaveBeenCalledWith({"type": "array", items: "number"}, null, false);
140-
expect(v._processRule).toHaveBeenCalledWith("number", null, false);
141-
expect(v._processRule).toHaveBeenCalledWith({"type": "string", min: 2 }, null, false);
138+
expect(v.compileSchemaRule).toHaveBeenCalledTimes(3);
139+
expect(v.compileSchemaRule).toHaveBeenCalledWith({"type": "array", items: "number"});
140+
expect(v.compileSchemaRule).toHaveBeenCalledWith("number");
141+
expect(v.compileSchemaRule).toHaveBeenCalledWith({"type": "string", min: 2 });
142142
});
143143

144144
it("should throw error is the schema is null", () => {
@@ -175,8 +175,7 @@ describe("Test compile (unit test)", () => {
175175
}).toThrowError("Invalid 'unknow' type in validator schema!");
176176
});
177177

178-
it.skip("should throw error if object has array props", () => {
179-
// TODO: This schema compiles, but never matches anything
178+
it("should throw error if object has array props", () => {
180179
const schema = {
181180
invalid: { type: "object", props: [ { type: "string" }, { type: "number" } ] }
182181
};
@@ -197,7 +196,8 @@ describe("Test compile (unit test)", () => {
197196
});
198197
});
199198

200-
describe("Test _processRule", () => {
199+
// Skip tests for earlier compiler internals
200+
describe.skip("Test _processRule", () => {
201201

202202
const v = new Validator();
203203
v.compile = jest.fn();

0 commit comments

Comments
 (0)