ata-validator 0.10.3 → 0.11.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 +49 -12
- package/bin/ata.js +144 -0
- package/index.js +88 -11
- package/lib/js-compiler.js +66 -50
- package/lib/ts-gen.js +161 -0
- package/package.json +4 -1
- 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
|
@@ -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.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)
|
package/lib/js-compiler.js
CHANGED
|
@@ -927,15 +927,26 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
927
927
|
if (!types || types.length !== 1) return null
|
|
928
928
|
const t = types[0]
|
|
929
929
|
|
|
930
|
+
// If access is already a simple identifier (optional-property hoist, a `_o0`
|
|
931
|
+
// or similar), skip the `{const _v = access}` wrapping and use it directly.
|
|
932
|
+
const isIdent = /^_[a-zA-Z]\w*$/.test(access)
|
|
933
|
+
const bind = (conds) => isIdent
|
|
934
|
+
? `if(${conds.join('||').replace(/\b_v\b/g, access)})return false`
|
|
935
|
+
: `{const _v=${access};if(${conds.join('||')})return false}`
|
|
936
|
+
|
|
930
937
|
if (t === 'string') {
|
|
938
|
+
if (schema.pattern || schema.format) return null
|
|
939
|
+
// Both bounds set: hoist _cpLen once so ASCII strings are not scanned twice.
|
|
940
|
+
if (schema.minLength !== undefined && schema.maxLength !== undefined) {
|
|
941
|
+
const v2 = isIdent ? access : '_v'
|
|
942
|
+
const prelude = isIdent ? '' : `const _v=${access};`
|
|
943
|
+
return `{${prelude}if(typeof ${v2}!=='string')return false;const _lv=_cpLen(${v2});if(_lv<${schema.minLength}||_lv>${schema.maxLength})return false}`
|
|
944
|
+
}
|
|
931
945
|
const conds = [`typeof _v!=='string'`]
|
|
932
946
|
if (schema.minLength !== undefined) conds.push(`_cpLen(_v)<${schema.minLength}`)
|
|
933
947
|
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}`
|
|
948
|
+
if (conds.length < 2) return null
|
|
949
|
+
return bind(conds)
|
|
939
950
|
}
|
|
940
951
|
|
|
941
952
|
if (t === 'integer') {
|
|
@@ -946,8 +957,7 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
946
957
|
if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
|
|
947
958
|
if (schema.multipleOf !== undefined) conds.push(`_v%${schema.multipleOf}!==0`)
|
|
948
959
|
if (conds.length < 2) return null
|
|
949
|
-
|
|
950
|
-
return `{const _v=${access};if(${conds.join('||')})return false}`
|
|
960
|
+
return bind(conds)
|
|
951
961
|
}
|
|
952
962
|
|
|
953
963
|
if (t === 'number') {
|
|
@@ -958,13 +968,25 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
958
968
|
if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
|
|
959
969
|
if (schema.multipleOf !== undefined) conds.push(`_v%${schema.multipleOf}!==0`)
|
|
960
970
|
if (conds.length < 2) return null
|
|
961
|
-
|
|
962
|
-
return `{const _v=${access};if(${conds.join('||')})return false}`
|
|
971
|
+
return bind(conds)
|
|
963
972
|
}
|
|
964
973
|
|
|
965
974
|
return null
|
|
966
975
|
}
|
|
967
976
|
|
|
977
|
+
// Deferred checks (additionalProperties, unevaluatedProperties, ...) reference
|
|
978
|
+
// the current node variable (`${v}`). Deferring them to the end of the root
|
|
979
|
+
// function is only safe when we're at the root (`v === 'd'`). For nested
|
|
980
|
+
// nodes, emit inline so block-scoped variables like `_o0` stay in scope.
|
|
981
|
+
function _deferOrInline(ctx, lines, v, check) {
|
|
982
|
+
if (v === 'd') {
|
|
983
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
984
|
+
ctx.deferredChecks.push(check)
|
|
985
|
+
} else {
|
|
986
|
+
lines.push(check)
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
968
990
|
// knownType: if parent already verified the type, skip redundant guards.
|
|
969
991
|
// 'object' = we know v is a non-null non-array object
|
|
970
992
|
// 'array' = we know v is an array
|
|
@@ -1176,9 +1198,21 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1176
1198
|
if (schema.exclusiveMaximum !== undefined) lines.push(isNum ? `if(${v}>=${schema.exclusiveMaximum})return false` : `if(typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum})return false`)
|
|
1177
1199
|
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
1200
|
|
|
1179
|
-
// string — skip type guard if known string
|
|
1180
|
-
|
|
1181
|
-
|
|
1201
|
+
// string — skip type guard if known string. When both bounds are set, call
|
|
1202
|
+
// _cpLen once and compare the cached result so ASCII strings do not get
|
|
1203
|
+
// scanned twice.
|
|
1204
|
+
if (schema.minLength !== undefined && schema.maxLength !== undefined) {
|
|
1205
|
+
const li = ctx.varCounter++
|
|
1206
|
+
const lv = `_l${li}`
|
|
1207
|
+
if (isStr) {
|
|
1208
|
+
lines.push(`{const ${lv}=_cpLen(${v});if(${lv}<${schema.minLength}||${lv}>${schema.maxLength})return false}`)
|
|
1209
|
+
} else {
|
|
1210
|
+
lines.push(`if(typeof ${v}==='string'){const ${lv}=_cpLen(${v});if(${lv}<${schema.minLength}||${lv}>${schema.maxLength})return false}`)
|
|
1211
|
+
}
|
|
1212
|
+
} else {
|
|
1213
|
+
if (schema.minLength !== undefined) lines.push(isStr ? `if(_cpLen(${v})<${schema.minLength})return false` : `if(typeof ${v}==='string'&&_cpLen(${v})<${schema.minLength})return false`)
|
|
1214
|
+
if (schema.maxLength !== undefined) lines.push(isStr ? `if(_cpLen(${v})>${schema.maxLength})return false` : `if(typeof ${v}==='string'&&_cpLen(${v})>${schema.maxLength})return false`)
|
|
1215
|
+
}
|
|
1182
1216
|
|
|
1183
1217
|
// array size — skip guard if known array
|
|
1184
1218
|
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 +1271,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1237
1271
|
? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
|
|
1238
1272
|
: `if(Object.keys(${v}).length!==${propCount})return false`)
|
|
1239
1273
|
: `for(var _k in ${v})if(${Object.keys(schema.properties).map(k => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
|
|
1240
|
-
|
|
1241
|
-
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1274
|
+
_deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1242
1275
|
}
|
|
1243
1276
|
|
|
1244
1277
|
// dependentRequired
|
|
@@ -1586,19 +1619,16 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1586
1619
|
inner = propCount <= 15
|
|
1587
1620
|
? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
|
|
1588
1621
|
: `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}}`)
|
|
1622
|
+
_deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1591
1623
|
}
|
|
1592
1624
|
// else: already emitted early (before properties)
|
|
1593
1625
|
} else if (propCount > 0) {
|
|
1594
1626
|
// TRICK 3: charCodeAt switch tree
|
|
1595
1627
|
inner = genCharCodeSwitch(knownKeys, v)
|
|
1596
|
-
|
|
1597
|
-
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1628
|
+
_deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1598
1629
|
} else {
|
|
1599
1630
|
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}}`)
|
|
1631
|
+
_deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1602
1632
|
}
|
|
1603
1633
|
} else if (typeof schema.unevaluatedProperties === 'object') {
|
|
1604
1634
|
// unevaluatedProperties: {schema} — validate unknown keys
|
|
@@ -1611,8 +1641,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1611
1641
|
const keyChecks = knownKeys.map(k => `${ukVar}===${JSON.stringify(k)}`).join('||')
|
|
1612
1642
|
const skipKnown = knownKeys.length > 0 ? `if(${keyChecks})continue;` : ''
|
|
1613
1643
|
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}}`)
|
|
1644
|
+
_deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1616
1645
|
}
|
|
1617
1646
|
}
|
|
1618
1647
|
} else {
|
|
@@ -1730,8 +1759,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1730
1759
|
const inner = staticCheck
|
|
1731
1760
|
? `for(var _k in ${v}){if(${staticCheck})continue;${dynamicCheck}return false}`
|
|
1732
1761
|
: `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}}}`)
|
|
1762
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1735
1763
|
} else {
|
|
1736
1764
|
// Fallback: plain object tracking
|
|
1737
1765
|
const ei = ctx.varCounter++
|
|
@@ -1756,8 +1784,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1756
1784
|
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
1785
|
}
|
|
1758
1786
|
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}}}`)
|
|
1787
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1761
1788
|
}
|
|
1762
1789
|
} else if (schema.dependentSchemas) {
|
|
1763
1790
|
// dependentSchemas: conditional merge at runtime
|
|
@@ -1772,8 +1799,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1772
1799
|
}
|
|
1773
1800
|
}
|
|
1774
1801
|
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}}}`)
|
|
1802
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1777
1803
|
} else {
|
|
1778
1804
|
// General fallback: collect all patternProperties from root + allOf sub-schemas + if
|
|
1779
1805
|
// and use runtime regex matching
|
|
@@ -1833,12 +1859,10 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1833
1859
|
const rootPatCheck = rootReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
|
|
1834
1860
|
const ifPatCheck = ifReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
|
|
1835
1861
|
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}}}`)
|
|
1862
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1838
1863
|
} else {
|
|
1839
1864
|
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}}}`)
|
|
1865
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1842
1866
|
}
|
|
1843
1867
|
}
|
|
1844
1868
|
}
|
|
@@ -1879,8 +1903,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1879
1903
|
if (subLines2.length > 0) {
|
|
1880
1904
|
const check = subLines2.join(';')
|
|
1881
1905
|
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}}}`)
|
|
1906
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1884
1907
|
} else {
|
|
1885
1908
|
lines.push('}')
|
|
1886
1909
|
}
|
|
@@ -1905,8 +1928,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1905
1928
|
// TRICK 6: Array.length comparison only
|
|
1906
1929
|
const maxIdx = evalResult.items || 0
|
|
1907
1930
|
const inner = `if(${v}.length>${maxIdx})return false`
|
|
1908
|
-
|
|
1909
|
-
ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1931
|
+
_deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1910
1932
|
} else if (typeof schema.unevaluatedItems === 'object') {
|
|
1911
1933
|
const maxIdx = evalResult.items || 0
|
|
1912
1934
|
const ui = ctx.varCounter++
|
|
@@ -1917,8 +1939,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1917
1939
|
if (subLines.length > 0) {
|
|
1918
1940
|
const check = subLines.join(';')
|
|
1919
1941
|
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}}`)
|
|
1942
|
+
_deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1922
1943
|
}
|
|
1923
1944
|
}
|
|
1924
1945
|
} else {
|
|
@@ -1970,8 +1991,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1970
1991
|
}
|
|
1971
1992
|
if (schema.unevaluatedItems === false) {
|
|
1972
1993
|
const inner = `if(${v}.length>${evVar})return false`
|
|
1973
|
-
|
|
1974
|
-
ctx.deferredChecks.push(isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
|
|
1994
|
+
_deferOrInline(ctx, lines, v, isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
|
|
1975
1995
|
} else {
|
|
1976
1996
|
const ui = ctx.varCounter++
|
|
1977
1997
|
const elemVar = `_ue${ui}`
|
|
@@ -1981,8 +2001,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1981
2001
|
if (subLines.length > 0) {
|
|
1982
2002
|
const check = subLines.join(';')
|
|
1983
2003
|
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}}}`)
|
|
2004
|
+
_deferOrInline(ctx, lines, v, isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
|
|
1986
2005
|
} else {
|
|
1987
2006
|
lines.push('}')
|
|
1988
2007
|
}
|
|
@@ -2038,8 +2057,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2038
2057
|
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
2058
|
if (schema.unevaluatedItems === false) {
|
|
2040
2059
|
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 + '}')
|
|
2060
|
+
_deferOrInline(ctx, lines, v, inner + '}')
|
|
2043
2061
|
} else {
|
|
2044
2062
|
// unevaluatedItems: {schema}
|
|
2045
2063
|
const ui = ctx.varCounter++
|
|
@@ -2049,8 +2067,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2049
2067
|
if (subLines.length > 0) {
|
|
2050
2068
|
const check = subLines.join(';')
|
|
2051
2069
|
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 + '}')
|
|
2070
|
+
_deferOrInline(ctx, lines, v, inner + '}')
|
|
2054
2071
|
} else {
|
|
2055
2072
|
lines.push('}')
|
|
2056
2073
|
}
|
|
@@ -2059,8 +2076,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2059
2076
|
// Fallback: use static base index (may not be fully correct for all dynamic cases)
|
|
2060
2077
|
const maxIdx = evalResult.items || 0
|
|
2061
2078
|
const inner = `if(${v}.length>${maxIdx})return false`
|
|
2062
|
-
|
|
2063
|
-
ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
2079
|
+
_deferOrInline(ctx, lines, v, isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
2064
2080
|
}
|
|
2065
2081
|
}
|
|
2066
2082
|
}
|
|
@@ -2802,12 +2818,12 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
|
2802
2818
|
`\n return _e?{valid:false,errors:_e}:R`
|
|
2803
2819
|
|
|
2804
2820
|
try {
|
|
2805
|
-
if (process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
|
|
2821
|
+
if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
|
|
2806
2822
|
const factory = new Function('R' + (closureParams ? ',' + closureParams : ''),
|
|
2807
2823
|
`return function(d){${inner}}`)
|
|
2808
2824
|
return factory(VALID_RESULT, ...ctx.closureVals)
|
|
2809
2825
|
} catch (e) {
|
|
2810
|
-
if (process.env.ATA_DEBUG) console.error('compileToJSCombined error:', e.message, '\n', inner.slice(0, 500))
|
|
2826
|
+
if (typeof process !== 'undefined' && process.env && process.env.ATA_DEBUG) console.error('compileToJSCombined error:', e.message, '\n', inner.slice(0, 500))
|
|
2811
2827
|
return null
|
|
2812
2828
|
}
|
|
2813
2829
|
}
|
package/lib/ts-gen.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
const isObjectShape = rootType.startsWith('{') || rootType.startsWith('Record<');
|
|
129
|
+
const rootDecl = isObjectShape && !rootType.includes(' | ')
|
|
130
|
+
? `export interface ${rootName} ${rootType}`
|
|
131
|
+
: `export type ${rootName} = ${rootType};`;
|
|
132
|
+
|
|
133
|
+
return `// Auto-generated by ata-validator — do not edit.
|
|
134
|
+
${defLines.length ? defLines.join('\n\n') + '\n\n' : ''}${rootDecl}
|
|
135
|
+
|
|
136
|
+
export interface ValidationError {
|
|
137
|
+
keyword?: string;
|
|
138
|
+
instancePath?: string;
|
|
139
|
+
schemaPath?: string;
|
|
140
|
+
params?: Record<string, unknown>;
|
|
141
|
+
message?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ValidResult {
|
|
145
|
+
valid: true;
|
|
146
|
+
errors: readonly never[];
|
|
147
|
+
}
|
|
148
|
+
export interface InvalidResult {
|
|
149
|
+
valid: false;
|
|
150
|
+
errors: readonly ValidationError[];
|
|
151
|
+
}
|
|
152
|
+
export type Result = ValidResult | InvalidResult;
|
|
153
|
+
|
|
154
|
+
export declare function isValid(data: unknown): data is ${rootName};
|
|
155
|
+
export declare function validate(data: unknown): Result;
|
|
156
|
+
declare const _default: { validate: typeof validate; isValid: typeof isValid };
|
|
157
|
+
export default _default;
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
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.0",
|
|
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
|
},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|