ata-validator 0.10.2 → 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 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
- ### 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();
@@ -17,7 +17,6 @@
17
17
  #include <vector>
18
18
 
19
19
  #include "ata.h"
20
- #include <simdjson.h>
21
20
 
22
21
  // ============================================================================
23
22
  // V8 Direct Object Traversal Engine
@@ -798,67 +797,6 @@ static void validate_napi(const schema_node_ptr& node,
798
797
  // N-API Binding
799
798
  // ============================================================================
800
799
 
801
- // ============================================================================
802
- // simdjson DOM to V8 JS Object conversion
803
- // ============================================================================
804
-
805
- static Napi::Value dom_to_napi(Napi::Env env, simdjson::dom::element el) {
806
- using namespace simdjson;
807
- switch (el.type()) {
808
- case dom::element_type::OBJECT: {
809
- auto obj = Napi::Object::New(env);
810
- for (auto [key, val] : dom::object(el)) {
811
- obj.Set(std::string(key), dom_to_napi(env, val));
812
- }
813
- return obj;
814
- }
815
- case dom::element_type::ARRAY: {
816
- dom::array arr = el;
817
- auto jsArr = Napi::Array::New(env, arr.size());
818
- uint32_t i = 0;
819
- for (auto val : arr) {
820
- jsArr.Set(i++, dom_to_napi(env, val));
821
- }
822
- return jsArr;
823
- }
824
- case dom::element_type::STRING: {
825
- std::string_view sv;
826
- el.get(sv);
827
- return Napi::String::New(env, sv.data(), sv.length());
828
- }
829
- case dom::element_type::INT64: {
830
- int64_t v;
831
- el.get(v);
832
- return Napi::Number::New(env, static_cast<double>(v));
833
- }
834
- case dom::element_type::UINT64: {
835
- uint64_t v;
836
- el.get(v);
837
- return Napi::Number::New(env, static_cast<double>(v));
838
- }
839
- case dom::element_type::DOUBLE: {
840
- double v;
841
- el.get(v);
842
- return Napi::Number::New(env, v);
843
- }
844
- case dom::element_type::BOOL: {
845
- bool v;
846
- el.get(v);
847
- return Napi::Boolean::New(env, v);
848
- }
849
- case dom::element_type::NULL_VALUE:
850
- return env.Null();
851
- default:
852
- return env.Undefined();
853
- }
854
- }
855
-
856
- // Thread-local simdjson DOM parser for parseJSON / validateAndParse
857
- static simdjson::dom::parser& tl_dom_parser() {
858
- thread_local simdjson::dom::parser parser;
859
- return parser;
860
- }
861
-
862
800
  static Napi::Object make_result(Napi::Env env,
863
801
  const ata::validation_result& result) {
864
802
  Napi::Object obj = Napi::Object::New(env);
@@ -884,8 +822,7 @@ class CompiledSchema : public Napi::ObjectWrap<CompiledSchema> {
884
822
  {InstanceMethod("validate", &CompiledSchema::Validate),
885
823
  InstanceMethod("validateJSON", &CompiledSchema::ValidateJSON),
886
824
  InstanceMethod("validateDirect", &CompiledSchema::ValidateDirect),
887
- InstanceMethod("isValidJSON", &CompiledSchema::IsValidJSON),
888
- InstanceMethod("validateAndParse", &CompiledSchema::ValidateAndParse)});
825
+ InstanceMethod("isValidJSON", &CompiledSchema::IsValidJSON)});
889
826
  auto* constructor = new Napi::FunctionReference();
890
827
  *constructor = Napi::Persistent(func);
891
828
  env.SetInstanceData(constructor);
@@ -1007,77 +944,6 @@ class CompiledSchema : public Napi::ObjectWrap<CompiledSchema> {
1007
944
  return ValidateDirectImpl(env, info[0]);
1008
945
  }
1009
946
 
