@oh-my-pi/omptype 18.16.0 → 18.18.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.
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Schema resource registry: base URIs, `$id`/`$anchor`/`$dynamicAnchor`
3
+ * identifiers, dialects, vocabularies, and `$ref` resolution across the root
4
+ * document and caller-supplied documents.
5
+ *
6
+ * Only subschemas under known applicator keywords are indexed, so an `$id`
7
+ * inside `enum` or an unknown keyword is not an identifier. JSON Pointer
8
+ * fragments may still point anywhere; their target inherits the base URI of
9
+ * the nearest indexed ancestor.
10
+ */
11
+ import { OmpTypeError } from "../errors.js";
12
+ import { parseLocalRefPointer } from "../local-ref.js";
13
+ /** Vocabulary flags gating keyword groups; resources default to all of them. */
14
+ export const VOCAB_APPLICATOR = 1;
15
+ export const VOCAB_VALIDATION = 2;
16
+ export const VOCAB_UNEVALUATED = 4;
17
+ const VOCAB_ALL = VOCAB_APPLICATOR | VOCAB_VALIDATION | VOCAB_UNEVALUATED;
18
+ /** Base URI for documents without an absolute `$id`. */
19
+ const DEFAULT_BASE = "omptype:/schema.json";
20
+ const SCHEMAS = 1;
21
+ const SCHEMA_MAP = 2;
22
+ /**
23
+ * Keywords holding subschemas: a schema or array of schemas, or a map of names
24
+ * to schemas. A `Map`, so `then` is no thenable key and `constructor` no hit.
25
+ */
26
+ const SUBSCHEMA_KEYWORDS = new Map([
27
+ ["items", SCHEMAS],
28
+ ["additionalItems", SCHEMAS],
29
+ ["prefixItems", SCHEMAS],
30
+ ["contains", SCHEMAS],
31
+ ["additionalProperties", SCHEMAS],
32
+ ["propertyNames", SCHEMAS],
33
+ ["unevaluatedItems", SCHEMAS],
34
+ ["unevaluatedProperties", SCHEMAS],
35
+ ["not", SCHEMAS],
36
+ ["if", SCHEMAS],
37
+ ["then", SCHEMAS],
38
+ ["else", SCHEMAS],
39
+ ["allOf", SCHEMAS],
40
+ ["anyOf", SCHEMAS],
41
+ ["oneOf", SCHEMAS],
42
+ ["contentSchema", SCHEMAS],
43
+ ["properties", SCHEMA_MAP],
44
+ ["patternProperties", SCHEMA_MAP],
45
+ ["$defs", SCHEMA_MAP],
46
+ ["definitions", SCHEMA_MAP],
47
+ ["dependentSchemas", SCHEMA_MAP],
48
+ ["dependencies", SCHEMA_MAP],
49
+ ]);
50
+ const VOCABULARIES = {
51
+ applicator: VOCAB_APPLICATOR,
52
+ validation: VOCAB_VALIDATION,
53
+ unevaluated: VOCAB_UNEVALUATED,
54
+ };
55
+ /** Registry of the documents one `fromJsonSchema` call can reference. */
56
+ export class Registry {
57
+ #resources = new Map();
58
+ #anchors = new Map();
59
+ /** Per-registry index: one subschema object can sit at different bases in different registries. */
60
+ #locations = new Map();
61
+ /** Caller-supplied documents by fragment-free URI, indexed on first use. */
62
+ #pending = new Map();
63
+ #draft;
64
+ constructor(draft, documents) {
65
+ this.#draft = draft;
66
+ if (documents === undefined)
67
+ return;
68
+ for (const key in documents) {
69
+ const uri = stripFragment(parseUri(key) ?? key);
70
+ this.#pending.set(uri, documents[key]);
71
+ }
72
+ }
73
+ /** Index `schema` as the root document and return its target. */
74
+ addRoot(schema) {
75
+ if (typeof schema !== "object" || schema === null) {
76
+ return {
77
+ schema,
78
+ location: { base: DEFAULT_BASE, resource: this.#newResource(DEFAULT_BASE, {}, this.#draft) },
79
+ };
80
+ }
81
+ return this.#addDocument(DEFAULT_BASE, schema);
82
+ }
83
+ /** Location recorded for an indexed subschema. */
84
+ location(schema) {
85
+ return this.#locations.get(schema);
86
+ }
87
+ /**
88
+ * Resolve `ref` against `base` to a schema and its location.
89
+ *
90
+ * # Errors
91
+ * Throws {@link OmpTypeError} for malformed URI references or fragments, and
92
+ * for targets no known document contains.
93
+ */
94
+ resolve(ref, base) {
95
+ let uri;
96
+ let fragment;
97
+ if (ref.charCodeAt(0) === 35) {
98
+ uri = base;
99
+ fragment = ref.slice(1);
100
+ }
101
+ else {
102
+ const absolute = parseUri(ref, base);
103
+ if (absolute === undefined)
104
+ throw new OmpTypeError(`invalid $ref: ${ref}`);
105
+ const hash = absolute.indexOf("#");
106
+ uri = hash < 0 ? absolute : absolute.slice(0, hash);
107
+ fragment = hash < 0 ? "" : absolute.slice(hash + 1);
108
+ }
109
+ const resource = this.#resource(uri);
110
+ if (resource === undefined)
111
+ throw new OmpTypeError(`unresolved $ref: ${ref}`);
112
+ if (fragment === "")
113
+ return { schema: resource.root, location: this.#locations.get(resource.root) };
114
+ if (fragment.charCodeAt(0) === 47)
115
+ return this.#pointer(resource, fragment, ref);
116
+ let name;
117
+ try {
118
+ name = decodeURIComponent(fragment);
119
+ }
120
+ catch {
121
+ throw new OmpTypeError(`invalid $ref: ${ref}`);
122
+ }
123
+ const anchor = this.#anchors.get(`${uri}#${name}`) ?? this.#anchorFromPending(`${uri}#${name}`);
124
+ if (anchor === undefined)
125
+ throw new OmpTypeError(`unresolved $ref: ${ref}`);
126
+ return anchor;
127
+ }
128
+ #anchorFromPending(key) {
129
+ if (this.#pending.size === 0)
130
+ return undefined;
131
+ this.#indexPending();
132
+ return this.#anchors.get(key);
133
+ }
134
+ #pointer(resource, fragment, ref) {
135
+ const tokens = parseLocalRefPointer(`#${fragment}`);
136
+ if (tokens === undefined)
137
+ throw new OmpTypeError(`invalid $ref: ${ref}`);
138
+ let node = resource.root;
139
+ let location = this.#locations.get(resource.root);
140
+ for (const token of tokens) {
141
+ if (Array.isArray(node)) {
142
+ if (!/^(?:0|[1-9]\d*)$/.test(token) || Number(token) >= node.length) {
143
+ throw new OmpTypeError(`unresolved $ref: ${ref}`);
144
+ }
145
+ node = node[Number(token)];
146
+ }
147
+ else if (typeof node === "object" && node !== null && Object.hasOwn(node, token)) {
148
+ node = node[token];
149
+ }
150
+ else {
151
+ throw new OmpTypeError(`unresolved $ref: ${ref}`);
152
+ }
153
+ if (typeof node === "object" && node !== null)
154
+ location = this.#locations.get(node) ?? location;
155
+ }
156
+ return { schema: node, location };
157
+ }
158
+ #resource(uri) {
159
+ const known = this.#resources.get(uri);
160
+ if (known !== undefined)
161
+ return known;
162
+ const document = this.#pending.get(uri);
163
+ if (document !== undefined) {
164
+ this.#pending.delete(uri);
165
+ if (typeof document === "object" && document !== null)
166
+ this.#addDocument(uri, document);
167
+ return this.#resources.get(uri);
168
+ }
169
+ // The URI may name a resource embedded in a document not yet indexed.
170
+ if (this.#pending.size === 0)
171
+ return undefined;
172
+ this.#indexPending();
173
+ return this.#resources.get(uri);
174
+ }
175
+ #indexPending() {
176
+ for (const [uri, document] of this.#pending) {
177
+ this.#pending.delete(uri);
178
+ if (typeof document === "object" && document !== null && !this.#resources.has(uri)) {
179
+ this.#addDocument(uri, document);
180
+ }
181
+ }
182
+ }
183
+ #addDocument(retrieval, root) {
184
+ const resource = this.#newResource(retrieval, root, this.#draft);
185
+ const location = { base: retrieval, resource };
186
+ this.#index(root, location);
187
+ const own = this.#locations.get(root) ?? location;
188
+ return { schema: root, location: own };
189
+ }
190
+ #newResource(uri, root, inherited) {
191
+ const dialect = root.$schema;
192
+ let draft = inherited;
193
+ let vocab = VOCAB_ALL;
194
+ if (typeof dialect === "string") {
195
+ const resolved = this.#dialect(stripFragment(parseUri(dialect) ?? dialect), 0);
196
+ if (resolved !== undefined) {
197
+ draft = resolved.draft ?? inherited;
198
+ vocab = resolved.vocab;
199
+ }
200
+ }
201
+ const resource = { uri, root, draft, vocab, dynamicAnchors: new Map() };
202
+ if (!this.#resources.has(uri))
203
+ this.#resources.set(uri, resource);
204
+ return resource;
205
+ }
206
+ /** Draft and vocabularies a `$schema` URI selects; unknown dialects inherit. */
207
+ #dialect(uri, depth) {
208
+ if (/^https?:\/\/json-schema\.org\/draft-0[3-7]\/schema$/.test(uri))
209
+ return { draft: "7", vocab: VOCAB_ALL };
210
+ if (/^https?:\/\/json-schema\.org\/draft\/2020-12\/schema$/.test(uri))
211
+ return { draft: "2020-12", vocab: VOCAB_ALL };
212
+ if (/^https?:\/\/json-schema\.org\/draft\/2019-09\/schema$/.test(uri))
213
+ return { draft: "2019-09", vocab: VOCAB_ALL };
214
+ if (depth > 8)
215
+ return undefined;
216
+ const meta = this.#resources.get(uri)?.root ?? this.#pending.get(uri);
217
+ if (typeof meta !== "object" || meta === null)
218
+ return undefined;
219
+ const metaSchema = meta;
220
+ const parent = typeof metaSchema.$schema === "string" && stripFragment(metaSchema.$schema) !== uri
221
+ ? this.#dialect(stripFragment(parseUri(metaSchema.$schema) ?? metaSchema.$schema), depth + 1)
222
+ : undefined;
223
+ const vocabularies = metaSchema.$vocabulary;
224
+ if (typeof vocabularies !== "object" || vocabularies === null)
225
+ return parent;
226
+ let vocab = 0;
227
+ for (const vocabulary in vocabularies) {
228
+ const name = vocabulary.slice(vocabulary.lastIndexOf("/") + 1);
229
+ vocab |= VOCABULARIES[name] ?? 0;
230
+ // Draft 2019-09 kept the unevaluated keywords in its applicator vocabulary.
231
+ if (name === "applicator" && vocabulary.includes("/2019-09/"))
232
+ vocab |= VOCAB_UNEVALUATED;
233
+ }
234
+ return { draft: parent?.draft ?? "2020-12", vocab };
235
+ }
236
+ #index(node, parent) {
237
+ if (typeof node !== "object" || node === null || Array.isArray(node))
238
+ return;
239
+ if (this.#locations.has(node))
240
+ return;
241
+ const schema = node;
242
+ let location = parent;
243
+ const draft7 = parent.resource.draft === "7";
244
+ const id = schema.$id;
245
+ let idAnchor;
246
+ if (typeof id === "string" && !(draft7 && typeof schema.$ref === "string")) {
247
+ if (id.charCodeAt(0) === 35) {
248
+ // Draft 7 location-independent identifier: `"$id": "#name"`.
249
+ idAnchor = id.slice(1);
250
+ }
251
+ else {
252
+ const absolute = parseUri(id, parent.base);
253
+ if (absolute !== undefined) {
254
+ const hash = absolute.indexOf("#");
255
+ const uri = hash < 0 ? absolute : absolute.slice(0, hash);
256
+ if (hash >= 0 && hash < absolute.length - 1)
257
+ idAnchor = absolute.slice(hash + 1);
258
+ const resource = node === parent.resource.root && parent.base === parent.resource.uri
259
+ ? this.#adoptRoot(parent.resource, uri)
260
+ : this.#newResource(uri, schema, parent.resource.draft);
261
+ location = { base: uri, resource };
262
+ }
263
+ }
264
+ }
265
+ this.#locations.set(schema, location);
266
+ const anchor = schema.$anchor;
267
+ const dynamicAnchor = schema.$dynamicAnchor;
268
+ if (idAnchor !== undefined || typeof anchor === "string" || typeof dynamicAnchor === "string") {
269
+ const target = { schema, location };
270
+ if (idAnchor !== undefined)
271
+ this.#anchors.set(`${location.base}#${idAnchor}`, target);
272
+ if (typeof anchor === "string")
273
+ this.#anchors.set(`${location.base}#${anchor}`, target);
274
+ if (typeof dynamicAnchor === "string") {
275
+ this.#anchors.set(`${location.base}#${dynamicAnchor}`, target);
276
+ location.resource.dynamicAnchors.set(dynamicAnchor, schema);
277
+ }
278
+ }
279
+ for (const keyword in schema) {
280
+ const kind = SUBSCHEMA_KEYWORDS.get(keyword);
281
+ if (kind === undefined)
282
+ continue;
283
+ const value = schema[keyword];
284
+ if (kind === SCHEMAS) {
285
+ if (Array.isArray(value))
286
+ for (const item of value)
287
+ this.#index(item, location);
288
+ else
289
+ this.#index(value, location);
290
+ }
291
+ else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
292
+ for (const key in value)
293
+ this.#index(value[key], location);
294
+ }
295
+ }
296
+ }
297
+ /** The document root's own `$id` names the same resource as its retrieval URI. */
298
+ #adoptRoot(resource, uri) {
299
+ if (uri === resource.uri)
300
+ return resource;
301
+ const adopted = { ...resource, uri };
302
+ if (!this.#resources.has(uri))
303
+ this.#resources.set(uri, adopted);
304
+ // Retrieval URI keeps resolving to the root, now evaluated under its `$id`.
305
+ this.#resources.set(resource.uri, adopted);
306
+ return adopted;
307
+ }
308
+ }
309
+ /** Absolute form of `reference` resolved against `base`, or `undefined` when unparsable. */
310
+ function parseUri(reference, base) {
311
+ try {
312
+ return new URL(reference, base).href;
313
+ }
314
+ catch {
315
+ return undefined;
316
+ }
317
+ }
318
+ function stripFragment(uri) {
319
+ const hash = uri.indexOf("#");
320
+ return hash < 0 ? uri : uri.slice(0, hash);
321
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Runtime helpers shared by the closure validators (`closures.ts`) and the
3
+ * generated ones (`codegen.ts`): JSON value equality, `uniqueItems`,
4
+ * `multipleOf`, code-point string length, and the pattern cache.
5
+ */
6
+ const hasOwn = Object.hasOwn;
7
+ /** JSON equality: numbers by value, arrays by order, objects by key set regardless of order. */
8
+ export function jsonEqual(a, b) {
9
+ if (a === b)
10
+ return true;
11
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
12
+ return false;
13
+ const array = Array.isArray(a);
14
+ if (array !== Array.isArray(b))
15
+ return false;
16
+ if (array) {
17
+ const left = a;
18
+ const right = b;
19
+ if (left.length !== right.length)
20
+ return false;
21
+ for (let index = 0; index < left.length; index++)
22
+ if (!jsonEqual(left[index], right[index]))
23
+ return false;
24
+ return true;
25
+ }
26
+ const left = a;
27
+ const right = b;
28
+ let count = 0;
29
+ for (const key in left) {
30
+ if (!hasOwn(left, key))
31
+ continue;
32
+ if (!hasOwn(right, key) || !jsonEqual(left[key], right[key]))
33
+ return false;
34
+ count++;
35
+ }
36
+ for (const key in right)
37
+ if (hasOwn(right, key))
38
+ count--;
39
+ return count === 0;
40
+ }
41
+ /** True when `values` contains `value` under {@link jsonEqual}. */
42
+ export function jsonIncludes(values, value) {
43
+ for (const candidate of values)
44
+ if (jsonEqual(candidate, value))
45
+ return true;
46
+ return false;
47
+ }
48
+ /** `uniqueItems`: no two elements are {@link jsonEqual}. */
49
+ export function uniqueItems(items) {
50
+ const n = items.length;
51
+ if (n < 2)
52
+ return true;
53
+ let primitive = true;
54
+ for (let index = 0; index < n; index++) {
55
+ const item = items[index];
56
+ if (typeof item === "object" && item !== null) {
57
+ primitive = false;
58
+ break;
59
+ }
60
+ }
61
+ // SameValueZero matches JSON equality for primitives (0 equals -0).
62
+ if (primitive && n > 16)
63
+ return new Set(items).size === n;
64
+ for (let i = 1; i < n; i++) {
65
+ const item = items[i];
66
+ for (let j = 0; j < i; j++) {
67
+ if (primitive ? item === items[j] : jsonEqual(item, items[j]))
68
+ return false;
69
+ }
70
+ }
71
+ return true;
72
+ }
73
+ /**
74
+ * `multipleOf` with float tolerance: `0.0075` is a multiple of `0.0001` even
75
+ * though the IEEE quotient is `74.99999999999999`. An overflowing quotient is
76
+ * a multiple only when `divisor` divides 1 (every integer is a multiple of 0.5).
77
+ */
78
+ export function isMultipleOf(value, divisor) {
79
+ const quotient = value / divisor;
80
+ if (Number.isInteger(quotient))
81
+ return true;
82
+ if (!Number.isFinite(quotient))
83
+ return Number.isInteger(value) && Number.isInteger(1 / divisor);
84
+ return Math.abs(quotient - Math.round(quotient)) <= Number.EPSILON * Math.abs(quotient);
85
+ }
86
+ /** String length in Unicode code points, as `minLength`/`maxLength` count it. */
87
+ export function codePointLength(value) {
88
+ let length = value.length;
89
+ for (let index = 0; index < value.length - 1; index++) {
90
+ const high = value.charCodeAt(index);
91
+ if (high < 0xd800 || high > 0xdbff)
92
+ continue;
93
+ const low = value.charCodeAt(index + 1);
94
+ if (low >= 0xdc00 && low <= 0xdfff) {
95
+ length--;
96
+ index++;
97
+ }
98
+ }
99
+ return length;
100
+ }
101
+ const patterns = new Map();
102
+ const PATTERN_CACHE_LIMIT = 1024;
103
+ /**
104
+ * Compile a JSON Schema `pattern` as an ECMAScript regex with the `u` flag,
105
+ * falling back to legacy syntax for patterns that only parse without it.
106
+ * Regexes are stateless (no `g`/`y`), so equal sources share one instance.
107
+ *
108
+ * # Errors
109
+ * Throws `SyntaxError` when the source is not a regular expression at all.
110
+ */
111
+ export function compilePattern(source) {
112
+ let regex = patterns.get(source);
113
+ if (regex !== undefined)
114
+ return regex;
115
+ try {
116
+ regex = new RegExp(source, "u");
117
+ }
118
+ catch {
119
+ regex = new RegExp(source);
120
+ }
121
+ if (patterns.size >= PATTERN_CACHE_LIMIT)
122
+ patterns.clear();
123
+ patterns.set(source, regex);
124
+ return regex;
125
+ }
126
+ /** Property names inherited by plain objects; reading them needs an own-property check. */
127
+ export function isInheritedName(key) {
128
+ return key in Object.prototype;
129
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * JSON Schema validator engine behind `fromJsonSchema`: plans the document,
3
+ * interprets the plan for the first calls, and swaps in generated code once a
4
+ * schema proves hot. Where code generation is forbidden (strict CSP) the
5
+ * interpreter keeps serving every call.
6
+ */
7
+ import { generate } from "./codegen.js";
8
+ import { Sink, validate } from "./interpret.js";
9
+ import { Planner } from "./plan.js";
10
+ import { Registry } from "./resolve.js";
11
+ /** Calls interpreted before the JIT generates code; generation costs about as much as a hundred interpreted checks. */
12
+ const JIT_THRESHOLD = 64;
13
+ /** A compiled JSON Schema. */
14
+ export class Validator {
15
+ /** Allocation-free validity test; safe to detach (`const check = validator.check`). */
16
+ check;
17
+ /** Whether some `properties` subschema declares a `default` for {@link fillDefaults} to apply. */
18
+ hasDefaults;
19
+ #root;
20
+ /**
21
+ * `onCompile` receives the steady-state check once the schema turns hot
22
+ * (generated code, or the interpreter where code generation is forbidden), so
23
+ * callers holding their own reference can drop the dispatch through `check`.
24
+ *
25
+ * # Errors
26
+ * Throws `OmpTypeError` for unresolvable or malformed `$ref`s, invalid
27
+ * patterns, and schema nodes that are neither booleans nor objects.
28
+ */
29
+ constructor(schema, options = {}, onCompile) {
30
+ const registry = new Registry(options.draft ?? "2020-12", options.schemas);
31
+ const planner = new Planner(registry, options.formats);
32
+ const root = planner.root(schema);
33
+ this.hasDefaults = planner.sawDefaults;
34
+ this.#root = root;
35
+ let calls = 0;
36
+ let impl = (value) => {
37
+ if (++calls < JIT_THRESHOLD)
38
+ return validate(root, value);
39
+ impl = generate(root) ?? ((input) => validate(root, input));
40
+ onCompile?.(impl);
41
+ return impl(value);
42
+ };
43
+ this.check = value => impl(value);
44
+ }
45
+ /** Every issue that makes `value` invalid (empty when it is valid). */
46
+ explain(value) {
47
+ const sink = new Sink();
48
+ validate(this.#root, value, sink);
49
+ return sink.issues;
50
+ }
51
+ /** `value` with absent declared properties set to their `default`, copied on write. */
52
+ fillDefaults(value) {
53
+ return fill(this.#root, value);
54
+ }
55
+ }
56
+ /** Defaults descend through in-place `$ref`/`allOf`, declared properties, and items. */
57
+ function fill(plan, value) {
58
+ let out = value;
59
+ if (plan.refs !== undefined)
60
+ for (const ref of plan.refs)
61
+ out = fill(ref, out);
62
+ if (plan.allOf !== undefined)
63
+ for (const branch of plan.allOf)
64
+ out = fill(branch, out);
65
+ if (typeof out !== "object" || out === null)
66
+ return out;
67
+ if (Array.isArray(out)) {
68
+ const prefix = plan.prefixItems ?? [];
69
+ if (prefix.length === 0 && plan.items === undefined)
70
+ return out;
71
+ let copy;
72
+ for (let index = 0; index < out.length; index++) {
73
+ const itemPlan = index < prefix.length ? prefix[index] : plan.items;
74
+ if (itemPlan === undefined)
75
+ continue;
76
+ const item = fill(itemPlan, out[index]);
77
+ if (item === out[index])
78
+ continue;
79
+ copy ??= [...out];
80
+ copy[index] = item;
81
+ }
82
+ return copy ?? out;
83
+ }
84
+ if (plan.properties === undefined)
85
+ return out;
86
+ const source = out;
87
+ let copy;
88
+ for (const { key, plan: propertyPlan } of plan.properties) {
89
+ if (!Object.hasOwn(source, key) || source[key] === undefined)
90
+ continue;
91
+ const filled = fill(propertyPlan, source[key]);
92
+ if (filled === source[key])
93
+ continue;
94
+ copy ??= { ...source };
95
+ copy[key] = filled;
96
+ }
97
+ if (plan.defaults !== undefined) {
98
+ for (const { key, value: fallback } of plan.defaults) {
99
+ if (Object.hasOwn(source, key) && source[key] !== undefined)
100
+ continue;
101
+ copy ??= { ...source };
102
+ copy[key] = typeof fallback === "object" && fallback !== null ? structuredClone(fallback) : fallback;
103
+ }
104
+ }
105
+ return copy ?? out;
106
+ }
@@ -283,7 +283,7 @@ function tRecord(key, value, opts) {
283
283
  return ctx.mustBe("an object with valid record keys");
284
284
  return true;
285
285
  });
286
- return applyMeta(base, opts);
286
+ return asSchema(applyMeta(base, opts));
287
287
  }
288
288
  function tOptional(schema, opts) {
289
289
  const marker = applyMeta(asRuntime(schema).or(asRuntime(type.raw("undefined"))), opts);
@@ -304,7 +304,7 @@ function tPartial(schema) {
304
304
  const props = {};
305
305
  for (const key in info.props)
306
306
  props[key] = asRuntime(info.props[key])[OPTIONAL_INNER] ? info.props[key] : tOptional(info.props[key]);
307
- return tObject(props, { additionalProperties: info.additionalProperties });
307
+ return asSchema(tObject(props, { additionalProperties: info.additionalProperties }));
308
308
  }
309
309
  function tRequired(schema) {
310
310
  const info = requireObject(schema, "Required");
@@ -312,7 +312,7 @@ function tRequired(schema) {
312
312
  for (const key in info.props) {
313
313
  props[key] = asRuntime(info.props[key])[OPTIONAL_INNER] ?? info.props[key];
314
314
  }
315
- return tObject(props, { additionalProperties: info.additionalProperties });
315
+ return asSchema(tObject(props, { additionalProperties: info.additionalProperties }));
316
316
  }
317
317
  function tPick(schema, keys) {
318
318
  const info = requireObject(schema, "Pick");
@@ -320,7 +320,7 @@ function tPick(schema, keys) {
320
320
  for (const key of keys)
321
321
  if (typeof key === "string" && info.props[key])
322
322
  props[key] = info.props[key];
323
- return tObject(props, { additionalProperties: info.additionalProperties });
323
+ return asSchema(tObject(props, { additionalProperties: info.additionalProperties }));
324
324
  }
325
325
  function tOmit(schema, keys) {
326
326
  const info = requireObject(schema, "Omit");
@@ -329,7 +329,7 @@ function tOmit(schema, keys) {
329
329
  for (const key in info.props)
330
330
  if (!omitted.has(key))
331
331
  props[key] = info.props[key];
332
- return tObject(props, { additionalProperties: info.additionalProperties });
332
+ return asSchema(tObject(props, { additionalProperties: info.additionalProperties }));
333
333
  }
334
334
  function tComposite(schemas, opts) {
335
335
  const props = {};
@@ -1,9 +1,14 @@
1
+ import { type ValidatorOptions } from "./jsonschema/validator.js";
1
2
  import { type BaseType } from "./type.js";
3
+ /** Options for {@link fromJsonSchema}. */
4
+ export type FromJsonSchemaOptions = ValidatorOptions;
2
5
  /**
3
6
  * Build a callable omptype schema from a JSON Schema document.
4
7
  *
5
8
  * # Errors
6
- * Throws {@link OmpTypeError} on malformed nodes, unresolvable `$ref`s, or
7
- * types omptype cannot represent.
9
+ * Throws `OmpTypeError` when a `$ref` is malformed or names a document or
10
+ * location that neither `schema` nor `options.schemas` contains, when a
11
+ * `pattern` is not a regular expression, or when a schema node is neither a
12
+ * boolean nor an object.
8
13
  */
9
- export declare function fromJsonSchema(schema: unknown): BaseType;
14
+ export declare function fromJsonSchema(schema: unknown, options?: FromJsonSchemaOptions): BaseType;
@@ -158,6 +158,8 @@ export type IR = IRAnalysis & ({
158
158
  k: "refine";
159
159
  base: IR;
160
160
  pred: (value: unknown) => boolean | OmpErrors;
161
+ /** Allocation-free equivalent of `pred(value) === true` for boolean-only paths (`allows`, unions). */
162
+ check?: (value: unknown) => boolean;
161
163
  expected: string;
162
164
  json?: Record<string, unknown>;
163
165
  desc?: string;
@@ -0,0 +1,6 @@
1
+ import { type Plan } from "./plan.js";
2
+ /**
3
+ * Generate a validator for `root`, or `undefined` when the runtime forbids
4
+ * code generation from strings.
5
+ */
6
+ export declare function generate(root: Plan): ((value: unknown) => value is unknown) | undefined;
@@ -0,0 +1,48 @@
1
+ import { type Plan } from "./plan.js";
2
+ /** One failure found while explaining an invalid value. */
3
+ export interface Issue {
4
+ readonly path: PropertyKey[];
5
+ readonly expected: string;
6
+ readonly data: unknown;
7
+ }
8
+ /** Issue collector for explain mode. */
9
+ export declare class Sink {
10
+ readonly issues: Issue[];
11
+ add(expected: string, data: unknown, key?: PropertyKey): void;
12
+ /** Prefix the paths of issues recorded since `from` with `key`. */
13
+ prefix(from: number, key: PropertyKey): void;
14
+ }
15
+ /** Locations of the current instance evaluated so far by successful subschemas. */
16
+ export interface Ann {
17
+ /** Evaluated property names, or `true` once every property is evaluated. */
18
+ props: Set<string> | true | undefined;
19
+ /** Every index below this is evaluated (`Infinity` for all). */
20
+ items: number;
21
+ /** Indices evaluated by `contains`. */
22
+ contained: Set<number> | undefined;
23
+ }
24
+ /** Human-readable expectation for a `T_*` mask. */
25
+ export declare function describeTypes(types: number): string;
26
+ /**
27
+ * Object property whose value selects the only `anyOf`/`oneOf` branch that can
28
+ * pass: every branch is an object schema requiring the property with distinct
29
+ * primitive `const`/`enum` values.
30
+ */
31
+ export interface Discriminator {
32
+ readonly key: string;
33
+ /** Tag value → branch index. */
34
+ readonly branches: Map<unknown, number>;
35
+ }
36
+ /** Follow plans that are a bare single `$ref`. */
37
+ export declare function unwrapRef(plan: Plan): Plan;
38
+ /** True when the plan's only constraint is its `refs`. */
39
+ export declare function isRefOnly(plan: Plan): boolean;
40
+ /** Find a {@link Discriminator} for union branches, if one exists. */
41
+ export declare function findDiscriminator(branches: readonly Plan[]): Discriminator | undefined;
42
+ /**
43
+ * Whether `value` satisfies `plan`.
44
+ *
45
+ * Infinite `$ref` cycles that never descend into the value (`{"$ref": "#"}`)
46
+ * overflow the stack, as they would in any evaluator.
47
+ */
48
+ export declare function validate(plan: Plan, value: unknown, sink?: Sink, ann?: Ann): boolean;