ata-validator 0.4.1 → 0.4.3
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/index.js +161 -21
- package/lib/js-compiler.js +658 -12
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/src/ata.cpp +223 -3
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const native = require("node-gyp-build")(__dirname);
|
|
2
|
-
const { compileToJS, compileToJSCodegen } = require("./lib/js-compiler");
|
|
2
|
+
const { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined } = require("./lib/js-compiler");
|
|
3
3
|
|
|
4
4
|
// Extract default values from a schema tree. Returns a function that applies
|
|
5
5
|
// defaults to an object in-place (mutates), or null if no defaults exist.
|
|
@@ -49,6 +49,131 @@ function collectDefaults(schema, actions, path) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Build a function that coerces property values to match schema types in-place.
|
|
53
|
+
// Handles string→number, string→integer, string→boolean, number→string, boolean→string.
|
|
54
|
+
function buildCoercer(schema) {
|
|
55
|
+
if (typeof schema !== 'object' || schema === null) return null;
|
|
56
|
+
const actions = [];
|
|
57
|
+
collectCoercions(schema, actions);
|
|
58
|
+
if (actions.length === 0) return null;
|
|
59
|
+
return (data) => {
|
|
60
|
+
for (let i = 0; i < actions.length; i++) actions[i](data);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectCoercions(schema, actions, path) {
|
|
65
|
+
if (typeof schema !== 'object' || schema === null) return;
|
|
66
|
+
const props = schema.properties;
|
|
67
|
+
if (!props) return;
|
|
68
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
69
|
+
if (!prop || typeof prop !== 'object' || !prop.type) continue;
|
|
70
|
+
const targetType = Array.isArray(prop.type) ? null : prop.type;
|
|
71
|
+
if (!targetType) continue;
|
|
72
|
+
|
|
73
|
+
const coerce = buildSingleCoercion(targetType);
|
|
74
|
+
if (!coerce) continue;
|
|
75
|
+
|
|
76
|
+
if (!path) {
|
|
77
|
+
actions.push((data) => {
|
|
78
|
+
if (typeof data === 'object' && data !== null && key in data) {
|
|
79
|
+
const coerced = coerce(data[key]);
|
|
80
|
+
if (coerced !== undefined) data[key] = coerced;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
const parentPath = path;
|
|
85
|
+
actions.push((data) => {
|
|
86
|
+
let target = data;
|
|
87
|
+
for (let j = 0; j < parentPath.length; j++) {
|
|
88
|
+
if (typeof target !== 'object' || target === null) return;
|
|
89
|
+
target = target[parentPath[j]];
|
|
90
|
+
}
|
|
91
|
+
if (typeof target === 'object' && target !== null && key in target) {
|
|
92
|
+
const coerced = coerce(target[key]);
|
|
93
|
+
if (coerced !== undefined) target[key] = coerced;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Recurse into nested object properties
|
|
99
|
+
if (prop.properties) {
|
|
100
|
+
collectCoercions(prop, actions, (path || []).concat(key));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildSingleCoercion(targetType) {
|
|
106
|
+
switch (targetType) {
|
|
107
|
+
case 'number': return (v) => {
|
|
108
|
+
if (typeof v === 'string') { const n = Number(v); if (v !== '' && !isNaN(n)) return n; }
|
|
109
|
+
if (typeof v === 'boolean') return v ? 1 : 0;
|
|
110
|
+
};
|
|
111
|
+
case 'integer': return (v) => {
|
|
112
|
+
if (typeof v === 'string') { const n = Number(v); if (v !== '' && Number.isInteger(n)) return n; }
|
|
113
|
+
if (typeof v === 'boolean') return v ? 1 : 0;
|
|
114
|
+
};
|
|
115
|
+
case 'string': return (v) => {
|
|
116
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
117
|
+
};
|
|
118
|
+
case 'boolean': return (v) => {
|
|
119
|
+
if (v === 'true' || v === '1') return true;
|
|
120
|
+
if (v === 'false' || v === '0') return false;
|
|
121
|
+
};
|
|
122
|
+
default: return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Build a function that removes properties not defined in schema.properties.
|
|
127
|
+
// Walks nested objects recursively.
|
|
128
|
+
function buildRemover(schema) {
|
|
129
|
+
if (typeof schema !== 'object' || schema === null) return null;
|
|
130
|
+
const actions = [];
|
|
131
|
+
collectRemovals(schema, actions);
|
|
132
|
+
if (actions.length === 0) return null;
|
|
133
|
+
return (data) => {
|
|
134
|
+
for (let i = 0; i < actions.length; i++) actions[i](data);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function collectRemovals(schema, actions, path) {
|
|
139
|
+
if (typeof schema !== 'object' || schema === null || !schema.properties) return;
|
|
140
|
+
|
|
141
|
+
// If this level has additionalProperties: false, add a removal action
|
|
142
|
+
if (schema.additionalProperties === false) {
|
|
143
|
+
const allowed = new Set(Object.keys(schema.properties));
|
|
144
|
+
if (!path) {
|
|
145
|
+
actions.push((data) => {
|
|
146
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) return;
|
|
147
|
+
const keys = Object.keys(data);
|
|
148
|
+
for (let i = 0; i < keys.length; i++) {
|
|
149
|
+
if (!allowed.has(keys[i])) delete data[keys[i]];
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
const parentPath = path;
|
|
154
|
+
actions.push((data) => {
|
|
155
|
+
let target = data;
|
|
156
|
+
for (let j = 0; j < parentPath.length; j++) {
|
|
157
|
+
if (typeof target !== 'object' || target === null) return;
|
|
158
|
+
target = target[parentPath[j]];
|
|
159
|
+
}
|
|
160
|
+
if (typeof target !== 'object' || target === null || Array.isArray(target)) return;
|
|
161
|
+
const keys = Object.keys(target);
|
|
162
|
+
for (let i = 0; i < keys.length; i++) {
|
|
163
|
+
if (!allowed.has(keys[i])) delete target[keys[i]];
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Always recurse into nested properties (they may have their own additionalProperties: false)
|
|
170
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
171
|
+
if (prop && typeof prop === 'object' && prop.properties) {
|
|
172
|
+
collectRemovals(prop, actions, (path || []).concat(key));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
52
177
|
const SIMDJSON_PADDING = 64;
|
|
53
178
|
const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
54
179
|
|
|
@@ -75,7 +200,8 @@ function createPaddedBuffer(jsonStr) {
|
|
|
75
200
|
}
|
|
76
201
|
|
|
77
202
|
class Validator {
|
|
78
|
-
constructor(schema) {
|
|
203
|
+
constructor(schema, opts) {
|
|
204
|
+
const options = opts || {};
|
|
79
205
|
const schemaStr =
|
|
80
206
|
typeof schema === "string" ? schema : JSON.stringify(schema);
|
|
81
207
|
const compiled = new native.CompiledSchema(schemaStr);
|
|
@@ -88,12 +214,29 @@ class Validator {
|
|
|
88
214
|
const jsFn = process.env.ATA_FORCE_NAPI
|
|
89
215
|
? null
|
|
90
216
|
: (compileToJSCodegen(schemaObj) || compileToJS(schemaObj));
|
|
217
|
+
// Combined validator: single pass, validates + collects errors, all optimized
|
|
218
|
+
const jsCombinedFn = process.env.ATA_FORCE_NAPI
|
|
219
|
+
? null
|
|
220
|
+
: compileToJSCombined(schemaObj, VALID_RESULT);
|
|
221
|
+
// Fallback error-collecting codegen (less optimized, for schemas combined can't handle)
|
|
222
|
+
const jsErrFn = (!jsCombinedFn && !process.env.ATA_FORCE_NAPI)
|
|
223
|
+
? compileToJSCodegenWithErrors(schemaObj)
|
|
224
|
+
: null;
|
|
91
225
|
this._jsFn = jsFn;
|
|
92
226
|
|
|
93
|
-
//
|
|
227
|
+
// Data mutators — applied in-place before validation
|
|
94
228
|
const applyDefaults = buildDefaultsApplier(schemaObj);
|
|
229
|
+
const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
|
|
230
|
+
const applyRemove = options.removeAdditional ? buildRemover(schemaObj) : null;
|
|
95
231
|
this._applyDefaults = applyDefaults;
|
|
96
232
|
|
|
233
|
+
// Combine all mutators into a single pre-validation step
|
|
234
|
+
const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
|
|
235
|
+
const preprocess = mutators.length === 0 ? null
|
|
236
|
+
: mutators.length === 1 ? mutators[0]
|
|
237
|
+
: (data) => { for (let i = 0; i < mutators.length; i++) mutators[i](data); };
|
|
238
|
+
this._preprocess = preprocess;
|
|
239
|
+
|
|
97
240
|
// Closure-capture: avoid `this` property lookup on every call.
|
|
98
241
|
// V8 keeps closure vars in registers — no hidden class traversal.
|
|
99
242
|
const fastSlot = this._fastSlot;
|
|
@@ -107,34 +250,31 @@ class Validator {
|
|
|
107
250
|
const useSimdjsonForLarge = !hasArrayTraversal;
|
|
108
251
|
|
|
109
252
|
if (jsFn) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
253
|
+
// Error handler: combined (optimized) → jsErrFn → NAPI fallback
|
|
254
|
+
const errFn = jsCombinedFn
|
|
255
|
+
? (d) => { try { return jsCombinedFn(d); } catch { return compiled.validate(d); } }
|
|
256
|
+
: jsErrFn
|
|
257
|
+
? (d) => { try { return jsErrFn(d, true); } catch { return compiled.validate(d); } }
|
|
258
|
+
: (d) => compiled.validate(d);
|
|
259
|
+
this.validate = preprocess
|
|
260
|
+
? (data) => { preprocess(data); return jsFn(data) ? VALID_RESULT : errFn(data); }
|
|
261
|
+
: (data) => jsFn(data) ? VALID_RESULT : errFn(data);
|
|
113
262
|
this.isValidObject = jsFn;
|
|
263
|
+
const jsonValidateFn = (obj) => jsFn(obj) ? VALID_RESULT : errFn(obj);
|
|
114
264
|
this.validateJSON = useSimdjsonForLarge
|
|
115
265
|
? (jsonStr) => {
|
|
116
|
-
// Selective schema: large docs use simdjson (skips irrelevant data)
|
|
117
266
|
if (jsonStr.length >= SIMDJSON_THRESHOLD) {
|
|
118
267
|
const buf = Buffer.from(jsonStr);
|
|
119
268
|
if (native.rawFastValidate(fastSlot, buf)) return VALID_RESULT;
|
|
120
269
|
return compiled.validateJSON(jsonStr);
|
|
121
270
|
}
|
|
122
|
-
try {
|
|
123
|
-
|
|
124
|
-
if (jsFn(obj)) return VALID_RESULT;
|
|
125
|
-
} catch (e) {
|
|
126
|
-
if (!(e instanceof SyntaxError)) throw e;
|
|
127
|
-
}
|
|
271
|
+
try { return jsonValidateFn(JSON.parse(jsonStr)); }
|
|
272
|
+
catch (e) { if (!(e instanceof SyntaxError)) throw e; }
|
|
128
273
|
return compiled.validateJSON(jsonStr);
|
|
129
274
|
}
|
|
130
275
|
: (jsonStr) => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const obj = JSON.parse(jsonStr);
|
|
134
|
-
if (jsFn(obj)) return VALID_RESULT;
|
|
135
|
-
} catch (e) {
|
|
136
|
-
if (!(e instanceof SyntaxError)) throw e;
|
|
137
|
-
}
|
|
276
|
+
try { return jsonValidateFn(JSON.parse(jsonStr)); }
|
|
277
|
+
catch (e) { if (!(e instanceof SyntaxError)) throw e; }
|
|
138
278
|
return compiled.validateJSON(jsonStr);
|
|
139
279
|
};
|
|
140
280
|
this.isValidJSON = useSimdjsonForLarge
|
|
@@ -177,7 +317,7 @@ class Validator {
|
|
|
177
317
|
|
|
178
318
|
// Fallback methods — only used when JS codegen is unavailable
|
|
179
319
|
validate(data) {
|
|
180
|
-
if (this.
|
|
320
|
+
if (this._preprocess) this._preprocess(data);
|
|
181
321
|
return this._compiled.validate(data);
|
|
182
322
|
}
|
|
183
323
|
|
package/lib/js-compiler.js
CHANGED
|
@@ -437,18 +437,30 @@ function codegenSafe(schema) {
|
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
//
|
|
440
|
+
// Keys that collide with Object.prototype
|
|
441
441
|
if (schema.required) {
|
|
442
442
|
for (const k of schema.required) {
|
|
443
443
|
if (UNSAFE_KEYS.has(k)) return false
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
|
+
if (schema.properties) {
|
|
447
|
+
for (const k of Object.keys(schema.properties)) {
|
|
448
|
+
if (UNSAFE_KEYS.has(k)) return false
|
|
449
|
+
if (k === '$ref') return false // property named "$ref" — confusing
|
|
450
|
+
}
|
|
451
|
+
}
|
|
446
452
|
|
|
447
453
|
// Unicode property escapes in pattern need 'u' flag — codegen uses RegExp without it
|
|
448
454
|
if (schema.pattern && /\\[pP]\{/.test(schema.pattern)) return false
|
|
449
455
|
|
|
450
|
-
// $ref —
|
|
451
|
-
if (schema.$ref)
|
|
456
|
+
// $ref — allow only simple local refs (#/$defs/Name), no $id, no sibling keywords
|
|
457
|
+
if (schema.$ref) {
|
|
458
|
+
if (!/^#\/(?:\$defs|definitions)\/[^/]+$/.test(schema.$ref)) return false
|
|
459
|
+
// Bail if $ref has sibling keywords (complex interaction)
|
|
460
|
+
const siblings = Object.keys(schema).filter(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema')
|
|
461
|
+
if (siblings.length > 0) return false
|
|
462
|
+
}
|
|
463
|
+
if (schema.$id) return false
|
|
452
464
|
|
|
453
465
|
// additionalProperties as schema — bail entirely, too many edge cases with allOf interaction
|
|
454
466
|
if (typeof schema.additionalProperties === 'object') return false
|
|
@@ -457,6 +469,19 @@ function codegenSafe(schema) {
|
|
|
457
469
|
// propertyNames: false — codegen doesn't handle this
|
|
458
470
|
if (schema.propertyNames === false) return false
|
|
459
471
|
|
|
472
|
+
// Check $defs: targets must be safe, names must be simple, no nested $ref chains
|
|
473
|
+
const defs = schema.$defs || schema.definitions
|
|
474
|
+
if (defs) {
|
|
475
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
476
|
+
if (/[~/"']/.test(name)) return false // special chars in def name
|
|
477
|
+
if (typeof def === 'boolean') return false
|
|
478
|
+
if (typeof def === 'object' && def !== null) {
|
|
479
|
+
if (def.$ref) return false // nested ref chain — bail
|
|
480
|
+
if (!codegenSafe(def)) return false
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
460
485
|
// Recurse into sub-schemas — bail on boolean schemas in any position
|
|
461
486
|
const subs = [
|
|
462
487
|
schema.items, schema.contains, schema.not,
|
|
@@ -498,8 +523,10 @@ function compileToJSCodegen(schema) {
|
|
|
498
523
|
genCode(schema, 'd', lines, ctx)
|
|
499
524
|
if (lines.length === 0) return () => true
|
|
500
525
|
|
|
501
|
-
const
|
|
502
|
-
|
|
526
|
+
const helperStr = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
|
|
527
|
+
const checkStr = lines.join('\n ')
|
|
528
|
+
const body = helperStr + checkStr + '\n return true'
|
|
529
|
+
|
|
503
530
|
try {
|
|
504
531
|
return new Function('d', body)
|
|
505
532
|
} catch {
|
|
@@ -648,16 +675,19 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
648
675
|
if (fc) lines.push(fc(v, isStr))
|
|
649
676
|
}
|
|
650
677
|
|
|
651
|
-
// uniqueItems —
|
|
678
|
+
// uniqueItems — tiered strategy based on expected array size
|
|
652
679
|
if (schema.uniqueItems) {
|
|
653
680
|
const si = ctx.varCounter++
|
|
654
|
-
// If items schema is a primitive type, skip JSON.stringify entirely
|
|
655
681
|
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
656
682
|
const isPrimItems = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
683
|
+
const maxItems = schema.maxItems
|
|
684
|
+
// Small primitive arrays (maxItems <= 16): nested loop is 6x faster than Set
|
|
685
|
+
// No allocation, no hash computation — just direct === comparison
|
|
686
|
+
const inner = isPrimItems && maxItems && maxItems <= 16
|
|
687
|
+
? `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k])return false}}`
|
|
688
|
+
: isPrimItems
|
|
689
|
+
? `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i]))return false;_s${si}.add(${v}[_i])}`
|
|
690
|
+
: `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k))return false;_s${si}.add(_k)}`
|
|
661
691
|
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
662
692
|
}
|
|
663
693
|
|
|
@@ -842,4 +872,620 @@ const FORMAT_CODEGEN = {
|
|
|
842
872
|
// Safe key escaping: use JSON.stringify to handle all special chars (newlines, null bytes, etc.)
|
|
843
873
|
function esc(s) { return JSON.stringify(s).slice(1, -1) }
|
|
844
874
|
|
|
845
|
-
|
|
875
|
+
// --- Error-collecting codegen: same checks, but pushes errors instead of returning false ---
|
|
876
|
+
// Returns a function: (data, allErrors) => { valid, errors }
|
|
877
|
+
// Valid path is still fast — only error path does extra work.
|
|
878
|
+
function compileToJSCodegenWithErrors(schema) {
|
|
879
|
+
if (typeof schema === 'boolean') {
|
|
880
|
+
return schema
|
|
881
|
+
? () => ({ valid: true, errors: [] })
|
|
882
|
+
: () => ({ valid: false, errors: [{ code: 'type_mismatch', path: '', message: 'schema is false' }] })
|
|
883
|
+
}
|
|
884
|
+
if (typeof schema !== 'object' || schema === null) return null
|
|
885
|
+
if (!codegenSafe(schema)) return null
|
|
886
|
+
if (schema.patternProperties || schema.dependentSchemas || schema.propertyNames) return null
|
|
887
|
+
|
|
888
|
+
const ctx = { varCounter: 0, helperCode: [], rootDefs: schema.$defs || schema.definitions || null, refStack: new Set() }
|
|
889
|
+
const lines = []
|
|
890
|
+
genCodeE(schema, 'd', '', lines, ctx)
|
|
891
|
+
if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
|
|
892
|
+
|
|
893
|
+
const body = `const _e=[];\n ` +
|
|
894
|
+
(ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
|
|
895
|
+
lines.join('\n ') +
|
|
896
|
+
`\n return{valid:_e.length===0,errors:_e}`
|
|
897
|
+
try {
|
|
898
|
+
return new Function('d', '_all', body)
|
|
899
|
+
} catch {
|
|
900
|
+
return null
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Error-collecting code generator.
|
|
905
|
+
// Instead of `return false`, pushes to `_e` array and optionally early-returns.
|
|
906
|
+
// `_all` parameter: if falsy, return after first error.
|
|
907
|
+
function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
908
|
+
if (typeof schema !== 'object' || schema === null) return
|
|
909
|
+
|
|
910
|
+
// $ref — resolve local refs
|
|
911
|
+
if (schema.$ref && ctx.rootDefs) {
|
|
912
|
+
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
913
|
+
if (m && ctx.rootDefs[m[1]]) {
|
|
914
|
+
genCodeE(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
919
|
+
if (types) {
|
|
920
|
+
const conds = types.map(t => {
|
|
921
|
+
switch (t) {
|
|
922
|
+
case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
923
|
+
case 'array': return `Array.isArray(${v})`
|
|
924
|
+
case 'string': return `typeof ${v}==='string'`
|
|
925
|
+
case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
|
|
926
|
+
case 'integer': return `Number.isInteger(${v})`
|
|
927
|
+
case 'boolean': return `typeof ${v}==='boolean'`
|
|
928
|
+
case 'null': return `${v}===null`
|
|
929
|
+
default: return 'true'
|
|
930
|
+
}
|
|
931
|
+
})
|
|
932
|
+
const expected = types.join(', ')
|
|
933
|
+
lines.push(`if(!(${conds.join('||')})){_e.push({code:'type_mismatch',path:${pathExpr||'""'},message:'expected ${expected}'});if(!_all)return{valid:false,errors:_e}}`)
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// In error mode, never assume type — always guard (data may have failed type check but allErrors continues)
|
|
937
|
+
const isObj = false
|
|
938
|
+
const isArr = false
|
|
939
|
+
const isStr = false
|
|
940
|
+
const isNum = false
|
|
941
|
+
|
|
942
|
+
const fail = (code, msg) => `_e.push({code:'${code}',path:${pathExpr||'""'},message:${msg}});if(!_all)return{valid:false,errors:_e}`
|
|
943
|
+
|
|
944
|
+
// enum
|
|
945
|
+
if (schema.enum) {
|
|
946
|
+
const vals = schema.enum
|
|
947
|
+
const primitives = vals.filter(v => v === null || typeof v !== 'object')
|
|
948
|
+
const objects = vals.filter(v => v !== null && typeof v === 'object')
|
|
949
|
+
const primChecks = primitives.map(p => `${v}===${JSON.stringify(p)}`).join('||')
|
|
950
|
+
const objChecks = objects.map(o => `JSON.stringify(${v})===${JSON.stringify(JSON.stringify(o))}`).join('||')
|
|
951
|
+
const allChecks = [primChecks, objChecks].filter(Boolean).join('||')
|
|
952
|
+
lines.push(`if(!(${allChecks || 'false'})){${fail('enum_mismatch', "'value not in enum'")}}`)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// const — use canonical (sorted-key) comparison for objects
|
|
956
|
+
if (schema.const !== undefined) {
|
|
957
|
+
const cv = schema.const
|
|
958
|
+
if (cv === null || typeof cv !== 'object') {
|
|
959
|
+
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const_mismatch', "'value does not match const'")}}`)
|
|
960
|
+
} else {
|
|
961
|
+
// Pre-compute canonical form of const value
|
|
962
|
+
const ci = ctx.varCounter++
|
|
963
|
+
const canonFn = `_cnE${ci}`
|
|
964
|
+
ctx.helperCode.push(`const ${canonFn}=function(x){if(x===null||typeof x!=='object')return JSON.stringify(x);if(Array.isArray(x))return'['+x.map(${canonFn}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+${canonFn}(x[k])}).join(',')+'}'};`)
|
|
965
|
+
const expected = canonFn + '(' + JSON.stringify(cv) + ')'
|
|
966
|
+
lines.push(`if(${canonFn}(${v})!==${expected}){${fail('const_mismatch', "'value does not match const'")}}`)
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// required — no destructuring in error mode (data might not be an object)
|
|
971
|
+
const requiredSet = new Set(schema.required || [])
|
|
972
|
+
const hoisted = {}
|
|
973
|
+
if (schema.required) {
|
|
974
|
+
for (const key of schema.required) {
|
|
975
|
+
const p = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
|
|
976
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){_e.push({code:'required_missing',path:${p},message:'missing required: ${esc(key)}'});if(!_all)return{valid:false,errors:_e}}`)
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// numeric
|
|
981
|
+
if (schema.minimum !== undefined) {
|
|
982
|
+
const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`
|
|
983
|
+
lines.push(`if(${c}){${fail('minimum_violation', `'minimum ${schema.minimum}'`)}}`)
|
|
984
|
+
}
|
|
985
|
+
if (schema.maximum !== undefined) {
|
|
986
|
+
const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`
|
|
987
|
+
lines.push(`if(${c}){${fail('maximum_violation', `'maximum ${schema.maximum}'`)}}`)
|
|
988
|
+
}
|
|
989
|
+
if (schema.exclusiveMinimum !== undefined) {
|
|
990
|
+
const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`
|
|
991
|
+
lines.push(`if(${c}){${fail('exclusive_minimum_violation', `'exclusiveMinimum ${schema.exclusiveMinimum}'`)}}`)
|
|
992
|
+
}
|
|
993
|
+
if (schema.exclusiveMaximum !== undefined) {
|
|
994
|
+
const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`
|
|
995
|
+
lines.push(`if(${c}){${fail('exclusive_maximum_violation', `'exclusiveMaximum ${schema.exclusiveMaximum}'`)}}`)
|
|
996
|
+
}
|
|
997
|
+
if (schema.multipleOf !== undefined) {
|
|
998
|
+
const m = schema.multipleOf
|
|
999
|
+
const ci = ctx.varCounter++
|
|
1000
|
+
// Use tolerance-based check for floating point (matches C++ behavior)
|
|
1001
|
+
lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multiple_of_violation', `'multipleOf ${m}'`)}}}`)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// string
|
|
1005
|
+
if (schema.minLength !== undefined) {
|
|
1006
|
+
const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`
|
|
1007
|
+
lines.push(`if(${c}){${fail('min_length_violation', `'minLength ${schema.minLength}'`)}}`)
|
|
1008
|
+
}
|
|
1009
|
+
if (schema.maxLength !== undefined) {
|
|
1010
|
+
const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`
|
|
1011
|
+
lines.push(`if(${c}){${fail('max_length_violation', `'maxLength ${schema.maxLength}'`)}}`)
|
|
1012
|
+
}
|
|
1013
|
+
if (schema.pattern) {
|
|
1014
|
+
const ri = ctx.varCounter++
|
|
1015
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
|
|
1016
|
+
const c = isStr ? `!_re${ri}.test(${v})` : `typeof ${v}==='string'&&!_re${ri}.test(${v})`
|
|
1017
|
+
lines.push(`if(${c}){${fail('pattern_mismatch', `'pattern mismatch'`)}}`)
|
|
1018
|
+
}
|
|
1019
|
+
if (schema.format) {
|
|
1020
|
+
const fc = FORMAT_CODEGEN[schema.format]
|
|
1021
|
+
// Format errors use the boolean codegen — just wrap with error push
|
|
1022
|
+
if (fc) {
|
|
1023
|
+
const ri = ctx.varCounter++
|
|
1024
|
+
const boolLines = []
|
|
1025
|
+
boolLines.push(fc(v, isStr))
|
|
1026
|
+
// Replace `return false` with error push in the format check
|
|
1027
|
+
const fmtCode = boolLines.join(';').replace(/return false/g,
|
|
1028
|
+
`{_e.push({code:'format_mismatch',path:${pathExpr||'""'},message:'format ${esc(schema.format)}'});if(!_all)return{valid:false,errors:_e}}`)
|
|
1029
|
+
lines.push(fmtCode)
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// array size
|
|
1034
|
+
if (schema.minItems !== undefined) {
|
|
1035
|
+
const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`
|
|
1036
|
+
lines.push(`if(${c}){${fail('min_items_violation', `'minItems ${schema.minItems}'`)}}`)
|
|
1037
|
+
}
|
|
1038
|
+
if (schema.maxItems !== undefined) {
|
|
1039
|
+
const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`
|
|
1040
|
+
lines.push(`if(${c}){${fail('max_items_violation', `'maxItems ${schema.maxItems}'`)}}`)
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// uniqueItems
|
|
1044
|
+
if (schema.uniqueItems) {
|
|
1045
|
+
const si = ctx.varCounter++
|
|
1046
|
+
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
1047
|
+
const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
1048
|
+
const inner = isPrim
|
|
1049
|
+
? `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i])){${fail('unique_items_violation', "'duplicate items'")};break};_s${si}.add(${v}[_i])}`
|
|
1050
|
+
: `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k)){${fail('unique_items_violation', "'duplicate items'")};break};_s${si}.add(_k)}`
|
|
1051
|
+
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// object size
|
|
1055
|
+
if (schema.minProperties !== undefined) {
|
|
1056
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('min_properties_violation', `'minProperties ${schema.minProperties}'`)}}`)
|
|
1057
|
+
}
|
|
1058
|
+
if (schema.maxProperties !== undefined) {
|
|
1059
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('max_properties_violation', `'maxProperties ${schema.maxProperties}'`)}}`)
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// additionalProperties: false
|
|
1063
|
+
if (schema.additionalProperties === false && schema.properties) {
|
|
1064
|
+
const allowed = Object.keys(schema.properties).map(k => `${JSON.stringify(k)}`).join(',')
|
|
1065
|
+
const ci = ctx.varCounter++
|
|
1066
|
+
const inner = `const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++){if(!_a${ci}.has(_k${ci}[_i])){_e.push({code:'additional_property',path:${pathExpr||'""'},message:'additional property: '+_k${ci}[_i]});if(!_all)return{valid:false,errors:_e}}}`
|
|
1067
|
+
lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// dependentRequired
|
|
1071
|
+
if (schema.dependentRequired) {
|
|
1072
|
+
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
1073
|
+
for (const dep of deps) {
|
|
1074
|
+
const p = pathExpr ? `${pathExpr}+'/${esc(dep)}'` : `'/${esc(dep)}'`
|
|
1075
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){_e.push({code:'required_missing',path:${p},message:'${esc(key)} requires ${esc(dep)}'});if(!_all)return{valid:false,errors:_e}}`)
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// properties — always guard (error mode, data may not be an object or may be array)
|
|
1081
|
+
if (schema.properties) {
|
|
1082
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
1083
|
+
const childPath = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
|
|
1084
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
1085
|
+
genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx)
|
|
1086
|
+
lines.push(`}`)
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// items — starts after prefixItems (Draft 2020-12 semantics)
|
|
1091
|
+
if (schema.items) {
|
|
1092
|
+
const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
|
|
1093
|
+
const idx = `_j${ctx.varCounter}`
|
|
1094
|
+
const elem = `_ei${ctx.varCounter}`
|
|
1095
|
+
ctx.varCounter++
|
|
1096
|
+
const childPath = pathExpr ? `${pathExpr}+'/'+${idx}` : `'/'+${idx}`
|
|
1097
|
+
lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
|
|
1098
|
+
genCodeE(schema.items, elem, childPath, lines, ctx)
|
|
1099
|
+
lines.push(`}}`)
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// prefixItems
|
|
1103
|
+
if (schema.prefixItems) {
|
|
1104
|
+
for (let i = 0; i < schema.prefixItems.length; i++) {
|
|
1105
|
+
const childPath = pathExpr ? `${pathExpr}+'/${i}'` : `'/${i}'`
|
|
1106
|
+
lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
|
|
1107
|
+
genCodeE(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx)
|
|
1108
|
+
lines.push(`}`)
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// contains
|
|
1113
|
+
if (schema.contains) {
|
|
1114
|
+
const ci = ctx.varCounter++
|
|
1115
|
+
const subLines = []
|
|
1116
|
+
genCode(schema.contains, `_cv`, subLines, ctx)
|
|
1117
|
+
const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
|
|
1118
|
+
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
1119
|
+
const maxC = schema.maxContains
|
|
1120
|
+
lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
|
|
1121
|
+
lines.push(`if(_cc${ci}<${minC}){${fail('contains_violation', `'contains: need at least ${minC} match(es)'`)}}`)
|
|
1122
|
+
if (maxC !== undefined) {
|
|
1123
|
+
lines.push(`if(_cc${ci}>${maxC}){${fail('contains_violation', `'contains: at most ${maxC} match(es)'`)}}`)
|
|
1124
|
+
}
|
|
1125
|
+
lines.push(`}`)
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// allOf
|
|
1129
|
+
if (schema.allOf) {
|
|
1130
|
+
for (const sub of schema.allOf) {
|
|
1131
|
+
genCodeE(sub, v, pathExpr, lines, ctx)
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// anyOf
|
|
1136
|
+
if (schema.anyOf) {
|
|
1137
|
+
const fi = ctx.varCounter++
|
|
1138
|
+
const fns = schema.anyOf.map((sub, i) => {
|
|
1139
|
+
const subLines = []
|
|
1140
|
+
genCode(sub, '_av', subLines, ctx)
|
|
1141
|
+
return subLines.length === 0 ? `function(_av){return true}` : `function(_av){${subLines.join(';')};return true}`
|
|
1142
|
+
})
|
|
1143
|
+
lines.push(`{const _af${fi}=[${fns.join(',')}];let _am${fi}=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am${fi}=true;break}}if(!_am${fi}){${fail('any_of_failed', "'no anyOf matched'")}}}`)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// oneOf
|
|
1147
|
+
if (schema.oneOf) {
|
|
1148
|
+
const fi = ctx.varCounter++
|
|
1149
|
+
const fns = schema.oneOf.map((sub, i) => {
|
|
1150
|
+
const subLines = []
|
|
1151
|
+
genCode(sub, '_ov', subLines, ctx)
|
|
1152
|
+
return subLines.length === 0 ? `function(_ov){return true}` : `function(_ov){${subLines.join(';')};return true}`
|
|
1153
|
+
})
|
|
1154
|
+
lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc${fi}=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc${fi}++;if(_oc${fi}>1)break}if(_oc${fi}!==1){${fail('one_of_failed', "'oneOf: expected 1 match, got '+_oc"+fi)}}}`)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// not
|
|
1158
|
+
if (schema.not) {
|
|
1159
|
+
const subLines = []
|
|
1160
|
+
genCode(schema.not, '_nv', subLines, ctx)
|
|
1161
|
+
const nfn = subLines.length === 0 ? `function(_nv){return true}` : `function(_nv){${subLines.join(';')};return true}`
|
|
1162
|
+
const fi = ctx.varCounter++
|
|
1163
|
+
lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not_failed', "'should not match'")}}}`)
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// if/then/else
|
|
1167
|
+
if (schema.if) {
|
|
1168
|
+
const ifLines = []
|
|
1169
|
+
genCode(schema.if, '_iv', ifLines, ctx)
|
|
1170
|
+
const fi = ctx.varCounter++
|
|
1171
|
+
const ifFn = ifLines.length === 0
|
|
1172
|
+
? `function(_iv){return true}`
|
|
1173
|
+
: `function(_iv){${ifLines.join(';')};return true}`
|
|
1174
|
+
lines.push(`{const _if${fi}=${ifFn}`)
|
|
1175
|
+
if (schema.then) {
|
|
1176
|
+
lines.push(`if(_if${fi}(${v})){`)
|
|
1177
|
+
genCodeE(schema.then, v, pathExpr, lines, ctx)
|
|
1178
|
+
lines.push(`}`)
|
|
1179
|
+
}
|
|
1180
|
+
if (schema.else) {
|
|
1181
|
+
lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`)
|
|
1182
|
+
genCodeE(schema.else, v, pathExpr, lines, ctx)
|
|
1183
|
+
lines.push(`}`)
|
|
1184
|
+
}
|
|
1185
|
+
lines.push(`}`)
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// --- Combined validator: single pass, validates + collects errors ---
|
|
1190
|
+
// Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
|
|
1191
|
+
// Avoids double-pass (jsFn → false → errFn runs same checks again).
|
|
1192
|
+
// Uses type-aware optimizations: after type check passes, skip guards.
|
|
1193
|
+
function compileToJSCombined(schema, VALID_RESULT) {
|
|
1194
|
+
if (typeof schema === 'boolean') {
|
|
1195
|
+
return schema
|
|
1196
|
+
? () => VALID_RESULT
|
|
1197
|
+
: () => ({ valid: false, errors: [{ code: 'type_mismatch', path: '', message: 'schema is false' }] })
|
|
1198
|
+
}
|
|
1199
|
+
if (typeof schema !== 'object' || schema === null) return null
|
|
1200
|
+
if (!codegenSafe(schema)) return null
|
|
1201
|
+
if (schema.patternProperties || schema.dependentSchemas || schema.propertyNames) return null
|
|
1202
|
+
|
|
1203
|
+
const ctx = { varCounter: 0, helperCode: [], closureVars: [], closureVals: [],
|
|
1204
|
+
rootDefs: schema.$defs || schema.definitions || null, refStack: new Set() }
|
|
1205
|
+
const lines = []
|
|
1206
|
+
genCodeC(schema, 'd', '', lines, ctx)
|
|
1207
|
+
if (lines.length === 0) return () => VALID_RESULT
|
|
1208
|
+
|
|
1209
|
+
// Use factory pattern: closure vars (regexes, etc.) created once, not per call
|
|
1210
|
+
const closureParams = ctx.closureVars.join(',')
|
|
1211
|
+
const inner = `const _e=[];\n ` +
|
|
1212
|
+
(ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
|
|
1213
|
+
lines.join('\n ') +
|
|
1214
|
+
`\n return _e.length===0?R:{valid:false,errors:_e}`
|
|
1215
|
+
|
|
1216
|
+
try {
|
|
1217
|
+
const factory = new Function('R' + (closureParams ? ',' + closureParams : ''),
|
|
1218
|
+
`return function(d){${inner}}`)
|
|
1219
|
+
return factory(VALID_RESULT, ...ctx.closureVals)
|
|
1220
|
+
} catch {
|
|
1221
|
+
return null
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Combined code generator: type-aware like genCode, error-collecting like genCodeE.
|
|
1226
|
+
// After type check passes → use optimizations (destructuring, no guards).
|
|
1227
|
+
// If type check fails → push error, skip property checks (they'd crash).
|
|
1228
|
+
function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
1229
|
+
if (typeof schema !== 'object' || schema === null) return
|
|
1230
|
+
|
|
1231
|
+
// $ref — resolve local refs
|
|
1232
|
+
if (schema.$ref && ctx.rootDefs) {
|
|
1233
|
+
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
1234
|
+
if (m && ctx.rootDefs[m[1]]) {
|
|
1235
|
+
genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx)
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
1240
|
+
let isObj = false, isArr = false, isStr = false, isNum = false
|
|
1241
|
+
|
|
1242
|
+
const fail = (code, msg) => `_e.push({code:'${code}',path:${pathExpr||'""'},message:${msg}})`
|
|
1243
|
+
|
|
1244
|
+
if (types) {
|
|
1245
|
+
const conds = types.map(t => {
|
|
1246
|
+
switch (t) {
|
|
1247
|
+
case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
1248
|
+
case 'array': return `Array.isArray(${v})`
|
|
1249
|
+
case 'string': return `typeof ${v}==='string'`
|
|
1250
|
+
case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
|
|
1251
|
+
case 'integer': return `Number.isInteger(${v})`
|
|
1252
|
+
case 'boolean': return `typeof ${v}==='boolean'`
|
|
1253
|
+
case 'null': return `${v}===null`
|
|
1254
|
+
default: return 'true'
|
|
1255
|
+
}
|
|
1256
|
+
})
|
|
1257
|
+
const expected = types.join(', ')
|
|
1258
|
+
// Type check: push error but continue — wrap remaining in type-success block
|
|
1259
|
+
const typeOk = `_tok${ctx.varCounter++}`
|
|
1260
|
+
lines.push(`const ${typeOk}=${conds.join('||')}`)
|
|
1261
|
+
lines.push(`if(!${typeOk}){${fail('type_mismatch', `'expected ${expected}'`)}}`)
|
|
1262
|
+
// Subsequent optimized code runs inside if(typeOk){...}
|
|
1263
|
+
if (types.length === 1) {
|
|
1264
|
+
isObj = types[0] === 'object'
|
|
1265
|
+
isArr = types[0] === 'array'
|
|
1266
|
+
isStr = types[0] === 'string'
|
|
1267
|
+
isNum = types[0] === 'number' || types[0] === 'integer'
|
|
1268
|
+
}
|
|
1269
|
+
lines.push(`if(${typeOk}){`)
|
|
1270
|
+
// We'll close this block at the end of genCodeC — mark it
|
|
1271
|
+
ctx._typeBlock = true
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// enum
|
|
1275
|
+
if (schema.enum) {
|
|
1276
|
+
const vals = schema.enum
|
|
1277
|
+
const primitives = vals.filter(v => v === null || typeof v !== 'object')
|
|
1278
|
+
const objects = vals.filter(v => v !== null && typeof v === 'object')
|
|
1279
|
+
const primChecks = primitives.map(p => `${v}===${JSON.stringify(p)}`).join('||')
|
|
1280
|
+
const objChecks = objects.map(o => `JSON.stringify(${v})===${JSON.stringify(JSON.stringify(o))}`).join('||')
|
|
1281
|
+
const allChecks = [primChecks, objChecks].filter(Boolean).join('||')
|
|
1282
|
+
lines.push(`if(!(${allChecks || 'false'})){${fail('enum_mismatch', "'value not in enum'")}}`)
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// const
|
|
1286
|
+
if (schema.const !== undefined) {
|
|
1287
|
+
const cv = schema.const
|
|
1288
|
+
if (cv === null || typeof cv !== 'object') {
|
|
1289
|
+
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const_mismatch', "'const mismatch'")}}`)
|
|
1290
|
+
} else {
|
|
1291
|
+
const ci = ctx.varCounter++
|
|
1292
|
+
const canonFn = `_cn${ci}`
|
|
1293
|
+
ctx.helperCode.push(`const ${canonFn}=function(x){if(x===null||typeof x!=='object')return JSON.stringify(x);if(Array.isArray(x))return'['+x.map(${canonFn}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+${canonFn}(x[k])}).join(',')+'}'};`)
|
|
1294
|
+
lines.push(`if(${canonFn}(${v})!==${canonFn}(${JSON.stringify(cv)})){${fail('const_mismatch', "'const mismatch'")}}`)
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// required — use destructuring when type is object (SAFE because type check already passed)
|
|
1299
|
+
const requiredSet = new Set(schema.required || [])
|
|
1300
|
+
const hoisted = {}
|
|
1301
|
+
if (schema.required && schema.properties && isObj) {
|
|
1302
|
+
const destructKeys = []
|
|
1303
|
+
for (const key of schema.required) {
|
|
1304
|
+
if (schema.properties[key]) {
|
|
1305
|
+
const lv = `_h${ctx.varCounter++}`
|
|
1306
|
+
hoisted[key] = lv
|
|
1307
|
+
destructKeys.push(`${JSON.stringify(key)}:${lv}`)
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (destructKeys.length > 0) lines.push(`const{${destructKeys.join(',')}}=${v}`)
|
|
1311
|
+
for (const key of schema.required) {
|
|
1312
|
+
const check = hoisted[key] ? `${hoisted[key]}===undefined` : `${v}[${JSON.stringify(key)}]===undefined`
|
|
1313
|
+
const p = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
|
|
1314
|
+
lines.push(`if(${check}){${`_e.push({code:'required_missing',path:${p},message:'missing: ${esc(key)}'})`}}`)
|
|
1315
|
+
}
|
|
1316
|
+
} else if (schema.required) {
|
|
1317
|
+
for (const key of schema.required) {
|
|
1318
|
+
const p = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
|
|
1319
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){_e.push({code:'required_missing',path:${p},message:'missing: ${esc(key)}'})}`)
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// numeric — skip type guard if known
|
|
1324
|
+
if (schema.minimum !== undefined) { const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`; lines.push(`if(${c}){${fail('minimum_violation', `'min ${schema.minimum}'`)}}`) }
|
|
1325
|
+
if (schema.maximum !== undefined) { const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`; lines.push(`if(${c}){${fail('maximum_violation', `'max ${schema.maximum}'`)}}`) }
|
|
1326
|
+
if (schema.exclusiveMinimum !== undefined) { const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`; lines.push(`if(${c}){${fail('exclusive_minimum_violation', `'excMin ${schema.exclusiveMinimum}'`)}}`) }
|
|
1327
|
+
if (schema.exclusiveMaximum !== undefined) { const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`; lines.push(`if(${c}){${fail('exclusive_maximum_violation', `'excMax ${schema.exclusiveMaximum}'`)}}`) }
|
|
1328
|
+
if (schema.multipleOf !== undefined) {
|
|
1329
|
+
const m = schema.multipleOf
|
|
1330
|
+
const ci = ctx.varCounter++
|
|
1331
|
+
lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multiple_of_violation', `'multipleOf ${m}'`)}}}`)
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// string — skip guard if known
|
|
1335
|
+
if (schema.minLength !== undefined) { const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`; lines.push(`if(${c}){${fail('min_length_violation', `'minLength ${schema.minLength}'`)}}`) }
|
|
1336
|
+
if (schema.maxLength !== undefined) { const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`; lines.push(`if(${c}){${fail('max_length_violation', `'maxLength ${schema.maxLength}'`)}}`) }
|
|
1337
|
+
if (schema.pattern) {
|
|
1338
|
+
const ri = ctx.varCounter++
|
|
1339
|
+
const reVar = `_re${ri}`
|
|
1340
|
+
ctx.closureVars.push(reVar)
|
|
1341
|
+
ctx.closureVals.push(new RegExp(schema.pattern))
|
|
1342
|
+
const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
|
|
1343
|
+
lines.push(`if(${c}){${fail('pattern_mismatch', "'pattern mismatch'")}}`)
|
|
1344
|
+
}
|
|
1345
|
+
if (schema.format) {
|
|
1346
|
+
const fc = FORMAT_CODEGEN[schema.format]
|
|
1347
|
+
if (fc) {
|
|
1348
|
+
const code = fc(v, isStr).replace(/return false/g, `{${fail('format_mismatch', `'format ${esc(schema.format)}'`)}}`)
|
|
1349
|
+
lines.push(code)
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// array size
|
|
1354
|
+
if (schema.minItems !== undefined) { const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`; lines.push(`if(${c}){${fail('min_items_violation', `'minItems ${schema.minItems}'`)}}`) }
|
|
1355
|
+
if (schema.maxItems !== undefined) { const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`; lines.push(`if(${c}){${fail('max_items_violation', `'maxItems ${schema.maxItems}'`)}}`) }
|
|
1356
|
+
|
|
1357
|
+
// uniqueItems
|
|
1358
|
+
if (schema.uniqueItems) {
|
|
1359
|
+
const si = ctx.varCounter++
|
|
1360
|
+
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
1361
|
+
const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
1362
|
+
const inner = isPrim
|
|
1363
|
+
? `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i])){${fail('unique_items_violation', "'duplicates'")};break};_s${si}.add(${v}[_i])}`
|
|
1364
|
+
: `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k)){${fail('unique_items_violation', "'duplicates'")};break};_s${si}.add(_k)}`
|
|
1365
|
+
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// object size
|
|
1369
|
+
if (schema.minProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('min_properties_violation', `'minProperties ${schema.minProperties}'`)}}`)
|
|
1370
|
+
if (schema.maxProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('max_properties_violation', `'maxProperties ${schema.maxProperties}'`)}}`)
|
|
1371
|
+
|
|
1372
|
+
// additionalProperties
|
|
1373
|
+
if (schema.additionalProperties === false && schema.properties) {
|
|
1374
|
+
const allowed = Object.keys(schema.properties).map(k => JSON.stringify(k)).join(',')
|
|
1375
|
+
const ci = ctx.varCounter++
|
|
1376
|
+
lines.push(isObj
|
|
1377
|
+
? `{const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i])){${fail('additional_property', `'extra: '+_k${ci}[_i]`)}}}`
|
|
1378
|
+
: `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i])){${fail('additional_property', `'extra: '+_k${ci}[_i]`)}}}`)
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// dependentRequired
|
|
1382
|
+
if (schema.dependentRequired) {
|
|
1383
|
+
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
1384
|
+
for (const dep of deps) {
|
|
1385
|
+
const p = pathExpr ? `${pathExpr}+'/${esc(dep)}'` : `'/${esc(dep)}'`
|
|
1386
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){_e.push({code:'required_missing',path:${p},message:'${esc(key)} requires ${esc(dep)}'})}`)
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// properties — use hoisted vars for required+known-object, full guard otherwise
|
|
1392
|
+
if (schema.properties) {
|
|
1393
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
1394
|
+
const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
|
|
1395
|
+
const childPath = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
|
|
1396
|
+
if (requiredSet.has(key) && isObj) {
|
|
1397
|
+
lines.push(`if(${pv}!==undefined){`)
|
|
1398
|
+
genCodeC(prop, pv, childPath, lines, ctx)
|
|
1399
|
+
lines.push(`}`)
|
|
1400
|
+
} else if (isObj) {
|
|
1401
|
+
const oi = ctx.varCounter++
|
|
1402
|
+
lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined){`)
|
|
1403
|
+
genCodeC(prop, `_o${oi}`, childPath, lines, ctx)
|
|
1404
|
+
lines.push(`}}`)
|
|
1405
|
+
} else {
|
|
1406
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
1407
|
+
genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx)
|
|
1408
|
+
lines.push(`}`)
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// items
|
|
1414
|
+
if (schema.items) {
|
|
1415
|
+
const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
|
|
1416
|
+
const idx = `_j${ctx.varCounter}`, elem = `_ei${ctx.varCounter}`
|
|
1417
|
+
ctx.varCounter++
|
|
1418
|
+
const childPath = pathExpr ? `${pathExpr}+'/'+${idx}` : `'/'+${idx}`
|
|
1419
|
+
lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
|
|
1420
|
+
genCodeC(schema.items, elem, childPath, lines, ctx)
|
|
1421
|
+
lines.push(`}}`)
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// prefixItems
|
|
1425
|
+
if (schema.prefixItems) {
|
|
1426
|
+
for (let i = 0; i < schema.prefixItems.length; i++) {
|
|
1427
|
+
const childPath = pathExpr ? `${pathExpr}+'/${i}'` : `'/${i}'`
|
|
1428
|
+
lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
|
|
1429
|
+
genCodeC(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx)
|
|
1430
|
+
lines.push(`}`)
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// contains
|
|
1435
|
+
if (schema.contains) {
|
|
1436
|
+
const ci = ctx.varCounter++
|
|
1437
|
+
const subLines = []
|
|
1438
|
+
genCode(schema.contains, `_cv`, subLines, ctx)
|
|
1439
|
+
const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
|
|
1440
|
+
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
1441
|
+
const maxC = schema.maxContains
|
|
1442
|
+
lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
|
|
1443
|
+
lines.push(`if(_cc${ci}<${minC}){${fail('contains_violation', `'need ${minC}+ matches'`)}}`)
|
|
1444
|
+
if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains_violation', `'max ${maxC} matches'`)}}`)
|
|
1445
|
+
lines.push(`}`)
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// allOf
|
|
1449
|
+
if (schema.allOf) { for (const sub of schema.allOf) genCodeC(sub, v, pathExpr, lines, ctx) }
|
|
1450
|
+
|
|
1451
|
+
// anyOf
|
|
1452
|
+
if (schema.anyOf) {
|
|
1453
|
+
const fi = ctx.varCounter++
|
|
1454
|
+
const fns = schema.anyOf.map(sub => { const sl = []; genCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
|
|
1455
|
+
lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('any_of_failed', "'no match'")}}}`)
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// oneOf
|
|
1459
|
+
if (schema.oneOf) {
|
|
1460
|
+
const fi = ctx.varCounter++
|
|
1461
|
+
const fns = schema.oneOf.map(sub => { const sl = []; genCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
|
|
1462
|
+
lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('one_of_failed', "'need exactly 1'")}}}`)
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// not
|
|
1466
|
+
if (schema.not) {
|
|
1467
|
+
const sl = []; genCode(schema.not, '_nv', sl, ctx)
|
|
1468
|
+
const nfn = sl.length === 0 ? `function(_nv){return true}` : `function(_nv){${sl.join(';')};return true}`
|
|
1469
|
+
const fi = ctx.varCounter++
|
|
1470
|
+
lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not_failed', "'should not match'")}}}`)
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// if/then/else
|
|
1474
|
+
if (schema.if) {
|
|
1475
|
+
const sl = []; genCode(schema.if, '_iv', sl, ctx)
|
|
1476
|
+
const fi = ctx.varCounter++
|
|
1477
|
+
const ifFn = sl.length === 0 ? `function(_iv){return true}` : `function(_iv){${sl.join(';')};return true}`
|
|
1478
|
+
lines.push(`{const _if${fi}=${ifFn}`)
|
|
1479
|
+
if (schema.then) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx); lines.push(`}`) }
|
|
1480
|
+
if (schema.else) { lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx); lines.push(`}`) }
|
|
1481
|
+
lines.push(`}`)
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// Close type-success block if opened
|
|
1485
|
+
if (ctx._typeBlock) {
|
|
1486
|
+
lines.push(`}`)
|
|
1487
|
+
ctx._typeBlock = false
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Ultra-fast JSON Schema validator. Beats ajv on every valid-path benchmark: 1.1x–2.7x faster validate(obj), 151x faster compilation, 5.9x faster parallel batch. Speculative validation with V8-optimized JS codegen, simdjson, multi-core. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
Binary file
|
package/src/ata.cpp
CHANGED
|
@@ -626,6 +626,12 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
626
626
|
std::vector<validation_error>& errors,
|
|
627
627
|
bool all_errors = true);
|
|
628
628
|
|
|
629
|
+
// Fast boolean-only tree walker — no error collection, no string allocation.
|
|
630
|
+
// Uses [[likely]]/[[unlikely]] hints. Returns true if valid.
|
|
631
|
+
static bool validate_fast(const schema_node_ptr& node,
|
|
632
|
+
dom::element value,
|
|
633
|
+
const compiled_schema& ctx);
|
|
634
|
+
|
|
629
635
|
// Macro for early termination
|
|
630
636
|
#define ATA_CHECK_EARLY() if (!all_errors && !errors.empty()) return
|
|
631
637
|
|
|
@@ -1200,6 +1206,221 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1200
1206
|
}
|
|
1201
1207
|
}
|
|
1202
1208
|
|
|
1209
|
+
// Fast boolean-only tree walker — stripped of all error collection.
|
|
1210
|
+
// No std::string allocation, no path tracking, no error messages.
|
|
1211
|
+
// Returns true if valid. Uses [[likely]]/[[unlikely]] branch hints.
|
|
1212
|
+
static bool validate_fast(const schema_node_ptr& node,
|
|
1213
|
+
dom::element value,
|
|
1214
|
+
const compiled_schema& ctx) {
|
|
1215
|
+
if (!node) [[unlikely]] return true;
|
|
1216
|
+
|
|
1217
|
+
if (node->boolean_schema.has_value()) [[unlikely]]
|
|
1218
|
+
return node->boolean_schema.value();
|
|
1219
|
+
|
|
1220
|
+
// $ref
|
|
1221
|
+
if (!node->ref.empty()) [[unlikely]] {
|
|
1222
|
+
auto it = ctx.defs.find(node->ref);
|
|
1223
|
+
if (it != ctx.defs.end()) {
|
|
1224
|
+
if (!validate_fast(it->second, value, ctx)) return false;
|
|
1225
|
+
} else if (node->ref == "#" && ctx.root) {
|
|
1226
|
+
if (!validate_fast(ctx.root, value, ctx)) return false;
|
|
1227
|
+
} else {
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// type
|
|
1233
|
+
if (!node->types.empty()) {
|
|
1234
|
+
bool match = false;
|
|
1235
|
+
for (const auto& t : node->types) {
|
|
1236
|
+
if (type_matches(value, t)) { match = true; break; }
|
|
1237
|
+
}
|
|
1238
|
+
if (!match) [[unlikely]] return false;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// enum
|
|
1242
|
+
if (!node->enum_values_minified.empty()) {
|
|
1243
|
+
auto val_str = canonical_json(value);
|
|
1244
|
+
bool found = false;
|
|
1245
|
+
for (const auto& ev : node->enum_values_minified) {
|
|
1246
|
+
if (ev == val_str) { found = true; break; }
|
|
1247
|
+
}
|
|
1248
|
+
if (!found) [[unlikely]] return false;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// const
|
|
1252
|
+
if (node->const_value_raw.has_value()) {
|
|
1253
|
+
if (canonical_json(value) != node->const_value_raw.value()) [[unlikely]] return false;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
auto actual_type = type_of_sv(value);
|
|
1257
|
+
|
|
1258
|
+
// Numeric
|
|
1259
|
+
if (actual_type == "integer" || actual_type == "number") {
|
|
1260
|
+
double v = to_double(value);
|
|
1261
|
+
if (node->minimum.has_value() && v < node->minimum.value()) return false;
|
|
1262
|
+
if (node->maximum.has_value() && v > node->maximum.value()) return false;
|
|
1263
|
+
if (node->exclusive_minimum.has_value() && v <= node->exclusive_minimum.value()) return false;
|
|
1264
|
+
if (node->exclusive_maximum.has_value() && v >= node->exclusive_maximum.value()) return false;
|
|
1265
|
+
if (node->multiple_of.has_value()) {
|
|
1266
|
+
double rem = std::fmod(v, node->multiple_of.value());
|
|
1267
|
+
if (std::abs(rem) > 1e-8 && std::abs(rem - node->multiple_of.value()) > 1e-8) return false;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// String
|
|
1272
|
+
if (actual_type == "string") {
|
|
1273
|
+
std::string_view sv;
|
|
1274
|
+
value.get(sv);
|
|
1275
|
+
uint64_t len = utf8_length(sv);
|
|
1276
|
+
if (node->min_length.has_value() && len < node->min_length.value()) return false;
|
|
1277
|
+
if (node->max_length.has_value() && len > node->max_length.value()) return false;
|
|
1278
|
+
if (node->compiled_pattern) {
|
|
1279
|
+
if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *node->compiled_pattern))
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
if (node->format.has_value() && !check_format(sv, node->format.value())) return false;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Array
|
|
1286
|
+
if (actual_type == "array" && value.is<dom::array>()) {
|
|
1287
|
+
dom::array arr; value.get(arr);
|
|
1288
|
+
uint64_t arr_size = 0;
|
|
1289
|
+
for ([[maybe_unused]] auto _ : arr) ++arr_size;
|
|
1290
|
+
|
|
1291
|
+
if (node->min_items.has_value() && arr_size < node->min_items.value()) return false;
|
|
1292
|
+
if (node->max_items.has_value() && arr_size > node->max_items.value()) return false;
|
|
1293
|
+
|
|
1294
|
+
if (node->unique_items) {
|
|
1295
|
+
std::set<std::string> seen;
|
|
1296
|
+
for (auto item : arr) {
|
|
1297
|
+
if (!seen.insert(canonical_json(item)).second) return false;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
{ uint64_t idx = 0;
|
|
1302
|
+
for (auto item : arr) {
|
|
1303
|
+
if (idx < node->prefix_items.size()) {
|
|
1304
|
+
if (!validate_fast(node->prefix_items[idx], item, ctx)) return false;
|
|
1305
|
+
} else if (node->items_schema) {
|
|
1306
|
+
if (!validate_fast(node->items_schema, item, ctx)) return false;
|
|
1307
|
+
}
|
|
1308
|
+
++idx;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (node->contains_schema) {
|
|
1313
|
+
uint64_t match_count = 0;
|
|
1314
|
+
for (auto item : arr) {
|
|
1315
|
+
if (validate_fast(node->contains_schema, item, ctx)) ++match_count;
|
|
1316
|
+
}
|
|
1317
|
+
uint64_t min_c = node->min_contains.value_or(1);
|
|
1318
|
+
uint64_t max_c = node->max_contains.value_or(arr_size);
|
|
1319
|
+
if (match_count < min_c || match_count > max_c) return false;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Object
|
|
1324
|
+
if (actual_type == "object" && value.is<dom::object>()) {
|
|
1325
|
+
dom::object obj; value.get(obj);
|
|
1326
|
+
|
|
1327
|
+
if (node->min_properties.has_value() || node->max_properties.has_value()) {
|
|
1328
|
+
uint64_t n = 0;
|
|
1329
|
+
for ([[maybe_unused]] auto _ : obj) ++n;
|
|
1330
|
+
if (node->min_properties.has_value() && n < node->min_properties.value()) return false;
|
|
1331
|
+
if (node->max_properties.has_value() && n > node->max_properties.value()) return false;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
for (const auto& req : node->required) {
|
|
1335
|
+
dom::element d;
|
|
1336
|
+
if (obj[req].get(d) != SUCCESS) [[unlikely]] return false;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
for (auto [key, val] : obj) {
|
|
1340
|
+
std::string_view key_sv(key);
|
|
1341
|
+
bool matched = false;
|
|
1342
|
+
|
|
1343
|
+
auto it = node->properties.find(std::string(key_sv));
|
|
1344
|
+
if (it != node->properties.end()) {
|
|
1345
|
+
if (!validate_fast(it->second, val, ctx)) return false;
|
|
1346
|
+
matched = true;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
for (const auto& pp : node->pattern_properties) {
|
|
1350
|
+
if (pp.compiled && re2::RE2::PartialMatch(
|
|
1351
|
+
re2::StringPiece(key_sv.data(), key_sv.size()), *pp.compiled)) {
|
|
1352
|
+
if (!validate_fast(pp.schema, val, ctx)) return false;
|
|
1353
|
+
matched = true;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if (!matched) {
|
|
1358
|
+
if (node->additional_properties_bool.has_value() &&
|
|
1359
|
+
!node->additional_properties_bool.value()) return false;
|
|
1360
|
+
if (node->additional_properties_schema &&
|
|
1361
|
+
!validate_fast(node->additional_properties_schema, val, ctx)) return false;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
for (const auto& [prop, deps] : node->dependent_required) {
|
|
1366
|
+
dom::element d;
|
|
1367
|
+
if (obj[prop].get(d) == SUCCESS) {
|
|
1368
|
+
for (const auto& dep : deps) {
|
|
1369
|
+
dom::element dd;
|
|
1370
|
+
if (obj[dep].get(dd) != SUCCESS) return false;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
for (const auto& [prop, schema] : node->dependent_schemas) {
|
|
1376
|
+
dom::element d;
|
|
1377
|
+
if (obj[prop].get(d) == SUCCESS) {
|
|
1378
|
+
if (!validate_fast(schema, value, ctx)) return false;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// allOf
|
|
1384
|
+
for (const auto& sub : node->all_of) {
|
|
1385
|
+
if (!validate_fast(sub, value, ctx)) return false;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// anyOf
|
|
1389
|
+
if (!node->any_of.empty()) {
|
|
1390
|
+
bool any = false;
|
|
1391
|
+
for (const auto& sub : node->any_of) {
|
|
1392
|
+
if (validate_fast(sub, value, ctx)) { any = true; break; }
|
|
1393
|
+
}
|
|
1394
|
+
if (!any) return false;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// oneOf
|
|
1398
|
+
if (!node->one_of.empty()) {
|
|
1399
|
+
int n = 0;
|
|
1400
|
+
for (const auto& sub : node->one_of) {
|
|
1401
|
+
if (validate_fast(sub, value, ctx)) ++n;
|
|
1402
|
+
if (n > 1) return false;
|
|
1403
|
+
}
|
|
1404
|
+
if (n != 1) return false;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// not
|
|
1408
|
+
if (node->not_schema) {
|
|
1409
|
+
if (validate_fast(node->not_schema, value, ctx)) return false;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// if/then/else
|
|
1413
|
+
if (node->if_schema) {
|
|
1414
|
+
if (validate_fast(node->if_schema, value, ctx)) {
|
|
1415
|
+
if (node->then_schema && !validate_fast(node->then_schema, value, ctx)) return false;
|
|
1416
|
+
} else {
|
|
1417
|
+
if (node->else_schema && !validate_fast(node->else_schema, value, ctx)) return false;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
return true;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1203
1424
|
// --- Codegen compiler ---
|
|
1204
1425
|
static void cg_compile(const schema_node* n, cg::plan& p,
|
|
1205
1426
|
std::vector<cg::ins>& out) {
|
|
@@ -1660,9 +1881,8 @@ bool is_valid_prepadded(const schema_ref& schema, const char* data, size_t lengt
|
|
|
1660
1881
|
return cg_exec(schema.impl->gen_plan, schema.impl->gen_plan.code, result.value());
|
|
1661
1882
|
}
|
|
1662
1883
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
return errors.empty();
|
|
1884
|
+
// Use fast boolean-only tree walker — no error collection overhead
|
|
1885
|
+
return validate_fast(schema.impl->root, result.value(), *schema.impl);
|
|
1666
1886
|
}
|
|
1667
1887
|
|
|
1668
1888
|
} // namespace ata
|