@texaryn/schema-json 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +28 -0
  3. package/THIRD-PARTY-NOTICES.md +7 -0
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +464 -13
  6. package/dist/adapter.js.map +1 -1
  7. package/dist/bare-maps.js +1 -1
  8. package/dist/identity.d.ts +11 -2
  9. package/dist/identity.d.ts.map +1 -1
  10. package/dist/identity.js +103 -22
  11. package/dist/identity.js.map +1 -1
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/local-pointer-aliases.d.ts +4 -0
  17. package/dist/local-pointer-aliases.d.ts.map +1 -0
  18. package/dist/local-pointer-aliases.js +345 -0
  19. package/dist/local-pointer-aliases.js.map +1 -0
  20. package/dist/normalize.d.ts +1 -1
  21. package/dist/normalize.d.ts.map +1 -1
  22. package/dist/normalize.js +3 -1
  23. package/dist/normalize.js.map +1 -1
  24. package/dist/projection-validation-coherence.d.ts +11 -0
  25. package/dist/projection-validation-coherence.d.ts.map +1 -0
  26. package/dist/projection-validation-coherence.js +213 -0
  27. package/dist/projection-validation-coherence.js.map +1 -0
  28. package/dist/projection.d.ts +2 -2
  29. package/dist/projection.d.ts.map +1 -1
  30. package/dist/projection.js +917 -152
  31. package/dist/projection.js.map +1 -1
  32. package/dist/resources.d.ts +19 -0
  33. package/dist/resources.d.ts.map +1 -0
  34. package/dist/resources.js +730 -0
  35. package/dist/resources.js.map +1 -0
  36. package/dist/root-reference.js +1 -1
  37. package/dist/schema-graph.d.ts +7 -0
  38. package/dist/schema-graph.d.ts.map +1 -1
  39. package/dist/schema-graph.js +79 -20
  40. package/dist/schema-graph.js.map +1 -1
  41. package/dist/submission-projection.d.ts +5 -0
  42. package/dist/submission-projection.d.ts.map +1 -0
  43. package/dist/submission-projection.js +233 -0
  44. package/dist/submission-projection.js.map +1 -0
  45. package/dist/types.d.ts +6 -1
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/vendor/json-schema-library/LICENSE.md +9 -0
  48. package/dist/vendor/json-schema-library/index.mjs +2 -0
  49. package/dist/vendor/json-schema-library/index.mjs.map +1 -0
  50. package/package.json +8 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @texaryn/schema-json
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8605d64: Adds opt-in projected submissions and rejects reference scope collisions that give validation and projection different assertions. The runtime validates and submits the same independent snapshot while retaining live form data.
8
+ - 82f38e9: Add explicit view expansion for recursion and budget boundaries.
9
+ - 8605d64: Adds host supplied external schema resolution with a configurable resource limit.
10
+ - 82f38e9: Adds opt in dynamic scope projection for supported local references and host supplied external resources.
11
+
12
+ ### Patch Changes
13
+
14
+ - 1adb31d: Patches dependent schema reduction and includes the fixed runtime in published adapter builds, so invalid `oneOf` dependencies remain projectable for installed consumers.
15
+ - 8605d64: Corrects projection through `allOf` references and applies dialect-specific defaults beside `$ref`.
16
+ - 8605d64: Preserves inherited dialects and Draft 7 identifiers when compiling retrieved schemas.
17
+ - 6f6bd70: Projects fields from retained inactive conditional branches and keeps them aligned with Hyperjump.
18
+ - 8605d64: Projects fields from the selected Draft 7 `then` or `else` reference.
19
+ - 82f38e9: Reuse static property candidates across data projections and skip reductions that cannot change an object's projected shape.
20
+ - 82f38e9: Avoid repeated branch validation for missing values during a single schema projection.
21
+ - 8605d64: Projects existing and required fields governed by `additionalProperties`.
22
+ - Updated dependencies [8605d64]
23
+ - Updated dependencies [82f38e9]
24
+ - Updated dependencies [8605d64]
25
+ - @texaryn/core@0.14.0
26
+
3
27
  ## 0.7.1
4
28
 
5
29
  ### Patch Changes
package/README.md CHANGED
@@ -85,6 +85,7 @@ The adapter currently handles the schema features required by Texaryn's runtime
85
85
  - arrays
86
86
  - enums
87
87
  - local `$ref`, including recursive references
88
+ - opt-in dynamic scope projection for supported `$dynamicRef` and `$recursiveRef` paths
88
89
  - `if` / `then` / `else`
89
90
  - `oneOf`
90
91
  - `anyOf`
