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 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
- ### Standalone Pre-compilation
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
- Pre-compile schemas to JS files for near-zero startup. No native addon needed at runtime.
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('./compiled.js', v.toStandalone());
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 (5 routes): ajv 6.0ms → ata 0.5ms (12x faster, no build step needed)**
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
- this._ensureCodegen();
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 (hasDynRef && _isCodegen && jsFn) {
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
  };
@@ -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 — bail entirely, too many edge cases with allOf interaction
679
- if (typeof schema.additionalProperties === 'object') return false
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 && !schema.pattern && !schema.format) return null // not worth combining
935
- // pattern and format need separate statements, fall back if present
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
- const vi = ctx.varCounter++
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
- const vi = ctx.varCounter++
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
- if (schema.minLength !== undefined) lines.push(isStr ? `if(_cpLen(${v})<${schema.minLength})return false` : `if(typeof ${v}==='string'&&_cpLen(${v})<${schema.minLength})return false`)
1181
- if (schema.maxLength !== undefined) lines.push(isStr ? `if(_cpLen(${v})>${schema.maxLength})return false` : `if(typeof ${v}==='string'&&_cpLen(${v})>${schema.maxLength})return false`)
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
1241
- ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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.10.3",
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 <mertcanaltin01@gmail.com>",
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
  },