1010
- // Parse JSON with simdjson, validate against schema, return parsed JS object
1011
- Napi::Value ValidateAndParse(const Napi::CallbackInfo& info) {
1012
- Napi::Env env = info.Env();
1013
- if (info.Length() < 1) {
1014
- Napi::TypeError::New(env, "JSON string or Buffer expected")
1015
- .ThrowAsJavaScriptException();
1016
- return env.Undefined();
1017
- }
1018
-
1019
- const char* data;
1020
- size_t len;
1021
-
1022
- if (info[0].IsBuffer()) {
1023
- auto buf = info[0].As<Napi::Buffer<char>>();
1024
- data = buf.Data();
1025
- len = buf.Length();
1026
- } else if (info[0].IsString()) {
1027
- auto [d, l] = extract_string(env, info[0]);
1028
- data = d;
1029
- len = l;
1030
- } else {
1031
- Napi::TypeError::New(env, "JSON string or Buffer expected")
1032
- .ThrowAsJavaScriptException();
1033
- return env.Undefined();
1034
- }
1035
-
1036
- // Parse with simdjson
1037
- simdjson::padded_string padded(data, len);
1038
- auto& parser = tl_dom_parser();
1039
- auto doc_result = parser.parse(padded);
1040
- if (doc_result.error()) {
1041
- auto obj = Napi::Object::New(env);
1042
- obj.Set("valid", false);
1043
- obj.Set("value", env.Null());
1044
- auto errors = Napi::Array::New(env, 1);
1045
- auto err = Napi::Object::New(env);
1046
- err.Set("code", Napi::Number::New(env, static_cast<int>(ata::error_code::invalid_json)));
1047
- err.Set("path", Napi::String::New(env, ""));
1048
- err.Set("message", Napi::String::New(env, "Invalid JSON"));
1049
- errors[0u] = err;
1050
- obj.Set("errors", errors);
1051
- return obj;
1052
- }
1053
-
1054
- // Validate
1055
- auto valResult = ata::validate(schema_, std::string_view(data, len));
1056
-
1057
- // Convert DOM to JS object
1058
- Napi::Value jsValue = dom_to_napi(env, doc_result.value());
1059
-
1060
- // Build result
1061
- auto obj = Napi::Object::New(env);
1062
- obj.Set("valid", valResult.valid);
1063
- obj.Set("value", jsValue);
1064
- if (valResult.valid) {
1065
- obj.Set("errors", Napi::Array::New(env, 0));
1066
- } else {
1067
- Napi::Array errors = Napi::Array::New(env, valResult.errors.size());
1068
- for (size_t i = 0; i < valResult.errors.size(); ++i) {
1069
- Napi::Object err = Napi::Object::New(env);
1070
- err.Set("code",
1071
- Napi::Number::New(env, static_cast<int>(valResult.errors[i].code)));
1072
- err.Set("path", Napi::String::New(env, valResult.errors[i].path));
1073
- err.Set("message", Napi::String::New(env, valResult.errors[i].message));
1074
- errors[i] = err;
1075
- }
1076
- obj.Set("errors", errors);
1077
- }
1078
- return obj;
1079
- }
1080
-
1081
947
  private:
1082
948
  Napi::Value ValidateDirectImpl(Napi::Env env, Napi::Value value) {
1083
949
  compiled_schema_internal ctx;
@@ -1130,44 +996,6 @@ Napi::Value GetVersion(const Napi::CallbackInfo& info) {
1130
996
  return Napi::String::New(info.Env(), std::string(ata::version()));
1131
997
  }
1132
998
 
1133
- // Standalone JSON parser using simdjson — returns parsed JS object
1134
- Napi::Value ParseJSON(const Napi::CallbackInfo& info) {
1135
- Napi::Env env = info.Env();
1136
- if (info.Length() < 1) {
1137
- Napi::TypeError::New(env, "JSON string or Buffer expected")
1138
- .ThrowAsJavaScriptException();
1139
- return env.Undefined();
1140
- }
1141
-
1142
- const char* data;
1143
- size_t len;
1144
-
1145
- if (info[0].IsBuffer()) {
1146
- auto buf = info[0].As<Napi::Buffer<char>>();
1147
- data = buf.Data();
1148
- len = buf.Length();
1149
- } else if (info[0].IsString()) {
1150
- auto [d, l] = CompiledSchema::extract_string(env, info[0]);
1151
- data = d;
1152
- len = l;
1153
- } else {
1154
- Napi::TypeError::New(env, "JSON string or Buffer expected")
1155
- .ThrowAsJavaScriptException();
1156
- return env.Undefined();
1157
- }
1158
-
1159
- // Parse with simdjson using thread-local parser
1160
- simdjson::padded_string padded(data, len);
1161
- auto& parser = tl_dom_parser();
1162
- auto result = parser.parse(padded);
1163
- if (result.error()) {
1164
- Napi::Error::New(env, "Invalid JSON").ThrowAsJavaScriptException();
1165
- return env.Undefined();
1166
- }
1167
-
1168
- return dom_to_napi(env, result.value());
1169
- }
1170
-
1171
999
  // --- Thread Pool ---
1172
1000
  class ThreadPool {
1173
1001
  public:
@@ -1703,7 +1531,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1703
1531
  CompiledSchema::Init(env, exports);
1704
1532
  exports.Set("validate", Napi::Function::New(env, ValidateOneShot));
1705
1533
  exports.Set("version", Napi::Function::New(env, GetVersion));
1706
- exports.Set("parseJSON", Napi::Function::New(env, ParseJSON));
1707
1534
  exports.Set("fastRegister", Napi::Function::New(env, FastRegister));
1708
1535
  exports.Set("fastValidate", Napi::Function::New(env, FastValidateSlow));
1709
1536
 
package/include/ata.h CHANGED
@@ -8,16 +8,16 @@
8
8
  #include <variant>
9
9
  #include <vector>
10
10
 
11
- #define ATA_VERSION "0.9.0"
11
+ #define ATA_VERSION "0.10.3"
12
12
 
13
13
  namespace ata {
14
14
 
15
15
  inline constexpr uint32_t VERSION_MAJOR = 0;
16
- inline constexpr uint32_t VERSION_MINOR = 9;
17
- inline constexpr uint32_t VERSION_REVISION = 0;
16
+ inline constexpr uint32_t VERSION_MINOR = 10;
17
+ inline constexpr uint32_t VERSION_REVISION = 3;
18
18
 
19
19
  inline constexpr std::string_view version() noexcept {
20
- return "0.9.0";
20
+ return "0.10.3";
21
21
  }
22
22
 
23
23
  enum class error_code : uint8_t {
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)
@@ -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 && !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}`
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
- const vi = ctx.varCounter++
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
- const vi = ctx.varCounter++
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
- 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`)
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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
- if (!ctx.deferredChecks) ctx.deferredChecks = []
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.10.2",
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
  },
