ata-validator 0.18.2 → 0.21.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/lib/aot.js ADDED
@@ -0,0 +1,410 @@
1
+ 'use strict';
2
+
3
+ // All ahead-of-time (AOT) code generation lives here so the default and browser
4
+ // entries can stay free of `fs`, `path`, `__dirname`, and `pkg-prebuilds`.
5
+ // Browser bundles get `lib/aot.browser.js` instead (see package.json `browser`
6
+ // field), which throws if anyone tries to call an AOT function from the browser.
7
+ //
8
+ // Public surface (called by index.js and ata-validator/build):
9
+ // toStandalone(validator)
10
+ // toStandaloneModule(validator, opts)
11
+ // bundle(Validator, schemas, opts)
12
+ // bundleStandalone(Validator, schemas, opts)
13
+ // bundleCompact(Validator, schemas, opts)
14
+ // loadBundle(Validator, mods, schemas, opts)
15
+
16
+ const { compileToJSCodegenWithErrors } = require('./js-compiler');
17
+ const SAFE_REGEX_SOURCE = require('./safe-regex-source');
18
+
19
+ // Embedded verbatim in standalone modules so the output file has no runtime
20
+ // dependency on ata-validator. ASCII fast-path plus surrogate-aware slow path.
21
+ const _CP_LEN_SOURCE = `function _cpLen(s) {
22
+ const len = s.length;
23
+ for (let i = 0; i < len; i++) {
24
+ if (s.charCodeAt(i) >= 0xD800 && s.charCodeAt(i) <= 0xDBFF) {
25
+ let n = 0; for (const _ of s) n++; return n;
26
+ }
27
+ }
28
+ return len;
29
+ }`;
30
+
31
+ // The linear-time regex engine, inlined verbatim into standalone output so a
32
+ // compiled module that uses safe `pattern` matchers has no runtime dependency
33
+ // on ata-validator. The engine source is baked into `lib/safe-regex-source.js`
34
+ // at build time (see `scripts/regen-safe-regex-source.js`), so this path has
35
+ // no `fs`/`path`/`__dirname` reads — safe in browser bundles too. The embed
36
+ // strips the strict directive and CommonJS exports and adds the `__ataSafeRe`
37
+ // alias the emitted code calls. The engine has no eval/new Function, so the
38
+ // embed is CSP-safe.
39
+ let _safeRegexEmbed = null;
40
+ function getSafeRegexEmbed() {
41
+ if (_safeRegexEmbed === null) {
42
+ const body = SAFE_REGEX_SOURCE
43
+ .replace(/^'use strict'\s*\n/, '')
44
+ .replace(/\nmodule\.exports[^\n]*\n?/, '\n');
45
+ _safeRegexEmbed = body.trimEnd() + '\nconst __ataSafeRe = compileSafe;';
46
+ }
47
+ return _safeRegexEmbed;
48
+ }
49
+
50
+ // Returns the engine embed when any supplied compiled function references the
51
+ // safe matcher (jsFn._usesSafeRe), else an empty string so non-pattern modules
52
+ // pay zero bytes.
53
+ function safeRePrelude(...fns) {
54
+ return fns.some((f) => f && f._usesSafeRe) ? getSafeRegexEmbed() + '\n' : '';
55
+ }
56
+
57
+ // --- Standalone pre-compilation ---
58
+ // Generate a JS module string that can be written to a file.
59
+ // On next startup, load with Validator.fromStandalone() -- zero compile time.
60
+ function toStandalone(validator) {
61
+ validator._ensureCompiled();
62
+ const jsFn = validator._jsFn;
63
+ if (!jsFn || !jsFn._source) return null;
64
+ const src = jsFn._source;
65
+ const hybridSrc = jsFn._hybridSource || '';
66
+ const preambleSrc = jsFn._preambleSource || '';
67
+
68
+ // Also capture error function source for zero-compile standalone load
69
+ const jsErrFn = compileToJSCodegenWithErrors(
70
+ typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
71
+ );
72
+ const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
73
+
74
+ return `// Auto-generated by ata-validator — do not edit
75
+ 'use strict';
76
+ ${_CP_LEN_SOURCE}
77
+ ${safeRePrelude(jsFn, jsErrFn)}${preambleSrc}
78
+ const boolFn = function(d) {
79
+ ${src}
80
+ };
81
+ const hybridFactory = function(R, E) {
82
+ return function(d) {
83
+ ${hybridSrc}
84
+ };
85
+ };
86
+ ${errSrc ? `const errFn = function(d, _all) {\n ${errSrc}\n};` : 'const errFn = null;'}
87
+ module.exports = { boolFn, hybridFactory, errFn };
88
+ `;
89
+ }
90
+
91
+ // --- Fully standalone module ---
92
+ // Generates a self-contained module that can be imported directly without
93
+ // pulling in ata-validator at runtime. Browser bundle gets only the
94
+ // generated validator (~2 KB typical) instead of the 165 KB compiler.
95
+ //
96
+ // import { validate, isValid } from './user-validator.mjs'
97
+ // if (isValid(data)) { ... }
98
+ //
99
+ // format: 'esm' | 'cjs'. Default 'esm'.
100
+ // abortEarly: if true, invalid result is a shared stub; smaller output.
101
+ function toStandaloneModule(validator, opts) {
102
+ validator._ensureCompiled();
103
+ const jsFn = validator._jsFn;
104
+ if (!jsFn || !jsFn._source) return null;
105
+ const format = (opts && opts.format) || 'esm';
106
+ const abortEarly = !!(opts && opts.abortEarly);
107
+ const source = !!(opts && opts.source);
108
+ const sourceMap = opts && opts.sourceMap ? opts.sourceMap : null;
109
+ const schemaFile = opts && opts.schemaFile ? opts.schemaFile : null;
110
+ const src = jsFn._source;
111
+
112
+ let errCore = '';
113
+ let jsErrFn = null;
114
+ if (!abortEarly) {
115
+ jsErrFn = compileToJSCodegenWithErrors(
116
+ typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
117
+ null,
118
+ validator._userFormats,
119
+ (source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
120
+ );
121
+ const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
122
+ if (errSrc) {
123
+ errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
124
+ }
125
+ }
126
+
127
+ // Schema-source frames are baked as literals inside each emitted error so
128
+ // consumers don't need a runtime lookup. We still expose the schema file
129
+ // as a sentinel constant when --source is on — handy for introspection
130
+ // and visible in source graphs. With --no-source, the constant is omitted
131
+ // entirely so size budgets and grep-based "is this source-mapped?" checks
132
+ // both work.
133
+ const schemaSourceConst = (source && schemaFile)
134
+ ? `const __ATA_SCHEMA_SOURCE__ = ${JSON.stringify({ file: schemaFile })};\n`
135
+ : '';
136
+
137
+ // Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
138
+ let closureDecls = '';
139
+ if (jsFn._closures && jsFn._closures.length > 0) {
140
+ const lines = [];
141
+ for (const { name, val } of jsFn._closures) {
142
+ if (Array.isArray(val)) {
143
+ lines.push(`const ${name} = ${JSON.stringify(val)};`);
144
+ continue;
145
+ }
146
+ if (val && val.__ataSafe) {
147
+ lines.push(`const ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
148
+ } else if (val instanceof RegExp) {
149
+ const flags = val.flags;
150
+ lines.push(`const ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
151
+ } else if (val instanceof Set) {
152
+ lines.push(`const ${name} = new Set(${JSON.stringify([...val])});`);
153
+ } else if (typeof val === 'function') {
154
+ // new Function('_ppv', body) — extract body from toString()
155
+ const str = val.toString();
156
+ // Matches: "function anonymous(_ppv\n) {\nbody\n}" or "function(_ppv){body}"
157
+ const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/)
158
+ const body = m ? m[1].trim() : str;
159
+ lines.push(`const ${name} = function(_ppv) { ${body} };`);
160
+ }
161
+ }
162
+ if (lines.length) closureDecls = lines.join('\n') + '\n';
163
+ }
164
+
165
+ // Hoisted oneOf/anyOf branch checks live in the boolean fn's preamble (the
166
+ // runtime emits them before the function). The standalone module must declare
167
+ // them at module scope too, or _fn references undefined names (e.g. _af1_b0).
168
+ const preambleDecls = jsFn._preambleSource ? jsFn._preambleSource + '\n' : '';
169
+
170
+ // User-supplied format functions are referenced as _uf_<name> by both the
171
+ // boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
172
+ // so the standalone module stays self-contained.
173
+ let formatDecls = '';
174
+ if (jsFn._formatClosures && jsFn._formatClosures.length > 0) {
175
+ formatDecls = jsFn._formatClosures
176
+ .map(({ name, fn }) => `const ${name} = ${fn.toString()};`)
177
+ .join('\n') + '\n';
178
+ }
179
+
180
+ const validBody = errCore
181
+ ? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
182
+ : 'return _fn(data) ? VALID : ABORT';
183
+
184
+ const exports = format === 'esm'
185
+ ? `export { validate, isValid };\nexport default { validate, isValid };\n`
186
+ : `module.exports = { validate, isValid };\nmodule.exports.default = module.exports;\n`;
187
+
188
+ return `// Auto-generated by ata-validator — do not edit.
189
+ // Schema is embedded; runtime has zero dependency on ata-validator.
190
+ 'use strict';
191
+ ${_CP_LEN_SOURCE}
192
+ ${safeRePrelude(jsFn, jsErrFn)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
193
+ const ABORT = Object.freeze({
194
+ valid: false,
195
+ errors: Object.freeze([Object.freeze({
196
+ code: 'ATA9000',
197
+ message: 'validation failed',
198
+ keyword: '__abort_early__',
199
+ path: '',
200
+ })]),
201
+ });
202
+ ${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
203
+ ${src}
204
+ };
205
+ ${errCore}function isValid(data) { return _fn(data); }
206
+ function validate(data) { ${validBody}; }
207
+ ${exports}`;
208
+ }
209
+
210
+ // Bundle multiple validators into a single JS file for fast startup.
211
+ // Usage:
212
+ // const bundle = Validator.bundle([schema1, schema2, ...]);
213
+ // fs.writeFileSync('validators.js', bundle);
214
+ // // On startup:
215
+ // const validators = Validator.loadBundle(require('./validators.js'), [schema1, schema2, ...]);
216
+ function bundle(Validator, schemas, opts) {
217
+ const parts = schemas.map((schema) => {
218
+ const v = new Validator(schema, opts);
219
+ const standalone = toStandalone(v);
220
+ if (!standalone) return 'null';
221
+ return (
222
+ '(function(){' +
223
+ standalone
224
+ .replace("'use strict';", '')
225
+ .replace('module.exports = ', 'return ') +
226
+ '})()'
227
+ );
228
+ });
229
+ return "'use strict';\nmodule.exports = [\n" + parts.join(',\n') + '\n];\n';
230
+ }
231
+
232
+ // Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
233
+ // opts.format: 'cjs' (default) or 'esm'.
234
+ // opts.formats: { name: fn } — embedded in the output via Function#toString.
235
+ function bundleStandalone(Validator, schemas, opts) {
236
+ // Cross-schema $ref resolution: only meaningful when at least one schema has
237
+ // an $id. Skip the schemas-as-map plumbing when none of them do.
238
+ const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
239
+ const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
240
+ const format = (opts && opts.format) || 'cjs';
241
+ const R = 'Object.freeze({valid:true,errors:Object.freeze([])})';
242
+ let bundleUsesSafeRe = false;
243
+ const fns = schemas.map((schema) => {
244
+ const v = new Validator(schema, bundleOpts);
245
+ v._ensureCompiled();
246
+ const jsFn = v._jsFn;
247
+ if (!jsFn || !jsFn._hybridSource) return 'null';
248
+ const jsErrFn = compileToJSCodegenWithErrors(
249
+ typeof schema === 'string' ? JSON.parse(schema) : schema,
250
+ v._schemaMap,
251
+ v._userFormats,
252
+ );
253
+ if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
254
+ const errBody =
255
+ jsErrFn && jsErrFn._errSource
256
+ ? jsErrFn._errSource
257
+ : "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
258
+ // Serialize custom format closures so the bundle has no runtime dep on ata.
259
+ let preamble = '';
260
+ if (jsFn._formatClosures) {
261
+ preamble = jsFn._formatClosures
262
+ .map(({ name, fn }) => `var ${name}=${fn.toString()};`)
263
+ .join('\n');
264
+ }
265
+ // Include hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) so the
266
+ // bundle output is self-contained. `toStandalone` emits this same source
267
+ // for single-schema standalone output.
268
+ if (jsFn._preambleSource) {
269
+ preamble = preamble ? `${preamble}\n${jsFn._preambleSource}` : jsFn._preambleSource;
270
+ }
271
+ if (opts && opts.verbose) {
272
+ // Embed the schema and a small resolver so errors carry parentSchema.
273
+ const schemaLit = JSON.stringify(typeof schema === 'string' ? JSON.parse(schema) : schema);
274
+ return `(function(R){${preamble}var _S=${schemaLit};function _PS(p){if(!p||p[0]!=='#')return undefined;var s=p.slice(1);if(!s)return _S;var ps=s.split('/').filter(Boolean).map(function(x){return x.replace(/~1/g,'/').replace(/~0/g,'~')});var t=_S;for(var i=0;i<ps.length-1;i++){if(t==null||typeof t!=='object')return undefined;t=t[ps[i]]}return t}var E=function(d){var _all=true;${errBody}};var _v=function(d){${jsFn._hybridSource}};return function(d){var r=_v(d);if(r&&r.valid===false&&r.errors){var es=[];for(var i=0;i<r.errors.length;i++){var e=r.errors[i];es.push(Object.assign({},e,{parentSchema:_PS(e.schemaPath)}))}return{valid:false,errors:es}}return r}})(R)`;
275
+ }
276
+ return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
277
+ });
278
+ const arr = `[${fns.join(',')}]`;
279
+ const safeEmbed = bundleUsesSafeRe ? getSafeRegexEmbed() + '\n' : '';
280
+ if (format === 'esm') {
281
+ return `// Auto-generated by ata-validator — do not edit\n${safeEmbed}const R=${R};\nconst validators=${arr};\nexport default validators;\nexport { validators };\n`;
282
+ }
283
+ return `'use strict';\n${safeEmbed}var R=${R};\nmodule.exports=[${fns.join(',')}];\n`;
284
+ }
285
+
286
+ // Compact bundle: deduplicated code. Shared template functions + per-schema params.
287
+ // Much smaller file → faster V8 parse → faster startup.
288
+ // opts.format: 'cjs' (default) or 'esm'.
289
+ function bundleCompact(Validator, schemas, opts) {
290
+ const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
291
+ const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
292
+ const format = (opts && opts.format) || 'cjs';
293
+ let bundleUsesSafeRe = false;
294
+ // Analyze schemas and group by structure
295
+ const entries = schemas.map((schema) => {
296
+ const v = new Validator(schema, bundleOpts);
297
+ v._ensureCompiled();
298
+ const jsFn = v._jsFn;
299
+ if (!jsFn || !jsFn._hybridSource) return null;
300
+ const jsErrFn = compileToJSCodegenWithErrors(
301
+ typeof schema === 'string' ? JSON.parse(schema) : schema,
302
+ v._schemaMap,
303
+ v._userFormats,
304
+ );
305
+ if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
306
+ // Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
307
+ // hybrid body or it references undefined names. Prepending keeps dedup honest:
308
+ // schemas with different branch sets no longer collide on body alone.
309
+ const hybrid = jsFn._preambleSource
310
+ ? `${jsFn._preambleSource}\n${jsFn._hybridSource}`
311
+ : jsFn._hybridSource;
312
+ return {
313
+ hybrid,
314
+ err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
315
+ fmt: jsFn._formatClosures || null,
316
+ };
317
+ });
318
+
319
+ // Deduplicate function bodies — many schemas produce identical or near-identical code
320
+ const bodyMap = new Map(); // body → index
321
+ const bodies = [];
322
+ const errMap = new Map();
323
+ const errBodies = [];
324
+
325
+ const indices = entries.map((e) => {
326
+ if (!e) return [-1, -1];
327
+ let hi = bodyMap.get(e.hybrid);
328
+ if (hi === undefined) {
329
+ hi = bodies.length;
330
+ bodies.push(e.hybrid);
331
+ bodyMap.set(e.hybrid, hi);
332
+ }
333
+ let ei = -1;
334
+ if (e.err) {
335
+ ei = errMap.get(e.err);
336
+ if (ei === undefined) {
337
+ ei = errBodies.length;
338
+ errBodies.push(e.err);
339
+ errMap.set(e.err, ei);
340
+ }
341
+ }
342
+ return [hi, ei];
343
+ });
344
+
345
+ // Generate compact bundle
346
+ const isEsm = format === 'esm';
347
+ let out = isEsm
348
+ ? '// Auto-generated by ata-validator — do not edit\n'
349
+ : "'use strict';\n";
350
+ if (bundleUsesSafeRe) out += getSafeRegexEmbed() + '\n';
351
+ const declKW = isEsm ? 'const' : 'var';
352
+ out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
353
+
354
+ // User format functions are referenced as _uf_<name> by the hybrid and error
355
+ // bodies. Collect them across all schemas (deduped by name) and embed via
356
+ // Function#toString so the bundle stays self-contained.
357
+ const fmtSeen = new Set();
358
+ for (const e of entries) {
359
+ if (!e || !e.fmt) continue;
360
+ for (const { name, fn } of e.fmt) {
361
+ if (fmtSeen.has(name)) continue;
362
+ fmtSeen.add(name);
363
+ out += `${declKW} ${name}=${fn.toString()};\n`;
364
+ }
365
+ }
366
+
367
+ // Shared hybrid factories
368
+ out += `${declKW} H=[\n`;
369
+ out += bodies
370
+ .map((b) => `function(R,E){return function(d){${b}}}`)
371
+ .join(',\n');
372
+ out += '\n];\n';
373
+
374
+ // Shared error functions
375
+ out += `${declKW} EF=[\n`;
376
+ out += errBodies.map((b) => `function(d){var _all=true;${b}}`).join(',\n');
377
+ out += '\n];\n';
378
+
379
+ // Build validators from shared templates
380
+ const arrBody = indices
381
+ .map(([hi, ei]) => {
382
+ if (hi < 0) return 'null';
383
+ if (ei >= 0) return `H[${hi}](R,EF[${ei}])`;
384
+ return `H[${hi}](R,function(){return{valid:false,errors:[]}})`;
385
+ })
386
+ .join(',');
387
+ if (isEsm) {
388
+ out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators };\n`;
389
+ } else {
390
+ out += `module.exports=[${arrBody}];\n`;
391
+ }
392
+
393
+ return out;
394
+ }
395
+
396
+ function loadBundle(Validator, mods, schemas, opts) {
397
+ return schemas.map((schema, i) => {
398
+ if (mods[i]) return Validator.fromStandalone(mods[i], schema, opts);
399
+ return new Validator(schema, opts);
400
+ });
401
+ }
402
+
403
+ module.exports = {
404
+ toStandalone,
405
+ toStandaloneModule,
406
+ bundle,
407
+ bundleStandalone,
408
+ bundleCompact,
409
+ loadBundle,
410
+ };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ // Custom error messages via the `errorMessage` keyword.
4
+ //
5
+ // A subschema may declare an `errorMessage` keyword to override the generated
6
+ // `message` on any error it owns:
7
+ //
8
+ // errorMessage: "single message for any failure of this subschema"
9
+ // errorMessage: {
10
+ // <keyword>: "msg", // e.g. minimum, type, pattern, format
11
+ // required: "msg" | { prop: "msg" },// keyed by missing property, or one string
12
+ // additionalProperties: "msg",
13
+ // _: "fallback msg", // used when no keyword-specific entry matches
14
+ // }
15
+ //
16
+ // Resolution: each error's owning subschema is located from its schemaPath (the
17
+ // keyword is the last pointer segment, the owner is everything before it). If the
18
+ // owner declares an errorMessage, the error's `message` is replaced. Errors are
19
+ // cloned, never mutated, so frozen/shared results stay intact. When no schema in
20
+ // the tree carries an errorMessage the decorator is never installed, so the
21
+ // validate hot path keeps its original cost.
22
+
23
+ // Locate the schema object that owns the failing keyword. Mirrors
24
+ // resolveSchemaByPath() in index.js: the last pointer segment is the keyword,
25
+ // so the owner is the node reached by walking every segment before it.
26
+ function resolveOwner (rootSchema, schemaPath) {
27
+ if (!schemaPath || typeof schemaPath !== 'string' || schemaPath[0] !== '#') return undefined;
28
+ const stripped = schemaPath.slice(1);
29
+ if (!stripped || stripped === '/') return rootSchema;
30
+ const parts = stripped.split('/').filter(Boolean).map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
31
+ let target = rootSchema;
32
+ for (let i = 0; i < parts.length - 1; i++) {
33
+ if (target == null || typeof target !== 'object') return undefined;
34
+ target = target[parts[i]];
35
+ }
36
+ return target;
37
+ }
38
+
39
+ // Pick the override string for a single error from its owner's errorMessage.
40
+ // Returns undefined when nothing matches, leaving the default message in place.
41
+ function pickMessage (em, err) {
42
+ if (em == null) return undefined;
43
+ if (typeof em === 'string') return em;
44
+ if (typeof em !== 'object') return undefined;
45
+
46
+ const kw = err.keyword;
47
+
48
+ if (kw === 'required') {
49
+ const r = em.required;
50
+ if (typeof r === 'string') return r;
51
+ if (r && typeof r === 'object') {
52
+ const prop = err.params && err.params.missingProperty;
53
+ if (prop != null && typeof r[prop] === 'string') return r[prop];
54
+ }
55
+ }
56
+
57
+ if (kw != null && typeof em[kw] === 'string') return em[kw];
58
+ if (typeof em._ === 'string') return em._;
59
+ return undefined;
60
+ }
61
+
62
+ // Returns true when the schema string is worth scanning at validate time.
63
+ function schemaHasErrorMessages (schemaStr) {
64
+ return typeof schemaStr === 'string' && schemaStr.indexOf('"errorMessage"') !== -1;
65
+ }
66
+
67
+ // Apply overrides across an error array. Returns the same array reference when
68
+ // nothing changed (so callers can cheaply detect a no-op), or a new array with
69
+ // cloned-and-overridden entries.
70
+ function applyErrorMessages (errors, rootSchema) {
71
+ if (!errors || !errors.length) return errors;
72
+ let changed = false;
73
+ const out = new Array(errors.length);
74
+ for (let i = 0; i < errors.length; i++) {
75
+ const err = errors[i];
76
+ out[i] = err;
77
+ if (!err || typeof err.schemaPath !== 'string') continue;
78
+ const owner = resolveOwner(rootSchema, err.schemaPath);
79
+ if (!owner || typeof owner !== 'object') continue;
80
+ const msg = pickMessage(owner.errorMessage, err);
81
+ if (msg == null) continue;
82
+ out[i] = Object.assign({}, err, { message: msg });
83
+ changed = true;
84
+ }
85
+ return changed ? out : errors;
86
+ }
87
+
88
+ module.exports = { schemaHasErrorMessages, applyErrorMessages, resolveOwner, pickMessage };
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ // Browser stub: there is no native addon in the browser. Swapped in via the
4
+ // package.json `browser` field so bundlers never resolve `pkg-prebuilds`.
5
+
6
+ module.exports = function loadNative() {
7
+ return null;
8
+ };
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ // Locate and load the optional native addon (pkg-prebuilds). Lives in its own
4
+ // module so the default and browser entries can stay free of `pkg-prebuilds`,
5
+ // `__dirname`, and `path` references. In browser bundles this file is replaced
6
+ // with `native-load.browser.js` via the package.json `browser` field, so the
7
+ // `pkg-prebuilds` import never enters the bundle in the first place.
8
+ //
9
+ // Returns the loaded native binding object, or `null` if no prebuild matches
10
+ // (musl/Alpine, unsupported arch, devs running before `npm run build`, etc.).
11
+ // The JS codegen path covers all core validation when this returns null.
12
+
13
+ module.exports = function loadNative() {
14
+ try {
15
+ const path = require('path');
16
+ const pkg = require('pkg-prebuilds');
17
+ return pkg(path.join(__dirname, '..'), require('../binding-options'));
18
+ } catch {
19
+ return null;
20
+ }
21
+ };
package/lib/refine.js ADDED
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ // Async refinement support for the `/t` builder.
4
+ //
5
+ // JSON Schema is synchronous by design and cannot express a check that needs to
6
+ // await (uniqueness, a remote lookup, cross-field business rules). Those live on
7
+ // the builder via `t.refine(schema, check, opts)`. A refinement is carried on a
8
+ // Symbol key (like the OPTIONAL marker), so it is invisible to Object.keys,
9
+ // JSON.stringify, and ata's codegen: `new Validator(schema)` still performs
10
+ // plain structural validation and ignores refinements entirely.
11
+ //
12
+ // `validateAsync()` (in index.js) runs structural validation first and only
13
+ // awaits the refinements when the value is structurally valid — a refinement
14
+ // body may assume the value already has the right shape.
15
+
16
+ const REFINE = Symbol.for('ata.t.refine');
17
+
18
+ // Pull the refinement list off a schema node, or null when there is none.
19
+ function getRefinements (schema) {
20
+ if (!schema || typeof schema !== 'object') return null;
21
+ const r = schema[REFINE];
22
+ return Array.isArray(r) && r.length ? r : null;
23
+ }
24
+
25
+ // Attach a refinement to a schema, composing with any already present. Returns a
26
+ // new object; the input is not mutated. Enumerable Symbol keys (OPTIONAL, prior
27
+ // refinements) are copied by the spread, then the refinement list is replaced.
28
+ function attach (schema, check, opts) {
29
+ if (typeof check !== 'function') {
30
+ throw new TypeError('t.refine(schema, check, opts?) — check must be a function');
31
+ }
32
+ const prev = (schema && Array.isArray(schema[REFINE])) ? schema[REFINE] : [];
33
+ const o = opts || {};
34
+ const entry = { check, message: o.message, path: o.path || '' };
35
+ return Object.assign({}, schema, { [REFINE]: prev.concat(entry) });
36
+ }
37
+
38
+ // Build a single refinement issue in the validator's error shape.
39
+ function issue (entry, message) {
40
+ return {
41
+ keyword: 'refine',
42
+ instancePath: entry.path || '',
43
+ path: entry.path || '',
44
+ schemaPath: '',
45
+ params: {},
46
+ message: message != null ? message : (entry.message || 'value failed refinement'),
47
+ };
48
+ }
49
+
50
+ // Evaluate every refinement against `data`, awaiting async checks concurrently.
51
+ // A check that returns falsy — or throws — yields an issue. Returns the issues
52
+ // array (empty when all pass).
53
+ async function runRefinements (refinements, data) {
54
+ const issues = [];
55
+ await Promise.all(refinements.map(async (entry) => {
56
+ let passed;
57
+ try {
58
+ passed = await entry.check(data);
59
+ } catch (e) {
60
+ issues.push(issue(entry, entry.message || (e && e.message) || 'refinement threw'));
61
+ return;
62
+ }
63
+ if (!passed) issues.push(issue(entry));
64
+ }));
65
+ return issues;
66
+ }
67
+
68
+ module.exports = { REFINE, getRefinements, attach, runRefinements };
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ // Auto-generated by scripts/regen-safe-regex-source.js — do not edit.
4
+ // Mirrors `lib/safe-regex.js` verbatim so `lib/aot.js` can embed the engine
5
+ // into standalone output without a runtime `fs` read. Kept in sync by
6
+ // `tests/test_safe_regex_source_sync.js`.
7
+
8
+ module.exports = "'use strict'\n\n// Linear-time regex engine for JSON Schema `pattern`, used in place of JS RegExp\n// so an adversarial input cannot trigger catastrophic backtracking (ReDoS).\n//\n// It is a Pike VM: the pattern compiles to a small instruction program, and the\n// VM simulates all NFA threads in lockstep over the input, deduping by program\n// counter. Runtime is O(input * program), with no backtracking.\n//\n// Supported (the RE2 subset, which is what ata's native path also accepts):\n// literals, ., character classes, \\d \\w \\s \\D \\W \\S, anchors ^ $, quantifiers\n// * + ? {n} {n,} {n,m} (greedy or lazy, same language for a boolean test),\n// groups ( ) (?: ), alternation |. Backreferences and lookaround are not\n// supported by linear engines; compileSafe throws on them so the caller can\n// decide (ata's codegen rejects such schemas rather than risk a hang).\n\nconst WS = [[9, 13], [32, 32], [160, 160]]\nconst DIGIT = [[48, 57]]\nconst WORD = [[48, 57], [65, 90], [97, 122], [95, 95]]\n\nfunction parse (src) {\n let i = 0\n const len = src.length\n const peek = () => src[i]\n const eof = () => i >= len\n\n function parseAlt () {\n const opts = [parseConcat()]\n while (!eof() && peek() === '|') { i++; opts.push(parseConcat()) }\n return opts.length === 1 ? opts[0] : { t: 'alt', opts }\n }\n\n function parseConcat () {\n const parts = []\n while (!eof() && peek() !== '|' && peek() !== ')') parts.push(parseRepeat())\n if (parts.length === 0) return { t: 'empty' }\n return parts.length === 1 ? parts[0] : { t: 'concat', parts }\n }\n\n function parseRepeat () {\n let node = parseAtom()\n while (!eof()) {\n const ch = peek()\n if (ch === '*') { i++; node = { t: 'star', child: node } }\n else if (ch === '+') { i++; node = { t: 'plus', child: node } }\n else if (ch === '?') { i++; node = { t: 'quest', child: node } }\n else if (ch === '{') {\n const saved = i\n const q = tryQuantifier()\n if (!q) { i = saved; break }\n node = { t: 'repeat', child: node, min: q.min, max: q.max }\n } else break\n // a trailing ? makes the quantifier lazy; same language for a boolean test\n if (!eof() && peek() === '?') i++\n }\n return node\n }\n\n function tryQuantifier () {\n // assumes current char is '{'\n i++\n let min = ''\n while (!eof() && /[0-9]/.test(peek())) { min += peek(); i++ }\n if (min === '') return null\n let max\n if (peek() === '}') { i++; return { min: +min, max: +min } }\n if (peek() === ',') {\n i++\n let m = ''\n while (!eof() && /[0-9]/.test(peek())) { m += peek(); i++ }\n if (peek() !== '}') return null\n i++\n max = m === '' ? Infinity : +m\n return { min: +min, max }\n }\n return null\n }\n\n function parseAtom () {\n const ch = peek()\n if (ch === '(') {\n i++\n if (src[i] === '?') {\n if (src[i + 1] === ':') { i += 2 }\n else throw new Error('unsupported group (lookaround/named) in pattern')\n }\n const child = parseAlt()\n if (peek() !== ')') throw new Error('unbalanced ( in pattern')\n i++\n return { t: 'group', child }\n }\n if (ch === '[') return parseClass()\n if (ch === '.') { i++; return { t: 'any' } }\n if (ch === '^') { i++; return { t: 'bol' } }\n if (ch === '$') { i++; return { t: 'eol' } }\n if (ch === '\\\\') return parseEscape(false)\n if (ch === ')' || ch === '|') return { t: 'empty' }\n i++\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n\n function parseClass () {\n i++ // [\n let neg = false\n if (peek() === '^') { neg = true; i++ }\n const ranges = []\n while (!eof() && peek() !== ']') {\n let lo\n if (peek() === '\\\\') {\n const esc = parseEscape(true)\n if (esc.t === 'classpart') { for (const r of esc.ranges) ranges.push(r); continue }\n lo = esc.c\n } else { lo = peek().charCodeAt(0); i++ }\n if (peek() === '-' && src[i + 1] !== ']' && i + 1 < len) {\n i++ // -\n let hi\n if (peek() === '\\\\') { const e = parseEscape(true); hi = e.c } else { hi = peek().charCodeAt(0); i++ }\n ranges.push([lo, hi])\n } else {\n ranges.push([lo, lo])\n }\n }\n if (peek() !== ']') throw new Error('unbalanced [ in pattern')\n i++\n return { t: 'class', neg, ranges }\n }\n\n function parseEscape (inClass) {\n i++ // backslash\n if (eof()) throw new Error('trailing backslash in pattern')\n const ch = peek(); i++\n switch (ch) {\n case 'd': return inClass ? { t: 'classpart', ranges: DIGIT } : { t: 'class', neg: false, ranges: DIGIT }\n case 'w': return inClass ? { t: 'classpart', ranges: WORD } : { t: 'class', neg: false, ranges: WORD }\n case 's': return inClass ? { t: 'classpart', ranges: WS } : { t: 'class', neg: false, ranges: WS }\n case 'D': if (inClass) throw new Error('\\\\D inside a class is not supported'); return { t: 'class', neg: true, ranges: DIGIT }\n case 'W': if (inClass) throw new Error('\\\\W inside a class is not supported'); return { t: 'class', neg: true, ranges: WORD }\n case 'S': if (inClass) throw new Error('\\\\S inside a class is not supported'); return { t: 'class', neg: true, ranges: WS }\n case 'n': return { t: 'char', c: 10 }\n case 'r': return { t: 'char', c: 13 }\n case 't': return { t: 'char', c: 9 }\n case 'f': return { t: 'char', c: 12 }\n case 'v': return { t: 'char', c: 11 }\n case '0': return { t: 'char', c: 0 }\n case 'x': { const h = src.slice(i, i + 2); i += 2; return { t: 'char', c: parseInt(h, 16) } }\n case 'u': { const h = src.slice(i, i + 4); i += 4; return { t: 'char', c: parseInt(h, 16) } }\n case 'b': if (inClass) return { t: 'char', c: 8 }; throw new Error('\\\\b word boundary is not supported')\n default:\n if (/[1-9]/.test(ch)) throw new Error('backreferences are not supported in pattern')\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n }\n\n const ast = parseAlt()\n if (!eof()) throw new Error('unexpected \"' + peek() + '\" in pattern')\n return ast\n}\n\nfunction compileProg (ast) {\n const prog = []\n const emit = (op, extra) => { const idx = prog.length; prog.push(Object.assign({ op }, extra)); return idx }\n\n function rec (n) {\n switch (n.t) {\n case 'empty': break\n case 'char': emit('char', { c: n.c }); break\n case 'any': emit('any'); break\n case 'class': emit('class', { neg: n.neg, ranges: n.ranges }); break\n case 'bol': emit('bol'); break\n case 'eol': emit('eol'); break\n case 'group': rec(n.child); break\n case 'concat': for (const p of n.parts) rec(p); break\n case 'alt': {\n const jmps = []\n for (let k = 0; k < n.opts.length; k++) {\n if (k < n.opts.length - 1) {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.opts[k])\n jmps.push(emit('jmp', { x: 0 }))\n prog[sp].y = prog.length\n } else {\n rec(n.opts[k])\n }\n }\n for (const j of jmps) prog[j].x = prog.length\n break\n }\n case 'star': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n emit('jmp', { x: sp })\n prog[sp].y = prog.length\n break\n }\n case 'plus': {\n const start = prog.length\n rec(n.child)\n const sp = emit('split', { x: start, y: 0 })\n prog[sp].y = prog.length\n break\n }\n case 'quest': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n prog[sp].y = prog.length\n break\n }\n case 'repeat': {\n for (let k = 0; k < n.min; k++) rec(n.child)\n if (n.max === Infinity) {\n if (n.min === 0) rec({ t: 'star', child: n.child })\n else rec({ t: 'star', child: n.child })\n } else {\n for (let k = 0; k < n.max - n.min; k++) rec({ t: 'quest', child: n.child })\n }\n break\n }\n }\n }\n\n rec(ast)\n emit('match')\n return prog\n}\n\nfunction matchClass (instr, c) {\n let inside = false\n const r = instr.ranges\n for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }\n return instr.neg ? !inside : inside\n}\n\nfunction makeRunner (prog) {\n const n = prog.length\n const lastGen = new Int32Array(n).fill(-1)\n let gen = 0\n const stack = []\n\n function addThread (list, pc, pos, len) {\n stack.length = 0\n stack.push(pc)\n while (stack.length) {\n const p = stack.pop()\n if (lastGen[p] === gen) continue\n lastGen[p] = gen\n const I = prog[p]\n switch (I.op) {\n case 'jmp': stack.push(I.x); break\n case 'split': stack.push(I.y); stack.push(I.x); break\n case 'bol': if (pos === 0) stack.push(p + 1); break\n case 'eol': if (pos === len) stack.push(p + 1); break\n default: list.push(p)\n }\n }\n }\n\n return function test (s) {\n const len = s.length\n let clist = []\n let nlist = []\n gen++\n addThread(clist, 0, 0, len)\n for (let pos = 0; pos <= len; pos++) {\n const c = pos < len ? s.charCodeAt(pos) : -1\n gen++\n nlist.length = 0\n for (let k = 0; k < clist.length; k++) {\n const pc = clist[k]\n const I = prog[pc]\n if (I.op === 'match') return true\n else if (I.op === 'char') { if (c === I.c) addThread(nlist, pc + 1, pos + 1, len) }\n else if (I.op === 'any') { if (c !== -1 && c !== 10) addThread(nlist, pc + 1, pos + 1, len) }\n else if (I.op === 'class') { if (c !== -1 && matchClass(I, c)) addThread(nlist, pc + 1, pos + 1, len) }\n }\n if (pos < len) addThread(nlist, 0, pos + 1, len)\n const tmp = clist; clist = nlist; nlist = tmp\n }\n return false\n }\n}\n\nfunction compileSafe (pattern) {\n const prog = compileProg(parse(pattern))\n const runner = makeRunner(prog)\n // `__ataSafe` brands the result so the standalone serializer can tell a safe\n // matcher apart from a RegExp and emit `__ataSafeRe(source)` instead.\n return { test: runner, source: pattern, __ataSafe: true }\n}\n\n// True when the linear engine can represent `src`. Used by the codegen to decide\n// between the safe matcher and a JS RegExp fallback for patterns outside the\n// supported (RE2) subset (backreferences, lookaround, etc.).\nfunction patternIsSafe (src) {\n try { compileSafe(src); return true } catch { return false }\n}\n\nmodule.exports = { compileSafe, patternIsSafe }\n";