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.
- package/README.md +159 -37
- package/bin/ata.js +35 -2
- package/compat.js +15 -2
- package/compiled-standard.d.ts +6 -0
- package/compiled-standard.mjs +66 -0
- package/index.d.ts +7 -0
- package/index.js +10 -0
- package/lib/aot-build.js +27 -1
- package/lib/aot-impl.js +108 -17
- package/lib/buffer-gate.js +25 -14
- package/lib/bun-target.js +21 -5
- package/lib/clone-emit.js +115 -10
- package/lib/codegen-engine.js +94 -18
- package/lib/compiled-common.js +11 -4
- package/lib/compiled-standard.js +33 -0
- package/lib/compiled-verdict.js +2 -2
- package/lib/compiled.js +1 -1
- package/lib/data-positions.js +18 -0
- package/lib/defaults.js +101 -23
- package/lib/describe-schema.js +38 -5
- package/lib/diagnose.js +51 -29
- package/lib/enrich-error.js +23 -1
- package/lib/error-sort-rank.js +12 -0
- package/lib/formats-source.js +12 -1
- package/lib/formats.js +19 -19
- package/lib/interpreter-core.js +23 -4
- package/lib/js-compiler-errors.js +70 -72
- package/lib/js-compiler.js +212 -72
- package/lib/json-rejections.js +1 -1
- package/lib/migrate.js +24 -5
- package/lib/native-attach.browser.js +7 -0
- package/lib/native-attach.js +76 -0
- package/lib/native-positions.js +7 -0
- package/lib/plan-compiler.js +7 -1
- package/lib/regex-linear.js +88 -0
- package/lib/rejections.js +13 -3
- package/lib/render-compact.js +5 -5
- package/lib/render-pretty.js +23 -10
- package/lib/safe-regex.js +29 -4
- package/lib/scan-compiler.js +16 -4
- package/lib/schema-diff.js +103 -0
- package/lib/stream.js +293 -0
- package/lib/tier0.js +6 -2
- package/lib/ts-gen.js +25 -12
- package/lib/unique-items.js +11 -3
- package/lib/validator-core.js +61 -66
- package/lib/verify.js +25 -6
- package/lib/version.js +1 -1
- package/package.json +24 -8
- package/stream.d.ts +51 -0
- package/stream.js +3 -0
- package/stream.mjs +3 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ata-validator
|
|
2
2
|
|
|
3
|
-
JSON Schema validation
|
|
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
|
[](https://www.npmjs.com/package/ata-validator)
|
|
6
6
|
[](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,
|
|
151
|
-
| ata, compiled module | 3,
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
| zod |
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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 });
|