ata-validator 0.10.3 → 0.11.1
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 +50 -13
- package/bin/ata.js +144 -0
- package/index.d.ts +8 -0
- package/index.js +91 -11
- package/lib/js-compiler.js +130 -52
- package/lib/ts-gen.js +164 -0
- package/package.json +7 -2
- package/prebuilds/ata-darwin-arm64/node-napi-v10.node +0 -0
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/prebuilds/ata-linux-arm64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-arm64-musl/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-x64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-x64-musl/node-napi-v10.node +0 -0
- package/prebuilds/ata-win32-x64/node-napi-v10.node +0 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjson/simdjson). Multi-core parallel validation, RE2 regex, codegen bytecode engine. Standard Schema V1 compatible.
|
|
4
4
|
|
|
5
|
-
**[ata-validator.com](https://ata-validator.com)** | **[API Docs](docs/API.md)** | **[Contributing](CONTRIBUTING.md)**
|
|
5
|
+
**[ata-validator.com](https://ata-validator.com)** | **[API Docs](docs/API.md)** | **[Migrate from ajv](docs/migration-from-ajv.md)** | **[Contributing](CONTRIBUTING.md)**
|
|
6
6
|
|
|
7
7
|
## Performance
|
|
8
8
|
|
|
@@ -211,29 +211,66 @@ const v = new Validator(schema, {
|
|
|
211
211
|
coerceTypes: true, // "42" → 42 for integer fields
|
|
212
212
|
removeAdditional: true, // strip properties not in schema
|
|
213
213
|
schemas: [otherSchema], // cross-schema $ref registry
|
|
214
|
+
abortEarly: true, // skip detailed error collection on failure (~4x faster on invalid data)
|
|
214
215
|
});
|
|
215
216
|
```
|
|
216
217
|
|
|
217
|
-
|
|
218
|
+
`abortEarly` returns a shared `{ valid: false, errors: [{ message: 'validation failed' }] }` on failure instead of running the detailed error collector. Useful when the caller only needs a pass/fail decision (Fastify route guards, high-throughput gatekeepers, request rejection at the edge).
|
|
218
219
|
|
|
219
|
-
|
|
220
|
+
### Build-time compile (`ata compile`)
|
|
221
|
+
|
|
222
|
+
The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser — typical output is ~1 KB gzipped compared to ~27 KB for the full runtime.
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
npx ata compile schemas/user.json -o src/generated/user.validator.mjs
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The CLI emits two files: the validator itself and a paired `.d.mts` (or `.d.cts`) with the inferred TypeScript type plus an `isValid` type predicate.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { isValid, validate, type User } from './user.validator.mjs'
|
|
232
|
+
|
|
233
|
+
const incoming: unknown = JSON.parse(req.body)
|
|
234
|
+
|
|
235
|
+
if (isValid(incoming)) {
|
|
236
|
+
// TypeScript narrows to User here
|
|
237
|
+
incoming.id // number
|
|
238
|
+
incoming.role // 'admin' | 'user' | 'guest' | undefined
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const r = validate(incoming)
|
|
242
|
+
// { valid: true, errors: [] } | { valid: false, errors: ValidationError[] }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
CLI options:
|
|
246
|
+
|
|
247
|
+
| Flag | Default | Description |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| `-o, --output <file>` | `<schema>.validator.mjs` | Output path |
|
|
250
|
+
| `-f, --format <fmt>` | `esm` | `esm` or `cjs` |
|
|
251
|
+
| `--name <TypeName>` | from filename | Root type name in the `.d.ts` |
|
|
252
|
+
| `--abort-early` | off | Generate the stub-error variant (~0.5 KB gzipped) |
|
|
253
|
+
| `--no-types` | off | Skip the `.d.mts` / `.d.cts` output |
|
|
254
|
+
|
|
255
|
+
Typical bundle sizes (10-field user schema, gzipped):
|
|
256
|
+
|
|
257
|
+
| Variant | Size | Notes |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `ata-validator` runtime | ~27 KB | Full compiler + all keywords |
|
|
260
|
+
| `ata compile` (standard) | **~1.1 KB** | Validator + detailed error collector |
|
|
261
|
+
| `ata compile --abort-early` | **~0.5 KB** | Validator + stub errors only |
|
|
262
|
+
|
|
263
|
+
Programmatic API if you prefer to script it:
|
|
220
264
|
|
|
221
265
|
```javascript
|
|
222
266
|
const fs = require('fs');
|
|
267
|
+
const { Validator } = require('ata-validator');
|
|
223
268
|
|
|
224
|
-
// Build phase (once)
|
|
225
269
|
const v = new Validator(schema);
|
|
226
|
-
fs.writeFileSync('./
|
|
227
|
-
|
|
228
|
-
// Read phase (every startup) - 0.6μs per schema, pure JS
|
|
229
|
-
const v2 = Validator.fromStandalone(require('./compiled.js'), schema);
|
|
230
|
-
|
|
231
|
-
// Bundle multiple schemas - deduplicated, single file
|
|
232
|
-
fs.writeFileSync('./bundle.js', Validator.bundleCompact(schemas));
|
|
233
|
-
const validators = Validator.loadBundle(require('./bundle.js'), schemas);
|
|
270
|
+
fs.writeFileSync('./user.validator.mjs', v.toStandaloneModule({ format: 'esm' }));
|
|
234
271
|
```
|
|
235
272
|
|
|
236
|
-
**Fastify startup (
|
|
273
|
+
**Fastify startup (10 routes cold): ajv 12.6ms → ata 0.5ms (24x faster boot, no build step required)**
|
|
237
274
|
|
|
238
275
|
### Standard Schema V1
|
|
239
276
|
|
package/bin/ata.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function usage() {
|
|
8
|
+
process.stdout.write(`ata-validator CLI
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
ata compile <schema-file> [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-o, --output <file> Output path. Default: <schema-file>.validator.mjs
|
|
15
|
+
-f, --format <fmt> Module format: esm | cjs. Default: esm
|
|
16
|
+
--name <TypeName> Name of the top-level type in .d.ts. Default: inferred from filename
|
|
17
|
+
--no-types Skip .d.ts generation
|
|
18
|
+
--abort-early Use stub errors (smallest bundle)
|
|
19
|
+
-h, --help Show this message
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
ata compile schemas/user.json -o src/generated/user.validator.mjs
|
|
23
|
+
ata compile schemas/user.json --format cjs -o dist/user.validator.cjs
|
|
24
|
+
ata compile schemas/public-api.json --abort-early -o dist/api.mjs
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const out = { _: [], opts: {} };
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const a = argv[i];
|
|
32
|
+
if (a === '-h' || a === '--help') { out.opts.help = true; continue; }
|
|
33
|
+
if (a === '-o' || a === '--output') { out.opts.output = argv[++i]; continue; }
|
|
34
|
+
if (a === '-f' || a === '--format') { out.opts.format = argv[++i]; continue; }
|
|
35
|
+
if (a === '--name') { out.opts.name = argv[++i]; continue; }
|
|
36
|
+
if (a === '--no-types') { out.opts.types = false; continue; }
|
|
37
|
+
if (a === '--abort-early') { out.opts.abortEarly = true; continue; }
|
|
38
|
+
if (a.startsWith('-')) { throw new Error(`Unknown option: ${a}`); }
|
|
39
|
+
out._.push(a);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function inferOutput(inputPath, format) {
|
|
45
|
+
const dir = path.dirname(inputPath);
|
|
46
|
+
const base = path.basename(inputPath, path.extname(inputPath));
|
|
47
|
+
const ext = format === 'cjs' ? '.validator.cjs' : '.validator.mjs';
|
|
48
|
+
return path.join(dir, base + ext);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cmdCompile(args) {
|
|
52
|
+
if (args._.length === 0) {
|
|
53
|
+
process.stderr.write('error: missing <schema-file>\n\n');
|
|
54
|
+
usage();
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const input = args._[0];
|
|
58
|
+
const format = args.opts.format || 'esm';
|
|
59
|
+
if (format !== 'esm' && format !== 'cjs') {
|
|
60
|
+
process.stderr.write(`error: --format must be esm or cjs (got "${format}")\n`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const output = args.opts.output || inferOutput(input, format);
|
|
64
|
+
const abortEarly = !!args.opts.abortEarly;
|
|
65
|
+
|
|
66
|
+
let schemaStr;
|
|
67
|
+
try {
|
|
68
|
+
schemaStr = fs.readFileSync(input, 'utf8');
|
|
69
|
+
} catch (e) {
|
|
70
|
+
process.stderr.write(`error: cannot read ${input}: ${e.message}\n`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let schema;
|
|
75
|
+
try {
|
|
76
|
+
schema = JSON.parse(schemaStr);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
process.stderr.write(`error: ${input} is not valid JSON: ${e.message}\n`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { Validator } = require('..');
|
|
83
|
+
const v = new Validator(schema);
|
|
84
|
+
const src = v.toStandaloneModule({ format, abortEarly });
|
|
85
|
+
if (!src) {
|
|
86
|
+
process.stderr.write('error: schema is too complex for standalone compilation\n');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
91
|
+
fs.writeFileSync(output, src);
|
|
92
|
+
|
|
93
|
+
const sizeBytes = Buffer.byteLength(src, 'utf8');
|
|
94
|
+
process.stdout.write(`ata: compiled ${input} -> ${output} (${sizeBytes.toLocaleString()} bytes, ${format}${abortEarly ? ', abort-early' : ''})\n`);
|
|
95
|
+
|
|
96
|
+
// Emit paired declaration file unless --no-types.
|
|
97
|
+
// TypeScript resolution: .mjs -> .d.mts, .cjs -> .d.cts, .js -> .d.ts.
|
|
98
|
+
const emitTypes = args.opts.types !== false;
|
|
99
|
+
if (emitTypes) {
|
|
100
|
+
const { toTypeScript } = require('../lib/ts-gen');
|
|
101
|
+
const typeName = args.opts.name || path.basename(input, path.extname(input))
|
|
102
|
+
.replace(/[^A-Za-z0-9_]/g, '_')
|
|
103
|
+
.replace(/^([a-z])/, (m) => m.toUpperCase()) || 'Data';
|
|
104
|
+
const dts = toTypeScript(schema, { name: typeName });
|
|
105
|
+
const ext = path.extname(output);
|
|
106
|
+
const dtsExt = ext === '.mjs' ? '.d.mts'
|
|
107
|
+
: ext === '.cjs' ? '.d.cts'
|
|
108
|
+
: '.d.ts';
|
|
109
|
+
const dtsPath = output.slice(0, output.length - ext.length) + dtsExt;
|
|
110
|
+
const finalDtsPath = dtsPath === output ? output + dtsExt : dtsPath;
|
|
111
|
+
fs.writeFileSync(finalDtsPath, dts);
|
|
112
|
+
process.stdout.write(`ata: wrote types -> ${finalDtsPath}\n`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function main() {
|
|
117
|
+
const argv = process.argv.slice(2);
|
|
118
|
+
if (argv.length === 0) { usage(); process.exit(0); }
|
|
119
|
+
|
|
120
|
+
const cmd = argv[0];
|
|
121
|
+
if (cmd === '-h' || cmd === '--help' || cmd === 'help') { usage(); return; }
|
|
122
|
+
|
|
123
|
+
const rest = argv.slice(1);
|
|
124
|
+
let args;
|
|
125
|
+
try {
|
|
126
|
+
args = parseArgs(rest);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (args.opts.help) { usage(); return; }
|
|
133
|
+
|
|
134
|
+
if (cmd === 'compile') {
|
|
135
|
+
cmdCompile(args);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
process.stderr.write(`error: unknown command "${cmd}"\n\n`);
|
|
140
|
+
usage();
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
main();
|
package/index.d.ts
CHANGED
|
@@ -123,3 +123,11 @@ export function createPaddedBuffer(jsonStr: string): { buffer: Buffer; length: n
|
|
|
123
123
|
|
|
124
124
|
/** Required padding size for simdjson buffers. */
|
|
125
125
|
export const SIMDJSON_PADDING: number;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generate a TypeScript declaration (.d.ts source) for the given JSON Schema.
|
|
129
|
+
* Returns a string containing the generated type plus `isValid` / `validate`
|
|
130
|
+
* signatures. Used internally by the `ata compile` CLI and exposed for
|
|
131
|
+
* build-time integrations (Vite plugin, custom build steps).
|
|
132
|
+
*/
|
|
133
|
+
export function toTypeScript(schema: object, options?: { name?: string }): string;
|
package/index.js
CHANGED
|
@@ -272,6 +272,19 @@ const _identityCache = new WeakMap();
|
|
|
272
272
|
|
|
273
273
|
const SIMDJSON_PADDING = 64;
|
|
274
274
|
const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
275
|
+
const ABORT_EARLY_RESULT = Object.freeze({ valid: false, errors: Object.freeze([Object.freeze({ message: 'validation failed' })]) });
|
|
276
|
+
|
|
277
|
+
// Embedded verbatim in standalone modules so the output file has no runtime
|
|
278
|
+
// dependency on ata-validator. ASCII fast-path plus surrogate-aware slow path.
|
|
279
|
+
const _CP_LEN_SOURCE = `function _cpLen(s) {
|
|
280
|
+
const len = s.length;
|
|
281
|
+
for (let i = 0; i < len; i++) {
|
|
282
|
+
if (s.charCodeAt(i) >= 0xD800 && s.charCodeAt(i) <= 0xDBFF) {
|
|
283
|
+
let n = 0; for (const _ of s) n++; return n;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return len;
|
|
287
|
+
}`;
|
|
275
288
|
|
|
276
289
|
// Above this size, simdjson On Demand (selective field access) beats JSON.parse
|
|
277
290
|
// (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
|
|
@@ -359,7 +372,21 @@ class Validator {
|
|
|
359
372
|
return this.validate(data);
|
|
360
373
|
};
|
|
361
374
|
this.isValidObject = (data) => {
|
|
362
|
-
|
|
375
|
+
// Lazy: classify + build tier 0 plan on first call, not in constructor.
|
|
376
|
+
const _tier = classify(this._schemaObj);
|
|
377
|
+
if (_tier.tier === 0) {
|
|
378
|
+
const _plan = buildTier0Plan(this._schemaObj);
|
|
379
|
+
let _n = 0;
|
|
380
|
+
this.isValidObject = (d) => {
|
|
381
|
+
const r = tier0Validate(_plan, d);
|
|
382
|
+
if (++_n === 2) {
|
|
383
|
+
try { this._ensureCodegen(); } catch {}
|
|
384
|
+
}
|
|
385
|
+
return r;
|
|
386
|
+
};
|
|
387
|
+
} else {
|
|
388
|
+
this._ensureCodegen();
|
|
389
|
+
}
|
|
363
390
|
return this.isValidObject(data);
|
|
364
391
|
};
|
|
365
392
|
this.validateJSON = (jsonStr) => {
|
|
@@ -416,15 +443,6 @@ class Validator {
|
|
|
416
443
|
configurable: false,
|
|
417
444
|
});
|
|
418
445
|
|
|
419
|
-
// Tier 0 fast path: override isValidObject with a direct bound validator.
|
|
420
|
-
// All other methods (validate, validateJSON, etc.) stay on the lazy stubs above.
|
|
421
|
-
// Tier 0/1 are boolean-only; error paths continue through codegen.
|
|
422
|
-
const _tier = classify(schemaObj);
|
|
423
|
-
if (_tier.tier === 0) {
|
|
424
|
-
const _plan = buildTier0Plan(schemaObj);
|
|
425
|
-
this.isValidObject = (data) => tier0Validate(_plan, data);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
446
|
// Populate identity cache so repeated `new Validator(sameSchema)` short-circuits.
|
|
429
447
|
if (!opts && typeof schema === "object" && schema !== null) {
|
|
430
448
|
_identityCache.set(schema, this);
|
|
@@ -552,7 +570,14 @@ class Validator {
|
|
|
552
570
|
} catch {}
|
|
553
571
|
}
|
|
554
572
|
|
|
555
|
-
if (
|
|
573
|
+
if (options.abortEarly && jsFn && !hasDynRef) {
|
|
574
|
+
// Abort-early fast path: skip detailed error collection on failure.
|
|
575
|
+
// Returns a shared frozen result, no per-call allocation, no errFn work.
|
|
576
|
+
const _fn = jsFn;
|
|
577
|
+
this.validate = preprocess
|
|
578
|
+
? (data) => { preprocess(data); return _fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT; }
|
|
579
|
+
: (data) => (_fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT);
|
|
580
|
+
} else if (hasDynRef && _isCodegen && jsFn) {
|
|
556
581
|
// $dynamicRef with JS codegen: direct path, no wrapper layers
|
|
557
582
|
const _fn = jsFn, _efn = safeErrFn || errFn, _R = VALID_RESULT;
|
|
558
583
|
this.validate = preprocess
|
|
@@ -809,6 +834,7 @@ class Validator {
|
|
|
809
834
|
|
|
810
835
|
return `// Auto-generated by ata-validator — do not edit
|
|
811
836
|
'use strict';
|
|
837
|
+
${_CP_LEN_SOURCE}
|
|
812
838
|
const boolFn = function(d) {
|
|
813
839
|
${src}
|
|
814
840
|
};
|
|
@@ -822,6 +848,57 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
822
848
|
`;
|
|
823
849
|
}
|
|
824
850
|
|
|
851
|
+
// --- Fully standalone module ---
|
|
852
|
+
// Generates a self-contained module that can be imported directly without
|
|
853
|
+
// pulling in ata-validator at runtime. Browser bundle gets only the
|
|
854
|
+
// generated validator (~2 KB typical) instead of the 165 KB compiler.
|
|
855
|
+
//
|
|
856
|
+
// import { validate, isValid } from './user-validator.mjs'
|
|
857
|
+
// if (isValid(data)) { ... }
|
|
858
|
+
//
|
|
859
|
+
// format: 'esm' | 'cjs'. Default 'esm'.
|
|
860
|
+
// abortEarly: if true, invalid result is a shared stub; smaller output.
|
|
861
|
+
toStandaloneModule(opts) {
|
|
862
|
+
this._ensureCompiled();
|
|
863
|
+
const jsFn = this._jsFn;
|
|
864
|
+
if (!jsFn || !jsFn._source) return null;
|
|
865
|
+
const format = (opts && opts.format) || 'esm';
|
|
866
|
+
const abortEarly = !!(opts && opts.abortEarly);
|
|
867
|
+
const src = jsFn._source;
|
|
868
|
+
|
|
869
|
+
let errCore = '';
|
|
870
|
+
if (!abortEarly) {
|
|
871
|
+
const jsErrFn = compileToJSCodegenWithErrors(
|
|
872
|
+
typeof this._schemaObj === 'object' ? this._schemaObj : {},
|
|
873
|
+
);
|
|
874
|
+
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
875
|
+
if (errSrc) {
|
|
876
|
+
errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const validBody = errCore
|
|
881
|
+
? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
|
|
882
|
+
: 'return _fn(data) ? VALID : ABORT';
|
|
883
|
+
|
|
884
|
+
const exports = format === 'esm'
|
|
885
|
+
? `export { validate, isValid };\nexport default { validate, isValid };\n`
|
|
886
|
+
: `module.exports = { validate, isValid };\nmodule.exports.default = module.exports;\n`;
|
|
887
|
+
|
|
888
|
+
return `// Auto-generated by ata-validator — do not edit.
|
|
889
|
+
// Schema is embedded; runtime has zero dependency on ata-validator.
|
|
890
|
+
'use strict';
|
|
891
|
+
${_CP_LEN_SOURCE}
|
|
892
|
+
const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
893
|
+
const ABORT = Object.freeze({ valid: false, errors: Object.freeze([Object.freeze({ message: 'validation failed' })]) });
|
|
894
|
+
const _fn = function(d) {
|
|
895
|
+
${src}
|
|
896
|
+
};
|
|
897
|
+
${errCore}function isValid(data) { return _fn(data); }
|
|
898
|
+
function validate(data) { ${validBody}; }
|
|
899
|
+
${exports}`;
|
|
900
|
+
}
|
|
901
|
+
|
|
825
902
|
// Load a pre-compiled standalone module. Zero schema compilation.
|
|
826
903
|
// No NAPI, no native compile — pure JS. Startup in microseconds.
|
|
827
904
|
// Usage: const v = Validator.fromStandalone(require('./compiled.js'), schema, opts)
|
|
@@ -1139,6 +1216,8 @@ function compile(schema, opts) {
|
|
|
1139
1216
|
return fn;
|
|
1140
1217
|
}
|
|
1141
1218
|
|
|
1219
|
+
const { toTypeScript } = require("./lib/ts-gen");
|
|
1220
|
+
|
|
1142
1221
|
module.exports = {
|
|
1143
1222
|
Validator,
|
|
1144
1223
|
compile,
|
|
@@ -1147,4 +1226,5 @@ module.exports = {
|
|
|
1147
1226
|
createPaddedBuffer,
|
|
1148
1227
|
SIMDJSON_PADDING,
|
|
1149
1228
|
parseJSON,
|
|
1229
|
+
toTypeScript,
|
|
1150
1230
|
};
|
package/lib/js-compiler.js
CHANGED
|
@@ -675,8 +675,14 @@ function codegenSafe(schema, schemaMap) {
|
|
|
675
675
|
if (siblings.length > 0 && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
-
// additionalProperties as schema —
|
|
679
|
-
|
|
678
|
+
// additionalProperties as schema — supported when no composition (allOf/oneOf/anyOf)
|
|
679
|
+
// is in play, and no patternProperties (unified loop does not emit the
|
|
680
|
+
// per-key sub-schema walk yet). Closure path handles those cases.
|
|
681
|
+
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) {
|
|
682
|
+
if (schema.allOf || schema.oneOf || schema.anyOf) return false
|
|
683
|
+
if (schema.patternProperties) return false
|
|
684
|
+
if (!codegenSafe(schema.additionalProperties, schemaMap)) return false
|
|
685
|
+
}
|
|
680
686
|
if (schema.additionalProperties === false && !schema.properties) return false
|
|
681
687
|
|
|
682
688
|
// propertyNames: false — codegen doesn't handle this
|
|
@@ -729,6 +735,37 @@ function codegenSafe(schema, schemaMap) {
|
|
|
729
735
|
return true
|
|
730
736
|
}
|
|
731
737
|
|
|
738
|
+
// Returns true if the schema (or any nested sub-schema reachable through the
|
|
739
|
+
// usual keywords) has additionalProperties as a schema value. Used by the
|
|
740
|
+
// combined-codegen bail since that path does not emit the per-key loop yet.
|
|
741
|
+
function hasAdditionalPropertiesSchema(schema) {
|
|
742
|
+
if (typeof schema !== 'object' || schema === null) return false
|
|
743
|
+
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) return true
|
|
744
|
+
const objSubs = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
|
|
745
|
+
for (const key of objSubs) {
|
|
746
|
+
if (schema[key] && typeof schema[key] === 'object') {
|
|
747
|
+
for (const v of Object.values(schema[key])) {
|
|
748
|
+
if (hasAdditionalPropertiesSchema(v)) return true
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const arrSubs = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
|
|
753
|
+
for (const key of arrSubs) {
|
|
754
|
+
if (Array.isArray(schema[key])) {
|
|
755
|
+
for (const s of schema[key]) {
|
|
756
|
+
if (hasAdditionalPropertiesSchema(s)) return true
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const singleSubs = ['items', 'contains', 'not', 'if', 'then', 'else', 'propertyNames']
|
|
761
|
+
for (const key of singleSubs) {
|
|
762
|
+
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
763
|
+
if (hasAdditionalPropertiesSchema(schema[key])) return true
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return false
|
|
767
|
+
}
|
|
768
|
+
|
|
732
769
|
// --- Codegen mode: generates a single Function (NOT CSP-safe) ---
|
|
733
770
|
// This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
|
|
734
771
|
function compileToJSCodegen(schema, schemaMap) {
|
|
@@ -927,15 +964,26 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
927
964
|
if (!types || types.length !== 1) return null
|
|
928
965
|
const t = types[0]
|
|
929
966
|
|
|
967
|
+
// If access is already a simple identifier (optional-property hoist, a `_o0`
|
|
968
|
+
// or similar), skip the `{const _v = access}` wrapping and use it directly.
|
|
969
|
+
const isIdent = /^_[a-zA-Z]\w*$/.test(access)
|
|
970
|
+
const bind = (conds) => isIdent
|
|
971
|
+
? `if(${conds.join('||').replace(/\b_v\b/g, access)})return false`
|
|
972
|
+
: `{const _v=${access};if(${conds.join('||')})return false}`
|
|
973
|
+
|
|
930
974
|
if (t === 'string') {
|
|
975
|
+
if (schema.pattern || schema.format) return null
|
|
976
|
+
// Both bounds set: hoist _cpLen once so ASCII strings are not scanned twice.
|
|
977
|
+
if (schema.minLength !== undefined && schema.maxLength !== undefined) {
|
|
978
|
+
const v2 = isIdent ? access : '_v'
|
|
979
|
+
const prelude = isIdent ? '' : `const _v=${access};`
|
|
980
|
+
return `{${prelude}if(typeof ${v2}!=='string')return false;const _lv=_cpLen(${v2});if(_lv<${schema.minLength}||_lv>${schema.maxLength})return false}`
|
|
981
|
+
}
|
|
931
982
|
const conds = [`typeof _v!=='string'`]
|
|
932
983
|
if (schema.minLength !== undefined) conds.push(`_cpLen(_v)<${schema.minLength}`)
|
|
933
984
|
if (schema.maxLength !== undefined) conds.push(`_cpLen(_v)>${schema.maxLength}`)
|
|
934
|
-
if (conds.length < 2
|
|
935
|
-
|
|
936
|
-
if (schema.pattern || schema.format) return null
|
|
937
|
-
const vi = ctx.varCounter++
|
|
938
|
-
return `{const _v=${access};if(${conds.join('||')})return false}`
|
|
985
|
+
if (conds.length < 2) return null
|
|
986
|
+
return bind(conds)
|
|
939
987
|
}
|
|
940
988
|
|
|
941
989
|
if (t === 'integer') {
|
|
@@ -946,8 +994,7 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
946
994
|
if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
|
|
947
995
|
if (schema.multipleOf !== undefined) conds.push(`_v%${schema.multipleOf}!==0`)
|
|
948
996
|
if (conds.length < 2) return null
|
|
949
|
-
|
|
950
|
-
return `{const _v=${access};if(${conds.join('||')})return false}`
|
|
997
|
+
return bind(conds)
|
|
951
998
|
}
|
|
952
999
|
|
|
953
1000
|
if (t === 'number') {
|
|
@@ -958,13 +1005,25 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
958
1005
|
if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
|
|
959
1006
|
if (schema.multipleOf !== undefined) conds.push(`_v%${schema.multipleOf}!==0`)
|
|
960
1007
|
if (conds.length < 2) return null
|
|
961
|
-
|
|
962
|
-
return `{const _v=${access};if(${conds.join('||')})return false}`
|
|
1008
|
+
return bind(conds)
|
|
963
1009
|
}
|
|
964
1010
|
|
|
965
1011
|
return null
|
|
966
1012
|
}
|
|
967
1013
|
|
|
1014
|
+
// Deferred checks (additionalProperties, unevaluatedProperties, ...) reference
|
|
1015
|
+
// the current node variable (`${v}`). Deferring them to the end of the root
|
|
1016
|
+
// function is only safe when we're at the root (`v === 'd'`). For nested
|
|
1017
|
+
// nodes, emit inline so block-scoped variables like `_o0` stay in scope.
|
|
1018
|
+
function _deferOrInline(ctx, lines, v, check) {
|
|
1019
|
+
if (v === 'd') {
|
|
1020
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1021
|
+
ctx.deferredChecks.push(check)
|
|
1022
|
+
} else {
|
|
1023
|
+
lines.push(check)
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
968
1027
|
// knownType: if parent already verified the type, skip redundant guards.
|
|
969
1028
|
// 'object' = we know v is a non-null non-array object
|
|
970
1029
|
// 'array' = we know v is an array
|
|
@@ -1176,9 +1235,21 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1176
1235
|
if (schema.exclusiveMaximum !== undefined) lines.push(isNum ? `if(${v}>=${schema.exclusiveMaximum})return false` : `if(typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum})return false`)
|
|
1177
1236
|
if (schema.multipleOf !== undefined) lines.push(isNum ? `if(${v}%${schema.multipleOf}!==0)return false` : `if(typeof ${v}==='number'&&${v}%${schema.multipleOf}!==0)return false`)
|
|
1178
1237
|
|
|
1179
|
-
// string — skip type guard if known string
|
|
1180
|
-
|
|
1181
|
-
|
|
1238
|
+
// string — skip type guard if known string. When both bounds are set, call
|
|
1239
|
+
// _cpLen once and compare the cached result so ASCII strings do not get
|
|
1240
|
+
// scanned twice.
|
|
1241
|
+
if (schema.minLength !== undefined && schema.maxLength !== undefined) {
|
|
1242
|
+
const li = ctx.varCounter++
|
|
1243
|
+
const lv = `_l${li}`
|
|
1244
|
+
if (isStr) {
|
|
1245
|
+
lines.push(`{const ${lv}=_cpLen(${v});if(${lv}<${schema.minLength}||${lv}>${schema.maxLength})return false}`)
|
|
1246
|
+
} else {
|
|
1247
|
+
lines.push(`if(typeof ${v}==='string'){const ${lv}=_cpLen(${v});if(${lv}<${schema.minLength}||${lv}>${schema.maxLength})return false}`)
|
|
1248
|
+
}
|
|
1249
|
+
} else {
|
|
1250
|
+
if (schema.minLength !== undefined) lines.push(isStr ? `if(_cpLen(${v})<${schema.minLength})return false` : `if(typeof ${v}==='string'&&_cpLen(${v})<${schema.minLength})return false`)
|
|
1251
|
+
if (schema.maxLength !== undefined) lines.push(isStr ? `if(_cpLen(${v})>${schema.maxLength})return false` : `if(typeof ${v}==='string'&&_cpLen(${v})>${schema.maxLength})return false`)
|
|
1252
|
+
}
|
|
1182
1253
|
|
|
1183
1254
|
// array size — skip guard if known array
|
|
1184
1255
|
if (schema.minItems !== undefined) lines.push(isArr ? `if(${v}.length<${schema.minItems})return false` : `if(Array.isArray(${v})&&${v}.length<${schema.minItems})return false`)
|
|
@@ -1237,8 +1308,26 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1237
1308
|
? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
|
|
1238
1309
|
: `if(Object.keys(${v}).length!==${propCount})return false`)
|
|
1239
1310
|
: `for(var _k in ${v})if(${Object.keys(schema.properties).map(k => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
|
|
1240
|
-
|
|
1241
|
-
|
|
1311
|
+
_deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// additionalProperties as a schema: validate every non-declared property
|
|
1315
|
+
// against that sub-schema. Skip if patternProperties is present (handled by
|
|
1316
|
+
// the unified loop). Composition cases are filtered out by codegenSafe.
|
|
1317
|
+
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null && !schema.patternProperties) {
|
|
1318
|
+
const declared = schema.properties ? Object.keys(schema.properties) : []
|
|
1319
|
+
const skipCheck = declared.length === 0
|
|
1320
|
+
? null
|
|
1321
|
+
: declared.map(k => `_k===${JSON.stringify(k)}`).join('||')
|
|
1322
|
+
const subLines = []
|
|
1323
|
+
genCode(schema.additionalProperties, '_av', subLines, ctx)
|
|
1324
|
+
if (subLines.length > 0) {
|
|
1325
|
+
const body = subLines.join(';')
|
|
1326
|
+
const loop = skipCheck
|
|
1327
|
+
? `for(var _k in ${v}){if(${skipCheck})continue;const _av=${v}[_k];${body}}`
|
|
1328
|
+
: `for(var _k in ${v}){const _av=${v}[_k];${body}}`
|
|
1329
|
+
_deferOrInline(ctx, lines, v, isObj ? loop : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${loop}}`)
|
|
1330
|
+
}
|
|
1242
1331
|
}
|
|
1243
1332
|
|
|
1244
1333
|
// dependentRequired
|
|
@@ -1586,19 +1675,16 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1586
1675
|
inner = propCount <= 15
|
|
1587
1676
|
? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
|
|
1588
1677
|
: `if(Object.keys(${v}).length!==${propCount})return false`
|
|
1589
|
-
|
|
1590
|
-
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1678
|
+
_deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1591
1679
|
}
|
|
1592
1680
|
// else: already emitted early (before properties)
|
|
1593
1681
|
} else if (propCount > 0) {
|
|
1594
1682
|
// TRICK 3: charCodeAt switch tree
|
|
1595
1683
|
inner = genCharCodeSwitch(knownKeys, v)
|
|
1596
|
-
|
|
1597
|
-
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1684
|
+
_deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1598
1685
|
} else {
|
|
1599
1686
|
inner = `for(var _k in ${v})return false`
|
|
1600
|
-
|
|
1601
|
-
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1687
|
+
_deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1602
1688
|
}
|
|
1603
1689
|
} else if (typeof schema.unevaluatedProperties === 'object') {
|
|
1604
1690
|
// unevaluatedProperties: {schema} — validate unknown keys
|
|
@@ -1611,8 +1697,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1611
1697
|
const keyChecks = knownKeys.map(k => `${ukVar}===${JSON.stringify(k)}`).join('||')
|
|
1612
1698
|
const skipKnown = knownKeys.length > 0 ? `if(${keyChecks})continue;` : ''
|
|
1613
1699
|
const inner = `for(var ${ukVar} in ${v}){${skipKnown}${check}}`
|
|
1614
|
-
|
|
1615
|
-
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1700
|
+
_deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1616
1701
|
}
|
|
1617
1702
|
}
|
|
1618
1703
|
} else {
|
|
@@ -1730,8 +1815,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1730
1815
|
const inner = staticCheck
|
|
1731
1816
|
? `for(var _k in ${v}){if(${staticCheck})continue;${dynamicCheck}return false}`
|
|
1732
1817
|
: `for(var _k in ${v}){${dynamicCheck}return false}`
|
|
1733
|
-
|
|
1734
|
-
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1818
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1735
1819
|
} else {
|
|
1736
1820
|
// Fallback: plain object tracking
|
|
1737
1821
|
const ei = ctx.varCounter++
|
|
@@ -1756,8 +1840,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1756
1840
|
lines.push(`let _am${bfi}=false;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_am${bfi}=true;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}if(!_am${bfi})return false`)
|
|
1757
1841
|
}
|
|
1758
1842
|
const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
|
|
1759
|
-
|
|
1760
|
-
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1843
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1761
1844
|
}
|
|
1762
1845
|
} else if (schema.dependentSchemas) {
|
|
1763
1846
|
// dependentSchemas: conditional merge at runtime
|
|
@@ -1772,8 +1855,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1772
1855
|
}
|
|
1773
1856
|
}
|
|
1774
1857
|
const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
|
|
1775
|
-
|
|
1776
|
-
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1858
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1777
1859
|
} else {
|
|
1778
1860
|
// General fallback: collect all patternProperties from root + allOf sub-schemas + if
|
|
1779
1861
|
// and use runtime regex matching
|
|
@@ -1833,12 +1915,10 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1833
1915
|
const rootPatCheck = rootReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
|
|
1834
1916
|
const ifPatCheck = ifReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
|
|
1835
1917
|
const inner = `const _uif${ufi}=${ifFn};if(_uif${ufi}(${v})){for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}${ifPatCheck}return false}}else{for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}return false}}`
|
|
1836
|
-
|
|
1837
|
-
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1918
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1838
1919
|
} else {
|
|
1839
1920
|
const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
|
|
1840
|
-
|
|
1841
|
-
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1921
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1842
1922
|
}
|
|
1843
1923
|
}
|
|
1844
1924
|
}
|
|
@@ -1879,8 +1959,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1879
1959
|
if (subLines2.length > 0) {
|
|
1880
1960
|
const check = subLines2.join(';')
|
|
1881
1961
|
const inner = `for(var ${ukVar} in ${v}){if(${evVar}[${ukVar}])continue;${check}}`
|
|
1882
|
-
|
|
1883
|
-
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1962
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1884
1963
|
} else {
|
|
1885
1964
|
lines.push('}')
|
|
1886
1965
|
}
|
|
@@ -1905,8 +1984,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1905
1984
|
// TRICK 6: Array.length comparison only
|
|
1906
1985
|
const maxIdx = evalResult.items || 0
|
|
1907
1986
|
const inner = `if(${v}.length>${maxIdx})return false`
|
|
1908
|
-
|
|
1909
|
-
ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1987
|
+
_deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1910
1988
|
} else if (typeof schema.unevaluatedItems === 'object') {
|
|
1911
1989
|
const maxIdx = evalResult.items || 0
|
|
1912
1990
|
const ui = ctx.varCounter++
|
|
@@ -1917,8 +1995,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1917
1995
|
if (subLines.length > 0) {
|
|
1918
1996
|
const check = subLines.join(';')
|
|
1919
1997
|
const inner = `for(let ${idxVar}=${maxIdx};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
|
|
1920
|
-
|
|
1921
|
-
ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1998
|
+
_deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1922
1999
|
}
|
|
1923
2000
|
}
|
|
1924
2001
|
} else {
|
|
@@ -1970,8 +2047,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1970
2047
|
}
|
|
1971
2048
|
if (schema.unevaluatedItems === false) {
|
|
1972
2049
|
const inner = `if(${v}.length>${evVar})return false`
|
|
1973
|
-
|
|
1974
|
-
ctx.deferredChecks.push(isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
|
|
2050
|
+
_deferOrInline(ctx, lines, v, isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
|
|
1975
2051
|
} else {
|
|
1976
2052
|
const ui = ctx.varCounter++
|
|
1977
2053
|
const elemVar = `_ue${ui}`
|
|
@@ -1981,8 +2057,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1981
2057
|
if (subLines.length > 0) {
|
|
1982
2058
|
const check = subLines.join(';')
|
|
1983
2059
|
const inner = `for(let ${idxVar}=${evVar};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
|
|
1984
|
-
|
|
1985
|
-
ctx.deferredChecks.push(isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
|
|
2060
|
+
_deferOrInline(ctx, lines, v, isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
|
|
1986
2061
|
} else {
|
|
1987
2062
|
lines.push('}')
|
|
1988
2063
|
}
|
|
@@ -2038,8 +2113,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2038
2113
|
lines.push(`if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){for(let _cj=0;_cj<${cfnArr}.length;_cj++){if(${cfnArr}[_cj](${v}[_ci])){${evArr}[_ci]=true;break}}}}`)
|
|
2039
2114
|
if (schema.unevaluatedItems === false) {
|
|
2040
2115
|
const inner = `if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){if(!${evArr}[_ci])return false}}`
|
|
2041
|
-
|
|
2042
|
-
ctx.deferredChecks.push(inner + '}')
|
|
2116
|
+
_deferOrInline(ctx, lines, v, inner + '}')
|
|
2043
2117
|
} else {
|
|
2044
2118
|
// unevaluatedItems: {schema}
|
|
2045
2119
|
const ui = ctx.varCounter++
|
|
@@ -2049,8 +2123,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2049
2123
|
if (subLines.length > 0) {
|
|
2050
2124
|
const check = subLines.join(';')
|
|
2051
2125
|
const inner = `if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){if(!${evArr}[_ci]){const ${elemVar}=${v}[_ci];${check}}}}`
|
|
2052
|
-
|
|
2053
|
-
ctx.deferredChecks.push(inner + '}')
|
|
2126
|
+
_deferOrInline(ctx, lines, v, inner + '}')
|
|
2054
2127
|
} else {
|
|
2055
2128
|
lines.push('}')
|
|
2056
2129
|
}
|
|
@@ -2059,8 +2132,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2059
2132
|
// Fallback: use static base index (may not be fully correct for all dynamic cases)
|
|
2060
2133
|
const maxIdx = evalResult.items || 0
|
|
2061
2134
|
const inner = `if(${v}.length>${maxIdx})return false`
|
|
2062
|
-
|
|
2063
|
-
ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
2135
|
+
_deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
2064
2136
|
}
|
|
2065
2137
|
}
|
|
2066
2138
|
}
|
|
@@ -2235,6 +2307,9 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
|
|
|
2235
2307
|
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
2236
2308
|
// Bail on self-referencing schemas — error codegen doesn't support recursion
|
|
2237
2309
|
if (s.includes('"$ref":"#"')) return null
|
|
2310
|
+
// Bail on additionalProperties as a schema — genCodeE does not emit the
|
|
2311
|
+
// per-key sub-schema loop. Let the closure path handle detailed errors.
|
|
2312
|
+
if (hasAdditionalPropertiesSchema(schema)) return null
|
|
2238
2313
|
}
|
|
2239
2314
|
if (typeof schema === 'boolean') {
|
|
2240
2315
|
return schema
|
|
@@ -2736,6 +2811,9 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
|
2736
2811
|
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
2737
2812
|
// Bail on self-referencing schemas — combined codegen doesn't support recursion
|
|
2738
2813
|
if (s.includes('"$ref":"#"')) return null
|
|
2814
|
+
// Bail on additionalProperties as a schema — combined path doesn't emit the
|
|
2815
|
+
// per-key sub-schema loop yet. The jsFn + errFn fallback handles it correctly.
|
|
2816
|
+
if (hasAdditionalPropertiesSchema(schema)) return null
|
|
2739
2817
|
}
|
|
2740
2818
|
if (typeof schema === 'boolean') {
|
|
2741
2819
|
return schema
|
|
@@ -2802,12 +2880,12 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
|
2802
2880
|
`\n return _e?{valid:false,errors:_e}:R`
|
|
2803
2881
|
|
|
2804
2882
|
try {
|
|
2805
|
-
if (process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
|
|
2883
|
+
if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
|
|
2806
2884
|
const factory = new Function('R' + (closureParams ? ',' + closureParams : ''),
|
|
2807
2885
|
`return function(d){${inner}}`)
|
|
2808
2886
|
return factory(VALID_RESULT, ...ctx.closureVals)
|
|
2809
2887
|
} catch (e) {
|
|
2810
|
-
if (process.env.ATA_DEBUG) console.error('compileToJSCombined error:', e.message, '\n', inner.slice(0, 500))
|
|
2888
|
+
if (typeof process !== 'undefined' && process.env && process.env.ATA_DEBUG) console.error('compileToJSCombined error:', e.message, '\n', inner.slice(0, 500))
|
|
2811
2889
|
return null
|
|
2812
2890
|
}
|
|
2813
2891
|
}
|
package/lib/ts-gen.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Schema -> TypeScript type declaration.
|
|
4
|
+
// Emits an interface or type alias for each top-level schema, plus
|
|
5
|
+
// `isValid` (type predicate) and `validate` signatures.
|
|
6
|
+
//
|
|
7
|
+
// Scope: the common shapes in real-world APIs.
|
|
8
|
+
// - type: string | number | integer | boolean | null | array | object
|
|
9
|
+
// - properties + required (required field narrows to required, optional to optional)
|
|
10
|
+
// - enum (narrows to literal union)
|
|
11
|
+
// - const (narrows to literal)
|
|
12
|
+
// - items (array element type)
|
|
13
|
+
// - oneOf / anyOf (union)
|
|
14
|
+
// - $ref to local $defs (resolved by name)
|
|
15
|
+
// Falls back to `unknown` for shapes we cannot represent.
|
|
16
|
+
|
|
17
|
+
function renderValueType(schema, defs, depth = 0) {
|
|
18
|
+
if (depth > 32) return 'unknown';
|
|
19
|
+
if (schema === true) return 'unknown';
|
|
20
|
+
if (schema === false) return 'never';
|
|
21
|
+
if (typeof schema !== 'object' || schema === null) return 'unknown';
|
|
22
|
+
|
|
23
|
+
// $ref to local $defs
|
|
24
|
+
if (schema.$ref) {
|
|
25
|
+
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
|
|
26
|
+
if (m && defs && defs[m[1]]) return toTypeName(m[1]);
|
|
27
|
+
return 'unknown';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// const narrows to literal
|
|
31
|
+
if (schema.const !== undefined) return renderLiteral(schema.const);
|
|
32
|
+
|
|
33
|
+
// enum narrows to literal union
|
|
34
|
+
if (Array.isArray(schema.enum)) {
|
|
35
|
+
return schema.enum.map(renderLiteral).join(' | ') || 'never';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// oneOf / anyOf → union
|
|
39
|
+
if (Array.isArray(schema.oneOf)) {
|
|
40
|
+
return schema.oneOf.map((s) => renderValueType(s, defs, depth + 1)).join(' | ') || 'unknown';
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(schema.anyOf)) {
|
|
43
|
+
return schema.anyOf.map((s) => renderValueType(s, defs, depth + 1)).join(' | ') || 'unknown';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// type
|
|
47
|
+
const t = schema.type;
|
|
48
|
+
if (Array.isArray(t)) {
|
|
49
|
+
return t.map((tt) => renderValueType({ ...schema, type: tt }, defs, depth + 1)).join(' | ');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (t === 'string') return 'string';
|
|
53
|
+
if (t === 'number' || t === 'integer') return 'number';
|
|
54
|
+
if (t === 'boolean') return 'boolean';
|
|
55
|
+
if (t === 'null') return 'null';
|
|
56
|
+
|
|
57
|
+
if (t === 'array') {
|
|
58
|
+
const items = schema.items;
|
|
59
|
+
if (items === undefined || items === true) return 'unknown[]';
|
|
60
|
+
const inner = renderValueType(items, defs, depth + 1);
|
|
61
|
+
return inner.includes(' | ') ? `Array<${inner}>` : `${inner}[]`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (t === 'object' || (!t && schema.properties)) {
|
|
65
|
+
return renderObject(schema, defs, depth + 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return 'unknown';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderObject(schema, defs, depth) {
|
|
72
|
+
const props = schema.properties || {};
|
|
73
|
+
const required = new Set(schema.required || []);
|
|
74
|
+
const keys = Object.keys(props);
|
|
75
|
+
if (keys.length === 0) {
|
|
76
|
+
if (schema.additionalProperties === false) return 'Record<string, never>';
|
|
77
|
+
const ap = schema.additionalProperties;
|
|
78
|
+
if (ap && typeof ap === 'object') {
|
|
79
|
+
return `Record<string, ${renderValueType(ap, defs, depth + 1)}>`;
|
|
80
|
+
}
|
|
81
|
+
return 'Record<string, unknown>';
|
|
82
|
+
}
|
|
83
|
+
const lines = keys.map((k) => {
|
|
84
|
+
const t = renderValueType(props[k], defs, depth + 1);
|
|
85
|
+
const opt = required.has(k) ? '' : '?';
|
|
86
|
+
const safeKey = /^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k);
|
|
87
|
+
const desc = typeof props[k] === 'object' && props[k] && typeof props[k].description === 'string'
|
|
88
|
+
? ` /** ${props[k].description.replace(/\*\//g, '* /')} */\n`
|
|
89
|
+
: '';
|
|
90
|
+
return `${desc} ${safeKey}${opt}: ${t};`;
|
|
91
|
+
});
|
|
92
|
+
// extra keys when additionalProperties is present as a schema or true
|
|
93
|
+
const extra = schema.additionalProperties;
|
|
94
|
+
if (extra && typeof extra === 'object') {
|
|
95
|
+
lines.push(` [key: string]: ${renderValueType(extra, defs, depth + 1)};`);
|
|
96
|
+
}
|
|
97
|
+
return `{\n${lines.join('\n')}\n}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderLiteral(v) {
|
|
101
|
+
if (v === null) return 'null';
|
|
102
|
+
if (typeof v === 'string') return JSON.stringify(v);
|
|
103
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
104
|
+
return 'unknown';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toTypeName(name) {
|
|
108
|
+
const cleaned = String(name).replace(/[^A-Za-z0-9_]/g, '_');
|
|
109
|
+
if (/^[0-9]/.test(cleaned)) return `_${cleaned}`;
|
|
110
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Public: given a schema and optional type name, return a .d.ts source.
|
|
114
|
+
function toTypeScript(schema, opts) {
|
|
115
|
+
const options = opts || {};
|
|
116
|
+
const rootName = toTypeName(options.name || 'Data');
|
|
117
|
+
const defs = schema && (schema.$defs || schema.definitions);
|
|
118
|
+
|
|
119
|
+
const defLines = [];
|
|
120
|
+
if (defs && typeof defs === 'object') {
|
|
121
|
+
for (const [defName, defSchema] of Object.entries(defs)) {
|
|
122
|
+
const body = renderValueType(defSchema, defs, 0);
|
|
123
|
+
defLines.push(`export type ${toTypeName(defName)} = ${body};`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const rootType = renderValueType(schema, defs, 0);
|
|
128
|
+
// Use `interface` only for a pure object literal; otherwise fall back to
|
|
129
|
+
// `type`. Catches cases like `{...}[]` (array of object) and `Record<...>`
|
|
130
|
+
// which are valid TS but cannot be expressed as an interface body.
|
|
131
|
+
const isPureObjectLiteral = rootType.startsWith('{') && rootType.endsWith('}') && !rootType.includes(' | ');
|
|
132
|
+
const rootDecl = isPureObjectLiteral
|
|
133
|
+
? `export interface ${rootName} ${rootType}`
|
|
134
|
+
: `export type ${rootName} = ${rootType};`;
|
|
135
|
+
|
|
136
|
+
return `// Auto-generated by ata-validator — do not edit.
|
|
137
|
+
${defLines.length ? defLines.join('\n\n') + '\n\n' : ''}${rootDecl}
|
|
138
|
+
|
|
139
|
+
export interface ValidationError {
|
|
140
|
+
keyword?: string;
|
|
141
|
+
instancePath?: string;
|
|
142
|
+
schemaPath?: string;
|
|
143
|
+
params?: Record<string, unknown>;
|
|
144
|
+
message?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ValidResult {
|
|
148
|
+
valid: true;
|
|
149
|
+
errors: readonly never[];
|
|
150
|
+
}
|
|
151
|
+
export interface InvalidResult {
|
|
152
|
+
valid: false;
|
|
153
|
+
errors: readonly ValidationError[];
|
|
154
|
+
}
|
|
155
|
+
export type Result = ValidResult | InvalidResult;
|
|
156
|
+
|
|
157
|
+
export declare function isValid(data: unknown): data is ${rootName};
|
|
158
|
+
export declare function validate(data: unknown): Result;
|
|
159
|
+
declare const _default: { validate: typeof validate; isValid: typeof isValid };
|
|
160
|
+
export default _default;
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { toTypeScript };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Ultra-fast JSON Schema validator. 4.7x faster validation, 1,800x faster compilation. Works without native addon. Cross-schema $ref, Draft 2020-12 + Draft 7, V8-optimized JS codegen, simdjson, RE2, multi-core. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"./package.json": "./package.json"
|
|
24
24
|
},
|
|
25
25
|
"sideEffects": false,
|
|
26
|
+
"bin": {
|
|
27
|
+
"ata": "bin/ata.js"
|
|
28
|
+
},
|
|
26
29
|
"browser": {
|
|
27
30
|
"pkg-prebuilds": false
|
|
28
31
|
},
|
|
@@ -37,6 +40,7 @@
|
|
|
37
40
|
"test:compat": "node tests/test_compat.js",
|
|
38
41
|
"test:standard-schema": "node tests/test_standard_schema.js",
|
|
39
42
|
"test:browser": "node tests/test_browser.js",
|
|
43
|
+
"test:ts": "node tests/test_ts_gen.js",
|
|
40
44
|
"bench": "node benchmark/bench_large.js",
|
|
41
45
|
"fuzz": "node tests/fuzz_differential.js",
|
|
42
46
|
"fuzz:long": "FUZZ_ITERATIONS=100000 node tests/fuzz_differential.js",
|
|
@@ -57,7 +61,7 @@
|
|
|
57
61
|
"standard-schema",
|
|
58
62
|
"fastify"
|
|
59
63
|
],
|
|
60
|
-
"author": "Mert Can Altin <
|
|
64
|
+
"author": "Mert Can Altin <mertgold60@gmail.com>",
|
|
61
65
|
"license": "MIT",
|
|
62
66
|
"repository": {
|
|
63
67
|
"type": "git",
|
|
@@ -100,6 +104,7 @@
|
|
|
100
104
|
"cmake-js": "^8.0.0",
|
|
101
105
|
"mitata": "^1.0.34",
|
|
102
106
|
"typebox": "^1.1.7",
|
|
107
|
+
"typescript": "^6.0.3",
|
|
103
108
|
"valibot": "^1.3.1",
|
|
104
109
|
"zod": "^4.3.6"
|
|
105
110
|
},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|