Skip to content

Commit f596254

Browse files
committed
feat(validator): implement validator for simple types
1 parent 9c4ca0a commit f596254

11 files changed

Lines changed: 785 additions & 1 deletion

File tree

lib/internal/bootstrap/realm.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ const schemelessBlockList = new SafeSet([
130130
'quic',
131131
'test',
132132
'test/reporters',
133+
'validator',
133134
]);
134135
// Modules that will only be enabled at run time.
135-
const experimentalModuleList = new SafeSet(['ffi', 'sqlite', 'quic', 'stream/iter', 'zlib/iter']);
136+
const experimentalModuleList = new SafeSet(['ffi', 'sqlite', 'quic', 'stream/iter', 'validator', 'zlib/iter']);
136137

137138
// Set up process.binding() and process._linkedBinding().
138139
{

lib/internal/errors.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,6 +1900,11 @@ E('ERR_UNSUPPORTED_RESOLVE_REQUEST',
19001900
E('ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX', '%s', SyntaxError);
19011901
E('ERR_USE_AFTER_CLOSE', '%s was closed', Error);
19021902

1903+
E('ERR_VALIDATOR_INVALID_SCHEMA',
1904+
(path, reason) => {
1905+
if (path) return `Invalid schema at "${path}": ${reason}`;
1906+
return `Invalid schema: ${reason}`;
1907+
}, TypeError, HideStackFramesError);
19031908
// This should probably be a `TypeError`.
19041909
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
19051910
'At least one valid performance entry type is required', Error);

lib/internal/process/pre_execution.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ function prepareExecution(options) {
117117
setupFFI();
118118
setupSQLite();
119119
setupStreamIter();
120+
setupValidator();
120121
setupQuic();
121122
setupWebStorage();
122123
setupWebsocket();
@@ -412,6 +413,15 @@ function setupStreamIter() {
412413
BuiltinModule.allowRequireByUsers('zlib/iter');
413414
}
414415

416+
function setupValidator() {
417+
if (getOptionValue('--no-experimental-validator')) {
418+
return;
419+
}
420+
421+
const { BuiltinModule } = require('internal/bootstrap/realm');
422+
BuiltinModule.allowRequireByUsers('validator');
423+
}
424+
415425
function setupQuic() {
416426
if (!getOptionValue('--experimental-quic')) {
417427
return;

lib/internal/validator/compile.js

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
'use strict';
2+
3+
const {
4+
ArrayIsArray,
5+
ArrayPrototypeIncludes,
6+
ArrayPrototypeJoin,
7+
ArrayPrototypePush,
8+
ArrayPrototypeSlice,
9+
NumberIsFinite,
10+
NumberIsInteger,
11+
ObjectFreeze,
12+
ObjectKeys,
13+
RegExp,
14+
SafeSet,
15+
} = primordials;
16+
17+
const {
18+
codes: {
19+
ERR_INVALID_ARG_TYPE,
20+
ERR_VALIDATOR_INVALID_SCHEMA,
21+
},
22+
} = require('internal/errors');
23+
24+
const kValidTypes = new SafeSet([
25+
'string', 'number', 'integer', 'boolean', 'object', 'array', 'null',
26+
]);
27+
28+
const kStringConstraints = new SafeSet([
29+
'type', 'minLength', 'maxLength', 'pattern', 'enum', 'default',
30+
]);
31+
32+
const kNumberConstraints = new SafeSet([
33+
'type', 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum',
34+
'multipleOf', 'default',
35+
]);
36+
37+
const kArrayConstraints = new SafeSet([
38+
'type', 'items', 'minItems', 'maxItems', 'default',
39+
]);
40+
41+
const kObjectConstraints = new SafeSet([
42+
'type', 'properties', 'required', 'additionalProperties', 'default',
43+
]);
44+
45+
const kSimpleConstraints = new SafeSet([
46+
'type', 'default',
47+
]);
48+
49+
const kConstraintsByType = {
50+
__proto__: null,
51+
string: kStringConstraints,
52+
number: kNumberConstraints,
53+
integer: kNumberConstraints,
54+
boolean: kSimpleConstraints,
55+
null: kSimpleConstraints,
56+
array: kArrayConstraints,
57+
object: kObjectConstraints,
58+
};
59+
60+
function formatPath(parentPath, key) {
61+
if (parentPath === '') return key;
62+
return `${parentPath}.${key}`;
63+
}
64+
65+
function validateNonNegativeInteger(value, path, name) {
66+
if (!NumberIsInteger(value) || value < 0) {
67+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
68+
path, `"${name}" must be a non-negative integer`);
69+
}
70+
}
71+
72+
function validateFiniteNumber(value, path, name) {
73+
if (typeof value !== 'number' || !NumberIsFinite(value)) {
74+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
75+
path, `"${name}" must be a finite number`);
76+
}
77+
}
78+
79+
function checkUnknownConstraints(definition, allowedSet, path) {
80+
const keys = ObjectKeys(definition);
81+
for (let i = 0; i < keys.length; i++) {
82+
if (!allowedSet.has(keys[i])) {
83+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
84+
path,
85+
`unknown constraint "${keys[i]}" for type "${definition.type}"`);
86+
}
87+
}
88+
}
89+
90+
function compileSchemaNode(definition, path) {
91+
if (typeof definition !== 'object' || definition === null ||
92+
ArrayIsArray(definition)) {
93+
throw new ERR_INVALID_ARG_TYPE(
94+
path || 'definition', 'a plain object', definition);
95+
}
96+
97+
const type = definition.type;
98+
if (typeof type !== 'string' || !kValidTypes.has(type)) {
99+
const validTypes = ArrayPrototypeJoin(
100+
['string', 'number', 'integer', 'boolean', 'object', 'array', 'null'],
101+
', ');
102+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
103+
path, `"type" must be one of: ${validTypes}`);
104+
}
105+
106+
const allowedConstraints = kConstraintsByType[type];
107+
checkUnknownConstraints(definition, allowedConstraints, path);
108+
109+
const compiled = { __proto__: null, type };
110+
111+
if ('default' in definition) {
112+
compiled.default = definition.default;
113+
compiled.hasDefault = true;
114+
} else {
115+
compiled.hasDefault = false;
116+
}
117+
118+
switch (type) {
119+
case 'string':
120+
compileStringConstraints(definition, compiled, path);
121+
break;
122+
case 'number':
123+
case 'integer':
124+
compileNumberConstraints(definition, compiled, path);
125+
break;
126+
case 'array':
127+
compileArrayConstraints(definition, compiled, path);
128+
break;
129+
case 'object':
130+
compileObjectConstraints(definition, compiled, path);
131+
break;
132+
default:
133+
break;
134+
}
135+
136+
return ObjectFreeze(compiled);
137+
}
138+
139+
function compileStringConstraints(definition, compiled, path) {
140+
if ('minLength' in definition) {
141+
validateNonNegativeInteger(definition.minLength, path, 'minLength');
142+
compiled.minLength = definition.minLength;
143+
}
144+
if ('maxLength' in definition) {
145+
validateNonNegativeInteger(definition.maxLength, path, 'maxLength');
146+
compiled.maxLength = definition.maxLength;
147+
}
148+
if (compiled.minLength !== undefined && compiled.maxLength !== undefined &&
149+
compiled.minLength > compiled.maxLength) {
150+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
151+
path, '"minLength" must not be greater than "maxLength"');
152+
}
153+
if ('pattern' in definition) {
154+
if (typeof definition.pattern !== 'string') {
155+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
156+
path, '"pattern" must be a string');
157+
}
158+
try {
159+
compiled.pattern = new RegExp(definition.pattern);
160+
} catch {
161+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
162+
path, `"pattern" is not a valid regular expression: ${definition.pattern}`);
163+
}
164+
compiled.patternSource = definition.pattern;
165+
}
166+
if ('enum' in definition) {
167+
if (!ArrayIsArray(definition.enum) || definition.enum.length === 0) {
168+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
169+
path, '"enum" must be a non-empty array');
170+
}
171+
compiled.enum = ArrayPrototypeSlice(definition.enum);
172+
}
173+
}
174+
175+
function compileNumberConstraints(definition, compiled, path) {
176+
if ('minimum' in definition) {
177+
validateFiniteNumber(definition.minimum, path, 'minimum');
178+
compiled.minimum = definition.minimum;
179+
}
180+
if ('maximum' in definition) {
181+
validateFiniteNumber(definition.maximum, path, 'maximum');
182+
compiled.maximum = definition.maximum;
183+
}
184+
if ('exclusiveMinimum' in definition) {
185+
validateFiniteNumber(definition.exclusiveMinimum, path, 'exclusiveMinimum');
186+
compiled.exclusiveMinimum = definition.exclusiveMinimum;
187+
}
188+
if ('exclusiveMaximum' in definition) {
189+
validateFiniteNumber(definition.exclusiveMaximum, path, 'exclusiveMaximum');
190+
compiled.exclusiveMaximum = definition.exclusiveMaximum;
191+
}
192+
if ('multipleOf' in definition) {
193+
validateFiniteNumber(definition.multipleOf, path, 'multipleOf');
194+
if (definition.multipleOf <= 0) {
195+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
196+
path, '"multipleOf" must be greater than 0');
197+
}
198+
compiled.multipleOf = definition.multipleOf;
199+
}
200+
}
201+
202+
function compileArrayConstraints(definition, compiled, path) {
203+
if ('minItems' in definition) {
204+
validateNonNegativeInteger(definition.minItems, path, 'minItems');
205+
compiled.minItems = definition.minItems;
206+
}
207+
if ('maxItems' in definition) {
208+
validateNonNegativeInteger(definition.maxItems, path, 'maxItems');
209+
compiled.maxItems = definition.maxItems;
210+
}
211+
if (compiled.minItems !== undefined && compiled.maxItems !== undefined &&
212+
compiled.minItems > compiled.maxItems) {
213+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
214+
path, '"minItems" must not be greater than "maxItems"');
215+
}
216+
if ('items' in definition) {
217+
compiled.items = compileSchemaNode(definition.items, formatPath(path, 'items'));
218+
}
219+
}
220+
221+
function compileObjectConstraints(definition, compiled, path) {
222+
if ('properties' in definition) {
223+
if (typeof definition.properties !== 'object' ||
224+
definition.properties === null ||
225+
ArrayIsArray(definition.properties)) {
226+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
227+
path, '"properties" must be a plain object');
228+
}
229+
const propKeys = ObjectKeys(definition.properties);
230+
const compiledProps = { __proto__: null };
231+
const propNames = [];
232+
for (let i = 0; i < propKeys.length; i++) {
233+
const key = propKeys[i];
234+
ArrayPrototypePush(propNames, key);
235+
compiledProps[key] = compileSchemaNode(
236+
definition.properties[key],
237+
formatPath(path, `properties.${key}`));
238+
}
239+
compiled.properties = ObjectFreeze(compiledProps);
240+
compiled.propertyNames = propNames;
241+
}
242+
243+
if ('required' in definition) {
244+
if (!ArrayIsArray(definition.required)) {
245+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
246+
path, '"required" must be an array of strings');
247+
}
248+
for (let i = 0; i < definition.required.length; i++) {
249+
if (typeof definition.required[i] !== 'string') {
250+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
251+
path, '"required" must be an array of strings');
252+
}
253+
if (compiled.properties &&
254+
!ArrayPrototypeIncludes(compiled.propertyNames, definition.required[i])) {
255+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
256+
path,
257+
`required property "${definition.required[i]}" is not defined in "properties"`);
258+
}
259+
}
260+
compiled.required = ArrayPrototypeSlice(definition.required);
261+
}
262+
263+
if ('additionalProperties' in definition) {
264+
if (typeof definition.additionalProperties !== 'boolean') {
265+
throw new ERR_VALIDATOR_INVALID_SCHEMA(
266+
path, '"additionalProperties" must be a boolean');
267+
}
268+
compiled.additionalProperties = definition.additionalProperties;
269+
} else {
270+
compiled.additionalProperties = true;
271+
}
272+
}
273+
274+
/**
275+
* Validate and lower a user schema definition into a frozen, null-prototype
276+
* representation consumed by `validateValue()` and `applyDefaults()`.
277+
*
278+
* All schema errors (unknown type, bad constraints, malformed `required`,
279+
* non-compiling `pattern`, etc.) surface from this call as
280+
* `ERR_VALIDATOR_INVALID_SCHEMA` so they fail fast at `new Schema(...)`
281+
* rather than on each `validate(data)`.
282+
* @param {object} definition User schema definition.
283+
* @returns {object} Frozen compiled schema node.
284+
*/
285+
function compileSchema(definition) {
286+
if (typeof definition !== 'object' || definition === null ||
287+
ArrayIsArray(definition)) {
288+
throw new ERR_INVALID_ARG_TYPE('definition', 'a plain object', definition);
289+
}
290+
return compileSchemaNode(definition, '');
291+
}
292+
293+
module.exports = { compileSchema };

0 commit comments

Comments
 (0)