ata-validator 1.61.2 → 1.62.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.
Files changed (52) hide show
  1. package/README.md +159 -37
  2. package/bin/ata.js +35 -2
  3. package/compat.js +15 -2
  4. package/compiled-standard.d.ts +6 -0
  5. package/compiled-standard.mjs +66 -0
  6. package/index.d.ts +7 -0
  7. package/index.js +10 -0
  8. package/lib/aot-build.js +27 -1
  9. package/lib/aot-impl.js +108 -17
  10. package/lib/buffer-gate.js +25 -14
  11. package/lib/bun-target.js +21 -5
  12. package/lib/clone-emit.js +115 -10
  13. package/lib/codegen-engine.js +94 -18
  14. package/lib/compiled-common.js +11 -4
  15. package/lib/compiled-standard.js +33 -0
  16. package/lib/compiled-verdict.js +2 -2
  17. package/lib/compiled.js +1 -1
  18. package/lib/data-positions.js +18 -0
  19. package/lib/defaults.js +101 -23
  20. package/lib/describe-schema.js +38 -5
  21. package/lib/diagnose.js +51 -29
  22. package/lib/enrich-error.js +23 -1
  23. package/lib/error-sort-rank.js +12 -0
  24. package/lib/formats-source.js +12 -1
  25. package/lib/formats.js +19 -19
  26. package/lib/interpreter-core.js +23 -4
  27. package/lib/js-compiler-errors.js +70 -72
  28. package/lib/js-compiler.js +212 -72
  29. package/lib/json-rejections.js +1 -1
  30. package/lib/migrate.js +24 -5
  31. package/lib/native-attach.browser.js +7 -0
  32. package/lib/native-attach.js +76 -0
  33. package/lib/native-positions.js +7 -0
  34. package/lib/plan-compiler.js +7 -1
  35. package/lib/regex-linear.js +88 -0
  36. package/lib/rejections.js +13 -3
  37. package/lib/render-compact.js +5 -5
  38. package/lib/render-pretty.js +23 -10
  39. package/lib/safe-regex.js +29 -4
  40. package/lib/scan-compiler.js +16 -4
  41. package/lib/schema-diff.js +103 -0
  42. package/lib/stream.js +293 -0
  43. package/lib/tier0.js +6 -2
  44. package/lib/ts-gen.js +25 -12
  45. package/lib/unique-items.js +11 -3
  46. package/lib/validator-core.js +61 -66
  47. package/lib/verify.js +25 -6
  48. package/lib/version.js +1 -1
  49. package/package.json +24 -8
  50. package/stream.d.ts +51 -0
  51. package/stream.js +3 -0
  52. package/stream.mjs +3 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ata-validator
2
2
 