@@ -1,223 +0,0 @@
1
- 'use strict';
2
-
3
- // Schema-compiled JSON parser: combines parse + validate in one pass.
4
- // For known-shape schemas, skips JSON.parse entirely and builds typed
5
- // JS objects directly from the JSON string.
6
- //
7
- // Returns null on invalid JSON or validation failure.
8
- // Falls back to JSON.parse for unsupported schemas.
9
-
10
- function compileSchemaParser(schema) {
11
- if (!schema || typeof schema !== 'object') return null;
12
-
13
- // Array of objects
14
- if (schema.type === 'array' && schema.items && schema.items.type === 'object') {
15
- const itemParser = compileSchemaParser(schema.items);
16
- if (!itemParser) return null;
17
- return function parseArray(str) {
18
- const len = str.length;
19
- let pos = 0;
20
- while (pos < len && str.charCodeAt(pos) <= 32) pos++;
21
- if (str.charCodeAt(pos) !== 91) return null; // [
22
- pos++;
23
- const arr = [];
24
- let first = true;
25
- while (pos < len) {
26
- while (pos < len && str.charCodeAt(pos) <= 32) pos++;
27
- if (str.charCodeAt(pos) === 93) return arr; // ]
28
- if (!first) {
29
- if (str.charCodeAt(pos) !== 44) return null;
30
- pos++;
31
- while (pos < len && str.charCodeAt(pos) <= 32) pos++;
32
- }
33
- first = false;
34
- // Parse nested object inline
35
- const result = parseObject(str, pos, itemParser._keys, itemParser._keyTypes, itemParser._required, itemParser._additionalAllowed);
36
- if (result === null) return null;
37
- arr.push(result.value);
38
- pos = result.pos;
39
- }
40
- return null;
41
- };
42
- }
43
-
44
- // Wrapper object containing array
45
- if (schema.type === 'object' && schema.properties) {
46
- const props = schema.properties;
47
- const keys = Object.keys(props);
48
- for (const k of keys) {
49
- if (props[k] && props[k].type === 'array' && props[k].items) {
50
- // Has nested array — use the general parser
51
- return compileObjectParser(schema);
52
- }
53
- }
54
- return compileObjectParser(schema);
55
- }
56
-
57
- return null;
58
- }
59
-
60
- function compileObjectParser(schema) {
61
- if (!schema.properties) return null;
62
- const props = schema.properties;
63
- const keys = Object.keys(props);
64
- const required = new Set(schema.required || []);
65
- const additionalAllowed = schema.additionalProperties !== false;
66
-
67
- // Build a key→type map for O(1) lookup
68
- const keyTypes = {};
69
- for (const k of keys) {
70
- const p = props[k];
71
- if (!p || typeof p.type !== 'string') return null; // bail on complex
72
- if (p.type === 'object' || p.type === 'array') return null; // flat only for now
73
- keyTypes[k] = p;
74
- }
75
-
76
- // The compiled parser function
77
- return function parseAndValidate(str) {
78
- const len = str.length;
79
- let pos = 0;
80
-
81
- // Skip whitespace
82
- while (pos < len && (str.charCodeAt(pos) <= 32)) pos++;
83
- if (pos >= len || str.charCodeAt(pos) !== 123) return null; // {
84
- pos++;
85
-
86
- const result = {};
87
- let foundKeys = 0;
88
- let first = true;
89
-
90
- while (pos < len) {
91
- // Skip whitespace
92
- while (pos < len && str.charCodeAt(pos) <= 32) pos++;
93
- if (str.charCodeAt(pos) === 125) break; // }
94
-
95
- // Comma between entries
96
- if (!first) {
97
- if (str.charCodeAt(pos) !== 44) return null; // ,
98
- pos++;
99
- while (pos < len && str.charCodeAt(pos) <= 32) pos++;
100
- }
101
- first = false;
102
-
103
- // Key
104
- if (str.charCodeAt(pos) !== 34) return null; // "
105
- pos++;
106
- const keyStart = pos;
107
- while (pos < len && str.charCodeAt(pos) !== 34) {
108
- if (str.charCodeAt(pos) === 92) pos++; // skip escape
109
- pos++;
110
- }
111
- const key = str.substring(keyStart, pos);
112
- pos++; // closing "
113
-
114
- // Colon
115
- while (pos < len && str.charCodeAt(pos) <= 32) pos++;
116
- if (str.charCodeAt(pos) !== 58) return null; // :
117
- pos++;
118
- while (pos < len && str.charCodeAt(pos) <= 32) pos++;
119
-
120
- // Value — parse based on schema type if known key
121
- const propSchema = keyTypes[key];
122
- if (propSchema) {
123
- const parsed = parseValue(str, pos, propSchema);
124
- if (parsed === null) return null; // validation fail
125
- result[key] = parsed.value;
126
- pos = parsed.pos;
127
- foundKeys++;
128
- } else if (!additionalAllowed) {
129
- return null; // additional property not allowed
130
- } else {
131
- // Skip unknown value
132
- const skipped = skipValue(str, pos);
133
- if (skipped < 0) return null;
134
- pos = skipped;
135
- }
136
- }
137
-
138
- // Check required
139
- for (const r of required) {
140
- if (result[r] === undefined) return null;
141
- }
142
-
143
- return result;
144
- };
145
- }
146
-
147
- function parseValue(str, pos, schema) {
148
- const ch = str.charCodeAt(pos);
149
- const type = schema.type;
150
-
151
- if (type === 'string') {
152
- if (ch !== 34) return null; // must be "
153
- pos++;
154
- const start = pos;
155
- while (pos < str.length && str.charCodeAt(pos) !== 34) {
156
- if (str.charCodeAt(pos) === 92) pos++; // skip escape
157
- pos++;
158
- }
159
- const val = str.substring(start, pos);
160
- pos++; // closing "
161
- if (schema.minLength !== undefined && val.length < schema.minLength) return null;
162
- if (schema.maxLength !== undefined && val.length > schema.maxLength) return null;
163
- return { value: val, pos };
164
- }
165
-
166
- if (type === 'integer' || type === 'number') {
167
- const start = pos;
168
- if (str.charCodeAt(pos) === 45) pos++; // -
169
- while (pos < str.length && str.charCodeAt(pos) >= 48 && str.charCodeAt(pos) <= 57) pos++;
170
- if (type === 'number' && str.charCodeAt(pos) === 46) {
171
- pos++;
172
- while (pos < str.length && str.charCodeAt(pos) >= 48 && str.charCodeAt(pos) <= 57) pos++;
173
- }
174
- const val = +str.substring(start, pos);
175
- if (type === 'integer' && !Number.isInteger(val)) return null;
176
- if (schema.minimum !== undefined && val < schema.minimum) return null;
177
- if (schema.maximum !== undefined && val > schema.maximum) return null;
178
- return { value: val, pos };
179
- }
180
-
181
- if (type === 'boolean') {
182
- if (str.startsWith('true', pos)) return { value: true, pos: pos + 4 };
183
- if (str.startsWith('false', pos)) return { value: false, pos: pos + 5 };
184
- return null;
185
- }
186
-
187
- return null;
188
- }
189
-
190
- function skipValue(str, pos) {
191
- const ch = str.charCodeAt(pos);
192
- if (ch === 34) { // string
193
- pos++;
194
- while (pos < str.length && str.charCodeAt(pos) !== 34) {
195
- if (str.charCodeAt(pos) === 92) pos++;
196
- pos++;
197
- }
198
- return pos + 1;
199
- }
200
- if (ch === 123 || ch === 91) { // { or [
201
- let depth = 1;
202
- const open = ch;
203
- const close = ch === 123 ? 125 : 93;
204
- pos++;
205
- while (pos < str.length && depth > 0) {
206
- const c = str.charCodeAt(pos);
207
- if (c === open) depth++;
208
- else if (c === close) depth--;
209
- else if (c === 34) { pos++; while (pos < str.length && str.charCodeAt(pos) !== 34) { if (str.charCodeAt(pos) === 92) pos++; pos++; } }
210
- pos++;
211
- }
212
- return pos;
213
- }
214
- // number, true, false, null
215
- while (pos < str.length) {
216
- const c = str.charCodeAt(pos);
217
- if (c === 44 || c === 125 || c === 93 || c <= 32) break;
218
- pos++;
219
- }
220
- return pos;
221
- }
222
-
223
- module.exports = { compileSchemaParser };