@@ -101,10 +102,29 @@ A local `$ref` may point back to a schema that contains it, such as `{ propertie
101
102
 
102
103
  `boundaries` on an object's `NodeProjection` says why the projection withheld something beneath it: `recursion` when a descendant would repeat the object's schemas, `budget` when a fixed per-projection limit withheld members. The limits are 16 objects and 512 nodes, counted only over locations that exist because of the recursion, two or more levels below the data; they never cut a member of a location that holds data. A node the projection reached only by expanding the recursion carries `recursiveExpansion`, and `schema-defaults` initialization never writes there.
103
104
 
105
+ `boundaryTargets` has the next withheld instance pointer for each boundary reason, with a generation scoped token. Pass selected tokens to `project(data, { expandedBoundaryTokens })` to reveal those locations. The runtime also supplies `boundaryGeneration`; direct adapter callers can leave it at zero or increment it whenever replacing a container changes pointer ownership. Each token admits one target. The built in renderers show one action per boundary reason on an object and advance one target per click, so a large budget boundary does not create a button for every withheld field. Expansion changes the view only. It does not write form data, affect validation, or change initialization. React, React Bootstrap, React MUI, Vue, and Web Components provide these actions by default.
106
+
104
107
  A schema that applies itself at one instance location without crossing into a property or item, such as `{ allOf: [{ $ref: '#' }] }`, makes the evaluator recurse without end, so `createJsonSchemaAdapter` rejects it with `SameLocationCycleError`. Its `positions` field lists the schema positions of each cycle, and the message names one of them and the path through the cycle. The check reads the schema, not the data: a cycle behind an `if` is rejected even while the `if` does not hold, except the branches the specification never evaluates. It is conservative for dynamic references, treating a `$dynamicRef` to a `$dynamicAnchor` as reaching every `$dynamicAnchor` of that name and a `$recursiveRef` as reaching every schema that declares `$recursiveAnchor: true`, so a schema whose dynamic references could close such a cycle is rejected even when no evaluation selects it.
105
108
 
106
109
  The [JSON Schema support guide](https://texaryn.github.io/texaryn/guides/json-schema-support/#recursive-references) has the details, and ADR-007 records the decision.
107
110
 
111
+ ## Dynamic reference projection
112
+
113
+ Dynamic scope aware form projection is opt in with `dynamicReferenceProjection: 'local'`. It supports Draft 2020-12 `$dynamicRef` and Draft 2019-09 `$recursiveRef: '#'` under object properties or homogeneous array items, including references reached through `allOf`, `anyOf`, `oneOf`, conditionals and `dependentSchemas`.
114
+
115
+ External dynamic references use the host supplied `resolveResource` callback. The adapter does not fetch schemas. Without the option, the projector does not follow the active dynamic scope. The option rejects references below unsupported applicators and array keywords, root relative resource identifiers, and unsupported reference siblings. See [ADR-010](../../docs/adr/010-local-dynamic-reference-projection.md) for the exact boundaries.
116
+
117
+ ```ts
118
+ const adapter = await createJsonSchemaAdapter(schema, {
119
+ dynamicReferenceProjection: 'local',
120
+ resolveResource: (uri) => schemaResources.get(uri),
121
+ })
122
+ ```
123
+
124
+ Adapter creation also rejects a retained static `$ref` when json-schema-library resolves it to different declarations with different validation assertions in the validation tree and the normalised projection tree. The check covers the dialect's supported assertions, including boolean subschemas. The error is `ProjectionValidationDivergenceError`.
125
+
126
+ The adapter exposes `projectSubmission(data)` for supported schemas. It removes values declared only by inactive branches, keeps provisional fields, and preserves undeclared data so validation can still reject it. This opt-in treats an inactive declaration as form data to omit even when an enclosing object allows additional properties. `createFormRuntime` selects the mode with `submission: 'projected'`; it validates and submits the same independent snapshot, while the form's live data stays unchanged. Schemas with dynamic references or `unevaluatedProperties` or `unevaluatedItems` do not expose this capability yet. The private Hyperjump adapter also does not expose it. Runtime creation fails if projected submission is requested without the capability.
127
+
108
128
  ## json-schema-library version
109
129
 
110
130
  The dependency is pinned to exactly `11.6.2`. In Draft 7, json-schema-library 11.6.2 overwrites the registry entry that `"$ref": "#"` resolves through, so without a fix the reference reaches another node and validation is wrong. The adapter pins that entry to the document root, and every other entry the library files for its own location to the node compiled there, so a reduction during projection cannot replace a definition with a reduced copy. Because the fix depends on the library's internal registry, adapter creation throws an error naming json-schema-library 11.6.2 when the registry is shaped differently, or when a self-test on two probe schemas shows the fix no longer repairs validation. A newer json-schema-library release is not installed until a Texaryn release raises the pin after the registry fix and its self-test pass against it.
@@ -153,6 +173,7 @@ The result contains validation errors at that pointer or below it.
153
173
  ```ts
