schiva 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/package.json +45 -0
- package/src/closed-schema.js +11 -0
- package/src/compile.js +742 -0
- package/src/deep-equal.js +59 -0
- package/src/index.js +13 -0
- package/src/json-schema-refs.js +170 -0
- package/src/json-schema.js +420 -0
- package/src/schema.js +230 -0
- package/src/types/all-of.js +62 -0
- package/src/types/any-of.js +63 -0
- package/src/types/any.js +32 -0
- package/src/types/array-of.js +137 -0
- package/src/types/boolean.js +43 -0
- package/src/types/code-point-length.js +32 -0
- package/src/types/conditional.js +48 -0
- package/src/types/enum.js +51 -0
- package/src/types/float.js +84 -0
- package/src/types/has-duplicates.js +21 -0
- package/src/types/index.js +39 -0
- package/src/types/integer.js +45 -0
- package/src/types/never.js +36 -0
- package/src/types/not.js +49 -0
- package/src/types/obj.js +58 -0
- package/src/types/one-of.js +69 -0
- package/src/types/ref.js +49 -0
- package/src/types/string.js +86 -0
- package/src/types/validate-type.js +98 -0
- package/src/types/values.js +46 -0
- package/src/types/when.js +63 -0
package/src/compile.js
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
const { deepEqual } = require('./deep-equal');
|
|
2
|
+
const { Schema } = require('./schema');
|
|
3
|
+
const { ClosedSchema } = require('./closed-schema');
|
|
4
|
+
const {
|
|
5
|
+
AllOfType,
|
|
6
|
+
AnyOfType,
|
|
7
|
+
AnyType,
|
|
8
|
+
ArrayOfType,
|
|
9
|
+
BooleanType,
|
|
10
|
+
ConditionalType,
|
|
11
|
+
EnumType,
|
|
12
|
+
FloatType,
|
|
13
|
+
IntegerType,
|
|
14
|
+
NeverType,
|
|
15
|
+
NotType,
|
|
16
|
+
ObjType,
|
|
17
|
+
OneOfType,
|
|
18
|
+
RefType,
|
|
19
|
+
StringType,
|
|
20
|
+
ValuesType,
|
|
21
|
+
WhenType,
|
|
22
|
+
hasErrors,
|
|
23
|
+
toErrors,
|
|
24
|
+
} = require('./types');
|
|
25
|
+
const { codePointLength } = require('./types/code-point-length');
|
|
26
|
+
const { hasDuplicates } = require('./types/has-duplicates');
|
|
27
|
+
|
|
28
|
+
// Compiles a type tree into a single generated function, like ajv does, so validating a value runs inline code
|
|
29
|
+
// instead of one isValid()/errors() call per node. There are three modes:
|
|
30
|
+
// - check: returns true or false, like isValid().
|
|
31
|
+
// - first: returns the first error message or undefined, which is toErrors(type.errors(value))[0].
|
|
32
|
+
// - all: returns every error message, which is toErrors(type.errors(value)).
|
|
33
|
+
// Messages are built from the same text, in the same order, as the interpreted validate() of each type.
|
|
34
|
+
//
|
|
35
|
+
// The generated code snapshots the tree: changes made to the types after compiling are not seen.
|
|
36
|
+
// Schema keys and message texts are embedded with JSON.stringify, finite numbers as literals; any other value is
|
|
37
|
+
// passed in through the `c` array. Types that are not built-in (custom classes and subclasses) run their own
|
|
38
|
+
// isValid()/errors().
|
|
39
|
+
|
|
40
|
+
const MAX_INLINE_KEYS = 8;
|
|
41
|
+
|
|
42
|
+
// A check this long (in characters of generated code) goes into its own function instead of being inlined.
|
|
43
|
+
const MAX_INLINE_CODE = 4000;
|
|
44
|
+
|
|
45
|
+
// Checks for a value that is neither undefined nor null, like isJsonType().
|
|
46
|
+
const JSON_TYPE_CHECKS = {
|
|
47
|
+
object: (v) => `typeof ${v} === 'object' && !Array.isArray(${v})`,
|
|
48
|
+
array: (v) => `Array.isArray(${v})`,
|
|
49
|
+
string: (v) => `typeof ${v} === 'string'`,
|
|
50
|
+
number: (v) => `typeof ${v} === 'number'`,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
54
|
+
|
|
55
|
+
function ownerOf(obj, name) {
|
|
56
|
+
let proto = obj;
|
|
57
|
+
while (proto && !hasOwn(proto, name)) {
|
|
58
|
+
proto = Object.getPrototypeOf(proto);
|
|
59
|
+
}
|
|
60
|
+
return proto;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A subclass that overrides validate() but inherits isValid() must be checked through validate().
|
|
64
|
+
function checksThroughValidate(type) {
|
|
65
|
+
const validateOwner = ownerOf(type, 'validate');
|
|
66
|
+
const isValidOwner = ownerOf(type, 'isValid');
|
|
67
|
+
return validateOwner !== isValidOwner && Object.prototype.isPrototypeOf.call(isValidOwner, validateOwner);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A `path` is a JS expression giving the fieldName passed to validate(): 'undefined' at the root, 'p' in the function
|
|
71
|
+
// of a reference target (where it can be undefined), and otherwise an expression that gives a string. Paths are only
|
|
72
|
+
// evaluated to build messages.
|
|
73
|
+
|
|
74
|
+
// Name of a node in its messages, as validate() defaults fieldName to 'Value'.
|
|
75
|
+
function valuePath(path) {
|
|
76
|
+
if (path === 'undefined') {
|
|
77
|
+
return '"Value"';
|
|
78
|
+
}
|
|
79
|
+
return path === 'p' ? '(p === undefined ? "Value" : p)' : path;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Name of a Schema in its messages, as Schema uses fieldName || 'Value'.
|
|
83
|
+
function schemaName(path) {
|
|
84
|
+
return path === 'undefined' ? '"Value"' : `(${path} || "Value")`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Name of a Schema key, as Schema uses fieldName ? `${fieldName}.${key}` : key. `key` is a JS expression.
|
|
88
|
+
function keyPath(path, key) {
|
|
89
|
+
return path === 'undefined' ? key : `J(${path}, ${key})`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Same text as ValuesType.validate().
|
|
93
|
+
function formatValue(value) {
|
|
94
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function valuesMessage(values) {
|
|
98
|
+
if (values.length === 1) {
|
|
99
|
+
return ` must be equal to ${formatValue(values[0])}`;
|
|
100
|
+
}
|
|
101
|
+
return ` must be one of: ${values.map(formatValue).join(', ')}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const OBJECT_METHODS = ['constructor', 'valueOf', 'toString'];
|
|
105
|
+
|
|
106
|
+
// Values made of plain objects, arrays and primitives, for which deepEqual() can be written out as code.
|
|
107
|
+
function isPlainValue(value) {
|
|
108
|
+
if (value === null || typeof value !== 'object') {
|
|
109
|
+
return typeof value !== 'bigint' && typeof value !== 'symbol' && typeof value !== 'function';
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
return (
|
|
113
|
+
Object.getPrototypeOf(value) === Array.prototype &&
|
|
114
|
+
Object.keys(value).length === value.length &&
|
|
115
|
+
value.every(isPlainValue)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return (
|
|
119
|
+
Object.getPrototypeOf(value) === Object.prototype &&
|
|
120
|
+
!OBJECT_METHODS.some((key) => hasOwn(value, key)) &&
|
|
121
|
+
Object.values(value).every(isPlainValue)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Expression for deepEqual(value, x), where `value` is a plain value, following the same steps: identity or NaN for
|
|
126
|
+
// primitives; for objects the same constructor, then the same length and elements (arrays) or the same key count
|
|
127
|
+
// and own keys (objects).
|
|
128
|
+
function equalsCode(value, x) {
|
|
129
|
+
if (value === null || typeof value !== 'object') {
|
|
130
|
+
if (typeof value === 'number' && Number.isNaN(value)) {
|
|
131
|
+
return `(typeof ${x} === 'number' && ${x} !== ${x})`;
|
|
132
|
+
}
|
|
133
|
+
if (typeof value === 'number') {
|
|
134
|
+
return `${x} === ${Number.isFinite(value) ? `(${value})` : `${value > 0 ? '' : '-'}Infinity`}`;
|
|
135
|
+
}
|
|
136
|
+
return `${x} === ${value === undefined ? 'undefined' : JSON.stringify(value)}`;
|
|
137
|
+
}
|
|
138
|
+
const isObject = `typeof ${x} === 'object' && ${x} !== null`;
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
const items = value.map((item, i) => equalsCode(item, `${x}[${i}]`));
|
|
141
|
+
return `(${[isObject, `${x}.constructor === Array`, `${x}.length === ${value.length}`, ...items].join(' && ')})`;
|
|
142
|
+
}
|
|
143
|
+
const keys = Object.keys(value);
|
|
144
|
+
const entries = keys.map((key) => {
|
|
145
|
+
const literal = JSON.stringify(key);
|
|
146
|
+
return `H.call(${x}, ${literal}) && ${equalsCode(value[key], `${x}[${literal}]`)}`;
|
|
147
|
+
});
|
|
148
|
+
return `(${[isObject, `${x}.constructor === Object`, `Object.keys(${x}).length === ${keys.length}`, ...entries].join(
|
|
149
|
+
' && '
|
|
150
|
+
)})`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Helpers for types that are not built-in, which run their own errors().
|
|
154
|
+
function firstError(type, value, fieldName) {
|
|
155
|
+
return toErrors(type.errors(value, fieldName))[0];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function pushErrors(out, type, value, fieldName) {
|
|
159
|
+
const errors = toErrors(type.errors(value, fieldName));
|
|
160
|
+
for (let i = 0; i < errors.length; i += 1) {
|
|
161
|
+
out.push(errors[i]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
class Generator {
|
|
166
|
+
constructor(mode) {
|
|
167
|
+
this.mode = mode;
|
|
168
|
+
this.constants = [];
|
|
169
|
+
this.nodes = [];
|
|
170
|
+
this.functions = [];
|
|
171
|
+
this.checkFunctions = new Map();
|
|
172
|
+
// Per mode, the function validating each reference target.
|
|
173
|
+
this.refFunctions = { check: new Map(), first: new Map(), all: new Map() };
|
|
174
|
+
this.count = 0;
|
|
175
|
+
// Nodes being generated, to fall back to their own isValid()/errors() if a tree refers to itself.
|
|
176
|
+
this.visiting = new Set();
|
|
177
|
+
// Statement for a failed check in 'check' mode: a return, or a break out of an inlined check.
|
|
178
|
+
this.fail = 'return false;';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
name(prefix) {
|
|
182
|
+
this.count += 1;
|
|
183
|
+
return `${prefix}${this.count}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
constant(value) {
|
|
187
|
+
this.constants.push(value);
|
|
188
|
+
return `c[${this.constants.length - 1}]`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
number(value) {
|
|
192
|
+
return typeof value === 'number' && Number.isFinite(value) ? `(${value})` : this.constant(value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
node(type) {
|
|
196
|
+
let index = this.nodes.indexOf(type);
|
|
197
|
+
if (index === -1) {
|
|
198
|
+
this.nodes.push(type);
|
|
199
|
+
index = this.nodes.length - 1;
|
|
200
|
+
}
|
|
201
|
+
return `n[${index}]`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Statement for a failed check; `message` gives the message expression and is only called when needed.
|
|
205
|
+
emit(message) {
|
|
206
|
+
if (this.mode === 'check') {
|
|
207
|
+
return this.fail;
|
|
208
|
+
}
|
|
209
|
+
return this.mode === 'first' ? `return ${message()};` : `out.push(${message()});`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Checks [condition, message, pre] run in order until one fails; `rest` runs when none fails. The optional `pre`
|
|
213
|
+
// statements run just before their condition, only when the previous checks passed.
|
|
214
|
+
chain(checks, rest = '') {
|
|
215
|
+
let code = '';
|
|
216
|
+
for (let i = 0; i < checks.length; i += 1) {
|
|
217
|
+
const [condition, message, pre] = checks[i];
|
|
218
|
+
if (pre) {
|
|
219
|
+
const remaining = this.chain([[condition, message], ...checks.slice(i + 1)], rest);
|
|
220
|
+
return `${code}${i ? 'else ' : ''}{\n${pre}${remaining}}\n`;
|
|
221
|
+
}
|
|
222
|
+
code += `${i ? 'else ' : ''}if (${condition}) { ${this.emit(message)} }\n`;
|
|
223
|
+
}
|
|
224
|
+
if (!rest) {
|
|
225
|
+
return code;
|
|
226
|
+
}
|
|
227
|
+
return checks.length ? `${code}else {\n${rest}}\n` : rest;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Generates a separate function in 'check' mode, where a failure returns false.
|
|
231
|
+
inFunction(generate) {
|
|
232
|
+
const { mode, fail, visiting } = this;
|
|
233
|
+
this.mode = 'check';
|
|
234
|
+
this.fail = 'return false;';
|
|
235
|
+
this.visiting = new Set();
|
|
236
|
+
const body = generate();
|
|
237
|
+
this.mode = mode;
|
|
238
|
+
this.fail = fail;
|
|
239
|
+
this.visiting = visiting;
|
|
240
|
+
return body;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Name of a boolean function checking `type`, shared by every use of the same node.
|
|
244
|
+
checkFunction(type) {
|
|
245
|
+
if (!this.checkFunctions.has(type)) {
|
|
246
|
+
const name = this.name('check');
|
|
247
|
+
this.checkFunctions.set(type, name);
|
|
248
|
+
const body = this.inFunction(() => this.generate(type, 'x', 'undefined'));
|
|
249
|
+
this.functions.push(`function ${name}(x) {\n${body}return true;\n}\n`);
|
|
250
|
+
}
|
|
251
|
+
return this.checkFunctions.get(type);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Code that runs `onPass` when the value in `v` satisfies `type`. The check is inlined in a labelled block that a
|
|
255
|
+
// failure breaks out of, which avoids a function call; a long one goes into a function instead.
|
|
256
|
+
inlineCheck(type, v, onPass) {
|
|
257
|
+
const { mode, fail } = this;
|
|
258
|
+
const label = this.name('L');
|
|
259
|
+
this.mode = 'check';
|
|
260
|
+
this.fail = `break ${label};`;
|
|
261
|
+
const body = this.generate(type, v, 'undefined');
|
|
262
|
+
this.mode = mode;
|
|
263
|
+
this.fail = fail;
|
|
264
|
+
if (body.length > MAX_INLINE_CODE) {
|
|
265
|
+
return `if (${this.checkFunction(type)}(${v})) { ${onPass} }\n`;
|
|
266
|
+
}
|
|
267
|
+
return `${label}: {\n${body}${onPass}\n}\n`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Name of the function validating a reference target in the current mode. It takes the value and, to build
|
|
271
|
+
// messages, the field name (and the error list in 'all' mode), so recursive schemas call it again.
|
|
272
|
+
refFunction(target) {
|
|
273
|
+
const functions = this.refFunctions[this.mode];
|
|
274
|
+
if (!functions.has(target)) {
|
|
275
|
+
const name = this.name(`ref_${this.mode}`);
|
|
276
|
+
functions.set(target, name);
|
|
277
|
+
const params = { check: 'x', first: 'x, p', all: 'x, p, out' }[this.mode];
|
|
278
|
+
const end = { check: 'return true;', first: 'return undefined;', all: '' }[this.mode];
|
|
279
|
+
// The target may be an outer node being generated: its function is generated on its own.
|
|
280
|
+
const { visiting, fail } = this;
|
|
281
|
+
this.visiting = new Set();
|
|
282
|
+
this.fail = 'return false;';
|
|
283
|
+
const body = this.generate(target, 'x', this.mode === 'check' ? 'undefined' : 'p');
|
|
284
|
+
this.visiting = visiting;
|
|
285
|
+
this.fail = fail;
|
|
286
|
+
this.functions.push(`function ${name}(${params}) {\n${body}${end}\n}\n`);
|
|
287
|
+
}
|
|
288
|
+
return functions.get(target);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Like RefType: undefined is checked here, any other value by the target.
|
|
292
|
+
ref(type, v, path) {
|
|
293
|
+
const onUndefined = type.isMandatory ? this.emit(() => `${valuePath(path)} + " is mandatory"`) : '';
|
|
294
|
+
const fn = this.refFunction(type.getTarget());
|
|
295
|
+
let call = `if (!${fn}(${v})) { ${this.fail} }\n`;
|
|
296
|
+
if (this.mode === 'first') {
|
|
297
|
+
const e = this.name('e');
|
|
298
|
+
call = `const ${e} = ${fn}(${v}, ${path});\nif (${e} !== undefined) { return ${e}; }\n`;
|
|
299
|
+
} else if (this.mode === 'all') {
|
|
300
|
+
call = `${fn}(${v}, ${path}, out);\n`;
|
|
301
|
+
}
|
|
302
|
+
return `if (${v} === undefined) { ${onUndefined} } else {\n${call}}\n`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Code validating the value held in variable `v` against `type`, with `path` giving its field name. When `known`
|
|
306
|
+
// names a JSON type, the value is known to be of that type (so neither undefined nor null): presence and that type
|
|
307
|
+
// are not checked again. Types that accept every value give no code.
|
|
308
|
+
generate(type, v, path, known = undefined) {
|
|
309
|
+
if (type.constructor === RefType) {
|
|
310
|
+
return this.ref(type, v, path);
|
|
311
|
+
}
|
|
312
|
+
if (this.visiting.has(type)) {
|
|
313
|
+
return this.custom(type, v, path);
|
|
314
|
+
}
|
|
315
|
+
this.visiting.add(type);
|
|
316
|
+
const isSchema = type.constructor === Schema || type.constructor === ClosedSchema;
|
|
317
|
+
const name = isSchema ? schemaName(path) : valuePath(path);
|
|
318
|
+
const body = this.body(type, v, path, name, known);
|
|
319
|
+
this.visiting.delete(type);
|
|
320
|
+
if (body === undefined) {
|
|
321
|
+
return this.custom(type, v, path);
|
|
322
|
+
}
|
|
323
|
+
const checks = this.chain(body.checks, body.rest);
|
|
324
|
+
if (known) {
|
|
325
|
+
return checks;
|
|
326
|
+
}
|
|
327
|
+
const text = (suffix) => () => `${name} + ${JSON.stringify(suffix)}`;
|
|
328
|
+
const onUndefined = type.isMandatory ? this.emit(text(' is mandatory')) : '';
|
|
329
|
+
const onNull = type.isNullable ? '' : this.emit(text(' cannot be null'));
|
|
330
|
+
if (!onUndefined && !onNull) {
|
|
331
|
+
return checks ? `if (${v} !== undefined && ${v} !== null) {\n${checks}}\n` : '';
|
|
332
|
+
}
|
|
333
|
+
return `if (${v} === undefined) { ${onUndefined} } else if (${v} === null) { ${onNull} } else {\n${checks}}\n`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
custom(type, v, path) {
|
|
337
|
+
const node = this.node(type);
|
|
338
|
+
const invalid = checksThroughValidate(type)
|
|
339
|
+
? `${this.constant(hasErrors)}(${node}.validate(${v}))`
|
|
340
|
+
: `!${node}.isValid(${v})`;
|
|
341
|
+
let onInvalid = this.fail;
|
|
342
|
+
if (this.mode === 'first') {
|
|
343
|
+
onInvalid = `return r(${node}, ${v}, ${path});`;
|
|
344
|
+
} else if (this.mode === 'all') {
|
|
345
|
+
onInvalid = `a(out, ${node}, ${v}, ${path});`;
|
|
346
|
+
}
|
|
347
|
+
return `if (${invalid}) { ${onInvalid} }\n`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Checks for a value that is neither undefined nor null, as { checks, rest }; undefined when the type is not a
|
|
351
|
+
// built-in one.
|
|
352
|
+
body(type, v, path, name, known) {
|
|
353
|
+
const text = (suffix) => () => `${name} + ${JSON.stringify(suffix)}`;
|
|
354
|
+
switch (type.constructor) {
|
|
355
|
+
case Schema:
|
|
356
|
+
case ClosedSchema:
|
|
357
|
+
return this.schema(type, v, path, name, text, known);
|
|
358
|
+
case ObjType:
|
|
359
|
+
return {
|
|
360
|
+
checks: [[`typeof ${v} !== 'object' || Array.isArray(${v})`, text(' must be an object')]],
|
|
361
|
+
rest: type.schema ? this.generate(type.schema, v, name) : '',
|
|
362
|
+
};
|
|
363
|
+
case ArrayOfType:
|
|
364
|
+
return this.arrayOf(type, v, name, text, known);
|
|
365
|
+
case AllOfType:
|
|
366
|
+
return { checks: [], rest: this.allOf(type, v, name) };
|
|
367
|
+
case ConditionalType: {
|
|
368
|
+
// Only the chosen branch is checked and reported, like ConditionalType.validate().
|
|
369
|
+
const branch = (branchType) => (branchType ? this.generate(branchType, v, name) : '');
|
|
370
|
+
const ok = this.name('ok');
|
|
371
|
+
const rest = `let ${ok} = false;\n${this.inlineCheck(type.ifType, v, `${ok} = true;`)}if (${ok}) {\n${branch(
|
|
372
|
+
type.thenType
|
|
373
|
+
)}} else {\n${branch(type.elseType)}}\n`;
|
|
374
|
+
return { checks: [], rest };
|
|
375
|
+
}
|
|
376
|
+
case AnyOfType:
|
|
377
|
+
return { checks: [], rest: this.anyOf(type, v, name) };
|
|
378
|
+
case OneOfType:
|
|
379
|
+
return this.oneOf(type, v, name, text);
|
|
380
|
+
case NotType: {
|
|
381
|
+
const ok = this.name('ok');
|
|
382
|
+
const pre = `let ${ok} = false;\n${this.inlineCheck(type.type, v, `${ok} = true;`)}`;
|
|
383
|
+
return { checks: [[ok, text(' must not match the excluded schema'), pre]] };
|
|
384
|
+
}
|
|
385
|
+
case StringType:
|
|
386
|
+
return { checks: this.string(type, v, text, known) };
|
|
387
|
+
case EnumType:
|
|
388
|
+
return {
|
|
389
|
+
checks: [
|
|
390
|
+
...this.string(type, v, text, known),
|
|
391
|
+
[`!${this.constant(new Set(type.options))}.has(${v})`, text(` must be one of: ${type.options.join(', ')}`)],
|
|
392
|
+
],
|
|
393
|
+
};
|
|
394
|
+
case FloatType:
|
|
395
|
+
return { checks: this.float(type, v, text) };
|
|
396
|
+
case IntegerType:
|
|
397
|
+
return { checks: [...this.float(type, v, text), [`!Number.isInteger(${v})`, text(' must be an integer')]] };
|
|
398
|
+
case BooleanType:
|
|
399
|
+
return { checks: [[`typeof ${v} !== 'boolean'`, text(' must be a boolean')]] };
|
|
400
|
+
case AnyType:
|
|
401
|
+
return { checks: [] };
|
|
402
|
+
case NeverType:
|
|
403
|
+
return { checks: [['true', text(' is not allowed')]] };
|
|
404
|
+
case ValuesType:
|
|
405
|
+
return { checks: [[this.notOneOf(type.values, v), text(valuesMessage(type.values))]] };
|
|
406
|
+
case WhenType:
|
|
407
|
+
// The field name goes through unchanged, like WhenType.validate().
|
|
408
|
+
return {
|
|
409
|
+
checks: [],
|
|
410
|
+
rest: `if (${JSON_TYPE_CHECKS[type.jsonType](v)}) {\n${this.generate(type.type, v, path, type.jsonType)}}\n`,
|
|
411
|
+
};
|
|
412
|
+
default:
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
string(type, v, text, known) {
|
|
418
|
+
const checks = known === 'string' ? [] : [[`typeof ${v} !== 'string'`, text(' must be a string')]];
|
|
419
|
+
// Code points are only counted near the limit, like hasFewerCodePoints() and hasMoreCodePoints().
|
|
420
|
+
const count = () => `${this.constant(codePointLength)}(${v})`;
|
|
421
|
+
if (type.min !== undefined) {
|
|
422
|
+
const allowEmpty = type.allowEmpty ?? !type.isMandatory;
|
|
423
|
+
const min = this.number(type.min);
|
|
424
|
+
const tooShort = type.countCodePoints
|
|
425
|
+
? `(${v}.length < ${min} || (${v}.length < 2 * ${min} && ${count()} < ${min}))`
|
|
426
|
+
: `${v}.length < ${min}`;
|
|
427
|
+
checks.push([
|
|
428
|
+
allowEmpty ? `${tooShort} && ${v}.length !== 0` : tooShort,
|
|
429
|
+
text(` must be at least ${type.min} characters long`),
|
|
430
|
+
]);
|
|
431
|
+
}
|
|
432
|
+
if (type.max !== undefined) {
|
|
433
|
+
const max = this.number(type.max);
|
|
434
|
+
const tooLong = type.countCodePoints
|
|
435
|
+
? `(${v}.length > 2 * ${max} || (${v}.length > ${max} && ${count()} > ${max}))`
|
|
436
|
+
: `${v}.length > ${max}`;
|
|
437
|
+
checks.push([tooLong, text(` must be at most ${type.max} characters long`)]);
|
|
438
|
+
}
|
|
439
|
+
if (type.pattern) {
|
|
440
|
+
checks.push([`!${this.constant(type.pattern)}.test(${v})`, text(' does not match the required pattern')]);
|
|
441
|
+
}
|
|
442
|
+
return checks;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
float(type, v, text) {
|
|
446
|
+
const limits = [
|
|
447
|
+
[type.min, '<', 'must be at least'],
|
|
448
|
+
[type.max, '>', 'must be at most'],
|
|
449
|
+
[type.exclusiveMin, '<=', 'must be greater than'],
|
|
450
|
+
[type.exclusiveMax, '>=', 'must be less than'],
|
|
451
|
+
];
|
|
452
|
+
const checks = [
|
|
453
|
+
[`!Number.isFinite(${v})`, text(' must be a number')],
|
|
454
|
+
...limits
|
|
455
|
+
.filter(([limit]) => limit !== undefined)
|
|
456
|
+
.map(([limit, operator, message]) => [`${v} ${operator} ${this.number(limit)}`, text(` ${message} ${limit}`)]),
|
|
457
|
+
];
|
|
458
|
+
if (type.multipleOf !== undefined) {
|
|
459
|
+
checks.push([
|
|
460
|
+
`!Number.isInteger(${v} / ${this.number(type.multipleOf)})`,
|
|
461
|
+
text(` must be a multiple of ${type.multipleOf}`),
|
|
462
|
+
]);
|
|
463
|
+
}
|
|
464
|
+
return checks;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Like ValuesType: `v` (neither undefined nor null) is deep-equal to none of the values. Plain values are compared
|
|
468
|
+
// with code written for them; others with deepEqual(), only for objects as it is false for anything else.
|
|
469
|
+
notOneOf(values, v) {
|
|
470
|
+
const matches = [];
|
|
471
|
+
values.forEach((value) => {
|
|
472
|
+
if (value === undefined || value === null) {
|
|
473
|
+
// Never equal to a value that is neither undefined nor null.
|
|
474
|
+
} else if (isPlainValue(value)) {
|
|
475
|
+
matches.push(equalsCode(value, v));
|
|
476
|
+
} else if (typeof value === 'object') {
|
|
477
|
+
matches.push(`(typeof ${v} === 'object' && ${this.constant(deepEqual)}(${this.constant(value)}, ${v}))`);
|
|
478
|
+
} else {
|
|
479
|
+
matches.push(`${v} === ${this.constant(value)}`);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
return matches.length ? `!(${matches.join(' || ')})` : 'true';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Errors of the first type that fails, like AllOfType.validate().
|
|
486
|
+
allOf(type, v, name) {
|
|
487
|
+
if (this.mode !== 'all') {
|
|
488
|
+
return type.types.map((item) => this.generate(item, v, name)).join('');
|
|
489
|
+
}
|
|
490
|
+
const failed = this.name('failed');
|
|
491
|
+
let code = `let ${failed} = false;\n`;
|
|
492
|
+
type.types.forEach((item) => {
|
|
493
|
+
const ok = this.name('ok');
|
|
494
|
+
code += `if (!${failed}) {\nlet ${ok} = false;\n${this.inlineCheck(item, v, `${ok} = true;`)}`;
|
|
495
|
+
code += `if (!${ok}) {\n${failed} = true;\n${this.generate(item, v, name)}}\n}\n`;
|
|
496
|
+
});
|
|
497
|
+
return code;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Code for a value that no alternative accepts: the errors of every alternative, like AnyOfType.validate().
|
|
501
|
+
noneMatches(types, v, name) {
|
|
502
|
+
if (this.mode === 'check') {
|
|
503
|
+
return this.fail;
|
|
504
|
+
}
|
|
505
|
+
if (this.mode === 'first') {
|
|
506
|
+
return this.generate(types[0], v, name);
|
|
507
|
+
}
|
|
508
|
+
return types.map((item) => this.generate(item, v, name)).join('');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
anyOf(type, v, name) {
|
|
512
|
+
if (!type.types || type.types.length === 0) {
|
|
513
|
+
return '';
|
|
514
|
+
}
|
|
515
|
+
const ok = this.name('ok');
|
|
516
|
+
let code = `let ${ok} = false;\n`;
|
|
517
|
+
type.types.forEach((item, i) => {
|
|
518
|
+
const check = this.inlineCheck(item, v, `${ok} = true;`);
|
|
519
|
+
code += i ? `if (!${ok}) {\n${check}}\n` : check;
|
|
520
|
+
});
|
|
521
|
+
return `${code}if (!${ok}) {\n${this.noneMatches(type.types, v, name)}}\n`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Counts up to two matching alternatives, like OneOfType.countMatches().
|
|
525
|
+
oneOf(type, v, name, text) {
|
|
526
|
+
if (type.types.length === 0) {
|
|
527
|
+
return { checks: [['true', text(' must match exactly one schema, but matches none')]] };
|
|
528
|
+
}
|
|
529
|
+
const m = this.name('m');
|
|
530
|
+
let rest = `let ${m} = 0;\n`;
|
|
531
|
+
type.types.forEach((item, i) => {
|
|
532
|
+
const check = this.inlineCheck(item, v, `${m} += 1;`);
|
|
533
|
+
rest += i > 1 ? `if (${m} < 2) {\n${check}}\n` : check;
|
|
534
|
+
});
|
|
535
|
+
const more = this.emit(text(' must match exactly one schema, but matches more than one'));
|
|
536
|
+
if (this.mode === 'check') {
|
|
537
|
+
rest += `if (${m} !== 1) { ${this.fail} }\n`;
|
|
538
|
+
} else {
|
|
539
|
+
rest += `if (${m} === 0) {\n${this.noneMatches(type.types, v, name)}} else if (${m} > 1) { ${more} }\n`;
|
|
540
|
+
}
|
|
541
|
+
return { checks: [], rest };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
arrayOf(type, v, name, text, known) {
|
|
545
|
+
const checks = known === 'array' ? [] : [[`!Array.isArray(${v})`, text(' must be an array')]];
|
|
546
|
+
if (type.min !== undefined) {
|
|
547
|
+
checks.push([`${v}.length < ${this.number(type.min)}`, text(` must have at least ${type.min} elements`)]);
|
|
548
|
+
}
|
|
549
|
+
if (type.max !== undefined) {
|
|
550
|
+
checks.push([`${v}.length > ${this.number(type.max)}`, text(` must have at most ${type.max} elements`)]);
|
|
551
|
+
}
|
|
552
|
+
if (type.unique) {
|
|
553
|
+
checks.push([`${this.constant(hasDuplicates)}(${v})`, text(' must not have duplicate elements')]);
|
|
554
|
+
}
|
|
555
|
+
if (type.contains) {
|
|
556
|
+
// Runs only when the checks before it pass, like ArrayOfType.hasMatch().
|
|
557
|
+
const found = this.name('found');
|
|
558
|
+
const i = this.name('i');
|
|
559
|
+
const x = this.name('v');
|
|
560
|
+
const pre = `let ${found} = false;\nfor (let ${i} = 0; ${i} < ${v}.length && !${found}; ${i} += 1) {\nconst ${x} = ${v}[${i}];\n${this.inlineCheck(
|
|
561
|
+
type.contains,
|
|
562
|
+
x,
|
|
563
|
+
`${found} = true;`
|
|
564
|
+
)}}\n`;
|
|
565
|
+
checks.push([`!${found}`, text(' must contain at least one matching element'), pre]);
|
|
566
|
+
}
|
|
567
|
+
let rest = '';
|
|
568
|
+
if (Array.isArray(type.type)) {
|
|
569
|
+
type.type.forEach((item, i) => {
|
|
570
|
+
const x = this.name('v');
|
|
571
|
+
rest += `const ${x} = ${v}[${i}];\n${this.generate(item, x, `(${name} + "[${i}]")`)}`;
|
|
572
|
+
});
|
|
573
|
+
if (type.additionalType) {
|
|
574
|
+
const i = this.name('i');
|
|
575
|
+
const x = this.name('v');
|
|
576
|
+
rest += `for (let ${i} = ${type.type.length}; ${i} < ${v}.length; ${i} += 1) {\nconst ${x} = ${v}[${i}];\n`;
|
|
577
|
+
rest += `${this.generate(type.additionalType, x, `(${name} + "[" + ${i} + "]")`)}}\n`;
|
|
578
|
+
}
|
|
579
|
+
} else if (type.type) {
|
|
580
|
+
const i = this.name('i');
|
|
581
|
+
const x = this.name('v');
|
|
582
|
+
rest += `for (let ${i} = 0; ${i} < ${v}.length; ${i} += 1) {\nconst ${x} = ${v}[${i}];\n`;
|
|
583
|
+
rest += `${this.generate(type.type, x, `(${name} + "[" + ${i} + "]")`)}}\n`;
|
|
584
|
+
}
|
|
585
|
+
return { checks, rest };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Same order as Schema.errors(): declared keys, then extra keys, then property counts.
|
|
589
|
+
schema(type, v, path, name, text, known) {
|
|
590
|
+
let keysCode = '';
|
|
591
|
+
type.keys.forEach((key) => {
|
|
592
|
+
const x = this.name('v');
|
|
593
|
+
const literal = JSON.stringify(key);
|
|
594
|
+
const code = this.generate(type.schema[key], x, keyPath(path, literal));
|
|
595
|
+
// A key whose type accepts anything is not read.
|
|
596
|
+
if (code) {
|
|
597
|
+
// Own properties only, like Schema's ownValue(). A value read from a plain object is its own unless
|
|
598
|
+
// Object.prototype has the key, so the slower own-property check only runs in that case or for other
|
|
599
|
+
// prototypes. The prototype is read with __proto__, as there: Object.getPrototypeOf() halves the speed.
|
|
600
|
+
keysCode += `let ${x} = ${v}[${literal}];\n`;
|
|
601
|
+
keysCode += `if (${x} !== undefined && (!${v}plain || ${literal} in OP) && !H.call(${v}, ${literal})) { ${x} = undefined; }\n`;
|
|
602
|
+
keysCode += code;
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
let rest = keysCode ? `const ${v}plain = ${v}.__proto__ === OP;\n${keysCode}` : '';
|
|
606
|
+
const checkExtra = !type.isOpen || type.additionalType;
|
|
607
|
+
const countKeys = type.minProperties !== undefined || type.maxProperties !== undefined;
|
|
608
|
+
const { patternTypes } = type;
|
|
609
|
+
if (checkExtra || countKeys || patternTypes.length > 0 || type.propertyNameType) {
|
|
610
|
+
const count = this.name('count');
|
|
611
|
+
const k = this.name('k');
|
|
612
|
+
const keyName = keyPath(path, k);
|
|
613
|
+
rest += `let ${count} = 0;\nfor (const ${k} in ${v}) {\n`;
|
|
614
|
+
rest += `if (!H.call(${v}, ${k})) { continue; }\n${count} += 1;\n`;
|
|
615
|
+
if (type.propertyNameType) {
|
|
616
|
+
rest += this.generate(type.propertyNameType, k, `("Key " + ${keyName})`);
|
|
617
|
+
}
|
|
618
|
+
// Keys matching a pattern satisfy its type and are not extra keys, like Schema.errors().
|
|
619
|
+
const matched = this.name('matched');
|
|
620
|
+
if (patternTypes.length > 0) {
|
|
621
|
+
rest += `let ${matched} = false;\n`;
|
|
622
|
+
patternTypes.forEach(({ pattern, type: patternType }) => {
|
|
623
|
+
const x = this.name('v');
|
|
624
|
+
rest += `if (${this.constant(pattern)}.test(${k})) {\n${matched} = true;\nconst ${x} = ${v}[${k}];\n`;
|
|
625
|
+
rest += `${this.generate(patternType, x, keyName)}}\n`;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
if (checkExtra) {
|
|
629
|
+
const declared =
|
|
630
|
+
type.keys.length <= MAX_INLINE_KEYS
|
|
631
|
+
? type.keys.map((key) => `${k} === ${JSON.stringify(key)}`).join(' || ') || 'false'
|
|
632
|
+
: `${this.constant(type.keySet)}.has(${k})`;
|
|
633
|
+
const accepted = patternTypes.length > 0 ? `${declared} || ${matched}` : declared;
|
|
634
|
+
rest += `if (!(${accepted})) {\n`;
|
|
635
|
+
if (!type.isOpen) {
|
|
636
|
+
rest += this.emit(() => `"Unexpected key: " + ${keyName}`);
|
|
637
|
+
} else {
|
|
638
|
+
const x = this.name('v');
|
|
639
|
+
rest += `const ${x} = ${v}[${k}];\n${this.generate(type.additionalType, x, keyName)}`;
|
|
640
|
+
}
|
|
641
|
+
rest += '}\n';
|
|
642
|
+
}
|
|
643
|
+
rest += '}\n';
|
|
644
|
+
if (type.minProperties !== undefined) {
|
|
645
|
+
const message = text(` must have at least ${type.minProperties} properties`);
|
|
646
|
+
rest += `if (${count} < ${this.number(type.minProperties)}) { ${this.emit(message)} }\n`;
|
|
647
|
+
}
|
|
648
|
+
if (type.maxProperties !== undefined) {
|
|
649
|
+
const message = text(` must have at most ${type.maxProperties} properties`);
|
|
650
|
+
rest += `if (${count} > ${this.number(type.maxProperties)}) { ${this.emit(message)} }\n`;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
rest += this.dependencies(type, v, path);
|
|
654
|
+
return {
|
|
655
|
+
checks:
|
|
656
|
+
known === 'object' ? [] : [[`typeof ${v} !== 'object' || Array.isArray(${v})`, text(' must be an object')]],
|
|
657
|
+
rest,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Like Schema.errors(): a key is present when it is an own property that is not undefined.
|
|
662
|
+
dependencies(type, v, path) {
|
|
663
|
+
const isPresent = (literal) => `(H.call(${v}, ${literal}) && ${v}[${literal}] !== undefined)`;
|
|
664
|
+
return type.dependencies
|
|
665
|
+
.map(({ key, required, type: dependentType }) => {
|
|
666
|
+
const literal = JSON.stringify(key);
|
|
667
|
+
let code;
|
|
668
|
+
if (required) {
|
|
669
|
+
code = required
|
|
670
|
+
.map((property) => {
|
|
671
|
+
const propertyLiteral = JSON.stringify(property);
|
|
672
|
+
const message = () =>
|
|
673
|
+
`${keyPath(path, propertyLiteral)} + " is mandatory when " + ${keyPath(path, literal)} + " is present"`;
|
|
674
|
+
return `if (!${isPresent(propertyLiteral)}) { ${this.emit(message)} }\n`;
|
|
675
|
+
})
|
|
676
|
+
.join('');
|
|
677
|
+
} else {
|
|
678
|
+
code = this.generate(dependentType, v, path);
|
|
679
|
+
}
|
|
680
|
+
return `if (${isPresent(literal)}) {\n${code}}\n`;
|
|
681
|
+
})
|
|
682
|
+
.join('');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
build(type) {
|
|
686
|
+
const main = this.generate(type, 'v0', 'undefined');
|
|
687
|
+
const results = { check: ['', 'true'], first: ['', 'undefined'], all: ['const out = [];\n', 'out'] };
|
|
688
|
+
const [start, end] = results[this.mode];
|
|
689
|
+
const prologue = [
|
|
690
|
+
'"use strict";',
|
|
691
|
+
'const H = Object.prototype.hasOwnProperty;',
|
|
692
|
+
'const OP = Object.prototype;',
|
|
693
|
+
'function J(fieldName, key) { return fieldName ? fieldName + "." + key : key; }',
|
|
694
|
+
'',
|
|
695
|
+
].join('\n');
|
|
696
|
+
const source = `${prologue}${this.functions.join('')}return function validate(v0) {\n${start}${main}return ${end};\n};`;
|
|
697
|
+
// eslint-disable-next-line no-new-func -- code generation is the point: only keys, texts (JSON.stringify) and finite numbers are embedded
|
|
698
|
+
return new Function('c', 'n', 'r', 'a', source)(this.constants, this.nodes, firstError, pushErrors);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Returns a (value) => boolean function equivalent to type.isValid(value).
|
|
703
|
+
function compileIsValid(type) {
|
|
704
|
+
return new Generator('check').build(type);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Returns a (value) => message | undefined function giving the first error of type.validate(value).
|
|
708
|
+
function compileFirstError(type) {
|
|
709
|
+
return new Generator('first').build(type);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Returns a (value) => messages function equivalent to toErrors(type.errors(value)) for invalid values, and giving
|
|
713
|
+
// an empty array for valid ones.
|
|
714
|
+
function compileErrors(type) {
|
|
715
|
+
return new Generator('all').build(type);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Returns a (value) => errors function: every error message by default (empty when valid), or with
|
|
719
|
+
// allErrors: false only the first one, which stops at the first failing check.
|
|
720
|
+
// With errors: false it returns a (value) => boolean function instead, which builds no messages at all.
|
|
721
|
+
function compileType(type, options = {}) {
|
|
722
|
+
const { allErrors = true, errors = true } = options;
|
|
723
|
+
if (!errors) {
|
|
724
|
+
return compileIsValid(type);
|
|
725
|
+
}
|
|
726
|
+
if (allErrors) {
|
|
727
|
+
// One pass: checking validity first would walk invalid values twice.
|
|
728
|
+
return compileErrors(type);
|
|
729
|
+
}
|
|
730
|
+
const getFirstError = compileFirstError(type);
|
|
731
|
+
return (value) => {
|
|
732
|
+
const error = getFirstError(value);
|
|
733
|
+
return error === undefined ? [] : [error];
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
module.exports = {
|
|
738
|
+
compileErrors,
|
|
739
|
+
compileFirstError,
|
|
740
|
+
compileIsValid,
|
|
741
|
+
compileType,
|
|
742
|
+
};
|