ata-validator 0.18.1 → 0.20.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,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
+ };
@@ -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";
package/lib/t.js ADDED
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ // Chainable schema builder. Each `t.X(...)` returns a plain JSON Schema
4
+ // object, so the output drops straight into `new Validator(...)`,
5
+ // `defineSchema`, `Infer<S>`, and the AOT pipeline with no adapter. The
6
+ // builder is pure: no runtime state, no class instances, no closures held
7
+ // across calls.
8
+ //
9
+ // Optionality is carried by a Symbol-keyed marker that JSON Schema
10
+ // consumers (including ata's own codegen) never see, because they iterate
11
+ // `Object.keys`, which excludes Symbol keys. `t.object` reads the marker
12
+ // to compute `required` and strips it from the emitted property schema.
13
+
14
+ const OPTIONAL = Symbol.for('ata.t.optional');
15
+
16
+ function string(opts) {
17
+ return Object.assign({ type: 'string' }, opts);
18
+ }
19
+
20
+ function number(opts) {
21
+ return Object.assign({ type: 'number' }, opts);
22
+ }
23
+
24
+ function integer(opts) {
25
+ return Object.assign({ type: 'integer' }, opts);
26
+ }
27
+
28
+ function boolean() {
29
+ return { type: 'boolean' };
30
+ }
31
+
32
+ function nul() {
33
+ return { type: 'null' };
34
+ }
35
+
36
+ function literal(value) {
37
+ return { const: value };
38
+ }
39
+
40
+ function constant(value) {
41
+ return { const: value };
42
+ }
43
+
44
+ function enumOf(values) {
45
+ return { enum: values };
46
+ }
47
+
48
+ function array(items, opts) {
49
+ return Object.assign({ type: 'array', items }, opts);
50
+ }
51
+
52
+ function tuple(items, opts) {
53
+ // JSON Schema 2020-12 uses `prefixItems` for tuples. `items: false`
54
+ // closes the tail, and a default `minItems` matching the tuple length
55
+ // enforces "exactly N elements" so the runtime check matches the
56
+ // TypeScript tuple type that `Infer<S>` produces. A caller can override
57
+ // either bound through `opts`.
58
+ return Object.assign(
59
+ { type: 'array', prefixItems: items, items: false, minItems: items.length },
60
+ opts,
61
+ );
62
+ }
63
+
64
+ function record(values, opts) {
65
+ return Object.assign({ type: 'object', additionalProperties: values }, opts);
66
+ }
67
+
68
+ function object(properties, opts) {
69
+ const props = {};
70
+ const required = [];
71
+ const keys = Object.keys(properties);
72
+ for (const key of keys) {
73
+ const schema = properties[key];
74
+ if (schema && schema[OPTIONAL] === true) {
75
+ const { [OPTIONAL]: _drop, ...rest } = schema;
76
+ props[key] = rest;
77
+ } else {
78
+ props[key] = schema;
79
+ required.push(key);
80
+ }
81
+ }
82
+ const out = { type: 'object', properties: props };
83
+ if (required.length) out.required = required;
84
+ return Object.assign(out, opts);
85
+ }
86
+
87
+ function optional(schema) {
88
+ // Brand the schema so the parent `t.object` can detect it. The Symbol
89
+ // key is invisible to Object.keys / JSON.stringify and to ata's codegen,
90
+ // so a standalone `t.optional(t.string())` still validates as a string
91
+ // if passed directly to `new Validator(...)`.
92
+ return Object.assign({}, schema, { [OPTIONAL]: true });
93
+ }
94
+
95
+ function union(schemas) {
96
+ return { anyOf: schemas };
97
+ }
98
+
99
+ function intersect(schemas) {
100
+ return { allOf: schemas };
101
+ }
102
+
103
+ function ref(pointer) {
104
+ return { $ref: pointer };
105
+ }
106
+
107
+ function any() {
108
+ return {};
109
+ }
110
+
111
+ function unknown() {
112
+ return {};
113
+ }
114
+
115
+ function never() {
116
+ return { not: {} };
117
+ }
118
+
119
+ module.exports = {
120
+ string,
121
+ number,
122
+ integer,
123
+ boolean,
124
+ null: nul,
125
+ literal,
126
+ const: constant,
127
+ enum: enumOf,
128
+ array,
129
+ tuple,
130
+ record,
131
+ object,
132
+ optional,
133
+ union,
134
+ intersect,
135
+ ref,
136
+ any,
137
+ unknown,
138
+ never,
139
+ OPTIONAL,
140
+ };
package/lib/version.js ADDED
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ // The package version, exported as a plain string so neither the default nor
4
+ // the browser entry has to `require('./package.json')`. Bundling package.json
5
+ // would drag dependency strings (e.g. `pkg-prebuilds`) into browser builds
6
+ // even though the runtime never touches them.
7
+ //
8
+ // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
+
10
+ module.exports = '0.20.0';