154
174
  import {
155
175
  createJsonSchemaAdapter,
176
+ ProjectionValidationDivergenceError,
156
177
  SameLocationCycleError,
157
178
  type AdapterConfig,
158
179
  type Dialect,
@@ -174,6 +195,9 @@ const adapter = await createJsonSchemaAdapter(schema, {
174
195
  ```ts
175
196
  interface AdapterConfig {
176
197
  defaultDialect?: Dialect
198
+ dynamicReferenceProjection?: 'local'
199
+ resolveResource?: (uri: string) => unknown | undefined | Promise<unknown | undefined>
200
+ maxExternalResources?: number
177
201
  }
178
202
  ```
179
203
 
@@ -198,6 +222,10 @@ try {
198
222
  }
199
223
  ```
200
224
 
225
+ ### `ProjectionValidationDivergenceError`
226
+
227
+ Thrown by `createJsonSchemaAdapter` when one retained static `$ref` resolves to different declarations with different validation assertions for validation and projection. Its `reference` and `sourcePosition` identify the reference site. `validationPosition` and `projectionPosition` identify the two resolved schema positions.
228
+
201
229
  ## Architecture
202
230
 
203
231
  The adapter is deliberately separate from `@texaryn/core`:
@@ -1,5 +1,12 @@
1
1
  # Third-party notices
2
2
 
3
+ ## json-schema-library
4
+
5
+ The ESM runtime at `dist/vendor/json-schema-library/` is adapted from
6
+ `json-schema-library` 11.6.2 by Sascha Goldhofer and is distributed under the
7
+ MIT License. Its full license is included at
8
+ `dist/vendor/json-schema-library/LICENSE.md`.
9
+
3
10
  ## JSON Schema metaschemas
4
11
 
5
12
  `src/metaschemas/` contains the published JSON Schema metaschema documents for
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAmC,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAMxF,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAElE,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,OAAO,EACf,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,iBAAiB,CAAC,CAE5B;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,iBAAiB,CAAC,CA8B5B"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAmC,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAQxF,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAElE,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,OAAO,EACf,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,iBAAiB,CAAC,CAE5B;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,iBAAiB,CAAC,CAwF5B"}
package/dist/adapter.js CHANGED
@@ -1,12 +1,16 @@
1
- import { compileSchema, } from 'json-schema-library';
1
+ import { compileSchema, isSchemaNode, } from './vendor/json-schema-library/index.mjs';
2
2
  import { detectDialect } from './dialect.js';
3
3
  import { loadMetaschemas, referencedDialects } from './metaschemas/index.js';
4
+ import { loadExternalResources } from './resources.js';
5
+ import { materializeLocalPointerAliases } from './local-pointer-aliases.js';
4
6
  import { newProjectionCache } from './identity.js';
5
7
  import { buildProjection, DEFAULT_LIMITS } from './projection.js';
8
+ import { assertProjectionValidationCoherence } from './projection-validation-coherence.js';
9
+ import { projectSubmissionData, supportsSubmissionProjection } from './submission-projection.js';
6
10
  import { withoutUnreachableBranches } from './normalize.js';
7
11
  import { DRAFTS } from './bare-maps.js';
8
12
  import { fixRootReference } from './root-reference.js';
9
- import { buildSchemaGraph, rejectSameLocationCycles, cyclicPositions, markPositions } from './schema-graph.js';
13
+ import { buildSchemaGraph, rejectSameLocationCycles, cyclicPositions, markPositions, POSITION } from './schema-graph.js';
10
14
  import { pointerResolver } from './instance-pointer.js';
11
15
  export async function createJsonSchemaAdapter(schema, config) {
12
16
  return createAdapter(schema, config, DEFAULT_LIMITS);
@@ -15,21 +19,68 @@ export async function createAdapter(schema, config, limits) {
15
19
  const dialect = detectDialect(schema, {
16
20
  defaultDialect: config?.defaultDialect ?? 'draft-07',
17
21
  });
22
+ if (config?.dynamicReferenceProjection === 'local' && dialect === 'draft-07') {
23
+ throw new TypeError('Local dynamic reference projection requires Draft 2019-09 or Draft 2020-12.');
24
+ }
18
25
  // json-schema-library carries no metaschema documents, so a schema referencing its own fails closed.
19
- const referenced = referencedDialects(schema);
20
- const remotes = referenced.length > 0 ? await loadMetaschemas(referenced) : undefined;
26
+ const externalRemotes = await loadExternalResources(schema, dialect, config);
27
+ const referenced = referencedDialects([schema, ...externalRemotes]);
28
+ const metaschemas = referenced.length > 0 ? await loadMetaschemas(referenced) : [];
29
+ const remotes = [...metaschemas, ...externalRemotes];
30
+ const graph = buildSchemaGraph(schema, dialect, remotes);
31
+ const referencesByDocument = new Map();
32
+ for (const target of graph.references.values()) {
33
+ const hash = target.indexOf('#');
34
+ if (hash < 0)
35
+ continue;
36
+ const prefix = target.slice(0, hash);
37
+ const pointers = referencesByDocument.get(prefix) ?? [];
38
+ pointers.push(target.slice(hash));
39
+ referencesByDocument.set(prefix, pointers);
40
+ }
21
41
  // json-schema-library evaluates some branches the specification never does, so only the projection drops them.
22
- const document = withoutUnreachableBranches(schema);
23
- const graph = buildSchemaGraph(document, dialect, remotes ?? []);
42
+ const document = withoutUnreachableBranches(schema, referencesByDocument.get('') ?? []);
43
+ const projectionRemotes = [
44
+ ...metaschemas,
45
+ ...externalRemotes.map((remote) => {
46
+ const id = remote.$id;
47
+ const base = typeof id === 'string' ? id.split('#', 1)[0] : '';
48
+ return withoutUnreachableBranches(remote, referencesByDocument.get(base) ?? []);
49
+ }),
50
+ ];
24
51
  rejectSameLocationCycles(graph);
25
- const marked = markPositions(graph, document, remotes ?? []);
52
+ const marked = markPositions(graph, document, projectionRemotes);
26
53
  const validated = await prepareSchema(schema, dialect, remotes);
27
- const projected = await prepareSchema(marked.document, dialect, remotes && marked.remotes);
28
- const cache = newProjectionCache(dialect, cyclicPositions(graph), marked.at);
54
+ const projected = await prepareSchema(marked.document, dialect, marked.remotes);
55
+ const dynamicReferenceProjection = config?.dynamicReferenceProjection === 'local';
56
+ if (dynamicReferenceProjection) {
57
+ const externalResourceIds = new Set(externalRemotes.flatMap((remote) => {
58
+ const id = remote.$id;
59
+ return typeof id === 'string' ? [id.split('#', 1)[0]] : [];
60
+ }));
61
+ enableLocalDynamicReferenceScopes(validated, dialect, externalResourceIds);
62
+ enableLocalDynamicReferenceScopes(projected, dialect, externalResourceIds);
63
+ }
64
+ assertProjectionValidationCoherence(validated, projected, dialect);
65
+ const submissionSchema = supportsSubmissionProjection([schema, ...remotes], dialect)
66
+ ? await prepareSchema(marked.document, dialect, marked.remotes)
67
+ : undefined;
68
+ if (dynamicReferenceProjection && submissionSchema) {
69
+ const externalResourceIds = new Set(externalRemotes.flatMap((remote) => {
70
+ const id = remote.$id;
71
+ return typeof id === 'string' ? [id.split('#', 1)[0]] : [];
72
+ }));
73
+ enableLocalDynamicReferenceScopes(submissionSchema, dialect, externalResourceIds);
74
+ }
75
+ // A schema node can validate differently under different dynamic anchor scopes.
76
+ const cache = newProjectionCache(dialect, cyclicPositions(graph), marked.at, dynamicReferenceProjection, !dynamicReferenceProjection && !hasOneOf([marked.document, marked.remotes]), graph.retainedPositions);
29
77
  return {
30
- project(data) {
31
- return buildProjection(projected, data, cache, limits);
78
+ project(data, options) {
79
+ return buildProjection(projected, data, cache, limits, options);
32
80
  },
81
+ ...(submissionSchema
82
+ ? { projectSubmission: (data) => projectSubmissionData(validated, submissionSchema, data, dialect) }
83
+ : {}),
33
84
  validate(data) {
34
85
  return runValidation(validated, data);
35
86
  },
@@ -38,6 +89,386 @@ export async function createAdapter(schema, config, limits) {
38
89
  },
39
90
  };
40
91
  }
92
+ const DYNAMIC_REFERENCE_ANNOTATIONS = new Set([
93
+ '$anchor', '$comment', '$dynamicAnchor', '$id', '$schema', 'default', 'deprecated', 'description', 'examples',
94
+ 'readOnly', 'title', 'writeOnly', POSITION,
95
+ ]);
96
+ const DYNAMIC_REFERENCE_SCOPE_BARRIERS = new Set([
97
+ 'additionalItems', 'additionalProperties', 'contains', 'contentSchema', 'not', 'patternProperties', 'prefixItems',
98
+ 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties',
99
+ ]);
100
+ const SCHEMA_MAP_KEYWORDS = new Set([
101
+ '$defs', 'definitions', 'dependencies', 'dependentSchemas', 'patternProperties', 'properties',
102
+ ]);
103
+ const SCHEMA_ARRAY_KEYWORDS = new Set(['allOf', 'anyOf', 'oneOf', 'prefixItems']);
104
+ const SCHEMA_SINGLE_KEYWORDS = new Set([
105
+ 'additionalItems', 'additionalProperties', 'contains', 'contentSchema', 'else', 'if', 'not', 'propertyNames', 'then',
106
+ 'unevaluatedItems', 'unevaluatedProperties',
107
+ ]);
108
+ function enableLocalDynamicReferenceScopes(root, dialect, externalResourceIds) {
109
+ const compiledRoots = [
110
+ root,
111
+ ...Object.values(root.context.remotes).filter((remote) => {
112
+ if (!isSchemaNode(remote) || remote === root || typeof remote.$id !== 'string')
113
+ return false;
114
+ return externalResourceIds.has(remote.$id.split('#', 1)[0]);
115
+ }),
116
+ ];
117
+ const referencePolicies = new Map();
118
+ const recursiveReferencePolicies = new Map();
119
+ const unclassifiedCompiledReferencePolicies = new Set();
120
+ const classifiedReferencePolicies = new Set();
121
+ const activeValidationScope = [];
122
+ const validatorWrappers = new WeakMap();
123
+ const wrappedValidators = new WeakSet();
124
+ let validationDepth = 0;
125
+ let reductionDepth = 0;
126
+ for (const compiledRoot of compiledRoots) {
127
+ const authoredReferencePaths = collectAuthoredReferencePaths(compiledRoot.schema);
128
+ for (const node of compiledRoot.toSchemaNodes()) {
129
+ if (node.$id?.startsWith('/')) {
130
+ throw new TypeError('Local reference projection does not support root-relative resource identifiers.');
131
+ }
132
+ if (typeof node.schema !== 'object' || node.schema === null)
133
+ continue;
134
+ const schema = node.schema;
135
+ if (dialect === '2020-12' && '$recursiveRef' in schema) {
136
+ throw new TypeError('Local dynamic reference projection does not support $recursiveRef.');
137
+ }
138
+ const dynamicReference = dialect === '2020-12' && typeof schema.$dynamicRef === 'string';
139
+ const recursiveReference = dialect === '2019-09' && typeof schema.$recursiveRef === 'string';
140
+ if (!dynamicReference && !recursiveReference)
141
+ continue;
142
+ if (recursiveReference && schema.$recursiveRef !== '#') {
143
+ throw new TypeError('Local dynamic reference projection only supports the $recursiveRef value "#".');
144
+ }
145
+ const keyword = dynamicReference ? '$dynamicRef' : '$recursiveRef';
146
+ const reference = schema.$dynamicRef ?? schema.$recursiveRef;
147
+ const policyKey = referencePolicyKey(node, keyword, reference);
148
+ const authoredPaths = authoredReferencePaths.get(schema);
149
+ const parsedPath = authoredPaths ? undefined : schemaKeywordPath(compiledRoot, node.schemaLocation);
150
+ const keywordPaths = authoredPaths ?? (parsedPath ? [parsedPath] : undefined);
151
+ if (!keywordPaths) {
152
+ unclassifiedCompiledReferencePolicies.add(policyKey);
153
+ }
154
+ else {
155
+ classifiedReferencePolicies.add(policyKey);
156
+ for (const keywords of keywordPaths) {
157
+ if (!keywords.some((pathKeyword) => pathKeyword === 'properties' || pathKeyword === 'items') ||
158
+ keywords.some((pathKeyword) => DYNAMIC_REFERENCE_SCOPE_BARRIERS.has(pathKeyword))) {
159
+ throw new TypeError('Local dynamic reference projection does not support references below unsupported applicators or tuple items.');
160
+ }
161
+ }
162
+ }
163
+ if (recursiveReference && Object.keys(schema).some((keyword) => keyword !== '$recursiveRef' && keyword !== POSITION)) {
164
+ throw new TypeError('Local dynamic reference projection does not support siblings beside $recursiveRef.');
165
+ }
166
+ if (dynamicReference && Object.keys(schema).some((keyword) => keyword !== '$dynamicRef' && !DYNAMIC_REFERENCE_ANNOTATIONS.has(keyword))) {
167
+ throw new TypeError('Local dynamic reference projection does not support assertion siblings beside $dynamicRef.');
168
+ }
169
+ const staticTarget = node.resolveRef({ path: [] });
170
+ if (!isSchemaNode(staticTarget)) {
171
+ throw new TypeError(`Cannot resolve local reference at ${node.schemaLocation}.`);
172
+ }
173
+ const targetRoot = staticTarget.context.rootNode;
174
+ const targetRootId = targetRoot.$id?.split('#', 1)[0];
175
+ if (targetRoot !== root && (!targetRootId || !externalResourceIds.has(targetRootId))) {
176
+ throw new TypeError('Local dynamic reference projection does not support external targets unless they were loaded by the configured resource resolver.');
177
+ }
178
+ if (dynamicReference) {
179
+ const anchor = dynamicReferenceAnchor(schema.$dynamicRef);
180
+ const policy = anchor !== undefined && staticTarget.schema.$dynamicAnchor === anchor
181
+ ? anchor
182
+ : undefined;
183
+ referencePolicies.set(referencePolicyKey(node, '$dynamicRef', schema.$dynamicRef), policy);
184
+ }
185
+ else {
186
+ recursiveReferencePolicies.set(referencePolicyKey(node, '$recursiveRef', schema.$recursiveRef), staticTarget.schema.$recursiveAnchor === true);
187
+ }
188
+ }
189
+ }
190
+ for (const key of unclassifiedCompiledReferencePolicies) {
191
+ if (!classifiedReferencePolicies.has(key)) {
192
+ throw new TypeError('Cannot classify a compiled local dynamic reference.');
193
+ }
194
+ }
195
+ const instrumented = new WeakSet();
196
+ const instrument = (compiledRoot) => {
197
+ for (const node of compiledRoot.toSchemaNodes()) {
198
+ if (instrumented.has(node))
199
+ continue;
200
+ instrumented.add(node);
201
+ const schema = node.schema;
202
+ node.validators = node.validators.map((validator) => {
203
+ if (wrappedValidators.has(validator))
204
+ return validator;
205
+ const existing = validatorWrappers.get(validator);
206
+ if (existing)
207
+ return existing;
208
+ const wrapped = ((params) => {
209
+ activeValidationScope.push({ pointer: params.pointer, node: params.node });
210
+ try {
211
+ return validator(params);
212
+ }
213
+ finally {
214
+ activeValidationScope.pop();
215
+ }
216
+ });
217
+ Object.assign(wrapped, validator);
218
+ wrappedValidators.add(wrapped);
219
+ validatorWrappers.set(validator, wrapped);
220
+ return wrapped;
221
+ });
222
+ const validate = node.validate.bind(node);
223
+ node.validate = ((data, pointer = '#', path = []) => {
224
+ const outermost = validationDepth === 0;
225
+ const inheritedLength = activeValidationScope.length;
226
+ if (outermost)
227
+ activeValidationScope.push(...path);
228
+ validationDepth += 1;
229
+ try {
230
+ return validate(data, pointer, path);
231
+ }
232
+ finally {
233
+ validationDepth -= 1;
234
+ if (outermost)
235
+ activeValidationScope.length = inheritedLength;
236
+ }
237
+ });
238
+ const reduceNode = node.reduceNode.bind(node);
239
+ node.reduceNode = ((data, options = {}) => {
240
+ const outermost = reductionDepth === 0;
241
+ const inheritedLength = activeValidationScope.length;
242
+ if (outermost)
243
+ activeValidationScope.push(...(options.path ?? []));
244
+ if (activeValidationScope[activeValidationScope.length - 1]?.node !== node) {
245
+ activeValidationScope.push({ pointer: options.pointer ?? node.evaluationPath, node });
246
+ }
247
+ reductionDepth += 1;
248
+ try {
249
+ return reduceNode(data, options);
250
+ }
251
+ finally {
252
+ reductionDepth -= 1;
253
+ activeValidationScope.length = inheritedLength;
254
+ }
255
+ });
256
+ if (dialect === '2020-12' && typeof schema.$dynamicRef === 'string') {
257
+ const key = referencePolicyKey(node, '$dynamicRef', schema.$dynamicRef);
258
+ if (!referencePolicies.has(key)) {
259
+ throw new TypeError(`Cannot verify compiled local $dynamicRef at ${node.schemaLocation}.`);
260
+ }
261
+ const anchor = referencePolicies.get(key);
262
+ const resolveRef = node.resolveRef.bind(node);
263
+ node.resolveRef = ({ pointer, path = [] } = {}) => {
264
+ if (anchor === undefined) {
265
+ const staticPath = [];
266
+ const resolved = resolveRef({ pointer, path: staticPath });
267
+ if (path !== staticPath)
268
+ path.push(...staticPath);
269
+ return resolved;
270
+ }
271
+ const scopedPath = [];
272
+ const scope = (validationDepth > 0 || reductionDepth > 0) && activeValidationScope.length > 0
273
+ ? activeValidationScope
274
+ : path;
275
+ for (const entry of scope) {
276
+ const anchorNode = entry.node.context.dynamicAnchors[dynamicAnchorUri(entry.node.$id, anchor)];
277
+ if (isSchemaNode(anchorNode))
278
+ scopedPath.push({ pointer: entry.pointer, node: anchorNode });
279
+ }
280
+ const inheritedLength = scopedPath.length;
281
+ const resolved = resolveRef({ pointer, path: scopedPath });
282
+ if (path !== scopedPath)
283
+ path.push(...scopedPath.slice(inheritedLength));
284
+ return resolved;
285
+ };
286
+ }
287
+ if (dialect === '2019-09' && typeof schema.$recursiveRef === 'string') {
288
+ const key = referencePolicyKey(node, '$recursiveRef', schema.$recursiveRef);
289
+ let isRecursive = recursiveReferencePolicies.get(key);
290
+ if (isRecursive === undefined && !recursiveReferencePolicies.has(key)) {
291
+ if (schema.$recursiveRef !== '#') {
292
+ throw new TypeError(`Cannot verify compiled local $recursiveRef at ${node.schemaLocation}.`);
293
+ }
294
+ const staticTarget = node.resolveRef({ path: [] });
295
+ const targetRootId = isSchemaNode(staticTarget) ? staticTarget.context.rootNode.$id?.split('#', 1)[0] : undefined;
296
+ if (!isSchemaNode(staticTarget) ||
297
+ (staticTarget.context.rootNode !== root && (!targetRootId || !externalResourceIds.has(targetRootId)))) {
298
+ throw new TypeError(`Cannot verify compiled local $recursiveRef at ${node.schemaLocation}.`);
299
+ }
300
+ isRecursive = staticTarget.schema.$recursiveAnchor === true;
301
+ }
302
+ const resolveRef = node.resolveRef.bind(node);
303
+ node.resolveRef = ({ pointer, path = [] } = {}) => {
304
+ const staticPath = [];
305
+ const staticTarget = resolveRef({ pointer, path: staticPath });
306
+ if (!isRecursive || !isSchemaNode(staticTarget) || staticTarget.schema.$recursiveAnchor !== true) {
307
+ if (path !== staticPath)
308
+ path.push(...staticPath);
309
+ return staticTarget;
310
+ }
311
+ const scope = (validationDepth > 0 || reductionDepth > 0) && activeValidationScope.length > 0
312
+ ? activeValidationScope
313
+ : path;
314
+ const outerAnchor = scope.find(({ node: entry }) => entry.schema.$recursiveAnchor === true);
315
+ if (outerAnchor) {
316
+ const resourceRoot = outerAnchor.node.getNodeRef('#');
317
+ if (isSchemaNode(resourceRoot)) {
318
+ path.push({ pointer: pointer, node: resourceRoot });
319
+ return resourceRoot;
320
+ }
321
+ }
322
+ if (path !== staticPath)
323
+ path.push(...staticPath);
324
+ return staticTarget;
325
+ };
326
+ }
327
+ const compileSchema = node.compileSchema.bind(node);
328
+ node.compileSchema = ((schema, evaluationPath, schemaLocation, dynamicId) => {
329
+ const compiled = compileSchema(schema, evaluationPath, schemaLocation, dynamicId);
330
+ instrument(compiled);
331
+ return compiled;
332
+ });
333
+ }
334
+ };
335
+ for (const compiledRoot of compiledRoots)
336
+ instrument(compiledRoot);
337
+ }
338
+ function referencePolicyKey(node, keyword, reference) {
339
+ return `${node.$id ?? ''}\u0000${keyword}\u0000${reference}`;
340
+ }
341
+ function collectAuthoredReferencePaths(schema) {
342
+ const paths = new WeakMap();
343
+ const ancestors = new Set();
344
+ const visit = (value, keywords) => {
345
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || ancestors.has(value))
346
+ return;
347
+ ancestors.add(value);
348
+ const current = value;
349
+ if (typeof current.$dynamicRef === 'string' || typeof current.$recursiveRef === 'string') {
350
+ const found = paths.get(value) ?? [];
351
+ found.push(keywords);
352
+ paths.set(value, found);
353
+ }
354
+ for (const keyword of SCHEMA_MAP_KEYWORDS) {
355
+ const map = current[keyword];
356
+ if (typeof map !== 'object' || map === null || Array.isArray(map))
357
+ continue;
358
+ for (const child of Object.values(map))
359
+ visit(child, [...keywords, keyword]);
360
+ }
361
+ for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
362
+ const branches = current[keyword];
363
+ if (Array.isArray(branches)) {
364
+ for (const child of branches)
365
+ visit(child, [...keywords, keyword]);
366
+ }
367
+ }
368
+ for (const keyword of SCHEMA_SINGLE_KEYWORDS) {
369
+ visit(current[keyword], [...keywords, keyword]);
370
+ }
371
+ if (Object.hasOwn(current, 'items')) {
372
+ const items = current.items;
373
+ if (Array.isArray(items)) {
374
+ for (const child of items)
375
+ visit(child, [...keywords, 'prefixItems']);
376
+ }
377
+ else {
378
+ visit(items, [...keywords, 'items']);
379
+ }
380
+ }
381
+ ancestors.delete(value);
382
+ };
383
+ visit(schema, []);
384
+ return paths;
385
+ }
386
+ function schemaKeywordPath(root, schemaLocation) {
387
+ const hashIndex = schemaLocation.indexOf('#');
388
+ if (hashIndex < 0)
389
+ return undefined;
390
+ const fragment = schemaLocation.slice(hashIndex + 1);
391
+ if (!fragment.startsWith('/'))
392
+ return [];
393
+ const segments = fragment.slice(1).split('/');
394
+ const keywords = [];
395
+ let current = root.schema;
396
+ let index = 0;
397
+ while (index < segments.length) {
398
+ if (typeof current !== 'object' || current === null || Array.isArray(current))
399
+ return undefined;
400
+ const schema = current;
401
+ const segment = segments[index];
402
+ if (SCHEMA_MAP_KEYWORDS.has(segment)) {
403
+ keywords.push(segment);
404
+ const map = schema[segment];
405
+ const member = segments[index + 1];
406
+ if (typeof map !== 'object' || map === null || Array.isArray(map) || member === undefined ||
407
+ !Object.prototype.hasOwnProperty.call(map, member))
408
+ return undefined;
409
+ current = map[member];
410
+ index += 2;
411
+ continue;
412
+ }
413
+ if (SCHEMA_ARRAY_KEYWORDS.has(segment)) {
414
+ keywords.push(segment);
415
+ const array = schema[segment];
416
+ const itemIndex = Number(segments[index + 1]);
417
+ if (!Array.isArray(array) || !Number.isInteger(itemIndex) || itemIndex < 0 || itemIndex >= array.length) {
418
+ return undefined;
419
+ }
420
+ current = array[itemIndex];
421
+ index += 2;
422
+ continue;
423
+ }
424
+ if (segment === 'items') {
425
+ const items = schema.items;
426
+ if (Array.isArray(items)) {
427
+ keywords.push('prefixItems');
428
+ const itemIndex = Number(segments[index + 1]);
429
+ if (!Number.isInteger(itemIndex) || itemIndex < 0 || itemIndex >= items.length)
430
+ return undefined;
431
+ current = items[itemIndex];
432
+ index += 2;
433
+ }
434
+ else {
435
+ keywords.push(segment);
436
+ if (!(segment in schema))
437
+ return undefined;
438
+ current = items;
439
+ index += 1;
440
+ }
441
+ continue;
442
+ }
443
+ if (SCHEMA_SINGLE_KEYWORDS.has(segment)) {
444
+ keywords.push(segment);
445
+ if (!(segment in schema))
446
+ return undefined;
447
+ current = schema[segment];
448
+ index += 1;
449
+ continue;
450
+ }
451
+ return undefined;
452
+ }
453
+ return keywords;
454
+ }
455
+ function dynamicAnchorUri(resourceId, anchor) {
456
+ const currentId = resourceId ?? '#';
457
+ return `${currentId.replace(/#.*$/, '')}#${anchor}`;
458
+ }
459
+ function dynamicReferenceAnchor(reference) {
460
+ const hash = reference.indexOf('#');
461
+ if (hash < 0)
462
+ return undefined;
463
+ let fragment;
464
+ try {
465
+ fragment = decodeURIComponent(reference.slice(hash + 1));
466
+ }
467
+ catch {
468
+ throw new TypeError(`Local dynamic reference projection cannot decode anchor in ${reference}.`);
469
+ }
470
+ return fragment === '' || fragment.startsWith('/') ? undefined : fragment;
471
+ }
41
472
  // json-schema-library's `draft` compile option uses "draft-2019-09"/"draft-2020-12" rather
42
473
  // than this package's "2019-09"/"2020-12" Dialect values.
43
474
  function toDraftOption(dialect) {
@@ -53,15 +484,35 @@ async function prepareSchema(schema, dialect, remotes) {
53
484
  // annotation unless the format-assertion vocabulary is declared, so asserting it
54
485
  // is a deviation rather than a stricter setting.
55
486
  const formatAssertion = dialect === 'draft-07' ? undefined : false;
56
- const root = compileSchema(schema, {
487
+ const root = compileSchema(materializeLocalPointerAliases(schema, dialect), {
57
488
  drafts: DRAFTS,
58
489
  draft: toDraftOption(dialect),
59
490
  formatAssertion,
60
- remotes,
491
+ remotes: remotes?.map((remote) => materializeLocalPointerAliases(remote, dialect, true)),
61
492
  });
62
493
  fixRootReference(root, dialect);
63
494
  return root;
64
495
  }
496
+ function hasOneOf(value, visited = new WeakSet()) {
497
+ if (typeof value !== 'object' || value === null || visited.has(value))
498
+ return false;
499
+ visited.add(value);
500
+ let descriptors;
501
+ try {
502
+ descriptors = Object.getOwnPropertyDescriptors(value);
503
+ }
504
+ catch {
505
+ return true;
506
+ }
507
+ const properties = Object.values(descriptors);
508
+ if (properties.some((descriptor) => !('value' in descriptor)))
509
+ return true;
510
+ const oneOf = descriptors.oneOf;
511
+ if (oneOf && Array.isArray(oneOf.value) && oneOf.value.length > 0) {
512
+ return true;
513
+ }
514
+ return properties.some((descriptor) => hasOneOf(descriptor.value, visited));
515
+ }
65
516
  // jsl error codes are kebab-case ("min-length-error"); ValidationError.keyword is expected
66
517
  // to be the JSON Schema keyword itself ("minLength"). Unrecognized codes fall back to a
67
518
  // best-effort camelCase conversion of the code with any trailing "-error"/"-warning" removed.