ata-validator 0.2.0 → 0.4.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/README.md +118 -185
- package/binding/ata_napi.cpp +610 -7
- package/binding.gyp +1 -1
- package/include/ata.h +10 -2
- package/index.js +114 -3
- package/lib/js-compiler.js +845 -0
- package/package.json +3 -2
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/src/ata.cpp +78 -11
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Compile a JSON Schema into a pure JS validator function.
|
|
4
|
+
// No eval, no new Function, CSP-safe.
|
|
5
|
+
// Returns null if the schema is too complex for JS compilation.
|
|
6
|
+
|
|
7
|
+
function compileToJS(schema, defs) {
|
|
8
|
+
if (typeof schema === 'boolean') {
|
|
9
|
+
return schema ? () => true : () => false
|
|
10
|
+
}
|
|
11
|
+
if (typeof schema !== 'object' || schema === null) return null
|
|
12
|
+
|
|
13
|
+
// Bail if schema has edge cases that JS fast path gets wrong
|
|
14
|
+
if (!defs && !codegenSafe(schema)) return null
|
|
15
|
+
|
|
16
|
+
// Collect $defs early so sub-schemas can resolve $ref
|
|
17
|
+
const rootDefs = defs || collectDefs(schema)
|
|
18
|
+
|
|
19
|
+
// Bail on features that are too complex for JS fast path
|
|
20
|
+
if (schema.patternProperties ||
|
|
21
|
+
schema.dependentSchemas ||
|
|
22
|
+
schema.propertyNames) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const checks = []
|
|
27
|
+
|
|
28
|
+
// $ref (local only)
|
|
29
|
+
if (schema.$ref) {
|
|
30
|
+
const refFn = resolveRef(schema.$ref, rootDefs)
|
|
31
|
+
if (!refFn) return null
|
|
32
|
+
checks.push(refFn)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// type
|
|
36
|
+
if (schema.type) {
|
|
37
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type]
|
|
38
|
+
checks.push(buildTypeCheck(types))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// enum
|
|
42
|
+
if (schema.enum) {
|
|
43
|
+
const vals = schema.enum
|
|
44
|
+
const primitives = vals.filter(v => v === null || typeof v !== 'object')
|
|
45
|
+
const objects = vals.filter(v => v !== null && typeof v === 'object')
|
|
46
|
+
const primSet = new Set(primitives.map(v => v === null ? 'null' : typeof v === 'string' ? 's:' + v : 'n:' + v))
|
|
47
|
+
const objStrs = objects.map(v => JSON.stringify(v))
|
|
48
|
+
checks.push((d) => {
|
|
49
|
+
// Fast primitive check
|
|
50
|
+
const key = d === null ? 'null' : typeof d === 'string' ? 's:' + d : typeof d === 'number' || typeof d === 'boolean' ? 'n:' + d : null
|
|
51
|
+
if (key !== null && primSet.has(key)) return true
|
|
52
|
+
// Slow object check
|
|
53
|
+
const ds = JSON.stringify(d)
|
|
54
|
+
for (let i = 0; i < objStrs.length; i++) {
|
|
55
|
+
if (ds === objStrs[i]) return true
|
|
56
|
+
}
|
|
57
|
+
// Also check primitives by stringify for edge cases (boolean in enum)
|
|
58
|
+
for (let i = 0; i < primitives.length; i++) {
|
|
59
|
+
if (d === primitives[i]) return true
|
|
60
|
+
}
|
|
61
|
+
return false
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// const
|
|
66
|
+
if (schema.const !== undefined) {
|
|
67
|
+
const cv = schema.const
|
|
68
|
+
if (cv === null || typeof cv !== 'object') {
|
|
69
|
+
checks.push((d) => d === cv)
|
|
70
|
+
} else {
|
|
71
|
+
const cs = JSON.stringify(cv)
|
|
72
|
+
checks.push((d) => JSON.stringify(d) === cs)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// required
|
|
77
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
78
|
+
for (const key of schema.required) {
|
|
79
|
+
checks.push((d) => typeof d === 'object' && d !== null && key in d)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// properties
|
|
84
|
+
if (schema.properties) {
|
|
85
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
86
|
+
const propCheck = compileToJS(prop, rootDefs)
|
|
87
|
+
if (!propCheck) return null // bail if sub-schema too complex
|
|
88
|
+
checks.push((d) => {
|
|
89
|
+
if (typeof d !== 'object' || d === null || !(key in d)) return true
|
|
90
|
+
return propCheck(d[key])
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// additionalProperties
|
|
96
|
+
if (schema.additionalProperties !== undefined && schema.properties) {
|
|
97
|
+
if (schema.additionalProperties === false) {
|
|
98
|
+
const allowed = new Set(Object.keys(schema.properties))
|
|
99
|
+
checks.push((d) => {
|
|
100
|
+
if (typeof d !== 'object' || d === null || Array.isArray(d)) return true
|
|
101
|
+
const keys = Object.keys(d)
|
|
102
|
+
for (let i = 0; i < keys.length; i++) {
|
|
103
|
+
if (!allowed.has(keys[i])) return false
|
|
104
|
+
}
|
|
105
|
+
return true
|
|
106
|
+
})
|
|
107
|
+
} else if (typeof schema.additionalProperties === 'object') {
|
|
108
|
+
const apCheck = compileToJS(schema.additionalProperties, rootDefs)
|
|
109
|
+
if (!apCheck) return null
|
|
110
|
+
const known = new Set(Object.keys(schema.properties || {}))
|
|
111
|
+
checks.push((d) => {
|
|
112
|
+
if (typeof d !== 'object' || d === null || Array.isArray(d)) return true
|
|
113
|
+
const keys = Object.keys(d)
|
|
114
|
+
for (let i = 0; i < keys.length; i++) {
|
|
115
|
+
if (!known.has(keys[i]) && !apCheck(d[keys[i]])) return false
|
|
116
|
+
}
|
|
117
|
+
return true
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// dependentRequired
|
|
123
|
+
if (schema.dependentRequired) {
|
|
124
|
+
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
125
|
+
checks.push((d) => {
|
|
126
|
+
if (typeof d !== 'object' || d === null || !(key in d)) return true
|
|
127
|
+
for (let i = 0; i < deps.length; i++) {
|
|
128
|
+
if (!(deps[i] in d)) return false
|
|
129
|
+
}
|
|
130
|
+
return true
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// items
|
|
136
|
+
if (schema.items) {
|
|
137
|
+
const itemCheck = compileToJS(schema.items, rootDefs)
|
|
138
|
+
if (!itemCheck) return null
|
|
139
|
+
checks.push((d) => {
|
|
140
|
+
if (!Array.isArray(d)) return true
|
|
141
|
+
for (let i = 0; i < d.length; i++) {
|
|
142
|
+
if (!itemCheck(d[i])) return false
|
|
143
|
+
}
|
|
144
|
+
return true
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// prefixItems
|
|
149
|
+
if (schema.prefixItems) {
|
|
150
|
+
const prefixChecks = []
|
|
151
|
+
for (const ps of schema.prefixItems) {
|
|
152
|
+
const pc = compileToJS(ps, rootDefs)
|
|
153
|
+
if (!pc) return null
|
|
154
|
+
prefixChecks.push(pc)
|
|
155
|
+
}
|
|
156
|
+
checks.push((d) => {
|
|
157
|
+
if (!Array.isArray(d)) return true
|
|
158
|
+
for (let i = 0; i < prefixChecks.length && i < d.length; i++) {
|
|
159
|
+
if (!prefixChecks[i](d[i])) return false
|
|
160
|
+
}
|
|
161
|
+
return true
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// contains
|
|
166
|
+
if (schema.contains) {
|
|
167
|
+
const containsCheck = compileToJS(schema.contains, rootDefs)
|
|
168
|
+
if (!containsCheck) return null
|
|
169
|
+
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
170
|
+
const maxC = schema.maxContains !== undefined ? schema.maxContains : Infinity
|
|
171
|
+
checks.push((d) => {
|
|
172
|
+
if (!Array.isArray(d)) return true
|
|
173
|
+
let count = 0
|
|
174
|
+
for (let i = 0; i < d.length; i++) {
|
|
175
|
+
if (containsCheck(d[i])) count++
|
|
176
|
+
}
|
|
177
|
+
return count >= minC && count <= maxC
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// uniqueItems — sorted-key canonical form for correct object comparison
|
|
182
|
+
if (schema.uniqueItems) {
|
|
183
|
+
const canonical = (x) => {
|
|
184
|
+
if (x === null || typeof x !== 'object') return typeof x + ':' + x
|
|
185
|
+
if (Array.isArray(x)) return '[' + x.map(canonical).join(',') + ']'
|
|
186
|
+
return '{' + Object.keys(x).sort().map(k => JSON.stringify(k) + ':' + canonical(x[k])).join(',') + '}'
|
|
187
|
+
}
|
|
188
|
+
checks.push((d) => {
|
|
189
|
+
if (!Array.isArray(d)) return true
|
|
190
|
+
const seen = new Set()
|
|
191
|
+
for (let i = 0; i < d.length; i++) {
|
|
192
|
+
const key = canonical(d[i])
|
|
193
|
+
if (seen.has(key)) return false
|
|
194
|
+
seen.add(key)
|
|
195
|
+
}
|
|
196
|
+
return true
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// numeric
|
|
201
|
+
if (schema.minimum !== undefined) {
|
|
202
|
+
const min = schema.minimum
|
|
203
|
+
checks.push((d) => typeof d !== 'number' || d >= min)
|
|
204
|
+
}
|
|
205
|
+
if (schema.maximum !== undefined) {
|
|
206
|
+
const max = schema.maximum
|
|
207
|
+
checks.push((d) => typeof d !== 'number' || d <= max)
|
|
208
|
+
}
|
|
209
|
+
if (schema.exclusiveMinimum !== undefined) {
|
|
210
|
+
const min = schema.exclusiveMinimum
|
|
211
|
+
checks.push((d) => typeof d !== 'number' || d > min)
|
|
212
|
+
}
|
|
213
|
+
if (schema.exclusiveMaximum !== undefined) {
|
|
214
|
+
const max = schema.exclusiveMaximum
|
|
215
|
+
checks.push((d) => typeof d !== 'number' || d < max)
|
|
216
|
+
}
|
|
217
|
+
if (schema.multipleOf !== undefined) {
|
|
218
|
+
const div = schema.multipleOf
|
|
219
|
+
checks.push((d) => typeof d !== 'number' || d % div === 0)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// string
|
|
223
|
+
if (schema.minLength !== undefined) {
|
|
224
|
+
const min = schema.minLength
|
|
225
|
+
checks.push((d) => typeof d !== 'string' || d.length >= min)
|
|
226
|
+
}
|
|
227
|
+
if (schema.maxLength !== undefined) {
|
|
228
|
+
const max = schema.maxLength
|
|
229
|
+
checks.push((d) => typeof d !== 'string' || d.length <= max)
|
|
230
|
+
}
|
|
231
|
+
if (schema.pattern) {
|
|
232
|
+
try {
|
|
233
|
+
const re = new RegExp(schema.pattern)
|
|
234
|
+
checks.push((d) => typeof d !== 'string' || re.test(d))
|
|
235
|
+
} catch {
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// format — hand-written fast checks
|
|
241
|
+
if (schema.format) {
|
|
242
|
+
const fc = FORMAT_CHECKS[schema.format]
|
|
243
|
+
if (fc) checks.push((d) => typeof d !== 'string' || fc(d))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// array size
|
|
247
|
+
if (schema.minItems !== undefined) {
|
|
248
|
+
const min = schema.minItems
|
|
249
|
+
checks.push((d) => !Array.isArray(d) || d.length >= min)
|
|
250
|
+
}
|
|
251
|
+
if (schema.maxItems !== undefined) {
|
|
252
|
+
const max = schema.maxItems
|
|
253
|
+
checks.push((d) => !Array.isArray(d) || d.length <= max)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// object size
|
|
257
|
+
if (schema.minProperties !== undefined) {
|
|
258
|
+
const min = schema.minProperties
|
|
259
|
+
checks.push((d) => typeof d !== 'object' || d === null || Object.keys(d).length >= min)
|
|
260
|
+
}
|
|
261
|
+
if (schema.maxProperties !== undefined) {
|
|
262
|
+
const max = schema.maxProperties
|
|
263
|
+
checks.push((d) => typeof d !== 'object' || d === null || Object.keys(d).length <= max)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// allOf
|
|
267
|
+
if (schema.allOf) {
|
|
268
|
+
const subs = []
|
|
269
|
+
for (const s of schema.allOf) {
|
|
270
|
+
const fn = compileToJS(s, rootDefs)
|
|
271
|
+
if (!fn) return null
|
|
272
|
+
subs.push(fn)
|
|
273
|
+
}
|
|
274
|
+
checks.push((d) => {
|
|
275
|
+
for (let i = 0; i < subs.length; i++) {
|
|
276
|
+
if (!subs[i](d)) return false
|
|
277
|
+
}
|
|
278
|
+
return true
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// anyOf
|
|
283
|
+
if (schema.anyOf) {
|
|
284
|
+
const subs = []
|
|
285
|
+
for (const s of schema.anyOf) {
|
|
286
|
+
const fn = compileToJS(s, rootDefs)
|
|
287
|
+
if (!fn) return null
|
|
288
|
+
subs.push(fn)
|
|
289
|
+
}
|
|
290
|
+
checks.push((d) => {
|
|
291
|
+
for (let i = 0; i < subs.length; i++) {
|
|
292
|
+
if (subs[i](d)) return true
|
|
293
|
+
}
|
|
294
|
+
return false
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// oneOf
|
|
299
|
+
if (schema.oneOf) {
|
|
300
|
+
const subs = []
|
|
301
|
+
for (const s of schema.oneOf) {
|
|
302
|
+
const fn = compileToJS(s, rootDefs)
|
|
303
|
+
if (!fn) return null
|
|
304
|
+
subs.push(fn)
|
|
305
|
+
}
|
|
306
|
+
checks.push((d) => {
|
|
307
|
+
let count = 0
|
|
308
|
+
for (let i = 0; i < subs.length; i++) {
|
|
309
|
+
if (subs[i](d)) count++
|
|
310
|
+
if (count > 1) return false
|
|
311
|
+
}
|
|
312
|
+
return count === 1
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// not
|
|
317
|
+
if (schema.not) {
|
|
318
|
+
const notFn = compileToJS(schema.not, rootDefs)
|
|
319
|
+
if (!notFn) return null
|
|
320
|
+
checks.push((d) => !notFn(d))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// if/then/else
|
|
324
|
+
if (schema.if) {
|
|
325
|
+
const ifFn = compileToJS(schema.if, rootDefs)
|
|
326
|
+
if (!ifFn) return null
|
|
327
|
+
const thenFn = schema.then ? compileToJS(schema.then, rootDefs) : null
|
|
328
|
+
const elseFn = schema.else ? compileToJS(schema.else, rootDefs) : null
|
|
329
|
+
if (schema.then && !thenFn) return null
|
|
330
|
+
if (schema.else && !elseFn) return null
|
|
331
|
+
checks.push((d) => {
|
|
332
|
+
if (ifFn(d)) {
|
|
333
|
+
return thenFn ? thenFn(d) : true
|
|
334
|
+
} else {
|
|
335
|
+
return elseFn ? elseFn(d) : true
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (checks.length === 0) return () => true
|
|
341
|
+
if (checks.length === 1) return checks[0]
|
|
342
|
+
|
|
343
|
+
// Flatten to a single function — V8 JIT will inline
|
|
344
|
+
return (data) => {
|
|
345
|
+
for (let i = 0; i < checks.length; i++) {
|
|
346
|
+
if (!checks[i](data)) return false
|
|
347
|
+
}
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function collectDefs(schema) {
|
|
353
|
+
const defs = {}
|
|
354
|
+
const raw = schema.$defs || schema.definitions
|
|
355
|
+
if (raw && typeof raw === 'object') {
|
|
356
|
+
for (const [name, def] of Object.entries(raw)) {
|
|
357
|
+
// Lazy compile with circular guard — return true (permissive) on cycle
|
|
358
|
+
let cached = undefined
|
|
359
|
+
defs[name] = {
|
|
360
|
+
get fn() {
|
|
361
|
+
if (cached === undefined) {
|
|
362
|
+
cached = null // sentinel: compilation in progress
|
|
363
|
+
cached = compileToJS(def, defs)
|
|
364
|
+
}
|
|
365
|
+
// cached===null means circular ref or compile failure — be permissive
|
|
366
|
+
return cached || (() => true)
|
|
367
|
+
},
|
|
368
|
+
raw: def
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return defs
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function resolveRef(ref, defs) {
|
|
376
|
+
if (!defs) return null
|
|
377
|
+
// #/$defs/Name or #/definitions/Name
|
|
378
|
+
const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
379
|
+
if (!m) return null
|
|
380
|
+
const name = m[1]
|
|
381
|
+
const entry = defs[name]
|
|
382
|
+
if (!entry) return null
|
|
383
|
+
return (d) => {
|
|
384
|
+
const fn = entry.fn
|
|
385
|
+
return fn ? fn(d) : true
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function buildTypeCheck(types) {
|
|
390
|
+
if (types.length === 1) {
|
|
391
|
+
return TYPE_CHECKS[types[0]] || (() => true)
|
|
392
|
+
}
|
|
393
|
+
const fns = types.map(t => TYPE_CHECKS[t]).filter(Boolean)
|
|
394
|
+
return (d) => {
|
|
395
|
+
for (let i = 0; i < fns.length; i++) {
|
|
396
|
+
if (fns[i](d)) return true
|
|
397
|
+
}
|
|
398
|
+
return false
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const TYPE_CHECKS = {
|
|
403
|
+
string: (d) => typeof d === 'string',
|
|
404
|
+
number: (d) => typeof d === 'number' && isFinite(d),
|
|
405
|
+
integer: (d) => Number.isInteger(d),
|
|
406
|
+
boolean: (d) => typeof d === 'boolean',
|
|
407
|
+
null: (d) => d === null,
|
|
408
|
+
array: (d) => Array.isArray(d),
|
|
409
|
+
object: (d) => typeof d === 'object' && d !== null && !Array.isArray(d),
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const FORMAT_CHECKS = {
|
|
413
|
+
email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1 },
|
|
414
|
+
date: (s) => s.length === 10 && /^\d{4}-\d{2}-\d{2}$/.test(s),
|
|
415
|
+
uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
|
|
416
|
+
ipv4: (s) => { const p = s.split('.'); return p.length === 4 && p.every(n => { const v = +n; return v >= 0 && v <= 255 && String(v) === n }) },
|
|
417
|
+
hostname: (s) => s.length > 0 && s.length <= 253 && /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i.test(s),
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Dangerous JS property names that exist on Object.prototype
|
|
421
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
|
|
422
|
+
'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString'])
|
|
423
|
+
|
|
424
|
+
// Recursively check if a schema can be safely compiled to JS codegen.
|
|
425
|
+
// Returns false if any sub-schema contains features codegen gets wrong.
|
|
426
|
+
function codegenSafe(schema) {
|
|
427
|
+
if (typeof schema === 'boolean') return true
|
|
428
|
+
if (typeof schema !== 'object' || schema === null) return true
|
|
429
|
+
|
|
430
|
+
// Boolean sub-schemas anywhere cause bail — codegen doesn't handle schema=false correctly
|
|
431
|
+
if (schema.items === false || schema.items === true) return false
|
|
432
|
+
if (schema.additionalProperties === true) return true // permissive — fine
|
|
433
|
+
if (schema.properties) {
|
|
434
|
+
for (const v of Object.values(schema.properties)) {
|
|
435
|
+
if (typeof v === 'boolean') return false
|
|
436
|
+
if (!codegenSafe(v)) return false
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Required keys that collide with Object.prototype
|
|
441
|
+
if (schema.required) {
|
|
442
|
+
for (const k of schema.required) {
|
|
443
|
+
if (UNSAFE_KEYS.has(k)) return false
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Unicode property escapes in pattern need 'u' flag — codegen uses RegExp without it
|
|
448
|
+
if (schema.pattern && /\\[pP]\{/.test(schema.pattern)) return false
|
|
449
|
+
|
|
450
|
+
// $ref — bail entirely from codegen; ref resolution has too many edge cases
|
|
451
|
+
if (schema.$ref) return false
|
|
452
|
+
|
|
453
|
+
// additionalProperties as schema — bail entirely, too many edge cases with allOf interaction
|
|
454
|
+
if (typeof schema.additionalProperties === 'object') return false
|
|
455
|
+
if (schema.additionalProperties === false && !schema.properties) return false
|
|
456
|
+
|
|
457
|
+
// propertyNames: false — codegen doesn't handle this
|
|
458
|
+
if (schema.propertyNames === false) return false
|
|
459
|
+
|
|
460
|
+
// Recurse into sub-schemas — bail on boolean schemas in any position
|
|
461
|
+
const subs = [
|
|
462
|
+
schema.items, schema.contains, schema.not,
|
|
463
|
+
schema.if, schema.then, schema.else,
|
|
464
|
+
...(schema.prefixItems || []),
|
|
465
|
+
...(schema.allOf || []),
|
|
466
|
+
...(schema.anyOf || []),
|
|
467
|
+
...(schema.oneOf || []),
|
|
468
|
+
]
|
|
469
|
+
if (typeof schema.additionalProperties === 'object') subs.push(schema.additionalProperties)
|
|
470
|
+
for (const s of subs) {
|
|
471
|
+
if (s === undefined || s === null) continue
|
|
472
|
+
if (typeof s === 'boolean') return false // boolean sub-schema
|
|
473
|
+
if (!codegenSafe(s)) return false
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return true
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// --- Codegen mode: generates a single Function (NOT CSP-safe) ---
|
|
480
|
+
// This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
|
|
481
|
+
function compileToJSCodegen(schema) {
|
|
482
|
+
if (typeof schema === 'boolean') return schema ? () => true : () => false
|
|
483
|
+
if (typeof schema !== 'object' || schema === null) return null
|
|
484
|
+
|
|
485
|
+
// Bail if schema contains features that codegen can't handle correctly
|
|
486
|
+
if (!codegenSafe(schema)) return null
|
|
487
|
+
|
|
488
|
+
// Collect defs for $ref resolution
|
|
489
|
+
const rootDefs = schema.$defs || schema.definitions || null
|
|
490
|
+
|
|
491
|
+
// Bail only on truly unsupported features
|
|
492
|
+
if (schema.patternProperties ||
|
|
493
|
+
schema.dependentSchemas ||
|
|
494
|
+
schema.propertyNames) return null
|
|
495
|
+
|
|
496
|
+
const ctx = { varCounter: 0, helpers: [], helperCode: [], rootDefs, refStack: new Set() }
|
|
497
|
+
const lines = []
|
|
498
|
+
genCode(schema, 'd', lines, ctx)
|
|
499
|
+
if (lines.length === 0) return () => true
|
|
500
|
+
|
|
501
|
+
const body = (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
|
|
502
|
+
lines.join('\n ') + '\n return true'
|
|
503
|
+
try {
|
|
504
|
+
return new Function('d', body)
|
|
505
|
+
} catch {
|
|
506
|
+
return null
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// knownType: if parent already verified the type, skip redundant guards.
|
|
511
|
+
// 'object' = we know v is a non-null non-array object
|
|
512
|
+
// 'array' = we know v is an array
|
|
513
|
+
// 'string' / 'number' / 'integer' = we know the primitive type
|
|
514
|
+
function genCode(schema, v, lines, ctx, knownType) {
|
|
515
|
+
if (typeof schema !== 'object' || schema === null) return
|
|
516
|
+
|
|
517
|
+
// $ref — guard against circular references
|
|
518
|
+
if (schema.$ref && ctx.rootDefs) {
|
|
519
|
+
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
520
|
+
if (m && ctx.rootDefs[m[1]]) {
|
|
521
|
+
if (ctx.refStack.has(schema.$ref)) return // circular — bail
|
|
522
|
+
ctx.refStack.add(schema.$ref)
|
|
523
|
+
genCode(ctx.rootDefs[m[1]], v, lines, ctx, knownType)
|
|
524
|
+
ctx.refStack.delete(schema.$ref)
|
|
525
|
+
} else {
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Determine the single known type after this schema's type check
|
|
531
|
+
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
532
|
+
let effectiveType = knownType
|
|
533
|
+
if (types) {
|
|
534
|
+
if (!knownType) {
|
|
535
|
+
// Emit the type check
|
|
536
|
+
const conds = types.map(t => {
|
|
537
|
+
switch (t) {
|
|
538
|
+
case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
539
|
+
case 'array': return `Array.isArray(${v})`
|
|
540
|
+
case 'string': return `typeof ${v}==='string'`
|
|
541
|
+
case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
|
|
542
|
+
case 'integer': return `Number.isInteger(${v})`
|
|
543
|
+
case 'boolean': return `typeof ${v}==='boolean'`
|
|
544
|
+
case 'null': return `${v}===null`
|
|
545
|
+
default: return 'true'
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
lines.push(`if(!(${conds.join('||')}))return false`)
|
|
549
|
+
}
|
|
550
|
+
// If single type, downstream checks can skip guards
|
|
551
|
+
if (types.length === 1) effectiveType = types[0]
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const isObj = effectiveType === 'object'
|
|
555
|
+
const isArr = effectiveType === 'array'
|
|
556
|
+
const isStr = effectiveType === 'string'
|
|
557
|
+
const isNum = effectiveType === 'number' || effectiveType === 'integer'
|
|
558
|
+
const objGuard = isObj ? '' : `typeof ${v}==='object'&&${v}!==null&&`
|
|
559
|
+
const objCheck = isObj ? '' : `if(typeof ${v}!=='object'||${v}===null)return false;`
|
|
560
|
+
|
|
561
|
+
// enum
|
|
562
|
+
if (schema.enum) {
|
|
563
|
+
const vals = schema.enum
|
|
564
|
+
const primitives = vals.filter(v => v === null || typeof v !== 'object')
|
|
565
|
+
const objects = vals.filter(v => v !== null && typeof v === 'object')
|
|
566
|
+
const primChecks = primitives.map(p => `${v}===${JSON.stringify(p)}`).join('||')
|
|
567
|
+
const objChecks = objects.map(o => `JSON.stringify(${v})===${JSON.stringify(JSON.stringify(o))}`).join('||')
|
|
568
|
+
const allChecks = [primChecks, objChecks].filter(Boolean).join('||')
|
|
569
|
+
lines.push(`if(!(${allChecks || 'false'}))return false`)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// const
|
|
573
|
+
if (schema.const !== undefined) {
|
|
574
|
+
const cv = schema.const
|
|
575
|
+
if (cv === null || typeof cv !== 'object') {
|
|
576
|
+
lines.push(`if(${v}!==${JSON.stringify(cv)})return false`)
|
|
577
|
+
} else {
|
|
578
|
+
lines.push(`if(JSON.stringify(${v})!==${JSON.stringify(JSON.stringify(cv))})return false`)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Collect required keys so property checks can skip 'in' guard
|
|
583
|
+
const requiredSet = new Set(schema.required || [])
|
|
584
|
+
|
|
585
|
+
// required + property hoisting via destructuring.
|
|
586
|
+
// V8 TurboFan optimizes destructuring into a single batch hidden-class-aware read.
|
|
587
|
+
// `d.key !== undefined` is faster than `'key' in d` (no prototype chain walk).
|
|
588
|
+
const hoisted = {} // key -> local var name
|
|
589
|
+
if (schema.required && schema.properties && isObj) {
|
|
590
|
+
const destructKeys = []
|
|
591
|
+
const reqChecks = []
|
|
592
|
+
for (const key of schema.required) {
|
|
593
|
+
if (schema.properties[key]) {
|
|
594
|
+
const localVar = `_h${ctx.varCounter++}`
|
|
595
|
+
hoisted[key] = localVar
|
|
596
|
+
destructKeys.push(`${JSON.stringify(key)}:${localVar}`)
|
|
597
|
+
reqChecks.push(`${localVar}===undefined`)
|
|
598
|
+
} else {
|
|
599
|
+
// Required but no property schema — just check existence
|
|
600
|
+
reqChecks.push(`${v}[${JSON.stringify(key)}]===undefined`)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (destructKeys.length > 0) {
|
|
604
|
+
lines.push(`const{${destructKeys.join(',')}}=${v}`)
|
|
605
|
+
}
|
|
606
|
+
if (reqChecks.length > 0) {
|
|
607
|
+
lines.push(`if(${reqChecks.join('||')})return false`)
|
|
608
|
+
}
|
|
609
|
+
} else if (schema.required) {
|
|
610
|
+
if (isObj) {
|
|
611
|
+
const checks = schema.required.map(key => `${v}[${JSON.stringify(key)}]===undefined`)
|
|
612
|
+
lines.push(`if(${checks.join('||')})return false`)
|
|
613
|
+
} else {
|
|
614
|
+
for (const key of schema.required) {
|
|
615
|
+
lines.push(`if(typeof ${v}!=='object'||${v}===null||!(${JSON.stringify(key)} in ${v}))return false`)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// numeric — skip type guard if known numeric
|
|
621
|
+
if (schema.minimum !== undefined) lines.push(isNum ? `if(${v}<${schema.minimum})return false` : `if(typeof ${v}==='number'&&${v}<${schema.minimum})return false`)
|
|
622
|
+
if (schema.maximum !== undefined) lines.push(isNum ? `if(${v}>${schema.maximum})return false` : `if(typeof ${v}==='number'&&${v}>${schema.maximum})return false`)
|
|
623
|
+
if (schema.exclusiveMinimum !== undefined) lines.push(isNum ? `if(${v}<=${schema.exclusiveMinimum})return false` : `if(typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum})return false`)
|
|
624
|
+
if (schema.exclusiveMaximum !== undefined) lines.push(isNum ? `if(${v}>=${schema.exclusiveMaximum})return false` : `if(typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum})return false`)
|
|
625
|
+
if (schema.multipleOf !== undefined) lines.push(isNum ? `if(${v}%${schema.multipleOf}!==0)return false` : `if(typeof ${v}==='number'&&${v}%${schema.multipleOf}!==0)return false`)
|
|
626
|
+
|
|
627
|
+
// string — skip type guard if known string
|
|
628
|
+
if (schema.minLength !== undefined) lines.push(isStr ? `if(${v}.length<${schema.minLength})return false` : `if(typeof ${v}==='string'&&${v}.length<${schema.minLength})return false`)
|
|
629
|
+
if (schema.maxLength !== undefined) lines.push(isStr ? `if(${v}.length>${schema.maxLength})return false` : `if(typeof ${v}==='string'&&${v}.length>${schema.maxLength})return false`)
|
|
630
|
+
|
|
631
|
+
// array size — skip guard if known array
|
|
632
|
+
if (schema.minItems !== undefined) lines.push(isArr ? `if(${v}.length<${schema.minItems})return false` : `if(Array.isArray(${v})&&${v}.length<${schema.minItems})return false`)
|
|
633
|
+
if (schema.maxItems !== undefined) lines.push(isArr ? `if(${v}.length>${schema.maxItems})return false` : `if(Array.isArray(${v})&&${v}.length>${schema.maxItems})return false`)
|
|
634
|
+
|
|
635
|
+
// object size
|
|
636
|
+
if (schema.minProperties !== undefined) lines.push(`if(${objGuard}Object.keys(${v}).length<${schema.minProperties})return false`)
|
|
637
|
+
if (schema.maxProperties !== undefined) lines.push(`if(${objGuard}Object.keys(${v}).length>${schema.maxProperties})return false`)
|
|
638
|
+
|
|
639
|
+
if (schema.pattern) {
|
|
640
|
+
// Use RegExp constructor via helper to avoid injection from untrusted patterns
|
|
641
|
+
const ri = ctx.varCounter++
|
|
642
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
|
|
643
|
+
lines.push(isStr ? `if(!_re${ri}.test(${v}))return false` : `if(typeof ${v}==='string'&&!_re${ri}.test(${v}))return false`)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (schema.format) {
|
|
647
|
+
const fc = FORMAT_CODEGEN[schema.format]
|
|
648
|
+
if (fc) lines.push(fc(v, isStr))
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// uniqueItems — fast path for primitive arrays (no JSON.stringify)
|
|
652
|
+
if (schema.uniqueItems) {
|
|
653
|
+
const si = ctx.varCounter++
|
|
654
|
+
// If items schema is a primitive type, skip JSON.stringify entirely
|
|
655
|
+
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
656
|
+
const isPrimItems = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
657
|
+
// For objects: use sorted-key JSON.stringify to handle key order correctly per spec
|
|
658
|
+
const inner = isPrimItems
|
|
659
|
+
? `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])}`
|
|
660
|
+
: `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
|
+
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// additionalProperties
|
|
665
|
+
if (schema.additionalProperties === false && schema.properties) {
|
|
666
|
+
const allowed = Object.keys(schema.properties).map(k => `'${esc(k)}'`).join(',')
|
|
667
|
+
const ci = ctx.varCounter++
|
|
668
|
+
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]))return false`
|
|
669
|
+
lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// dependentRequired
|
|
673
|
+
if (schema.dependentRequired) {
|
|
674
|
+
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
675
|
+
const depChecks = deps.map(d => `!('${esc(d)}' in ${v})`).join('||')
|
|
676
|
+
lines.push(`if(${objGuard}'${esc(key)}' in ${v}&&(${depChecks}))return false`)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// properties — use hoisted vars for required props, hoist optional to locals too
|
|
681
|
+
if (schema.properties) {
|
|
682
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
683
|
+
if (requiredSet.has(key) && isObj) {
|
|
684
|
+
// Required + type:object — property exists, use destructured local
|
|
685
|
+
genCode(prop, hoisted[key] || `${v}[${JSON.stringify(key)}]`, lines, ctx)
|
|
686
|
+
} else if (isObj) {
|
|
687
|
+
// Optional — hoist to local, check undefined
|
|
688
|
+
const oi = ctx.varCounter++
|
|
689
|
+
const local = `_o${oi}`
|
|
690
|
+
lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined){`)
|
|
691
|
+
genCode(prop, local, lines, ctx)
|
|
692
|
+
lines.push(`}}`)
|
|
693
|
+
} else {
|
|
694
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}){`)
|
|
695
|
+
genCode(prop, `${v}[${JSON.stringify(key)}]`, lines, ctx)
|
|
696
|
+
lines.push(`}`)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// items — pass known type info to children
|
|
702
|
+
if (schema.items) {
|
|
703
|
+
const idx = `_j${ctx.varCounter}`
|
|
704
|
+
const elem = `_e${ctx.varCounter}`
|
|
705
|
+
ctx.varCounter++
|
|
706
|
+
lines.push(isArr
|
|
707
|
+
? `for(let ${idx}=0;${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`
|
|
708
|
+
: `if(Array.isArray(${v})){for(let ${idx}=0;${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
|
|
709
|
+
genCode(schema.items, elem, lines, ctx)
|
|
710
|
+
lines.push(isArr ? `}` : `}}`)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// prefixItems
|
|
714
|
+
if (schema.prefixItems) {
|
|
715
|
+
for (let i = 0; i < schema.prefixItems.length; i++) {
|
|
716
|
+
const elem = `_p${ctx.varCounter}_${i}`
|
|
717
|
+
lines.push(isArr
|
|
718
|
+
? `if(${v}.length>${i}){const ${elem}=${v}[${i}]`
|
|
719
|
+
: `if(Array.isArray(${v})&&${v}.length>${i}){const ${elem}=${v}[${i}]`)
|
|
720
|
+
genCode(schema.prefixItems[i], elem, lines, ctx)
|
|
721
|
+
lines.push(`}`)
|
|
722
|
+
}
|
|
723
|
+
ctx.varCounter++
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// contains — use helper function to avoid try/catch overhead
|
|
727
|
+
if (schema.contains) {
|
|
728
|
+
const ci = ctx.varCounter++
|
|
729
|
+
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
730
|
+
const maxC = schema.maxContains !== undefined ? schema.maxContains : Infinity
|
|
731
|
+
const subLines = []
|
|
732
|
+
genCode(schema.contains, `_cv`, subLines, ctx)
|
|
733
|
+
const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
|
|
734
|
+
const guard = isArr ? '' : `if(!Array.isArray(${v})){}else `
|
|
735
|
+
lines.push(`${guard}{const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0`)
|
|
736
|
+
lines.push(`for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
|
|
737
|
+
if (maxC === Infinity) {
|
|
738
|
+
lines.push(`if(_cc${ci}<${minC})return false}`)
|
|
739
|
+
} else {
|
|
740
|
+
lines.push(`if(_cc${ci}<${minC}||_cc${ci}>${maxC})return false}`)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// allOf — pass known type through
|
|
745
|
+
if (schema.allOf) {
|
|
746
|
+
for (const sub of schema.allOf) {
|
|
747
|
+
genCode(sub, v, lines, ctx, effectiveType)
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// anyOf — need function wrappers since genCode uses return false
|
|
752
|
+
if (schema.anyOf) {
|
|
753
|
+
const fns = []
|
|
754
|
+
for (let i = 0; i < schema.anyOf.length; i++) {
|
|
755
|
+
const subLines = []
|
|
756
|
+
genCode(schema.anyOf[i], '_av', subLines, ctx)
|
|
757
|
+
if (subLines.length === 0) {
|
|
758
|
+
fns.push(`function(_av){return true}`)
|
|
759
|
+
} else {
|
|
760
|
+
fns.push(`function(_av){${subLines.join(';')};return true}`)
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const fi = ctx.varCounter++
|
|
764
|
+
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})return false}`)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// oneOf
|
|
768
|
+
if (schema.oneOf) {
|
|
769
|
+
const fns = []
|
|
770
|
+
for (let i = 0; i < schema.oneOf.length; i++) {
|
|
771
|
+
const subLines = []
|
|
772
|
+
genCode(schema.oneOf[i], '_ov', subLines, ctx)
|
|
773
|
+
if (subLines.length === 0) {
|
|
774
|
+
fns.push(`function(_ov){return true}`)
|
|
775
|
+
} else {
|
|
776
|
+
fns.push(`function(_ov){${subLines.join(';')};return true}`)
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const fi = ctx.varCounter++
|
|
780
|
+
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)return false}if(_oc${fi}!==1)return false}`)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// not
|
|
784
|
+
if (schema.not) {
|
|
785
|
+
const subLines = []
|
|
786
|
+
genCode(schema.not, '_nv', subLines, ctx)
|
|
787
|
+
if (subLines.length === 0) {
|
|
788
|
+
lines.push(`return false`) // not:{} means nothing is valid
|
|
789
|
+
} else {
|
|
790
|
+
const fi = ctx.varCounter++
|
|
791
|
+
lines.push(`{const _nf${fi}=(function(_nv){${subLines.join(';')};return true});if(_nf${fi}(${v}))return false}`)
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// if/then/else
|
|
796
|
+
if (schema.if) {
|
|
797
|
+
const ifLines = []
|
|
798
|
+
genCode(schema.if, '_iv', ifLines, ctx)
|
|
799
|
+
const fi = ctx.varCounter++
|
|
800
|
+
const ifFn = ifLines.length === 0
|
|
801
|
+
? `function(_iv){return true}`
|
|
802
|
+
: `function(_iv){${ifLines.join(';')};return true}`
|
|
803
|
+
|
|
804
|
+
let thenFn = 'null', elseFn = 'null'
|
|
805
|
+
if (schema.then) {
|
|
806
|
+
const thenLines = []
|
|
807
|
+
genCode(schema.then, '_tv', thenLines, ctx)
|
|
808
|
+
thenFn = thenLines.length === 0
|
|
809
|
+
? `function(_tv){return true}`
|
|
810
|
+
: `function(_tv){${thenLines.join(';')};return true}`
|
|
811
|
+
}
|
|
812
|
+
if (schema.else) {
|
|
813
|
+
const elseLines = []
|
|
814
|
+
genCode(schema.else, '_ev', elseLines, ctx)
|
|
815
|
+
elseFn = elseLines.length === 0
|
|
816
|
+
? `function(_ev){return true}`
|
|
817
|
+
: `function(_ev){${elseLines.join(';')};return true}`
|
|
818
|
+
}
|
|
819
|
+
lines.push(`{const _if${fi}=${ifFn};const _th${fi}=${thenFn};const _el${fi}=${elseFn}`)
|
|
820
|
+
lines.push(`if(_if${fi}(${v})){if(_th${fi}&&!_th${fi}(${v}))return false}else{if(_el${fi}&&!_el${fi}(${v}))return false}}`)
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const FORMAT_CODEGEN = {
|
|
825
|
+
email: (v, isStr) => {
|
|
826
|
+
const guard = isStr ? '' : `typeof ${v}==='string'&&`
|
|
827
|
+
return isStr
|
|
828
|
+
? `{const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
|
|
829
|
+
: `if(typeof ${v}==='string'){const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
|
|
830
|
+
},
|
|
831
|
+
date: (v, isStr) => isStr
|
|
832
|
+
? `if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false`
|
|
833
|
+
: `if(typeof ${v}==='string'&&(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v})))return false`,
|
|
834
|
+
uuid: (v, isStr) => isStr
|
|
835
|
+
? `if(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v}))return false`
|
|
836
|
+
: `if(typeof ${v}==='string'&&(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v})))return false`,
|
|
837
|
+
ipv4: (v, isStr) => isStr
|
|
838
|
+
? `{const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`
|
|
839
|
+
: `if(typeof ${v}==='string'){const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`,
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Safe key escaping: use JSON.stringify to handle all special chars (newlines, null bytes, etc.)
|
|
843
|
+
function esc(s) { return JSON.stringify(s).slice(1, -1) }
|
|
844
|
+
|
|
845
|
+
module.exports = { compileToJS, compileToJSCodegen }
|