ata-validator 1.6.1 → 1.7.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/CHANGELOG.md +35 -0
- package/README.md +32 -14
- package/build.d.ts +12 -0
- package/index.d.ts +16 -4
- package/index.js +58 -13
- package/lib/aot-build.js +1 -0
- package/lib/aot.js +69 -19
- package/lib/buffer-gate.js +132 -0
- package/lib/draft7.js +24 -2
- package/lib/enrich-error.js +14 -4
- package/lib/error-codes.js +16 -9
- package/lib/interpreter.js +432 -200
- package/lib/js-compiler.js +240 -62
- package/lib/metaschemas.js +21 -0
- package/lib/safe-regex-source.js +1 -1
- package/lib/safe-regex.js +209 -29
- package/lib/version.js +1 -1
- package/package.json +8 -8
- package/t.d.ts +16 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
|
|
4
4
|
|
|
5
|
+
## 1.7.0 - 2026-08-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- The four code generation entry points disagreed with each other on 157 instances of the official suite. Most of it was the closure compiler, the boolean fallback behind `isValidObject()` when the code generator declines a schema: it ignored `unevaluatedProperties` and `unevaluatedItems` outright, treated a self-referencing `$ref: "#"` as always true, skipped `additionalProperties` whenever no `properties` map sat next to it, and passed strings in `date-time`, `time`, `uri` and `duration` format without checking them. Each of those accepted input it should have rejected. It now declines those schemas, so they reach an engine that handles them. The remaining disagreements were wrong rejections shared by both boolean paths: `required`, `minProperties` and `maxProperties` applied to non-objects, `multipleOf` had no tolerance for fractional divisors, `const` and `enum` compared objects by key order, and `items` started at index 0 when `prefixItems` was present. `isValidObject()` and `validate()` now agree on every suite instance.
|
|
10
|
+
- A `dependentSchemas` branch carrying `additionalProperties: false` had that check hoisted out of its condition to the top level of the compiled function, so the restriction applied whether or not the triggering property was present.
|
|
11
|
+
- The error path counted a property matched by `patternProperties` as additional under `additionalProperties: false`. The same path now reports the real pointer (`#/patternProperties/<pattern>/...`) for a failing pattern subschema instead of the synthetic `#/patternProperties`, so source frames resolve for those errors.
|
|
12
|
+
- The buffer APIs (`isValid`, `isValidJSON` above the simdjson threshold, `countValid`, `batchIsValid`, `isValidNDJSON`, `isValidParallel`, `isValidPrepadded`) disagreed with `validate()` on 245 of 2222 suite cases, and in 195 of those they accepted a document `validate()` rejects. The native walker behind them does not handle `contains`, `unevaluated*`, `dependencies`, `dependentSchemas`, `dependentRequired`, `propertyNames`, `patternProperties`, tuple-form `items` and `prefixItems`, cross-document `$ref`, embedded `$id`, an empty `enum`, a boolean root schema, Unicode property escapes in `pattern`, or the `hostname`, `date-time`, `time`, `uri-reference` and `duration` formats. For a schema using any of those, every buffer API now parses the bytes and answers through `validate()`; `lib/buffer-gate.js` holds the list. Schemas without them keep the zero-copy path. `tests/test_buffer_path_parity.js` now compares all three dialects, 3359 cases, and holds the disagreement count at zero.
|
|
13
|
+
- A value failing a `patternProperties` subschema was reported at runtime as a generic "value invalid for key" error on the parent object with the synthetic pointer `#/patternProperties`. The subschema is now generated in place, so the error comes from its own keyword, at the key's path (`/x-flag`), with the real pointer (`#/patternProperties/^x-/type`), and source frames resolve for it. Patterns containing `/` or `'` produce a correctly escaped pointer.
|
|
14
|
+
- `tests/test_codegen_entrypoint_agreement.js` drives the whole suite through each entry point directly and fails on any split verdict. It runs as part of `npm test`.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- A `$ref` to a dialect's meta-schema (`https://json-schema.org/draft/2020-12/schema`, `http://json-schema.org/draft-07/schema#`, any http/https or trailing-`#` spelling) resolves from copies vendored in `lib/metaschemas.js`, so "validate this schema against its dialect" works with no registry and no network. A copy supplied through `schemas` or `addSchema()` still wins. The 2020-12 meta-schema is eight documents joined by `$dynamicRef`; see the routing change below.
|
|
19
|
+
|
|
20
|
+
- `validator.engine()` reports which engine answers `validate()` for the schema: `'codegen'`, `'closure'`, `'native'` or `'interpreter'`. A diagnostic for startup logs and benchmarks. Measured over fourteen request-shaped schemas (body, params, query, shared `$ref`, `oneOf`, `if`/`then`, local `$defs`), thirteen take the generated path; `patternProperties` with `additionalProperties: false` is the one that goes to the interpreter.
|
|
21
|
+
- `formatMode: 'inject'` for `toStandaloneModule`, `bundleStandalone`, `bundleCompact` and `build()`. The output carries no custom format source; it exports `setFormats(map)` and looks each format up from that registry at validation time, with a named error if one is missing. This is for formats that cannot be serialized: functions that close over variables, bound functions, or code rewritten by coverage and transpile steps. The default `'embed'` is unchanged in behavior, but it now checks each function's source at build time and throws with the format's name when it would not survive embedding, instead of emitting a module that fails on first use.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Draft-07 is now read by its own rules rather than as 2020-12 with renamed keywords. A schema object carrying `$ref` is that reference and nothing else, so sibling keywords, `$id` included, are ignored, as the draft specifies. A fragment-only `$id` (`"$id": "#name"`) is a plain-name anchor. A document supplied through `schemas` or `addSchema()` that declares no `$schema` of its own is read under the root's draft. JSON Pointers written against array-form `items` still resolve after the keyword is normalized to `prefixItems`. Schemas that declare no `$schema` are unaffected. Draft 7 on the official suite goes from 916 to 927 of 927.
|
|
26
|
+
- The code generator now declines, and the interpreted engine answers, whenever a document reachable through a cross-document `$ref` uses `$dynamicRef`, `$dynamicAnchor`, `unevaluatedProperties`, `unevaluatedItems`, or an embedded `$id`. Before, the generator emitted a vacuous check for such a reference and accepted everything behind it: `{ "$ref": "https://json-schema.org/draft/2020-12/schema" }` accepted `{ "type": 1 }`. These checks now live in one function, `sharedCodegenGate`, that every entry point runs first. Draft 2020-12 goes from 1294 to 1298 of 1299 and the v1 dialect from 1131 to 1133 of 1133; the one remaining 2020-12 miss needs `$vocabulary`.
|
|
27
|
+
- The interpreted engine extends the dynamic scope whenever evaluation enters a schema resource, not only when it lands on the resource's root. A `$dynamicRef` reached through `first#/$defs/stuff` now sees resource `first` in scope, which closes the last `$dynamicRef` case the interpreter missed. With code generation blocked the figures are the same as with it: 1298 of 1299, 927 of 927, 1133 of 1133.
|
|
28
|
+
- The interpreted engine is between 4 and 14 times faster depending on the schema. Each schema node is resolved once into a fixed-shape plan (type bitmask, compiled pattern, looked-up format, one flag per keyword group) and children are linked at plan time, so the walk reads no schema properties and does no map lookups. On a six-field object schema a warm `validate()` went from 3786 ns to 343 ns; a `$ref`-heavy schema from 5354 ns to 366 ns; a recursive `$dynamicRef` tree from 5302 ns to 526 ns. The compiled path is unchanged at about 44 ns on the same schema.
|
|
29
|
+
- The linear-time pattern matcher now runs a lazily built DFA over the Thompson NFA, with cached ASCII transitions and a fallback to the NFA walk past 256 states. On typical anchored patterns it is within 1.2 to 3.5 times of V8's backtracking engine (`^[0-9]{5}$` 14 ns against 12 ns, an email pattern 70 ns against 21 ns) while keeping the linear bound: `^(a+)+$` against 100,000 characters takes 1 ms. This matcher backs `pattern`, `patternProperties` and `propertyNames` in every engine and in standalone output, so all of them gain.
|
|
30
|
+
|
|
31
|
+
## 1.6.2 - 2026-08-19
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- The Standard Schema surface carried no output type. `~standard.validate()` returned `{ value: unknown }` and the `types` carrier the specification defines for inference was missing, so every consumer that reads the validated type off `~standard` (Fastify, tRPC, TanStack Form, Drizzle) saw `unknown` and needed a cast. `~standard` is now typed against the validator's own data type, and `types.output` carries it. Type-only: the runtime object is unchanged, and the specification defines `types` as never present at runtime.
|
|
36
|
+
- Boolean schemas were rejected by the `Validator` constructor's TypeScript signature. `true` and `false` are schemas anywhere JSON Schema allows one, and both have always worked at runtime; only the types disagreed, which made a schema of unknown shape (`object | boolean`) impossible to pass without a cast. Nested boolean subschemas are still typed as objects, so `{ properties: { a: true } }` needs `defineSchema` or a cast.
|
|
37
|
+
- The `t` builder's option bags rejected vendor keywords. `t.object({}, { instanceof: 'Date' })` is what a custom keyword package expects to be given, and the option types only allowed the keywords the builder itself emits, so callers wrote `as never`. Every option bag now accepts unknown keywords alongside the typed ones.
|
|
38
|
+
- `tests/test_interop_types.ts` covers all three under `tsc --noEmit`.
|
|
39
|
+
|
|
5
40
|
## 1.6.1 - 2026-08-09
|
|
6
41
|
|
|
7
42
|
### Fixed
|
package/README.md
CHANGED
|
@@ -20,11 +20,11 @@ The `ata-validator` package itself is pure JavaScript. The native accelerator (s
|
|
|
20
20
|
npm install ata-validator --omit=optional
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one,
|
|
23
|
+
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1298 of 1299 Draft 2020-12 cases; the one case both miss needs `$vocabulary`, which ata does not implement. Only the buffer and parallel APIs (`isValid` on raw buffers, `countValid`, `batchIsValid`, `validateAndParse`) need the native engine and say so with a clear error.
|
|
24
24
|
|
|
25
|
-
Those four
|
|
25
|
+
Those four now agree with `validate()` on every case of the official suite, 3359 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
|
|
26
26
|
|
|
27
|
-
Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores
|
|
27
|
+
Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same 1298 of 1299 with code generation blocked. No flags, and on Workers no `nodejs_compat` either. See [docs/edge-runtimes.md](docs/edge-runtimes.md).
|
|
28
28
|
|
|
29
29
|
In your code:
|
|
30
30
|
|
|
@@ -43,15 +43,30 @@ The `.compiled.mjs` modules are self-contained: zero runtime dependency on ata-v
|
|
|
43
43
|
|
|
44
44
|
| Dimension | Schema | ata-AOT | AJV-runtime | Difference |
|
|
45
45
|
|---|---|---|---|---|
|
|
46
|
-
| Bundle (gzipped) | simple |
|
|
47
|
-
| Bundle (gzipped) | complex |
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
| Bundle (gzipped) | simple | 1.0 KB | 52.7 KB | 50.5x smaller |
|
|
47
|
+
| Bundle (gzipped) | complex | 4.8 KB | 52.7 KB | 11.0x smaller |
|
|
48
|
+
| Bundle (gzipped) | nested | 2.3 KB | 52.7 KB | 23.1x smaller |
|
|
49
|
+
| Cold start | simple | 21 ms | 40 ms | 1.9x faster |
|
|
50
|
+
| Throughput (1M ops) | simple | 258 Mops/s | 102 Mops/s | 2.5x faster |
|
|
51
|
+
| Compile time | simple | 8 µs | 1.61 ms | 191x faster |
|
|
52
|
+
|
|
53
|
+
Reproduce on your machine with `npm run bench:aot-vs-ajv`. Numbers from one run on Apple
|
|
54
|
+
M4 Pro, Node 25.2.1, 2026-08-13. Across three runs throughput moved between 258 and 278
|
|
55
|
+
Mops/s and the compile ratio between 150x and 199x, so treat the last two rows as an order
|
|
56
|
+
of magnitude rather than a constant.
|
|
57
|
+
|
|
58
|
+
The wins are largest on bundle size and compile time because AOT moves work from runtime to
|
|
59
|
+
build time. Throughput and cold start are also faster because the compiled validator is a
|
|
60
|
+
tight straight-line function with no schema-walk overhead.
|
|
61
|
+
|
|
62
|
+
What the table does not cover is data that fails. It measures compiled modules on input that
|
|
63
|
+
passes, and a rejection costs more than a verdict: ata builds an error carrying a code, the
|
|
64
|
+
offending value, a documentation link and a suggestion. On a five-field object schema a
|
|
65
|
+
passing payload costs about 17 ns and a rejected one about 155 ns. `abortEarly: true` or
|
|
66
|
+
`isValidObject()` skips that work when only the verdict matters. Schemas ata declines to
|
|
67
|
+
compile, mostly cross-document `$ref`, `$dynamicRef` and `unevaluated*`, run on the
|
|
68
|
+
interpreted engine: about 343 ns for a passing payload on a six-field object schema, eight
|
|
69
|
+
times the compiled path, measured warm on the same machine.
|
|
55
70
|
|
|
56
71
|
## Error messages
|
|
57
72
|
|
|
@@ -385,7 +400,10 @@ const { toStandaloneModule } = require('ata-validator/build');
|
|
|
385
400
|
fs.writeFileSync('./user.validator.mjs', toStandaloneModule(schema, { format: 'esm' }));
|
|
386
401
|
```
|
|
387
402
|
|
|
388
|
-
**Fastify startup
|
|
403
|
+
**Fastify startup, 10 route schemas, from a cold process to the first validated request:
|
|
404
|
+
ajv 19.6 ms, ata 3.1 ms, no build step required.** ata registers in 1.1 ms of that and
|
|
405
|
+
compiles on the first request, so counting only registration would overstate the gap.
|
|
406
|
+
Reproduce with `node benchmark/bench_fastify_boot.mjs`.
|
|
389
407
|
|
|
390
408
|
### Standard Schema V1
|
|
391
409
|
|
|
@@ -479,7 +497,7 @@ Both are implemented in the interpreted engine, so a v1 schema that uses `$dynam
|
|
|
479
497
|
|
|
480
498
|
### Known limitations
|
|
481
499
|
|
|
482
|
-
Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives
|
|
500
|
+
Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives 1298 of 1299 cases, 99.9%. Draft 7 gives 927 of 927. The v1 dialect gives 1133 of 1133. The one 2020-12 case that fails uses a custom meta-schema that drops the validation vocabulary through `$vocabulary`, which ata does not implement, so the validation keywords still apply. `npm run test:suite` reproduces all three and names it.
|
|
483
501
|
|
|
484
502
|
Areas that remain deliberate scope decisions for 1.x:
|
|
485
503
|
|
package/build.d.ts
CHANGED
|
@@ -76,6 +76,15 @@ export function watch(opts: BuildOptions, onReport?: (r: BuildReport) => void):
|
|
|
76
76
|
export interface BundleStandaloneOptions {
|
|
77
77
|
format?: 'cjs' | 'esm';
|
|
78
78
|
formats?: Record<string, (value: string) => boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* How custom format functions reach the output. 'embed' (default) writes
|
|
81
|
+
* each function's source into the module; it must be a plain function
|
|
82
|
+
* with no closed-over variables and no coverage instrumentation, or the
|
|
83
|
+
* build throws. 'inject' writes no function source: the module exports
|
|
84
|
+
* `setFormats(map)` and looks formats up from that registry at validation
|
|
85
|
+
* time, so the caller supplies them at load time.
|
|
86
|
+
*/
|
|
87
|
+
formatMode?: 'embed' | 'inject';
|
|
79
88
|
verbose?: boolean;
|
|
80
89
|
}
|
|
81
90
|
|
|
@@ -85,6 +94,9 @@ export interface ToStandaloneModuleOptions {
|
|
|
85
94
|
source?: boolean;
|
|
86
95
|
sourceMap?: unknown;
|
|
87
96
|
schemaFile?: string;
|
|
97
|
+
formats?: Record<string, (value: string) => boolean>;
|
|
98
|
+
/** See {@link BundleStandaloneOptions.formatMode}. */
|
|
99
|
+
formatMode?: 'embed' | 'inject';
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
/** Bundle multiple schemas into one self-contained module (no ata-validator runtime). */
|
package/index.d.ts
CHANGED
|
@@ -364,13 +364,13 @@ export interface BundleStandaloneOptions extends ValidatorOptions {
|
|
|
364
364
|
format?: 'esm' | 'cjs';
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
export interface StandardSchemaV1Props {
|
|
367
|
+
export interface StandardSchemaV1Props<Output = unknown, Input = unknown> {
|
|
368
368
|
version: 1;
|
|
369
369
|
vendor: "ata-validator";
|
|
370
370
|
validate(
|
|
371
371
|
value: unknown
|
|
372
372
|
):
|
|
373
|
-
| { value:
|
|
373
|
+
| { value: Output }
|
|
374
374
|
| {
|
|
375
375
|
issues: Array<{
|
|
376
376
|
message: string;
|
|
@@ -378,6 +378,11 @@ export interface StandardSchemaV1Props {
|
|
|
378
378
|
path?: ReadonlyArray<{ key: PropertyKey }>;
|
|
379
379
|
}>;
|
|
380
380
|
};
|
|
381
|
+
/**
|
|
382
|
+
* Type-only carrier the Standard Schema spec uses for inference: consumers
|
|
383
|
+
* read the validated type off `types.output`. Never present at runtime.
|
|
384
|
+
*/
|
|
385
|
+
readonly types?: { readonly input: Input; readonly output: Output } | undefined;
|
|
381
386
|
}
|
|
382
387
|
|
|
383
388
|
export interface StandaloneModule {
|
|
@@ -396,6 +401,13 @@ export interface Validator<T = unknown> {
|
|
|
396
401
|
|
|
397
402
|
/** Fast boolean check via JS codegen or tier 0 interpreter. No error collection. */
|
|
398
403
|
isValidObject(data: unknown): data is T;
|
|
404
|
+
/**
|
|
405
|
+
* Which engine answers `validate()` for this schema: 'codegen' (generated
|
|
406
|
+
* JS), 'closure' (the closure compiler), 'native' (the C++ engine, only
|
|
407
|
+
* for some $dynamicRef schemas) or 'interpreter'. The verdict is the same
|
|
408
|
+
* on every engine; the cost is not. A diagnostic, not a configuration.
|
|
409
|
+
*/
|
|
410
|
+
engine(): 'codegen' | 'closure' | 'native' | 'interpreter';
|
|
399
411
|
|
|
400
412
|
/** Validate a JSON string. Uses simdjson fast path for large documents. */
|
|
401
413
|
validateJSON(jsonString: string): ValidationResult<T>;
|
|
@@ -425,7 +437,7 @@ export interface Validator<T = unknown> {
|
|
|
425
437
|
isValidNDJSON(ndjsonBuffer: Buffer): boolean[];
|
|
426
438
|
|
|
427
439
|
/** Standard Schema V1 interface, compatible with Fastify, tRPC, TanStack, etc. */
|
|
428
|
-
readonly "~standard": StandardSchemaV1Props
|
|
440
|
+
readonly "~standard": StandardSchemaV1Props<T>;
|
|
429
441
|
}
|
|
430
442
|
|
|
431
443
|
/** Constructor + statics for {@link Validator}. */
|
|
@@ -433,7 +445,7 @@ export interface ValidatorConstructor {
|
|
|
433
445
|
/** Construct from a JSON Schema literal; the validated data type is inferred. */
|
|
434
446
|
new <const S extends JSONSchema>(schema: S, options?: ValidatorOptions): Validator<Infer<S>>;
|
|
435
447
|
/** Construct from a plain object/string schema, or with an explicit data type. */
|
|
436
|
-
new <T = unknown>(schema: object | string, options?: ValidatorOptions): Validator<T>;
|
|
448
|
+
new <T = unknown>(schema: object | string | boolean, options?: ValidatorOptions): Validator<T>;
|
|
437
449
|
|
|
438
450
|
/** Load a pre-compiled standalone module. Zero schema compilation at startup. */
|
|
439
451
|
fromStandalone<T = unknown>(mod: StandaloneModule, schema: object | string, options?: ValidatorOptions): Validator<T>;
|
package/index.js
CHANGED
|
@@ -426,33 +426,35 @@ function _deepCloneWithSymbols(v) {
|
|
|
426
426
|
// Normalize a caller-provided schema without mutating the original.
|
|
427
427
|
// Clones only when normalization would change the object (draft-07 keys
|
|
428
428
|
// present or nullable fields present). Internal-only — not exported.
|
|
429
|
-
function _normalizeCallerSchema(s) {
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
s.$schema === 'http://json-schema.org/draft-07/schema'
|
|
433
|
-
|
|
429
|
+
function _normalizeCallerSchema(s, inheritDraft7) {
|
|
430
|
+
const declares = s && typeof s === 'object' && s.$schema !== undefined
|
|
431
|
+
const needsDraft7 = declares
|
|
432
|
+
? (s.$schema === 'http://json-schema.org/draft-07/schema#' || s.$schema === 'http://json-schema.org/draft-07/schema')
|
|
433
|
+
: !!inheritDraft7
|
|
434
434
|
const str = JSON.stringify(s)
|
|
435
435
|
const copy = _deepCloneWithSymbols(s)
|
|
436
|
-
if (needsDraft7) normalizeDraft7(copy)
|
|
436
|
+
if (needsDraft7) normalizeDraft7(copy, true)
|
|
437
437
|
normalizeNullable(copy)
|
|
438
438
|
// Return original when normalization produced no change, copy otherwise.
|
|
439
439
|
// Change-detection uses JSON content only; symbols do not affect it.
|
|
440
440
|
return JSON.stringify(copy) === str ? s : copy
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
|
|
443
|
+
// `inheritDraft7` is true when the root schema is draft-07: a retrieved
|
|
444
|
+
// document that declares no dialect is read under the root's draft.
|
|
445
|
+
function buildSchemaMap(schemas, inheritDraft7) {
|
|
444
446
|
if (!schemas) return null
|
|
445
447
|
const map = new Map()
|
|
446
448
|
if (Array.isArray(schemas)) {
|
|
447
449
|
for (const s of schemas) {
|
|
448
|
-
const normalized = _normalizeCallerSchema(s)
|
|
450
|
+
const normalized = _normalizeCallerSchema(s, inheritDraft7)
|
|
449
451
|
const id = normalized.$id
|
|
450
452
|
if (!id) throw new Error('Schema in schemas option must have $id')
|
|
451
453
|
map.set(id, normalized)
|
|
452
454
|
}
|
|
453
455
|
} else {
|
|
454
456
|
for (const [key, s] of Object.entries(schemas)) {
|
|
455
|
-
const normalized = _normalizeCallerSchema(s)
|
|
457
|
+
const normalized = _normalizeCallerSchema(s, inheritDraft7)
|
|
456
458
|
// A retrieved document is addressable both by the URI it was registered
|
|
457
459
|
// under and by the $id it declares. Registering only the $id makes
|
|
458
460
|
// references to the retrieval URI unresolvable.
|
|
@@ -560,8 +562,10 @@ class Validator {
|
|
|
560
562
|
// When schema is an object, normalization runs on a clone so the caller's
|
|
561
563
|
// object is never touched.
|
|
562
564
|
let schemaObj = typeof schema === "string"
|
|
563
|
-
? JSON.parse(schema)
|
|
565
|
+
? _normalizeCallerSchema(JSON.parse(schema))
|
|
564
566
|
: _normalizeCallerSchema(schema);
|
|
567
|
+
const rootIsDraft7 = !!(schemaObj && typeof schemaObj === 'object' && typeof schemaObj.$schema === 'string' &&
|
|
568
|
+
(schemaObj.$schema === 'http://json-schema.org/draft-07/schema#' || schemaObj.$schema === 'http://json-schema.org/draft-07/schema'));
|
|
565
569
|
|
|
566
570
|
// assertFormat: false makes `format` annotation-only. Strip it on a clone
|
|
567
571
|
// so the caller's schema keeps the keyword.
|
|
@@ -579,11 +583,12 @@ class Validator {
|
|
|
579
583
|
this._compiled = null;
|
|
580
584
|
this._fastSlot = -1;
|
|
581
585
|
this._jsFn = null;
|
|
586
|
+
this._engine = undefined;
|
|
582
587
|
this._preprocess = null;
|
|
583
588
|
this._applyDefaults = null;
|
|
584
589
|
|
|
585
590
|
// Schema map for cross-schema $ref resolution
|
|
586
|
-
this._schemaMap = buildSchemaMap(options.schemas) || new Map();
|
|
591
|
+
this._schemaMap = buildSchemaMap(options.schemas, rootIsDraft7) || new Map();
|
|
587
592
|
|
|
588
593
|
// User-supplied format checkers: { formatName: (value) => boolean }.
|
|
589
594
|
// Looked up at runtime when a schema references a format the built-in
|
|
@@ -717,6 +722,20 @@ class Validator {
|
|
|
717
722
|
// Lazy stringify — only computed here, not in constructor
|
|
718
723
|
if (!this._schemaStr) this._schemaStr = JSON.stringify(schemaObj);
|
|
719
724
|
|
|
725
|
+
// A $ref to a meta-schema resolves from the vendored copies, so
|
|
726
|
+
// "validate this schema against its dialect" needs no network and no
|
|
727
|
+
// caller-supplied registry. Only schemas that mention json-schema.org in a
|
|
728
|
+
// reference pay for the lookup.
|
|
729
|
+
if (this._schemaStr.includes('json-schema.org/draft')) {
|
|
730
|
+
const { METASCHEMAS } = require('./lib/metaschemas');
|
|
731
|
+
for (const [id, meta] of METASCHEMAS) {
|
|
732
|
+
const bare = id.replace(/#$/, '');
|
|
733
|
+
for (const key of [id, bare, bare + '#', bare.replace(/^https:/, 'http:'), bare.replace(/^http:/, 'https:')]) {
|
|
734
|
+
if (!this._schemaMap.has(key)) this._schemaMap.set(key, meta);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
720
739
|
// Check cache first -- reuse compiled functions for same schema
|
|
721
740
|
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
|
722
741
|
const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
|
|
@@ -754,6 +773,7 @@ class Validator {
|
|
|
754
773
|
jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm, uf);
|
|
755
774
|
jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm, uf);
|
|
756
775
|
_isCodegen = !!_cgFn;
|
|
776
|
+
this._engine = _cgFn ? 'codegen' : jsFn ? 'closure' : null;
|
|
757
777
|
if (!uf) {
|
|
758
778
|
_compileCache.set(mapKey, { jsFn, combined: jsCombinedFn, errFn: jsErrFn, isCodegen: _isCodegen });
|
|
759
779
|
}
|
|
@@ -761,6 +781,7 @@ class Validator {
|
|
|
761
781
|
jsFn = null; jsCombinedFn = null; jsErrFn = null;
|
|
762
782
|
}
|
|
763
783
|
this._jsFn = jsFn;
|
|
784
|
+
if (this._engine === undefined) this._engine = cached ? (cached.isCodegen ? 'codegen' : jsFn ? 'closure' : null) : null;
|
|
764
785
|
|
|
765
786
|
// Data mutators -- try codegen first (12x faster), fallback to closure arrays.
|
|
766
787
|
// Follow cross-refs so coercion/defaults/removeAdditional see the referenced
|
|
@@ -1081,6 +1102,7 @@ class Validator {
|
|
|
1081
1102
|
if (_hasDynRef && !_hasUneval && !_hasPropDeps && !this._v1Dynamic) {
|
|
1082
1103
|
// validateJSON is the C++ path with full anchor-map support; the NAPI
|
|
1083
1104
|
// direct V8 `validate` path has no anchor maps.
|
|
1105
|
+
this._engine = 'native';
|
|
1084
1106
|
_validate = (data) => this._compiled.validateJSON(JSON.stringify(data));
|
|
1085
1107
|
this.validateJSON = (jsonStr) => this._compiled.validateJSON(jsonStr);
|
|
1086
1108
|
this.isValidJSON = (jsonStr) => this._compiled.isValidJSON(jsonStr);
|
|
@@ -1091,6 +1113,7 @@ class Validator {
|
|
|
1091
1113
|
formats: this._userFormats,
|
|
1092
1114
|
v1: isV1Dialect(schemaObj),
|
|
1093
1115
|
});
|
|
1116
|
+
this._engine = 'interpreter';
|
|
1094
1117
|
_validate = (data) => interp.validate(data);
|
|
1095
1118
|
this.validateJSON = (jsonStr) => {
|
|
1096
1119
|
try {
|
|
@@ -1149,6 +1172,7 @@ class Validator {
|
|
|
1149
1172
|
formats: this._userFormats,
|
|
1150
1173
|
v1: isV1Dialect(schemaObj),
|
|
1151
1174
|
});
|
|
1175
|
+
this._engine = 'interpreter';
|
|
1152
1176
|
const run = preprocess
|
|
1153
1177
|
? (data) => { preprocess(data); return interp.validate(data); }
|
|
1154
1178
|
: (data) => interp.validate(data);
|
|
@@ -1311,12 +1335,29 @@ class Validator {
|
|
|
1311
1335
|
}
|
|
1312
1336
|
}
|
|
1313
1337
|
|
|
1338
|
+
// The buffer APIs answer from the native walker, which disagrees with
|
|
1339
|
+
// validate() on shapes listed in lib/buffer-gate.js. For those schemas
|
|
1340
|
+
// every buffer entry point goes through validate() instead.
|
|
1341
|
+
if (native) {
|
|
1342
|
+
const { bufferNeedsSlowPath, installSlowBufferApis } = require('./lib/buffer-gate');
|
|
1343
|
+
if (bufferNeedsSlowPath(schemaObj, this._schemaMap)) installSlowBufferApis(this);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1314
1346
|
// Save to identity cache for ultra-fast reuse with same schema object
|
|
1315
1347
|
if (this._schemaObj && typeof this._schemaObj === 'object') {
|
|
1316
1348
|
_identityCache.set(this._schemaObj, this);
|
|
1317
1349
|
}
|
|
1318
1350
|
}
|
|
1319
1351
|
|
|
1352
|
+
// Which engine answers validate() for this schema: 'codegen' (generated
|
|
1353
|
+
// JS), 'closure' (the closure compiler, the boolean fallback), 'native'
|
|
1354
|
+
// (the C++ engine, only for some $dynamicRef schemas), or 'interpreter'.
|
|
1355
|
+
// A diagnostic: the answer is the same on every engine, the cost is not.
|
|
1356
|
+
engine() {
|
|
1357
|
+
this._ensureCompiled();
|
|
1358
|
+
return this._engine || 'interpreter';
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1320
1361
|
_ensureNative() {
|
|
1321
1362
|
if (this._nativeReady) return;
|
|
1322
1363
|
this._nativeReady = true;
|
|
@@ -1341,8 +1382,12 @@ class Validator {
|
|
|
1341
1382
|
if (!schema || !schema.$id) {
|
|
1342
1383
|
throw new Error('Schema must have $id')
|
|
1343
1384
|
}
|
|
1344
|
-
// Normalize a copy so the caller's object is never mutated.
|
|
1345
|
-
|
|
1385
|
+
// Normalize a copy so the caller's object is never mutated. A document
|
|
1386
|
+
// without a dialect of its own is read under the root's draft.
|
|
1387
|
+
const root = this._schemaObj
|
|
1388
|
+
const rootIsDraft7 = !!(root && typeof root === 'object' && typeof root.$schema === 'string' &&
|
|
1389
|
+
(root.$schema === 'http://json-schema.org/draft-07/schema#' || root.$schema === 'http://json-schema.org/draft-07/schema'))
|
|
1390
|
+
const normalized = _normalizeCallerSchema(schema, rootIsDraft7)
|
|
1346
1391
|
this._schemaMap.set(normalized.$id, normalized)
|
|
1347
1392
|
}
|
|
1348
1393
|
|
package/lib/aot-build.js
CHANGED
package/lib/aot.js
CHANGED
|
@@ -98,6 +98,48 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
98
98
|
//
|
|
99
99
|
// format: 'esm' | 'cjs'. Default 'esm'.
|
|
100
100
|
// abortEarly: if true, invalid result is a shared stub; smaller output.
|
|
101
|
+
|
|
102
|
+
// Custom format functions in standalone output.
|
|
103
|
+
//
|
|
104
|
+
// 'embed' (default) writes each function's source into the module through
|
|
105
|
+
// Function#toString. That only works for a plain function: one that closes
|
|
106
|
+
// over nothing and is not rewritten by a coverage or transpile step (istanbul
|
|
107
|
+
// injects `cov_` counters; a bundler may hoist helpers). The check below
|
|
108
|
+
// rejects those at build time with a named error instead of emitting a module
|
|
109
|
+
// that throws on first use.
|
|
110
|
+
//
|
|
111
|
+
// 'inject' writes no function source. The module exports `setFormats(map)`
|
|
112
|
+
// and each format is looked up from that registry when a value is checked,
|
|
113
|
+
// so the caller supplies the functions at load time.
|
|
114
|
+
function emitFormatDecls(closures, mode, declKW) {
|
|
115
|
+
if (!closures || closures.length === 0) return { decls: '', exportsSetFormats: false };
|
|
116
|
+
if (mode === 'inject') {
|
|
117
|
+
let out = `${declKW} __formats = Object.create(null);\n`;
|
|
118
|
+
out += `function setFormats(map) { for (const k in map) __formats[k] = map[k]; }\n`;
|
|
119
|
+
for (const { name, format } of closures) {
|
|
120
|
+
const key = JSON.stringify(format === null ? name.slice(4) : format);
|
|
121
|
+
out += `${declKW} ${name} = function (v) { const f = __formats[${key}]; if (typeof f !== 'function') throw new Error('ata: format ' + ${key} + ' is not registered; call setFormats({ [' + ${key} + ']: fn }) before validating'); return f(v); };\n`;
|
|
122
|
+
}
|
|
123
|
+
return { decls: out, exportsSetFormats: true };
|
|
124
|
+
}
|
|
125
|
+
let out = '';
|
|
126
|
+
for (const { name, fn, format } of closures) {
|
|
127
|
+
const src = fn.toString();
|
|
128
|
+
const label = format === null ? name : format;
|
|
129
|
+
if (/\bcov_[A-Za-z0-9_$]+\b/.test(src)) {
|
|
130
|
+
throw new Error(`ata: custom format "${label}" is instrumented for coverage and cannot be embedded; use { formatMode: 'inject' } and register it with setFormats() at load time`);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
// eslint-disable-next-line no-new-func
|
|
134
|
+
new Function('return (' + src + ')');
|
|
135
|
+
} catch {
|
|
136
|
+
throw new Error(`ata: custom format "${label}" has no standalone source (a bound function, a class method, or a native); use { formatMode: 'inject' } and register it with setFormats() at load time`);
|
|
137
|
+
}
|
|
138
|
+
out += `${declKW} ${name} = ${src};\n`;
|
|
139
|
+
}
|
|
140
|
+
return { decls: out, exportsSetFormats: false };
|
|
141
|
+
}
|
|
142
|
+
|
|
101
143
|
function toStandaloneModule(validator, opts) {
|
|
102
144
|
validator._ensureCompiled();
|
|
103
145
|
const jsFn = validator._jsFn;
|
|
@@ -170,20 +212,17 @@ function toStandaloneModule(validator, opts) {
|
|
|
170
212
|
// User-supplied format functions are referenced as _uf_<name> by both the
|
|
171
213
|
// boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
|
|
172
214
|
// so the standalone module stays self-contained.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
formatDecls = jsFn._formatClosures
|
|
176
|
-
.map(({ name, fn }) => `const ${name} = ${fn.toString()};`)
|
|
177
|
-
.join('\n') + '\n';
|
|
178
|
-
}
|
|
215
|
+
const fmt = emitFormatDecls(jsFn._formatClosures, opts && opts.formatMode, 'const');
|
|
216
|
+
const formatDecls = fmt.decls;
|
|
179
217
|
|
|
180
218
|
const validBody = errCore
|
|
181
219
|
? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
|
|
182
220
|
: 'return _fn(data) ? VALID : ABORT';
|
|
183
221
|
|
|
222
|
+
const names = fmt.exportsSetFormats ? 'validate, isValid, setFormats' : 'validate, isValid';
|
|
184
223
|
const exports = format === 'esm'
|
|
185
|
-
? `export {
|
|
186
|
-
: `module.exports = {
|
|
224
|
+
? `export { ${names} };\nexport default { ${names} };\n`
|
|
225
|
+
: `module.exports = { ${names} };\nmodule.exports.default = module.exports;\n`;
|
|
187
226
|
|
|
188
227
|
return `// Auto-generated by ata-validator — do not edit.
|
|
189
228
|
// Schema is embedded; runtime has zero dependency on ata-validator.
|
|
@@ -240,6 +279,7 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
240
279
|
const format = (opts && opts.format) || 'cjs';
|
|
241
280
|
const R = 'Object.freeze({valid:true,errors:Object.freeze([])})';
|
|
242
281
|
let bundleUsesSafeRe = false;
|
|
282
|
+
let bundleInjects = false;
|
|
243
283
|
const fns = schemas.map((schema) => {
|
|
244
284
|
const v = new Validator(schema, bundleOpts);
|
|
245
285
|
v._ensureCompiled();
|
|
@@ -255,12 +295,13 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
255
295
|
jsErrFn && jsErrFn._errSource
|
|
256
296
|
? jsErrFn._errSource
|
|
257
297
|
: "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
|
|
258
|
-
//
|
|
298
|
+
// Custom format closures: embedded, or bound to the bundle-level
|
|
299
|
+
// registry when opts.formats is 'inject'.
|
|
259
300
|
let preamble = '';
|
|
260
301
|
if (jsFn._formatClosures) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
302
|
+
const f = emitFormatDecls(jsFn._formatClosures, opts && opts.formatMode, 'var');
|
|
303
|
+
preamble = f.decls.replace(/^var __formats = [^\n]*\n|^function setFormats[^\n]*\n/gm, '');
|
|
304
|
+
if (f.exportsSetFormats) bundleInjects = true;
|
|
264
305
|
}
|
|
265
306
|
// Include hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) so the
|
|
266
307
|
// bundle output is self-contained. `toStandalone` emits this same source
|
|
@@ -277,10 +318,15 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
277
318
|
});
|
|
278
319
|
const arr = `[${fns.join(',')}]`;
|
|
279
320
|
const safeEmbed = bundleUsesSafeRe ? getSafeRegexEmbed() + '\n' : '';
|
|
321
|
+
const registry = bundleInjects
|
|
322
|
+
? `var __formats=Object.create(null);\nfunction setFormats(map){for(var k in map)__formats[k]=map[k]}\n`
|
|
323
|
+
: '';
|
|
280
324
|
if (format === 'esm') {
|
|
281
|
-
|
|
325
|
+
const extra = bundleInjects ? 'export { validators, setFormats };' : 'export { validators };';
|
|
326
|
+
return `// Auto-generated by ata-validator — do not edit\n${safeEmbed}${registry}const R=${R};\nconst validators=${arr};\nexport default validators;\n${extra}\n`;
|
|
282
327
|
}
|
|
283
|
-
|
|
328
|
+
const attach = bundleInjects ? 'module.exports.setFormats=setFormats;\n' : '';
|
|
329
|
+
return `'use strict';\n${safeEmbed}${registry}var R=${R};\nmodule.exports=[${fns.join(',')}];\n${attach}`;
|
|
284
330
|
}
|
|
285
331
|
|
|
286
332
|
// Compact bundle: deduplicated code. Shared template functions + per-schema params.
|
|
@@ -355,14 +401,17 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
355
401
|
// bodies. Collect them across all schemas (deduped by name) and embed via
|
|
356
402
|
// Function#toString so the bundle stays self-contained.
|
|
357
403
|
const fmtSeen = new Set();
|
|
404
|
+
const fmtAll = [];
|
|
358
405
|
for (const e of entries) {
|
|
359
406
|
if (!e || !e.fmt) continue;
|
|
360
|
-
for (const
|
|
361
|
-
if (fmtSeen.has(name)) continue;
|
|
362
|
-
fmtSeen.add(name);
|
|
363
|
-
|
|
407
|
+
for (const entry of e.fmt) {
|
|
408
|
+
if (fmtSeen.has(entry.name)) continue;
|
|
409
|
+
fmtSeen.add(entry.name);
|
|
410
|
+
fmtAll.push(entry);
|
|
364
411
|
}
|
|
365
412
|
}
|
|
413
|
+
const fmtOut = emitFormatDecls(fmtAll, opts && opts.formatMode, declKW);
|
|
414
|
+
out += fmtOut.decls;
|
|
366
415
|
|
|
367
416
|
// Shared hybrid factories
|
|
368
417
|
out += `${declKW} H=[\n`;
|
|
@@ -385,9 +434,10 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
385
434
|
})
|
|
386
435
|
.join(',');
|
|
387
436
|
if (isEsm) {
|
|
388
|
-
out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators };\n`;
|
|
437
|
+
out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators${fmtOut.exportsSetFormats ? ', setFormats' : ''} };\n`;
|
|
389
438
|
} else {
|
|
390
439
|
out += `module.exports=[${arrBody}];\n`;
|
|
440
|
+
if (fmtOut.exportsSetFormats) out += 'module.exports.setFormats=setFormats;\n';
|
|
391
441
|
}
|
|
392
442
|
|
|
393
443
|
return out;
|