3
- JSON Schema validation that compiles for speed and still runs where code generation is blocked. The compiled and interpreted engines answer identically, at 100% of the official suite in both modes, across Draft 2020-12, draft 7 and the [JSON Schema v1 dialect](#dialects). First-class TypeScript inference, and `ata build` emits a standalone module that imports nothing.
3
+ JSON Schema validation where the TypeScript type comes from the schema, or the schema from the type. Write the schema once and `validate` narrows to it; write the type once and the bundler plugin writes its schema in at build time. The compiled and interpreted engines answer identically, at 100% of the official suite in both modes, across Draft 2020-12, 2019-09, drafts 7, 6 and 4 and the [JSON Schema v1 dialect](#dialects), and `ata build` emits a standalone module that imports nothing, with its `.d.mts` beside it.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/ata-validator)](https://www.npmjs.com/package/ata-validator)
6
6
  [![License](https://img.shields.io/npm/l/ata-validator)](LICENSE)
@@ -10,6 +10,60 @@ JSON Schema validation that compiles for speed and still runs where code generat
10
10
 
11
11
  1.0 is a stability commitment: see [docs/STABILITY.md](docs/STABILITY.md) for the semver, deprecation, and error-code guarantees.
12
12
 
13
+ ## The type and the schema
14
+
15
+ One source of truth, in either direction. A schema written with `defineSchema` carries its
16
+ type: `validate` narrows `data` to it, and `Infer` pulls the type out for a signature.
17
+
18
+ ```ts
19
+ import { defineSchema, Validator, type Infer } from 'ata-validator'
20
+
21
+ const User = defineSchema({
22
+ type: 'object',
23
+ properties: {
24
+ id: { type: 'integer', minimum: 1 },
25
+ email: { type: 'string', format: 'email' },
26
+ role: { enum: ['admin', 'user'] },
27
+ },
28
+ required: ['id', 'email'],
29
+ })
30
+
31
+ const users = new Validator(User)
32
+ const r = users.validate(JSON.parse(body))
33
+ if (r.valid) {
34
+ r.data.id // number
35
+ r.data.role // 'admin' | 'user' | undefined
36
+ r.data.name // a type error: the schema has no name
37
+ }
38
+
39
+ type User = Infer<typeof User>
40
+ ```
41
+
42
+ Or start from the type. `schemaOf<T>()` has no run-time form; the bundler plugin reads `T`
43
+ with the TypeScript checker and writes its JSON Schema in place of the call, with the JSDoc
44
+ tags carrying what a type cannot say, and the `new Validator(...)` then compiles away like any
45
+ static schema.
46
+
47
+ ```ts
48
+ import { Validator, schemaOf } from 'ata-validator'
49
+
50
+ interface Account {
51
+ /** @minimum 1 */
52
+ id: number
53
+ /** @format email */
54
+ email: string
55
+ role?: 'admin' | 'user'
56
+ }
57
+
58
+ const accounts = new Validator(schemaOf<Account>())
59
+ ```
60
+
61
+ `ata build` writes a `.d.mts` next to every compiled module, with the inferred type and an
62
+ `isValid` predicate on it, so a module that imports nothing still narrows. The chainable
63
+ builder in `ata-validator/t`, `Infer` on `anyOf`, `allOf`, tuples and recursive `$ref`, and
64
+ the async `t.refine` are in [Type-safe schemas](#type-safe-schemas); the plugin's type reading
65
+ is in [its README](https://github.com/ata-core/unplugin-ata#a-schema-from-a-typescript-type-schemaofuser).
66
+
13
67
  ## Who uses ata
14
68
 
15
69
  Projects whose code depends on ata, from GitHub's dependency graph, a framework that lists its
@@ -147,16 +201,24 @@ documents, Node 24:
147
201
 
148
202
  | | documents a second, the verdict | same, `new Function` refused |
149
203
  |---|---:|---:|
150
- | ata | 3,295,880 | 607,614 |
151
- | ata, compiled module | 3,319,594 | 3,344,948 |
152
- | typebox | 1,638,813 | EvalError |
153
- | ajv | 1,285,492 | EvalError |
154
- | typia | 939,572 | 964,507 |
155
- | zod | 440,473 | 237,176 |
156
-
157
- Where ata is behind is in the same tables: on a four-field body that fails, ajv answers the
158
- verdict faster, and with every error read it does 6.6M a second against ata's 2.9M with rich
159
- errors (7.6M with `richErrors: false`).
204
+ | ata | 3,274,803 | 1,109,881 |
205
+ | ata, compiled module | 3,270,947 | 3,353,351 |
206
+ | typia | 1,957,392 | 1,931,134 |
207
+ | typebox | 1,615,239 | EvalError |
208
+ | ajv | 1,245,508 | EvalError |
209
+ | zod | 436,632 | 230,808 |
210
+
211
+ Run of 2026-10-30 with typia 15.2.2, which reuses a pattern's RegExp and shortcuts length
212
+ checks after a report from this benchmark; its order figure doubled.
213
+
214
+ The same repository runs every SchemaStore schema that ships sample documents, 446 of them,
215
+ against the default validator on those samples: ata ahead on 430, median 1.83x, 2.82x on
216
+ schemas of 50 to 200 KB, and never below 0.74x on the 16 where it is behind
217
+ ([RESULTS-schemastore.md](https://github.com/ata-core/validator-benchmarks/blob/main/RESULTS-schemastore.md),
218
+ Node 24, ata 1.61.3). Where ata is behind is
219
+ in the same tables: on a four-field body that fails, ajv answers the verdict faster (15.5M
220
+ against 10.4M), and with every error read it does 7.1M a second against ata's 3.6M with rich
221
+ errors, 5.2M with `richErrors: false`.
160
222
 
161
223
  ## Why AOT
162
224
 
@@ -399,31 +461,7 @@ v.countValid(ndjson); // number
399
461
 
400
462
  ### Type-safe schemas
401
463
 
402
- ata infers TypeScript types straight from plain JSON Schema. Write the schema once with `defineSchema`, and both runtime validation and the static type come from it, with no builder DSL and no second type declaration to keep in sync.
403
-
404
- ```ts
405
- import { defineSchema, Validator } from 'ata-validator'
406
-
407
- const userSchema = defineSchema({
408
- type: 'object',
409
- properties: {
410
- id: { type: 'integer', minimum: 1 },
411
- role: { type: 'string', enum: ['admin', 'user'] },
412
- },
413
- required: ['id'],
414
- })
415
-
416
- const v = new Validator(userSchema)
417
- const result = v.validate(data)
418
- if (result.valid) {
419
- result.data.id // number
420
- result.data.role // 'admin' | 'user' | undefined
421
- } else {
422
- // result.errors: ValidationError[]
423
- }
424
- ```
425
-
426
- `defineSchema` returns the schema untouched at runtime; in TypeScript it gives keyword autocomplete and an error when a value has the wrong shape, with no `as const` needed. `new Validator(schema)` carries the inferred type, so a successful `validate` narrows `result.data` with no manual annotation.
464
+ ata infers TypeScript types straight from plain JSON Schema, as in [the opening example](#the-type-and-the-schema): `defineSchema` returns the schema untouched at runtime and, in TypeScript, gives keyword autocomplete and an error when a value has the wrong shape, with no `as const` needed. `new Validator(schema)` carries the inferred type, so a successful `validate` narrows `result.data` with no manual annotation.
427
465
 
428
466
  #### Extracting the type: `Infer`
429
467
 
@@ -671,7 +709,11 @@ validator only ever called (`v.validate(x)`, `v.isValidObject(x)`), never handed
671
709
  it uses the bare wrappers of `ata-validator/compiled-bare` instead, which leave out Standard
672
710
  Schema and the extension hook nothing can reach there: 0.3 KB gzipped for both, and
673
711
  `ata-validator/compiled-bare-rich` for one that reads ata's own errors. The same form app
674
- bundles to 10.7 KB gzipped, or 3.1 KB with `richErrors: false` on the validator that reads errors. The errors
712
+ bundles to 10.7 KB gzipped, or 3.1 KB with `richErrors: false` on the validator that reads errors.
713
+ A validator handed only to a Standard Schema consumer the plugin knows (Hono's
714
+ `sValidator`, `hono-openapi`, TanStack Form, oRPC, tRPC, Elysia) takes `ata-validator/compiled-standard`, which
715
+ carries `~standard` alone on a module with plain errors: a Hono route on Cloudflare Workers goes
716
+ from 21.0 to 12.2 KB gzipped, 4.4 KB over Hono by itself. The errors
675
717
  then carry `keyword`, `instancePath`, `schemaPath`, `params` and `message`, as that option has
676
718
  always given them, without `received`, `suggestion` or `docUrl`.
677
719
 
@@ -728,6 +770,86 @@ works as a CI step. This is the check that found the two silent divergences
728
770
  fixed in 1.59.0 and 1.60.0, now in a form a project can run on every change.
729
771
  It does not run the `.bun.ts` target, which needs Bun's compiler.
730
772
 
773
+ ### What a schema change does (`ata diff`)
774
+
775
+ Reading two versions of a schema side by side says which keywords changed. It
776
+ does not say which documents change verdict, and that is the question before
777
+ a deploy: which requests that pass today get a 400 tomorrow. `ata diff` answers
778
+ it with documents:
779
+
780
+ ```bash
781
+ npx ata diff schemas/user.json schemas/user-v2.json --samples test/fixtures
782
+ ```
783
+
784
+ ```
785
+ 401 documents: 102 accepted by both, 293 rejected by both, 6 rejected only by user-v2.json, 0 accepted only by user-v2.json
786
+
787
+ Rejected by user-v2.json, accepted by user.json (a client sending these gets a rejection after the change):
788
+ minimum@/age: 4 documents, 1 of them the project's own, must be >= 18
789
+ {"email":"kid@example.com","name":"Kid","age":15} (test/fixtures/signup-teen.json)
790
+ maxLength@/name: 2 documents, must NOT have more than 40 characters
791
+ {"email":"a@b.co","name":"abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij...
792
+ ```
793
+
794
+ The documents are the ones `ata verify` makes, from both schemas (200 each by
795
+ default, half with one rule broken, then a value on either side of every
796
+ bound), plus the project's own samples. Each one is checked by a `Validator`
797
+ built from each schema, and the ones that change verdict are grouped by the
798
+ rule that decides them, with one example each, the project's own document
799
+ when there is one. The exit code is 1 when a document the old schema accepts
800
+ is rejected by the new one, so a pull request that tightens a schema fails
801
+ the check until someone looks. Generated documents are a sample, not a proof:
802
+ a change this reports is real, one it does not report may still exist.
803
+
804
+ ### Validation as the body arrives
805
+
806
+ A request that carries many documents, a JSON array or NDJSON, is usually read to the end,
807
+ parsed as a whole and then validated, so a bad third record is reported after the last byte.
808
+ `ata-validator/stream` validates each record the moment its last byte lands and stops reading
809
+ at the first failure, with memory that does not grow with the body.
810
+
811
+ ```js
812
+ import { validateStream } from 'ata-validator/stream'
813
+
814
+ app.post('/orders/import', async (c) => {
815
+ const r = await validateStream(orders, c.req.raw.body) // a Web ReadableStream, or a Node Readable
816
+ if (!r.valid) return c.json({ record: r.errors[0].record, errors: r.errors[0].errors }, 400)
817
+ return c.json({ imported: r.records })
818
+ })
819
+ ```
820
+
821
+ `v.stream(opts)` on Node, or `streamValidator(v, opts)` from `ata-validator/stream` anywhere (the lite, edge and browser entries do not carry the method, so their bundles stay what they were), is the same thing fed by hand: `push(chunk)` takes strings or bytes and returns
822
+ the first failure as soon as its record is complete, `end()` returns `{ valid, records, errors }`.
823
+ `onRecord(doc, index)` hands over each record that passed, parsed, so an import does not parse
824
+ twice; `stopOnFirst: false` lists every failing record instead of stopping. The format is read
825
+ from the first character (`[` is an array, anything else NDJSON) or set with `format`. A JSON
826
+ array is split by a scan that tracks strings and nesting, not by parsing it, so the elements are
827
+ parsed one at a time.
828
+
829
+ Measured on a 100 MB body of 128,419 order documents, read in 64 KB chunks, Node 25, Apple M4
830
+ Pro, one process per row:
831
+
832
+ | | invalid record 3 | invalid last record |
833
+ |---|---|---|
834
+ | read it all, `JSON.parse`, validate the array | 517 ms, 783 MB | 571 ms, 845 MB |
835
+ | read it all, split lines, parse and validate each | 237 ms, 553 MB | 462 ms, 660 MB |
836
+ | `ata-validator/stream`, JSON array | 13.5 ms, 122 MB, 0.1 MB read | 577 ms, 195 MB |
837
+ | `ata-validator/stream`, NDJSON | 19.6 ms, 122 MB, 0.1 MB read | 461 ms, 197 MB |
838
+
839
+ Peak RSS; the 100 MB figure is the process without the body. A body that is valid to the end
840
+ costs the same time as before and a third of the memory. `tests/test_stream.js` holds the stream
841
+ to whole-document validation under every way of cutting the bytes into chunks, including inside
842
+ multi-byte characters, strings with brackets and escaped quotes, nested arrays, scalars, CRLF
843
+ and blank lines, and checks that a failing source is destroyed or cancelled rather than read on.
844
+
845
+ A Node http request handed to `validateStream` is not destroyed at the failure: closing the
846
+ socket under the response turns the answer into a network error at the client (Node's `fetch`
847
+ uploading 100 MB got `write EPIPE`, Chrome "Failed to fetch", neither the 400). Reading stops and
848
+ the rest of the body is read and dropped, so the response goes out and memory stays flat. Node's
849
+ `fetch` then had the 400 after 24 ms and 1 MB sent; Chrome, which reads only after its upload,
850
+ after 58 ms for a 50 MB file over loopback; the Hono example above, whose Node adapter cancels
851
+ the stream without closing the socket, after 28 ms. A plain `Readable` is still destroyed.
852
+
731
853
  ### Bun, compiled ahead of time
732
854
 
733
855
  Bun's ahead-of-time compiler (`bun build --compile --aot`, an open pull
package/bin/ata.js CHANGED
@@ -15,6 +15,8 @@ Usage:
15
15
  ata migrate [dir] [options] Report what switching from ajv would change; --write applies it.
16
16
  ata verify <glob>... [options] Check that each schema's compiled module answers as the runtime
17
17
  does, on generated documents and the project's samples.
18
+ ata diff <old> <new> [options] What a change to a schema does: the documents the old schema
19
+ accepts and the new one rejects, and the reverse, by rule.
18
20
 
19
21
  Verify options:
20
22
  --generate <n> Documents made from each schema, half with one rule broken. Default: 200
@@ -23,6 +25,13 @@ Verify options:
23
25
  --json Report as JSON
24
26
  --out-dir, --suffix, --format Where the modules are, as given to ata build
25
27
 
28
+ Diff options:
29
+ --generate <n> Documents made from each schema. Default: 200
30
+ --samples <path> The project's own documents, checked too (repeatable)
31
+ --seed <n> Seed for the generated documents. Default: 1
32
+ --json Report as JSON
33
+ Exit status 1 when a document the old schema accepts is rejected by the new one.
34
+
26
35
  Validate options:
27
36
  --pretty Render errors with source frames (default on TTY)
28
37
  --compact Render errors as one line each (default when piped)
@@ -487,6 +496,24 @@ function cmdVerify (args) {
487
496
  })().catch((e) => { process.stderr.write(`error: ${e.message}\n`); process.exit(1); });
488
497
  }
489
498
 
499
+ function cmdDiff (args) {
500
+ if (args._.length < 2) {
501
+ process.stderr.write('error: ata diff <old-schema> <new-schema>\n');
502
+ process.exit(2);
503
+ }
504
+ const buildLib = require('../lib/aot-build');
505
+ const { diffSchemas, renderDiff, breaking } = require('../lib/schema-diff');
506
+ let oldSchema, newSchema;
507
+ try { oldSchema = buildLib.parseSchemaFile(args._[0]); newSchema = buildLib.parseSchemaFile(args._[1]); } catch (e) { process.stderr.write(`error: ${e.message}\n`); process.exit(2); }
508
+ const samples = args.opts.samples === undefined ? [] : [].concat(args.opts.samples);
509
+ let report;
510
+ try {
511
+ report = diffSchemas(oldSchema, newSchema, { generate: Number.isFinite(args.opts.generate) ? args.opts.generate : 200, seed: Number.isFinite(args.opts.seed) ? args.opts.seed : 1, samples });
512
+ } catch (e) { process.stderr.write(`error: ${e.message}\n`); process.exit(2); }
513
+ process.stdout.write(renderDiff(report, { json: !!args.opts.json, oldName: path.basename(args._[0]), newName: path.basename(args._[1]) }) + '\n');
514
+ process.exit(breaking(report) ? 1 : 0);
515
+ }
516
+
490
517
  function cmdValidate (args) {
491
518
  if (args._.length < 2) {
492
519
  process.stderr.write('error: ata validate <schema> <data-file>\n');
@@ -506,7 +533,8 @@ function cmdValidate (args) {
506
533
  try {
507
534
  schema = JSON.parse(schemaContent);
508
535
  } catch (e) {
509
- process.stderr.write(`error: ${schemaPath} is not valid JSON: ${e.message}\n`);
536
+ // The parser quotes the text it stopped at, and the text is the file's.
537
+ process.stderr.write(`error: ${schemaPath} is not valid JSON: ${require('../lib/diagnose').term(e.message)}\n`);
510
538
  process.exit(2);
511
539
  }
512
540
 
@@ -523,7 +551,7 @@ function cmdValidate (args) {
523
551
  try {
524
552
  v = new Validator(schema, { source: { path: schemaPath, content: schemaContent } });
525
553
  } catch (e) {
526
- process.stderr.write(`error: schema compile failed: ${e.message}\n`);
554
+ process.stderr.write(`error: schema compile failed: ${require('../lib/diagnose').term(e.message)}\n`);
527
555
  process.exit(2);
528
556
  }
529
557
 
@@ -592,6 +620,11 @@ function main() {
592
620
  return;
593
621
  }
594
622
 
623
+ if (cmd === 'diff') {
624
+ cmdDiff(args);
625
+ return;
626
+ }
627
+
595
628
  if (cmd === 'migrate') {
596
629
  const { migrate, formatReport } = require('../lib/migrate');
597
630
  const result = migrate(args._[0] || '.', { write: !!args.opts.write });
package/compat.js CHANGED
@@ -129,7 +129,9 @@ class Ata {
129
129
  const formatNames = Object.keys(this._formats);
130
130
  if (formatNames.length > 0) out.formats = { ...this._formats };
131
131
  const keywordNames = Object.keys(this._keywords);
132
- const kws = {};
132
+ // Null prototype: a keyword named __proto__ written into a plain object
133
+ // would set its prototype and never reach the validator.
134
+ const kws = Object.create(null);
133
135
  for (const name of keywordNames) if (this._keywords[name] !== true) kws[name] = this._keywords[name];
134
136
  if (Object.keys(kws).length > 0) out.keywords = kws;
135
137
  const registry = this._registrySnapshot();
@@ -646,8 +648,16 @@ function referencedDefaults(rootDoc, registry) {
646
648
  }
647
649
  return node;
648
650
  };
651
+ // `done`, per call: the schemas already applied to each object. A second
652
+ // application fills nothing new, and following every edge of a chain such
653
+ // as n_i = allOf [n_(i-1), n_(i-1)] applied the bottom schema 2^n times.
654
+ let done = null;
649
655
  const fill = (schema, data, active) => {
650
656
  if (schema === null || typeof schema !== 'object' || data === null || typeof data !== 'object' || active.has(schema)) return;
657
+ let applied = done.get(data);
658
+ if (applied === undefined) done.set(data, (applied = new Set()));
659
+ if (applied.has(schema)) return;
660
+ applied.add(schema);
651
661
  active.add(schema);
652
662
  if (typeof schema.$ref === 'string') fill(resolve(schema.$ref), data, active);
653
663
  if (Array.isArray(schema.allOf)) for (const sub of schema.allOf) fill(sub, data, active);
@@ -670,7 +680,10 @@ function referencedDefaults(rootDoc, registry) {
670
680
  }
671
681
  active.delete(schema);
672
682
  };
673
- return (data) => fill(rootDoc, data, new Set());
683
+ return (data) => {
684
+ done = new Map();
685
+ try { fill(rootDoc, data, new Set()); } finally { done = null; }
686
+ };
674
687
  }
675
688
 
676
689
  // The registry id a fragment-only key is kept under: a `#...` string is not an
@@ -0,0 +1,6 @@
1
+ // ata-validator/compiled-standard: the compiled-module wrapper for a validator
2
+ // a bundler plugin sees handed to Standard Schema consumers only.
3
+ import type { StandardSchemaV1Props } from './index.js';
4
+ import type { CompiledModule } from './compiled.js';
5
+
6
+ export function bareStandard<T = unknown>(mod: CompiledModule, schema: object | boolean, useDefaults?: boolean): { '~standard': StandardSchemaV1Props<T> };
@@ -0,0 +1,66 @@
1
+ // ata-validator/compiled-standard as an ES module of its own, not a re-export
2
+ // of the CommonJS file, so a bundler adds no interop code (see
3
+ // compiled-bare.mjs). The same `~standard` as lib/compiled-standard.js builds
4
+ // through lib/standard-schema.js; tests/test_compiled_standard.js holds the
5
+ // two to the same issues and the same schema documents.
6
+
7
+ // When the module's error function answers valid where its verdict said no,
8
+ // the disagreement is reported rather than turned into an acceptance.
9
+ const DISAGREES = [{ message: 'schema validation failed', instancePath: '' }];
10
+ const EMPTY_PATH = Object.freeze([]);
11
+
12
+ // A JSON pointer as a Standard Schema path: an all-digit segment without a
13
+ // leading zero is an array index and comes out as a number.
14
+ function pathOf(pointer) {
15
+ if (!pointer) return EMPTY_PATH;
16
+ const out = [];
17
+ for (const raw of pointer.split('/').slice(1)) {
18
+ const seg = raw.indexOf('~') >= 0 ? raw.replace(/~1/g, '/').replace(/~0/g, '~') : raw;
19
+ out.push({ key: /^(?:0|[1-9][0-9]*)$/.test(seg) ? Number(seg) : seg });
20
+ }
21
+ return Object.freeze(out);
22
+ }
23
+
24
+ // Standard JSON Schema: the document, as a copy, for its own dialect.
25
+ function jsonSchemaOf(schema) {
26
+ const convert = (options) => {
27
+ const target = options && options.target;
28
+ if (schema === true) return {};
29
+ if (schema === false) return { not: {} };
30
+ const declared = typeof schema.$schema === 'string' ? schema.$schema : '';
31
+ const dialect = declared === '' || declared.includes('draft/2020-12/schema') ? 'draft-2020-12'
32
+ : declared.includes('draft-07/schema') ? 'draft-07' : declared;
33
+ if (target !== dialect) {
34
+ throw new TypeError(`ata-validator cannot provide a ${dialect} schema as ${String(target)}; it does not convert between dialects`);
35
+ }
36
+ return JSON.parse(JSON.stringify(schema));
37
+ };
38
+ return Object.freeze({ input: convert, output: convert });
39
+ }
40
+
41
+ export function bareStandard(mod, schema, useDefaults) {
42
+ const fill = useDefaults !== false && typeof mod.applyDefaults === 'function' ? mod.applyDefaults : null;
43
+ const isValid = mod.isValid;
44
+ const errFn = mod.validate;
45
+ return {
46
+ '~standard': Object.freeze({
47
+ version: 1,
48
+ vendor: 'ata-validator',
49
+ jsonSchema: jsonSchemaOf(schema),
50
+ validate(value) {
51
+ if (fill !== null) fill(value);
52
+ if (isValid(value)) return { value };
53
+ const r = errFn(value);
54
+ const errors = r && r.valid === false ? r.errors : DISAGREES;
55
+ const issues = new Array(errors.length);
56
+ for (let i = 0; i < errors.length; i++) {
57
+ const e = errors[i];
58
+ issues[i] = { message: e.message || 'validation failed', path: pathOf(e.instancePath != null ? e.instancePath : (e.path || '')) };
59
+ }
60
+ return { issues };
61
+ },
62
+ }),
63
+ };
64
+ }
65
+
66
+ export default { bareStandard };
package/index.d.ts CHANGED
@@ -509,6 +509,13 @@ export interface Validator<T = unknown> {
509
509
  /** Validate a JSON string. Uses simdjson fast path for large documents. */
510
510
  validateJSON(jsonString: string): ValidationResult<T>;
511
511
 
512
+ /**
513
+ * Validation as the body arrives: a record stream over this validator for a
514
+ * JSON array or NDJSON fed in chunks, reporting the first failing record the
515
+ * moment it is complete. See `ata-validator/stream` for the types.
516
+ */
517
+ stream(opts?: import('./stream').StreamOptions): import('./stream').RecordStream;
518
+
512
519
  /** Fast boolean check for a JSON string */
513
520
  isValidJSON(jsonString: string): boolean;
514
521
 
package/index.js CHANGED
@@ -6,6 +6,16 @@
6
6
  // lib/codegen-engine.js, so that the edge entry (index.edge.js) and
7
7
  // ata-validator/lite can load the core without it.
8
8
  const core = require('./lib/validator-core.js');
9
+
10
+ // Validation as the body arrives: `v.stream(opts)` is a record stream over the
11
+ // validator (lib/stream.js), loaded on first use. Defined here, on the Node
12
+ // entry, and not in the core: the lite, edge and browser builds are measured
13
+ // to the byte and a static require from the core would put the stream into
14
+ // every one of them; there `ata-validator/stream` is imported where it is used.
15
+ Object.defineProperty(core.Validator.prototype, 'stream', {
16
+ configurable: true, writable: true,
17
+ value: function stream (opts) { return require('./lib/stream.js').streamValidator(this, opts); },
18
+ });
9
19
  // The code generator loads on first need, or once the event loop is free,
10
20
  // not with the package: it is most of what requiring the package parsed. The
11
21
  // timer is unref'd, so a script that has finished does not stay to load it.
package/lib/aot-build.js CHANGED
@@ -95,7 +95,13 @@ function runtimePathFor(outputs, opts, format) {
95
95
  let dir = path.resolve(path.dirname(outputs[0]));
96
96
  for (const o of outputs) {
97
97
  const d = path.resolve(path.dirname(o));
98
- while (dir !== d && !d.startsWith(dir + path.sep)) dir = path.dirname(dir);
98
+ // The root already ends in a separator ('/' or a drive root), and dirname of the
99
+ // root is the root: compared against root + sep the walk never ended.
100
+ while (dir !== d && !d.startsWith(dir.endsWith(path.sep) ? dir : dir + path.sep)) {
101
+ const up = path.dirname(dir);
102
+ if (up === dir) break;
103
+ dir = up;
104
+ }
99
105
  }
100
106
  return path.join(dir, name);
101
107
  }
@@ -245,6 +251,15 @@ async function buildWith(opts) {
245
251
  // check once warm.
246
252
  if (opts.mode !== undefined && opts.mode !== 'code' && opts.mode !== 'schema') throw new Error(`build: mode must be code or schema (got "${opts.mode}")`);
247
253
  const schemaMode = opts.mode === 'schema' && !opts.abortEarly;
254
+ // The options that change what a module holds, recorded with each cache
255
+ // entry and required to match. Without them a module built for production
256
+ // (no schema-source frames) was reported up to date by a development build
257
+ // and kept, since the input and the output had not changed.
258
+ const optionsKey = JSON.stringify([
259
+ !!opts.abortEarly, opts.onePass === true, !!opts.plainErrors, !!opts.verdictOnly,
260
+ opts.order === undefined ? null : opts.order, opts.formatMode === undefined ? null : opts.formatMode,
261
+ opts.baseUri === undefined ? null : opts.baseUri, opts.target === undefined ? null : opts.target, !!opts.types,
262
+ ]);
248
263
 
249
264
  const compiled = [];
250
265
  const cached = [];
@@ -367,6 +382,8 @@ async function buildWith(opts) {
367
382
  cacheEntry.output === output &&
368
383
  !!cacheEntry.sharable === sharable &&
369
384
  !!cacheEntry.schemaMode === schemaMode &&
385
+ cacheEntry.source === source &&
386
+ cacheEntry.options === optionsKey &&
370
387
  // A module built against the other inputs depends on them too, and
371
388
  // the registry is put together from such modules: always rebuilt.
372
389
  !cacheEntry.usesSiblings &&
@@ -522,6 +539,8 @@ async function buildWith(opts) {
522
539
  outputHash: hashContent(Buffer.from(src, 'utf8')),
523
540
  sharable,
524
541
  schemaMode,
542
+ source,
543
+ options: optionsKey,
525
544
  mode: e.mode,
526
545
  runtime: e.mode !== 'inline' ? runtime.path : undefined,
527
546
  fullGz: e.fullGz,
@@ -577,8 +596,13 @@ async function watch(opts, onReport) {
577
596
  }
578
597
  };
579
598
 
599
+ // The build writes its cache file, JSON and often beside the schemas; a
600
+ // change to it is the build's own and starts nothing, or every build woke
601
+ // the watcher for the next one.
602
+ const cachePath = opts.cacheFile ? path.resolve(opts.cacheFile) : null;
580
603
  const watchers = dirs.map((d) => fs.watch(d, (_event, filename) => {
581
604
  if (!filename) return;
605
+ if (cachePath !== null && path.resolve(d, filename) === cachePath) return;
582
606
  const ext = path.extname(filename).toLowerCase();
583
607
  if (ext !== '.json' && ext !== '.yaml' && ext !== '.yml') return;
584
608
  if (debounceTimer) clearTimeout(debounceTimer);
@@ -754,3 +778,5 @@ module.exports = {
754
778
  toStandaloneModule,
755
779
  schemaHash,
756
780
  };
781
+ // For tests only, not part of ata-validator/build.
782
+ Object.defineProperty(module.exports, '_runtimePathFor', { value: runtimePathFor, enumerable: false });