@earendil-works/pi-schema 0.0.0-stage → 0.0.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 +13 -0
- package/LICENSES/typebox-LICENSE +23 -0
- package/README.md +295 -2
- package/dist/annotations.d.ts +44 -0
- package/dist/annotations.d.ts.map +1 -0
- package/dist/annotations.js +193 -0
- package/dist/annotations.js.map +1 -0
- package/dist/builders.d.ts +114 -0
- package/dist/builders.d.ts.map +1 -0
- package/dist/builders.js +98 -0
- package/dist/builders.js.map +1 -0
- package/dist/checks.d.ts +87 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +447 -0
- package/dist/checks.js.map +1 -0
- package/dist/coerce.d.ts +53 -0
- package/dist/coerce.d.ts.map +1 -0
- package/dist/coerce.js +585 -0
- package/dist/coerce.js.map +1 -0
- package/dist/compiler.d.ts +53 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +532 -0
- package/dist/compiler.js.map +1 -0
- package/dist/explain.d.ts +29 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +381 -0
- package/dist/explain.js.map +1 -0
- package/dist/formats/index.d.ts +7 -0
- package/dist/formats/index.d.ts.map +1 -0
- package/dist/formats/index.js +51 -0
- package/dist/formats/index.js.map +1 -0
- package/dist/formats/typebox/date.d.ts +7 -0
- package/dist/formats/typebox/date.d.ts.map +1 -0
- package/dist/formats/typebox/date.js +47 -0
- package/dist/formats/typebox/date.js.map +1 -0
- package/dist/formats/typebox/date_time.d.ts +7 -0
- package/dist/formats/typebox/date_time.d.ts.map +1 -0
- package/dist/formats/typebox/date_time.js +39 -0
- package/dist/formats/typebox/date_time.js.map +1 -0
- package/dist/formats/typebox/duration.d.ts +6 -0
- package/dist/formats/typebox/duration.d.ts.map +1 -0
- package/dist/formats/typebox/duration.js +36 -0
- package/dist/formats/typebox/duration.js.map +1 -0
- package/dist/formats/typebox/email.d.ts +6 -0
- package/dist/formats/typebox/email.d.ts.map +1 -0
- package/dist/formats/typebox/email.js +36 -0
- package/dist/formats/typebox/email.js.map +1 -0
- package/dist/formats/typebox/hostname.d.ts +8 -0
- package/dist/formats/typebox/hostname.d.ts.map +1 -0
- package/dist/formats/typebox/hostname.js +38 -0
- package/dist/formats/typebox/hostname.js.map +1 -0
- package/dist/formats/typebox/idn_email.d.ts +6 -0
- package/dist/formats/typebox/idn_email.d.ts.map +1 -0
- package/dist/formats/typebox/idn_email.js +36 -0
- package/dist/formats/typebox/idn_email.js.map +1 -0
- package/dist/formats/typebox/idn_hostname.d.ts +9 -0
- package/dist/formats/typebox/idn_hostname.d.ts.map +1 -0
- package/dist/formats/typebox/idn_hostname.js +39 -0
- package/dist/formats/typebox/idn_hostname.js.map +1 -0
- package/dist/formats/typebox/idna/format/bidi.d.ts +4 -0
- package/dist/formats/typebox/idna/format/bidi.d.ts.map +1 -0
- package/dist/formats/typebox/idna/format/bidi.js +132 -0
- package/dist/formats/typebox/idna/format/bidi.js.map +1 -0
- package/dist/formats/typebox/idna/format/puny.d.ts +4 -0
- package/dist/formats/typebox/idna/format/puny.d.ts.map +1 -0
- package/dist/formats/typebox/idna/format/puny.js +172 -0
- package/dist/formats/typebox/idna/format/puny.js.map +1 -0
- package/dist/formats/typebox/idna/hostname.d.ts +2 -0
- package/dist/formats/typebox/idna/hostname.d.ts.map +1 -0
- package/dist/formats/typebox/idna/hostname.js +43 -0
- package/dist/formats/typebox/idna/hostname.js.map +1 -0
- package/dist/formats/typebox/idna/idn-hostname.d.ts +2 -0
- package/dist/formats/typebox/idna/idn-hostname.d.ts.map +1 -0
- package/dist/formats/typebox/idna/idn-hostname.js +71 -0
- package/dist/formats/typebox/idna/idn-hostname.js.map +1 -0
- package/dist/formats/typebox/idna/index.d.ts +3 -0
- package/dist/formats/typebox/idna/index.d.ts.map +1 -0
- package/dist/formats/typebox/idna/index.js +30 -0
- package/dist/formats/typebox/idna/index.js.map +1 -0
- package/dist/formats/typebox/idna/label/ascii.d.ts +2 -0
- package/dist/formats/typebox/idna/label/ascii.d.ts.map +1 -0
- package/dist/formats/typebox/idna/label/ascii.js +37 -0
- package/dist/formats/typebox/idna/label/ascii.js.map +1 -0
- package/dist/formats/typebox/idna/label/puny.d.ts +2 -0
- package/dist/formats/typebox/idna/label/puny.d.ts.map +1 -0
- package/dist/formats/typebox/idna/label/puny.js +56 -0
- package/dist/formats/typebox/idna/label/puny.js.map +1 -0
- package/dist/formats/typebox/idna/label/unicode.d.ts +2 -0
- package/dist/formats/typebox/idna/label/unicode.d.ts.map +1 -0
- package/dist/formats/typebox/idna/label/unicode.js +134 -0
- package/dist/formats/typebox/idna/label/unicode.js.map +1 -0
- package/dist/formats/typebox/idna/pattern/pattern.d.ts +31 -0
- package/dist/formats/typebox/idna/pattern/pattern.d.ts.map +1 -0
- package/dist/formats/typebox/idna/pattern/pattern.js +98 -0
- package/dist/formats/typebox/idna/pattern/pattern.js.map +1 -0
- package/dist/formats/typebox/ipv4.d.ts +6 -0
- package/dist/formats/typebox/ipv4.d.ts.map +1 -0
- package/dist/formats/typebox/ipv4.js +36 -0
- package/dist/formats/typebox/ipv4.js.map +1 -0
- package/dist/formats/typebox/ipv6.d.ts +6 -0
- package/dist/formats/typebox/ipv6.d.ts.map +1 -0
- package/dist/formats/typebox/ipv6.js +36 -0
- package/dist/formats/typebox/ipv6.js.map +1 -0
- package/dist/formats/typebox/iri.d.ts +6 -0
- package/dist/formats/typebox/iri.d.ts.map +1 -0
- package/dist/formats/typebox/iri.js +63 -0
- package/dist/formats/typebox/iri.js.map +1 -0
- package/dist/formats/typebox/iri_reference.d.ts +6 -0
- package/dist/formats/typebox/iri_reference.d.ts.map +1 -0
- package/dist/formats/typebox/iri_reference.js +40 -0
- package/dist/formats/typebox/iri_reference.js.map +1 -0
- package/dist/formats/typebox/json_pointer.d.ts +7 -0
- package/dist/formats/typebox/json_pointer.d.ts.map +1 -0
- package/dist/formats/typebox/json_pointer.js +37 -0
- package/dist/formats/typebox/json_pointer.js.map +1 -0
- package/dist/formats/typebox/json_pointer_uri_fragment.d.ts +7 -0
- package/dist/formats/typebox/json_pointer_uri_fragment.d.ts.map +1 -0
- package/dist/formats/typebox/json_pointer_uri_fragment.js +37 -0
- package/dist/formats/typebox/json_pointer_uri_fragment.js.map +1 -0
- package/dist/formats/typebox/regex.d.ts +6 -0
- package/dist/formats/typebox/regex.d.ts.map +1 -0
- package/dist/formats/typebox/regex.js +41 -0
- package/dist/formats/typebox/regex.js.map +1 -0
- package/dist/formats/typebox/relative_json_pointer.d.ts +7 -0
- package/dist/formats/typebox/relative_json_pointer.d.ts.map +1 -0
- package/dist/formats/typebox/relative_json_pointer.js +37 -0
- package/dist/formats/typebox/relative_json_pointer.js.map +1 -0
- package/dist/formats/typebox/time.d.ts +6 -0
- package/dist/formats/typebox/time.d.ts.map +1 -0
- package/dist/formats/typebox/time.js +60 -0
- package/dist/formats/typebox/time.js.map +1 -0
- package/dist/formats/typebox/uri.d.ts +6 -0
- package/dist/formats/typebox/uri.d.ts.map +1 -0
- package/dist/formats/typebox/uri.js +36 -0
- package/dist/formats/typebox/uri.js.map +1 -0
- package/dist/formats/typebox/uri_reference.d.ts +6 -0
- package/dist/formats/typebox/uri_reference.d.ts.map +1 -0
- package/dist/formats/typebox/uri_reference.js +36 -0
- package/dist/formats/typebox/uri_reference.js.map +1 -0
- package/dist/formats/typebox/uri_template.d.ts +6 -0
- package/dist/formats/typebox/uri_template.d.ts.map +1 -0
- package/dist/formats/typebox/uri_template.js +37 -0
- package/dist/formats/typebox/uri_template.js.map +1 -0
- package/dist/formats/typebox/url.d.ts +7 -0
- package/dist/formats/typebox/url.d.ts.map +1 -0
- package/dist/formats/typebox/url.js +36 -0
- package/dist/formats/typebox/url.js.map +1 -0
- package/dist/formats/typebox/uuid.d.ts +7 -0
- package/dist/formats/typebox/uuid.d.ts.map +1 -0
- package/dist/formats/typebox/uuid.js +37 -0
- package/dist/formats/typebox/uuid.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/resolver.d.ts +57 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +202 -0
- package/dist/resolver.js.map +1 -0
- package/dist/shared.d.ts +9 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +33 -0
- package/dist/shared.js.map +1 -0
- package/dist/typebox-static.d.ts +48 -0
- package/dist/typebox-static.d.ts.map +1 -0
- package/dist/typebox-static.js +5 -0
- package/dist/typebox-static.js.map +1 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/validator.d.ts +39 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +71 -0
- package/dist/validator.js.map +1 -0
- package/package.json +59 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Initial release: `compile(schema, options)` validates JSON Schema drafts 2020-12 and 7 without `eval` or `new Function`, compiling a schema once into plain closures. `Validator.check` is the boolean check (a type guard for schemas built with the builders), `Validator.errors` reports each failure with a JSON Pointer path. Format checks are in the optional `@earendil-works/pi-schema/formats` entry point.
|
|
8
|
+
- Schema builders (`str`, `num`, `int`, `bool`, `nul`, `unknown`, `lit`, `oneOf`, `obj`, `opt`, `arr`, `tuple`, `map`, `union`, `nullable`, `bytes`) that return plain JSON Schema typed with the values it accepts (`Static<typeof schema>`).
|
|
9
|
+
- `compile(schema, { coerce: true })` adds `coerce(value)` and `parse(value)` for loosely typed input such as a model's tool call arguments: optional `null`s dropped, TypeBox 1.3.27's `Value.Convert` rules keyed on JSON Schema keywords (numeric strings, `"true"`, single values to arrays, `const`/`enum` members, union branches), then Ajv-style primitive coercion by `type`. `coerce` copies only the paths it changes and returns the input itself when nothing changes; `parse` coerces, then checks. Validators compiled without the option have neither; the README lists every rule.
|
|
10
|
+
- `Static` also types TypeBox-built schemas, from TypeBox 1.x's type shapes and without importing TypeBox: String, Number, Integer, Boolean, Null, Literal, Array, Object (with `Type.Optional` and `Type.Readonly` properties), Union, Record, Unsafe, Unknown and Any give TypeBox's static type; other TypeBox kinds give `unknown`.
|
|
11
|
+
- `unresolvedRefs: "reject"` compiles a `$ref` that names nothing as `false` (the error is `unresolved reference <ref>`) instead of throwing, for schemas whose references may dangle.
|
|
12
|
+
- `formats` also checks `url` and `json-pointer-uri-fragment`, as TypeBox and ajv-formats do.
|
|
13
|
+
- `npm run bench` compares conformance, speed (with and without eval), setup cost, memory and bundle size with ata-validator (generated code and its interpreter), Ajv, Zod (native schemas, and `z.fromJSONSchema` for the test suite), TypeBox, @cfworker/json-schema and omptype.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
TypeBox
|
|
2
|
+
|
|
3
|
+
The MIT License (MIT)
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2017-2026 Haydn Paterson
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in
|
|
15
|
+
all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,296 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @earendil-works/pi-schema
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
JSON Schema builders and validation without `eval`. A schema compiles once into plain closures, so it checks values
|
|
4
|
+
at close to the speed of validators that generate code, also under a strict Content Security Policy, and needs no
|
|
5
|
+
dependencies.
|
|
6
|
+
|
|
7
|
+
- Drafts 2020-12 and 7. Passes 2,222 of the 2,223 core tests of the official
|
|
8
|
+
[JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
|
|
9
|
+
- Builders return plain JSON Schema typed with the values it accepts; a compiled check narrows to that type.
|
|
10
|
+
- Errors as JSON Pointer paths. A missing or unknown field is reported at the field.
|
|
11
|
+
- Never modifies a schema; frozen schemas work.
|
|
12
|
+
- 8.5 KB minified and gzipped. Format checks are an optional import.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @earendil-works/pi-schema
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { compile, int, obj, oneOf, opt, type Static, str } from "@earendil-works/pi-schema";
|
|
24
|
+
|
|
25
|
+
const Read = obj({
|
|
26
|
+
path: str({ description: "Path to the file", minLength: 1 }),
|
|
27
|
+
offset: opt(int({ minimum: 1 })),
|
|
28
|
+
mode: oneOf(["text", "binary"]),
|
|
29
|
+
});
|
|
30
|
+
type Read = Static<typeof Read>; // { path: string; mode: "text" | "binary"; offset?: number }
|
|
31
|
+
|
|
32
|
+
const read = compile(Read);
|
|
33
|
+
if (read.check(value)) value.path; // value: Read
|
|
34
|
+
|
|
35
|
+
read.errors({ offset: 0, extra: true });
|
|
36
|
+
// [
|
|
37
|
+
// { path: "/path", keyword: "required", message: "missing" },
|
|
38
|
+
// { path: "/mode", keyword: "required", message: "missing" },
|
|
39
|
+
// { path: "/offset", keyword: "minimum", message: "expected >= 1" },
|
|
40
|
+
// { path: "/extra", keyword: "additionalProperties", message: "unknown field" },
|
|
41
|
+
// ]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`Read` is plain JSON Schema (`{ type: "object", properties: {...}, required: ["path", "mode"], additionalProperties:
|
|
45
|
+
false }`), ready for a tool definition or `JSON.stringify`. `compile` takes any JSON Schema, built with the builders
|
|
46
|
+
or not.
|
|
47
|
+
|
|
48
|
+
`check(value)` is the fast path: a boolean, no allocations for most schemas, and a plain function you can pass
|
|
49
|
+
around. `errors(value)` explains a failure and returns `[]` for a valid value. It is built on the first failure and
|
|
50
|
+
kept, so a schema whose values always pass never pays for it.
|
|
51
|
+
|
|
52
|
+
### Builders
|
|
53
|
+
|
|
54
|
+
| builder | schema | type |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `str({ minLength, maxLength, pattern, format })` | `{ type: "string", ... }` | `string` |
|
|
57
|
+
| `num({ minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf })`, `int(...)` | `{ type: "number" }`, `{ type: "integer" }` | `number` |
|
|
58
|
+
| `bool()`, `nul()`, `unknown()` | `{ type: "boolean" }`, `{ type: "null" }`, `{}` | `boolean`, `null`, `unknown` |
|
|
59
|
+
| `lit(value)` | `{ const: value }` | the literal |
|
|
60
|
+
| `oneOf(["a", "b"])` | `{ enum: [...] }` | `"a" \| "b"` |
|
|
61
|
+
| `obj({ a: str(), b: opt(num()) }, { open })` | closed object; `open: true` allows other properties | `{ a: string; b?: number }` |
|
|
62
|
+
| `arr(items, { minItems, maxItems, uniqueItems })` | `{ type: "array", items }` | `T[]` |
|
|
63
|
+
| `tuple([str(), num()])` | `prefixItems`, no more items | `[string, number]` |
|
|
64
|
+
| `map(value)` | `{ type: "object", additionalProperties: value }` | `Record<string, T>` |
|
|
65
|
+
| `union([...])` | `{ anyOf: [...] }` | the union |
|
|
66
|
+
| `nullable(schema)` | `{ anyOf: [schema, { type: "null" }] }` | `T \| null` |
|
|
67
|
+
| `bytes({ minLength, maxLength })` | `{ type: "bytes", ... }` | `Uint8Array` |
|
|
68
|
+
|
|
69
|
+
Every builder also takes `title`, `description`, `examples`, `deprecated` and `default`. A `default` is an annotation;
|
|
70
|
+
validation never inserts it.
|
|
71
|
+
|
|
72
|
+
A union of objects tagged by a `lit()` property checks and reports by the tag: only the branch the tag selects runs.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
const Block = union([obj({ kind: lit("text"), text: str() }), obj({ kind: lit("image"), width: int({ minimum: 0 }) })]);
|
|
76
|
+
compile(Block).errors({ kind: "video" });
|
|
77
|
+
// [{ path: "/kind", keyword: "anyOf", message: 'expected one of ["text","image"]' }]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`Static` also types schemas built with TypeBox 1.x, read from TypeBox's type shapes (pi-schema does not import
|
|
81
|
+
TypeBox): `String`, `Number`, `Integer`, `Boolean`, `Null`, `Literal`, `Array`, `Object` (with `Type.Optional` and
|
|
82
|
+
`Type.Readonly` properties), `Union`, `Record`, `Unsafe`, `Unknown` and `Any` give TypeBox's own static type, so
|
|
83
|
+
`Static<typeof Type.Object({ path: Type.String() })>` is `{ path: string }`. Other TypeBox kinds (`Enum`, `Tuple`,
|
|
84
|
+
`Intersect`, `Cyclic`, ...) give `unknown` where they stand. Other JSON Schema objects give `unknown`.
|
|
85
|
+
|
|
86
|
+
### Options
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { formats } from "@earendil-works/pi-schema/formats";
|
|
90
|
+
|
|
91
|
+
const validator = compile(schema, {
|
|
92
|
+
draft: "7", // "2020-12" (default) or "7"
|
|
93
|
+
formats, // check `format`; without it `format` is an annotation, as 2020-12 specifies
|
|
94
|
+
schemas: { "https://example.com/point.json": point }, // documents for $refs to other documents, by URI
|
|
95
|
+
unresolvedRefs: "reject", // a $ref that names nothing rejects values at its place; default "throw"
|
|
96
|
+
coerce: true, // adds coerce() and parse(), see Coercion
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`formats` has every format 2020-12 defines (`date-time`, `email`, `uri`, `uuid`, `ipv6`, ...), plus `url` and
|
|
101
|
+
`json-pointer-uri-fragment`, which TypeBox and ajv-formats also check. Pass your own map to check fewer formats, or
|
|
102
|
+
custom ones.
|
|
103
|
+
|
|
104
|
+
A `$ref` that names nothing (a missing definition, a document not in `schemas`) throws when the schema compiles. With
|
|
105
|
+
`unresolvedRefs: "reject"` it compiles as `false` instead: only values at that place fail, with the error
|
|
106
|
+
`unresolved reference <ref>`, and the rest of the schema works. Use it for schemas from other systems, such as MCP
|
|
107
|
+
servers, whose references may dangle.
|
|
108
|
+
|
|
109
|
+
### TypeBox schemas
|
|
110
|
+
|
|
111
|
+
TypeBox 1.x schemas are JSON Schema, so they compile as they are:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import Type from "typebox";
|
|
115
|
+
|
|
116
|
+
const user = compile(Type.Object({ name: Type.String(), tags: Type.Array(Type.String()) }));
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
TypeBox's JavaScript-only types (`BigInt`, `Undefined`, `Function`, ...), refinements and codecs are not JSON Schema
|
|
120
|
+
and are not supported.
|
|
121
|
+
|
|
122
|
+
### Coercion
|
|
123
|
+
|
|
124
|
+
`compile(schema, { coerce: true })` adds `coerce()` and `parse()`, for loosely typed input such as a model's tool call
|
|
125
|
+
arguments. Without the option the validator has neither, and its `check` pays nothing for them: the coercion rules are
|
|
126
|
+
built on the first `coerce()` or `parse()`.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const read = compile(Read, { coerce: true });
|
|
130
|
+
|
|
131
|
+
read.coerce({ path: "a.txt", offset: "10", mode: "text" }); // { path: "a.txt", offset: 10, mode: "text" }
|
|
132
|
+
|
|
133
|
+
const result = read.parse({ path: "a.txt", offset: "ten", mode: "text" });
|
|
134
|
+
if (result.ok) result.value.offset; // number
|
|
135
|
+
else result.errors; // [{ path: "/offset", keyword: "type", message: "expected integer, got string" }]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`coerce(value)` returns the coerced value. It copies only the paths it changes, returns `value` itself when nothing
|
|
139
|
+
changes, and never modifies `value`. A value no rule applies to stays as it is. `parse(value)` coerces, then checks:
|
|
140
|
+
`{ ok: true, value }` with the coerced value, or `{ ok: false, errors }` with the errors of the coerced value.
|
|
141
|
+
|
|
142
|
+
Three passes run in order:
|
|
143
|
+
|
|
144
|
+
1. **Optional nulls.** A `null` in an optional property whose schema rejects `null` is dropped, as if omitted.
|
|
145
|
+
2. **Conversions**, ported from TypeBox 1.3.27's `Value.Convert` and keyed on JSON Schema keywords (the first that
|
|
146
|
+
applies to a schema): `$ref` follows the reference; `const` and `enum` take the member the value converts to;
|
|
147
|
+
`anyOf`/`oneOf` keep a value one branch accepts, else take the first branch's conversion the whole schema
|
|
148
|
+
accepts; `allOf` converts with each member in turn; then by `type`:
|
|
149
|
+
- `string`: numbers and booleans to their text, `null` to `"null"`.
|
|
150
|
+
- `number`: numeric strings (`""` is 0), `"true"`/`"false"`, booleans to 1/0, `null` to 0.
|
|
151
|
+
- `integer`: as `number`, then truncated (`"3.5"` and `3.5` become 3).
|
|
152
|
+
- `boolean`: `"true"`/`"false"` (any case), `"1"`/`"0"`, 1/0, `null` to `false`.
|
|
153
|
+
- `null`: `"null"`, `"undefined"`, `""`, `"0"`, 0 and `false`.
|
|
154
|
+
- `array`: a single value becomes a one-item array, then each item converts; a tuple (`prefixItems` or an `items`
|
|
155
|
+
array) converts its items and never wraps.
|
|
156
|
+
- `object`: each property converts by `properties`, `patternProperties` or `additionalProperties`.
|
|
157
|
+
3. **Primitive coercion by type**, as Ajv's `coerceTypes`, for what pass 2 leaves (type arrays such as
|
|
158
|
+
`["integer", "null"]`): numbers from numeric strings and booleans, booleans from `"true"`/`"false"` and 1/0,
|
|
159
|
+
strings from numbers and booleans, `null` from `""`, 0 and `false`, and `null` to `""`, 0 or `false`. A value that
|
|
160
|
+
already has one of a type array's types stays.
|
|
161
|
+
|
|
162
|
+
Where passes 2 and 3 disagree, pass 2 wins: `null` for a required string becomes `"null"`, not `""`. A union's
|
|
163
|
+
branches convert in order and only until one is accepted; a union node that also declares properties converts them into
|
|
164
|
+
each branch's candidate before checking it. A recursive schema (a tree through `$ref`) that meets a value it cannot
|
|
165
|
+
convert leaves the value as it is, so `check` reports it, and results through a `$ref` are reused within one call, so a
|
|
166
|
+
recursive union does not convert (or explain) the same child once per branch.
|
|
167
|
+
|
|
168
|
+
Pass 1 drops optional nulls inside the values of `properties`, `patternProperties` and `additionalProperties` alike; a
|
|
169
|
+
pattern or additional key itself is never dropped. Items past a draft-7 `items` array (`additionalItems`) are not
|
|
170
|
+
coerced by any pass.
|
|
171
|
+
|
|
172
|
+
### Values
|
|
173
|
+
|
|
174
|
+
- A property whose value is `undefined` counts as absent, as in TypeBox.
|
|
175
|
+
- `{ type: "bytes", minLength, maxLength }` matches a `Uint8Array` of that length, an extension for values that never
|
|
176
|
+
pass through JSON.
|
|
177
|
+
|
|
178
|
+
## Supported keywords
|
|
179
|
+
|
|
180
|
+
- Every validation keyword of drafts 2020-12 and 7, including draft 7's `dependencies` and `items` arrays with
|
|
181
|
+
`additionalItems`.
|
|
182
|
+
- `$id`, `$anchor`, `$ref` (local, by anchor, relative and remote), `$dynamicRef` and `$dynamicAnchor`.
|
|
183
|
+
- `unevaluatedProperties` and `unevaluatedItems`.
|
|
184
|
+
- `pattern` and `patternProperties` are ECMAScript regular expressions with the `u` flag.
|
|
185
|
+
- Not supported: meta-schemas that switch vocabularies on or off (`$vocabulary`), `contentMediaType` and
|
|
186
|
+
`contentEncoding` assertions (optional in the spec), and draft 2019-09's `$recursiveRef`.
|
|
187
|
+
|
|
188
|
+
## Benchmarks
|
|
189
|
+
|
|
190
|
+
Node 24.4.1 on one core of an i9-13900. The other libraries:
|
|
191
|
+
|
|
192
|
+
- ata-validator 1.61.3: its generated code, and its eval-free interpreter chosen with `engine: "interpreter"`;
|
|
193
|
+
- Ajv 8.20.0, with ajv-formats 3.0.1 for the format tests;
|
|
194
|
+
- Zod 4.6.7: native Zod schemas for speed, setup and memory, and `z.fromJSONSchema` for the test suite;
|
|
195
|
+
- TypeBox 1.3.27;
|
|
196
|
+
- @cfworker/json-schema 4.1.1;
|
|
197
|
+
- @oh-my-pi/omptype 18.13.3, through `fromJsonSchema`.
|
|
198
|
+
|
|
199
|
+
| test suite (tests passed) | pi-schema | ata | ata interpreter | Ajv | Zod | TypeBox | cfworker | omptype |
|
|
200
|
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
201
|
+
| draft 2020-12 core (1,298) | 1,297 | 1,298 | 1,298 | 1,236 | 693 | 1,276 | 1,249 | 863 |
|
|
202
|
+
| draft 7 core (925) | 925 | 925 | 925 | 917 | 632 | 924 | 918 | 659 |
|
|
203
|
+
| draft 2020-12 format (895), with format checks | 895 | 814 | 814 | 750 | 412 | 895 | 740 | 412 |
|
|
204
|
+
|
|
205
|
+
| check, valid value | pi-schema | ata | ata interpreter | Ajv | Zod | TypeBox | cfworker | omptype |
|
|
206
|
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
207
|
+
| nested object with tagged unions | 410 ns | 180 ns | 836 ns | 627 ns | 1.3 µs | 475 ns | 43 µs | 730 ns |
|
|
208
|
+
| union of 5 messages | 86 ns | 37 ns | 212 ns | 70 ns | 297 ns | 95 ns | 11 µs | 96 ns |
|
|
209
|
+
| tool schema (read) | 31 ns | 10 ns | 41 ns | 13 ns | 41 ns | 9 ns | 2.0 µs | 9 ns |
|
|
210
|
+
| MCP tool schema with `$ref` | 161 ns | 64 ns | 314 ns | 136 ns | 927 ns | 87 ns | 15 µs | 1.9 µs |
|
|
211
|
+
| recursive `$ref` schema | 755 ns | 307 ns | 1.9 µs | 378 ns | 2.7 µs | 1.2 µs | 41 µs | 7.5 µs |
|
|
212
|
+
|
|
213
|
+
| same, without eval (strict CSP) | pi-schema | ata | ata interpreter | Ajv | Zod | TypeBox | cfworker | omptype |
|
|
214
|
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
215
|
+
| nested object with tagged unions | 411 ns | 1.6 µs | 844 ns | fails | 3.4 µs | 44 µs | 44 µs | 2.7 µs |
|
|
216
|
+
| union of 5 messages | 87 ns | 396 ns | 209 ns | fails | 652 ns | 10 µs | 11 µs | 501 ns |
|
|
217
|
+
| MCP tool schema with `$ref` | 164 ns | 484 ns | 326 ns | fails | 2.0 µs | 39 µs | 13 µs | 2.2 µs |
|
|
218
|
+
| recursive `$ref` schema | 746 ns | 2.8 µs | 1.9 µs | fails | 7.3 µs | 130 µs | 42 µs | 9.0 µs |
|
|
219
|
+
|
|
220
|
+
| cost | pi-schema | ata | ata interpreter | Ajv | Zod | TypeBox | cfworker | omptype |
|
|
221
|
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
222
|
+
| compile a new schema | 20 µs | 4.9 µs | 1.3 µs | 1.4 ms | 151 µs | 210 µs | 22 µs | 28 µs |
|
|
223
|
+
| its first check | 0.8 µs | 335 µs | 87 µs | 195 µs | 466 µs | 41 µs | 7.1 µs | 109 µs |
|
|
224
|
+
| memory, 3,200 distinct schemas | 7.1 MB | 36.0 MB | 51.3 MB | 59.7 MB | 170.8 MB | 54.7 MB | 7.6 MB | 19.9 MB |
|
|
225
|
+
| bundle, minified + gzip | 8.5 KB | 134.0 KB | 134.0 KB | 39.7 KB | 24.4 KB | 35.2 KB | 5.8 KB | 38.8 KB |
|
|
226
|
+
|
|
227
|
+
Where code generation is unavailable:
|
|
228
|
+
|
|
229
|
+
- **ata** answers through its interpreted engine without being told. That automatic fallback measured slower than the
|
|
230
|
+
engine chosen with `engine: "interpreter"`.
|
|
231
|
+
- **Ajv** cannot compile at all: `compile()` throws `EvalError`.
|
|
232
|
+
- **Zod** notices and parses objects without its generated code.
|
|
233
|
+
- **TypeBox** falls back to its interpreter.
|
|
234
|
+
- **omptype** throws `EvalError` once per schema, then validates through its interpreter.
|
|
235
|
+
|
|
236
|
+
ata, Zod and omptype compile on a schema's first check, so their compile cost shows there. ata's bundle is for the
|
|
237
|
+
browser without code splitting; with splitting, its first load is about 53 KB plus a 76 KB generator chunk loaded
|
|
238
|
+
later.
|
|
239
|
+
|
|
240
|
+
Zod is not a JSON Schema validator:
|
|
241
|
+
|
|
242
|
+
- **Speed, setup and memory:** Zod gets the same schemas written as native Zod schemas (`z.strictObject`,
|
|
243
|
+
`z.discriminatedUnion`, `z.lazy`), with `.describe()` for the corpus descriptions, and `safeParse` as the check.
|
|
244
|
+
Its memory includes building the schemas, which the other libraries receive as JSON. Without the descriptions it
|
|
245
|
+
is 131 MB.
|
|
246
|
+
- **Test suite:** through `z.fromJSONSchema`, which is semi-experimental. It refuses `unevaluatedProperties`,
|
|
247
|
+
`unevaluatedItems`, `dependentSchemas`, `dependentRequired`, `if`/`then`/`else`, `not`, and every `$ref` that is
|
|
248
|
+
not local. A refused schema fails all its tests: 405 of the 1,298 draft 2020-12 core tests and 107 of the 925 draft
|
|
249
|
+
7 core tests.
|
|
250
|
+
- **Bundle:** measured with Zod's documented `import * as z from "zod"`. A named `import { z }` keeps every locale
|
|
251
|
+
(about 90 KB gzipped), and `z.fromJSONSchema` brings it to 40.0 KB; `zod/mini` is about 6 KB.
|
|
252
|
+
|
|
253
|
+
Flat objects of simple types check in 13–80 ns, against 7–15 ns for TypeBox. Without code generation, reading a
|
|
254
|
+
property whose name comes from a list costs a few nanoseconds.
|
|
255
|
+
|
|
256
|
+
### Reproduce
|
|
257
|
+
|
|
258
|
+
`npm run bench` in `packages/schema` takes about 12 minutes. It runs every library through its fastest boolean path
|
|
259
|
+
(ata's `isValidObject()`) and keeps the object each returns. ata runs with `ATA_NO_NATIVE=1`: its optional native
|
|
260
|
+
addon only serves its buffer APIs. The scripts are in `test/bench/`.
|
|
261
|
+
|
|
262
|
+
- **Conformance:** every library on the vendored test suite, with the remote documents and meta-schemas the tests
|
|
263
|
+
reference (Zod through `z.fromJSONSchema`, which takes no remote documents). ata reads the draft only from `$schema`, so draft 7 schemas without one get it declared. ata's defaults
|
|
264
|
+
(`useDefaults`, `assertFormat`) are turned off to match the other libraries; formats are checked only in the
|
|
265
|
+
format tests, by each library's own checks.
|
|
266
|
+
- **Speed:** realistic schemas, in ns per check after warm-up. Each result is the median of 15 rounds, with the
|
|
267
|
+
libraries' order rotated in each round.
|
|
268
|
+
- **Speed without eval:** the same, run with `--disallow-code-generation-from-strings`.
|
|
269
|
+
- **Setup:** compiling 1,000 schemas a library has not seen, and their first checks.
|
|
270
|
+
- **Memory:** 3,200 distinct tool-like schemas, compiled and each checked 3 times, each library in its own process.
|
|
271
|
+
- **Size:** esbuild browser bundles of what a validator needs, minified and gzipped.
|
|
272
|
+
|
|
273
|
+
### Code generated at build time
|
|
274
|
+
|
|
275
|
+
Ajv (`ajv/dist/standalone`) and ata (`ata build`, `ata compile`, `toStandaloneModule()`) can also generate their
|
|
276
|
+
validation code when the app is built. The app then imports plain modules and evaluates nothing at run time, so those
|
|
277
|
+
checks run at code-generation speed under a strict CSP. This is not in the benchmark, because it is a different
|
|
278
|
+
deal:
|
|
279
|
+
|
|
280
|
+
- **A build step per schema.** Every schema must be known when the app is built. Schemas that arrive at run time
|
|
281
|
+
(MCP servers, extensions, user input) cannot be validated this way.
|
|
282
|
+
- **Code per schema.** In a separate measurement on the same 3,200 schemas:
|
|
283
|
+
- Ajv: 1.8 ms to build and 8.9 KB of JavaScript per schema;
|
|
284
|
+
- ata: 1.1 ms and 13 KB per schema, or 0.3–0.8 KB gzipped when only its `isValid` export is bundled;
|
|
285
|
+
- importing all 3,200 generated validators took 40 MB of heap (Ajv, one module) and 68 MB (ata, one module per
|
|
286
|
+
schema).
|
|
287
|
+
|
|
288
|
+
pi-schema compiles at run time in 25 µs per schema, generates no code, and covers schemas known at build time and
|
|
289
|
+
schemas that arrive later.
|
|
290
|
+
|
|
291
|
+
## Development
|
|
292
|
+
|
|
293
|
+
`npm test` runs the official test suite (drafts 2020-12 and 7, including the optional and format tests, vendored in
|
|
294
|
+
`test/fixtures/`) and the package's own tests.
|
|
295
|
+
|
|
296
|
+
The format checks are adapted from TypeBox 1.3.27 (MIT); its license is in `LICENSES/`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Check } from "./types.ts";
|
|
2
|
+
/** What a schema evaluated: property names (or all of them), the evaluated item prefix, items matched by `contains`. */
|
|
3
|
+
export interface Annotations {
|
|
4
|
+
props: Set<string> | null;
|
|
5
|
+
allProps: boolean;
|
|
6
|
+
items: number;
|
|
7
|
+
allItems: boolean;
|
|
8
|
+
indexes: Set<number> | null;
|
|
9
|
+
}
|
|
10
|
+
/** A check that records what it evaluated into `a` when it passes. */
|
|
11
|
+
export type AnnotatingCheck = (value: unknown, a: Annotations) => boolean;
|
|
12
|
+
/** Empty annotations. */
|
|
13
|
+
export declare function newAnnotations(): Annotations;
|
|
14
|
+
export declare const ANNOTATING_TRUE: AnnotatingCheck;
|
|
15
|
+
export declare const ANNOTATING_FALSE: AnnotatingCheck;
|
|
16
|
+
/** The indirection a recursive reference goes through. */
|
|
17
|
+
export declare function indirectAnnotating(slot: {
|
|
18
|
+
check: AnnotatingCheck | undefined;
|
|
19
|
+
}): AnnotatingCheck;
|
|
20
|
+
/** A plain check in annotating position: it evaluates nothing that unevaluated* sees. */
|
|
21
|
+
export declare function ignoreAnnotations(check: Check): AnnotatingCheck;
|
|
22
|
+
/** An annotating check run as a plain check, with fresh annotations. */
|
|
23
|
+
export declare function withFreshAnnotations(check: AnnotatingCheck): Check;
|
|
24
|
+
/** `check` with `uri` pushed onto the dynamic scope while it runs. */
|
|
25
|
+
export declare function scopedAnnotating(scope: string[], uri: string, check: AnnotatingCheck): AnnotatingCheck;
|
|
26
|
+
/** unevaluated* sees only the annotations of its own schema, never those of its siblings in the parent. */
|
|
27
|
+
export declare function isolateAnnotations(check: AnnotatingCheck): AnnotatingCheck;
|
|
28
|
+
/** The non-annotating keywords, then each annotating part in order. */
|
|
29
|
+
export declare function annotateAll(plain: Check, parts: readonly AnnotatingCheck[]): AnnotatingCheck;
|
|
30
|
+
/** `properties`, `patternProperties` and `additionalProperties`. */
|
|
31
|
+
export declare function annotateObject(props: readonly (readonly [string, Check])[], patterns: readonly (readonly [RegExp, Check])[], additional: Check | undefined): AnnotatingCheck;
|
|
32
|
+
/** `prefixItems`/`items`/`additionalItems` and `contains` with its bounds. */
|
|
33
|
+
export declare function annotateArray(prefix: readonly Check[], rest: Check | undefined, contains: Check | undefined, minContains: number, maxContains: number): AnnotatingCheck;
|
|
34
|
+
/** `anyOf` (or `oneOf` when `exactlyOne`): every passing branch contributes its annotations. */
|
|
35
|
+
export declare function annotateAny(branches: readonly AnnotatingCheck[], exactlyOne: boolean): AnnotatingCheck;
|
|
36
|
+
/** `if`, `then` and `else`: a passing `if` contributes its annotations even without `then`. */
|
|
37
|
+
export declare function annotateIf(cond: AnnotatingCheck, then: AnnotatingCheck | undefined, otherwise: AnnotatingCheck | undefined): AnnotatingCheck;
|
|
38
|
+
/** One `dependentSchemas` entry. */
|
|
39
|
+
export declare function annotateDependent(key: string, check: AnnotatingCheck): AnnotatingCheck;
|
|
40
|
+
/** `unevaluatedProperties`: every property no other keyword evaluated passes `check`. */
|
|
41
|
+
export declare function unevaluatedProperties(check: Check): AnnotatingCheck;
|
|
42
|
+
/** `unevaluatedItems`: every item no other keyword evaluated passes `check`. */
|
|
43
|
+
export declare function unevaluatedItems(check: Check): AnnotatingCheck;
|
|
44
|
+
//# sourceMappingURL=annotations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../src/annotations.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,wHAAwH;AACxH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;CAC5B;AAED,sEAAsE;AACtE,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC;AAE1E,yBAAyB;AACzB,wBAAgB,cAAc,IAAI,WAAW,CAE5C;AAoBD,eAAO,MAAM,eAAe,EAAE,eAA4B,CAAC;AAC3D,eAAO,MAAM,gBAAgB,EAAE,eAA6B,CAAC;AAE7D,0DAA0D;AAC1D,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,eAAe,GAAG,SAAS,CAAA;CAAE,GAAG,eAAe,CAEhG;AAED,yFAAyF;AACzF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,CAE/D;AAED,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,eAAe,GAAG,KAAK,CAElE;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,eAAe,CAOtG;AAED,2GAA2G;AAC3G,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,eAAe,CAO1E;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,eAAe,EAAE,GAAG,eAAe,CAM5F;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAC7B,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,EAAE,EAC5C,QAAQ,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,EAAE,EAC/C,UAAU,EAAE,KAAK,GAAG,SAAS,GAC3B,eAAe,CA2BjB;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAC5B,MAAM,EAAE,SAAS,KAAK,EAAE,EACxB,IAAI,EAAE,KAAK,GAAG,SAAS,EACvB,QAAQ,EAAE,KAAK,GAAG,SAAS,EAC3B,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GACjB,eAAe,CAsBjB;AAED,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,EAAE,UAAU,EAAE,OAAO,GAAG,eAAe,CAYtG;AAED,+FAA+F;AAC/F,wBAAgB,UAAU,CACzB,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE,eAAe,GAAG,SAAS,EACjC,SAAS,EAAE,eAAe,GAAG,SAAS,GACpC,eAAe,CASjB;AAED,oCAAoC;AACpC,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,eAAe,CAEtF;AAED,yFAAyF;AACzF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,CAOnE;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,CAO9D"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Annotation tracking for `unevaluatedProperties` and `unevaluatedItems`: annotating checks record which
|
|
2
|
+
// properties and items a schema evaluated. Only schemas that use those keywords compile into these closures.
|
|
3
|
+
import { isObject } from "./checks.js";
|
|
4
|
+
/** Empty annotations. */
|
|
5
|
+
export function newAnnotations() {
|
|
6
|
+
return { props: null, allProps: false, items: 0, allItems: false, indexes: null };
|
|
7
|
+
}
|
|
8
|
+
function merge(a, b) {
|
|
9
|
+
if (b.props !== null)
|
|
10
|
+
for (const k of b.props)
|
|
11
|
+
addProp(a, k);
|
|
12
|
+
a.allProps ||= b.allProps;
|
|
13
|
+
if (b.items > a.items)
|
|
14
|
+
a.items = b.items;
|
|
15
|
+
a.allItems ||= b.allItems;
|
|
16
|
+
if (b.indexes !== null)
|
|
17
|
+
for (const i of b.indexes)
|
|
18
|
+
addIndex(a, i);
|
|
19
|
+
}
|
|
20
|
+
function addProp(a, key) {
|
|
21
|
+
if (a.props === null)
|
|
22
|
+
a.props = new Set();
|
|
23
|
+
a.props.add(key);
|
|
24
|
+
}
|
|
25
|
+
function addIndex(a, index) {
|
|
26
|
+
if (a.indexes === null)
|
|
27
|
+
a.indexes = new Set();
|
|
28
|
+
a.indexes.add(index);
|
|
29
|
+
}
|
|
30
|
+
export const ANNOTATING_TRUE = () => true;
|
|
31
|
+
export const ANNOTATING_FALSE = () => false;
|
|
32
|
+
/** The indirection a recursive reference goes through. */
|
|
33
|
+
export function indirectAnnotating(slot) {
|
|
34
|
+
return (v, a) => slot.check(v, a);
|
|
35
|
+
}
|
|
36
|
+
/** A plain check in annotating position: it evaluates nothing that unevaluated* sees. */
|
|
37
|
+
export function ignoreAnnotations(check) {
|
|
38
|
+
return (v) => check(v);
|
|
39
|
+
}
|
|
40
|
+
/** An annotating check run as a plain check, with fresh annotations. */
|
|
41
|
+
export function withFreshAnnotations(check) {
|
|
42
|
+
return (v) => check(v, newAnnotations());
|
|
43
|
+
}
|
|
44
|
+
/** `check` with `uri` pushed onto the dynamic scope while it runs. */
|
|
45
|
+
export function scopedAnnotating(scope, uri, check) {
|
|
46
|
+
return (v, a) => {
|
|
47
|
+
scope.push(uri);
|
|
48
|
+
const ok = check(v, a);
|
|
49
|
+
scope.pop();
|
|
50
|
+
return ok;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** unevaluated* sees only the annotations of its own schema, never those of its siblings in the parent. */
|
|
54
|
+
export function isolateAnnotations(check) {
|
|
55
|
+
return (v, a) => {
|
|
56
|
+
const b = newAnnotations();
|
|
57
|
+
if (!check(v, b))
|
|
58
|
+
return false;
|
|
59
|
+
merge(a, b);
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** The non-annotating keywords, then each annotating part in order. */
|
|
64
|
+
export function annotateAll(plain, parts) {
|
|
65
|
+
return (v, a) => {
|
|
66
|
+
if (!plain(v))
|
|
67
|
+
return false;
|
|
68
|
+
for (let i = 0; i < parts.length; i++)
|
|
69
|
+
if (!parts[i](v, a))
|
|
70
|
+
return false;
|
|
71
|
+
return true;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** `properties`, `patternProperties` and `additionalProperties`. */
|
|
75
|
+
export function annotateObject(props, patterns, additional) {
|
|
76
|
+
const declared = new Set(props.map(([k]) => k));
|
|
77
|
+
return (v, a) => {
|
|
78
|
+
if (!isObject(v))
|
|
79
|
+
return true;
|
|
80
|
+
const seen = [];
|
|
81
|
+
for (const [k, check] of props) {
|
|
82
|
+
if (!Object.hasOwn(v, k))
|
|
83
|
+
continue;
|
|
84
|
+
if (!check(v[k]))
|
|
85
|
+
return false;
|
|
86
|
+
seen.push(k);
|
|
87
|
+
}
|
|
88
|
+
for (const k of Object.keys(v)) {
|
|
89
|
+
let matched = declared.has(k);
|
|
90
|
+
for (const [re, check] of patterns) {
|
|
91
|
+
if (re.test(k)) {
|
|
92
|
+
if (!check(v[k]))
|
|
93
|
+
return false;
|
|
94
|
+
matched = true;
|
|
95
|
+
seen.push(k);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!matched && additional !== undefined) {
|
|
99
|
+
if (!additional(v[k]))
|
|
100
|
+
return false;
|
|
101
|
+
seen.push(k);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const k of seen)
|
|
105
|
+
addProp(a, k);
|
|
106
|
+
return true;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** `prefixItems`/`items`/`additionalItems` and `contains` with its bounds. */
|
|
110
|
+
export function annotateArray(prefix, rest, contains, minContains, maxContains) {
|
|
111
|
+
return (v, a) => {
|
|
112
|
+
if (!Array.isArray(v))
|
|
113
|
+
return true;
|
|
114
|
+
const m = Math.min(v.length, prefix.length);
|
|
115
|
+
for (let i = 0; i < m; i++)
|
|
116
|
+
if (!prefix[i](v[i]))
|
|
117
|
+
return false;
|
|
118
|
+
if (rest !== undefined) {
|
|
119
|
+
for (let i = prefix.length; i < v.length; i++)
|
|
120
|
+
if (!rest(v[i]))
|
|
121
|
+
return false;
|
|
122
|
+
a.allItems = true;
|
|
123
|
+
}
|
|
124
|
+
if (m > a.items)
|
|
125
|
+
a.items = m;
|
|
126
|
+
if (contains !== undefined) {
|
|
127
|
+
let n = 0;
|
|
128
|
+
for (let i = 0; i < v.length; i++) {
|
|
129
|
+
if (contains(v[i])) {
|
|
130
|
+
n++;
|
|
131
|
+
addIndex(a, i);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (n < minContains || n > maxContains)
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** `anyOf` (or `oneOf` when `exactlyOne`): every passing branch contributes its annotations. */
|
|
141
|
+
export function annotateAny(branches, exactlyOne) {
|
|
142
|
+
return (v, a) => {
|
|
143
|
+
let n = 0;
|
|
144
|
+
for (const branch of branches) {
|
|
145
|
+
const b = newAnnotations();
|
|
146
|
+
if (branch(v, b)) {
|
|
147
|
+
n++;
|
|
148
|
+
merge(a, b);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return exactlyOne ? n === 1 : n > 0;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** `if`, `then` and `else`: a passing `if` contributes its annotations even without `then`. */
|
|
155
|
+
export function annotateIf(cond, then, otherwise) {
|
|
156
|
+
return (v, a) => {
|
|
157
|
+
const b = newAnnotations();
|
|
158
|
+
if (cond(v, b)) {
|
|
159
|
+
merge(a, b);
|
|
160
|
+
return then === undefined || then(v, a);
|
|
161
|
+
}
|
|
162
|
+
return otherwise === undefined || otherwise(v, a);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** One `dependentSchemas` entry. */
|
|
166
|
+
export function annotateDependent(key, check) {
|
|
167
|
+
return (v, a) => !isObject(v) || !Object.hasOwn(v, key) || check(v, a);
|
|
168
|
+
}
|
|
169
|
+
/** `unevaluatedProperties`: every property no other keyword evaluated passes `check`. */
|
|
170
|
+
export function unevaluatedProperties(check) {
|
|
171
|
+
return (v, a) => {
|
|
172
|
+
if (!isObject(v) || a.allProps)
|
|
173
|
+
return true;
|
|
174
|
+
for (const k of Object.keys(v))
|
|
175
|
+
if (!(a.props?.has(k) ?? false) && !check(v[k]))
|
|
176
|
+
return false;
|
|
177
|
+
a.allProps = true;
|
|
178
|
+
return true;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** `unevaluatedItems`: every item no other keyword evaluated passes `check`. */
|
|
182
|
+
export function unevaluatedItems(check) {
|
|
183
|
+
return (v, a) => {
|
|
184
|
+
if (!Array.isArray(v) || a.allItems)
|
|
185
|
+
return true;
|
|
186
|
+
for (let i = a.items; i < v.length; i++)
|
|
187
|
+
if (!(a.indexes?.has(i) ?? false) && !check(v[i]))
|
|
188
|
+
return false;
|
|
189
|
+
a.allItems = true;
|
|
190
|
+
return true;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=annotations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annotations.js","sourceRoot":"","sources":["../src/annotations.ts"],"names":[],"mappings":"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","sourcesContent":["// Annotation tracking for `unevaluatedProperties` and `unevaluatedItems`: annotating checks record which\n// properties and items a schema evaluated. Only schemas that use those keywords compile into these closures.\n\nimport { isObject } from \"./checks.ts\";\nimport type { Check } from \"./types.ts\";\n\n/** What a schema evaluated: property names (or all of them), the evaluated item prefix, items matched by `contains`. */\nexport interface Annotations {\n\tprops: Set<string> | null;\n\tallProps: boolean;\n\titems: number;\n\tallItems: boolean;\n\tindexes: Set<number> | null;\n}\n\n/** A check that records what it evaluated into `a` when it passes. */\nexport type AnnotatingCheck = (value: unknown, a: Annotations) => boolean;\n\n/** Empty annotations. */\nexport function newAnnotations(): Annotations {\n\treturn { props: null, allProps: false, items: 0, allItems: false, indexes: null };\n}\n\nfunction merge(a: Annotations, b: Annotations): void {\n\tif (b.props !== null) for (const k of b.props) addProp(a, k);\n\ta.allProps ||= b.allProps;\n\tif (b.items > a.items) a.items = b.items;\n\ta.allItems ||= b.allItems;\n\tif (b.indexes !== null) for (const i of b.indexes) addIndex(a, i);\n}\n\nfunction addProp(a: Annotations, key: string): void {\n\tif (a.props === null) a.props = new Set();\n\ta.props.add(key);\n}\n\nfunction addIndex(a: Annotations, index: number): void {\n\tif (a.indexes === null) a.indexes = new Set();\n\ta.indexes.add(index);\n}\n\nexport const ANNOTATING_TRUE: AnnotatingCheck = () => true;\nexport const ANNOTATING_FALSE: AnnotatingCheck = () => false;\n\n/** The indirection a recursive reference goes through. */\nexport function indirectAnnotating(slot: { check: AnnotatingCheck | undefined }): AnnotatingCheck {\n\treturn (v, a) => (slot.check as AnnotatingCheck)(v, a);\n}\n\n/** A plain check in annotating position: it evaluates nothing that unevaluated* sees. */\nexport function ignoreAnnotations(check: Check): AnnotatingCheck {\n\treturn (v) => check(v);\n}\n\n/** An annotating check run as a plain check, with fresh annotations. */\nexport function withFreshAnnotations(check: AnnotatingCheck): Check {\n\treturn (v) => check(v, newAnnotations());\n}\n\n/** `check` with `uri` pushed onto the dynamic scope while it runs. */\nexport function scopedAnnotating(scope: string[], uri: string, check: AnnotatingCheck): AnnotatingCheck {\n\treturn (v, a) => {\n\t\tscope.push(uri);\n\t\tconst ok = check(v, a);\n\t\tscope.pop();\n\t\treturn ok;\n\t};\n}\n\n/** unevaluated* sees only the annotations of its own schema, never those of its siblings in the parent. */\nexport function isolateAnnotations(check: AnnotatingCheck): AnnotatingCheck {\n\treturn (v, a) => {\n\t\tconst b = newAnnotations();\n\t\tif (!check(v, b)) return false;\n\t\tmerge(a, b);\n\t\treturn true;\n\t};\n}\n\n/** The non-annotating keywords, then each annotating part in order. */\nexport function annotateAll(plain: Check, parts: readonly AnnotatingCheck[]): AnnotatingCheck {\n\treturn (v, a) => {\n\t\tif (!plain(v)) return false;\n\t\tfor (let i = 0; i < parts.length; i++) if (!parts[i](v, a)) return false;\n\t\treturn true;\n\t};\n}\n\n/** `properties`, `patternProperties` and `additionalProperties`. */\nexport function annotateObject(\n\tprops: readonly (readonly [string, Check])[],\n\tpatterns: readonly (readonly [RegExp, Check])[],\n\tadditional: Check | undefined,\n): AnnotatingCheck {\n\tconst declared = new Set(props.map(([k]) => k));\n\treturn (v, a) => {\n\t\tif (!isObject(v)) return true;\n\t\tconst seen: string[] = [];\n\t\tfor (const [k, check] of props) {\n\t\t\tif (!Object.hasOwn(v, k)) continue;\n\t\t\tif (!check(v[k])) return false;\n\t\t\tseen.push(k);\n\t\t}\n\t\tfor (const k of Object.keys(v)) {\n\t\t\tlet matched = declared.has(k);\n\t\t\tfor (const [re, check] of patterns) {\n\t\t\t\tif (re.test(k)) {\n\t\t\t\t\tif (!check(v[k])) return false;\n\t\t\t\t\tmatched = true;\n\t\t\t\t\tseen.push(k);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!matched && additional !== undefined) {\n\t\t\t\tif (!additional(v[k])) return false;\n\t\t\t\tseen.push(k);\n\t\t\t}\n\t\t}\n\t\tfor (const k of seen) addProp(a, k);\n\t\treturn true;\n\t};\n}\n\n/** `prefixItems`/`items`/`additionalItems` and `contains` with its bounds. */\nexport function annotateArray(\n\tprefix: readonly Check[],\n\trest: Check | undefined,\n\tcontains: Check | undefined,\n\tminContains: number,\n\tmaxContains: number,\n): AnnotatingCheck {\n\treturn (v, a) => {\n\t\tif (!Array.isArray(v)) return true;\n\t\tconst m = Math.min(v.length, prefix.length);\n\t\tfor (let i = 0; i < m; i++) if (!prefix[i](v[i])) return false;\n\t\tif (rest !== undefined) {\n\t\t\tfor (let i = prefix.length; i < v.length; i++) if (!rest(v[i])) return false;\n\t\t\ta.allItems = true;\n\t\t}\n\t\tif (m > a.items) a.items = m;\n\t\tif (contains !== undefined) {\n\t\t\tlet n = 0;\n\t\t\tfor (let i = 0; i < v.length; i++) {\n\t\t\t\tif (contains(v[i])) {\n\t\t\t\t\tn++;\n\t\t\t\t\taddIndex(a, i);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (n < minContains || n > maxContains) return false;\n\t\t}\n\t\treturn true;\n\t};\n}\n\n/** `anyOf` (or `oneOf` when `exactlyOne`): every passing branch contributes its annotations. */\nexport function annotateAny(branches: readonly AnnotatingCheck[], exactlyOne: boolean): AnnotatingCheck {\n\treturn (v, a) => {\n\t\tlet n = 0;\n\t\tfor (const branch of branches) {\n\t\t\tconst b = newAnnotations();\n\t\t\tif (branch(v, b)) {\n\t\t\t\tn++;\n\t\t\t\tmerge(a, b);\n\t\t\t}\n\t\t}\n\t\treturn exactlyOne ? n === 1 : n > 0;\n\t};\n}\n\n/** `if`, `then` and `else`: a passing `if` contributes its annotations even without `then`. */\nexport function annotateIf(\n\tcond: AnnotatingCheck,\n\tthen: AnnotatingCheck | undefined,\n\totherwise: AnnotatingCheck | undefined,\n): AnnotatingCheck {\n\treturn (v, a) => {\n\t\tconst b = newAnnotations();\n\t\tif (cond(v, b)) {\n\t\t\tmerge(a, b);\n\t\t\treturn then === undefined || then(v, a);\n\t\t}\n\t\treturn otherwise === undefined || otherwise(v, a);\n\t};\n}\n\n/** One `dependentSchemas` entry. */\nexport function annotateDependent(key: string, check: AnnotatingCheck): AnnotatingCheck {\n\treturn (v, a) => !isObject(v) || !Object.hasOwn(v, key) || check(v, a);\n}\n\n/** `unevaluatedProperties`: every property no other keyword evaluated passes `check`. */\nexport function unevaluatedProperties(check: Check): AnnotatingCheck {\n\treturn (v, a) => {\n\t\tif (!isObject(v) || a.allProps) return true;\n\t\tfor (const k of Object.keys(v)) if (!(a.props?.has(k) ?? false) && !check(v[k])) return false;\n\t\ta.allProps = true;\n\t\treturn true;\n\t};\n}\n\n/** `unevaluatedItems`: every item no other keyword evaluated passes `check`. */\nexport function unevaluatedItems(check: Check): AnnotatingCheck {\n\treturn (v, a) => {\n\t\tif (!Array.isArray(v) || a.allItems) return true;\n\t\tfor (let i = a.items; i < v.length; i++) if (!(a.indexes?.has(i) ?? false) && !check(v[i])) return false;\n\t\ta.allItems = true;\n\t\treturn true;\n\t};\n}\n"]}
|