@texaryn/schema-json 0.6.3 → 0.7.1

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 (43) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +34 -2
  3. package/dist/adapter.d.ts +2 -0
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +42 -25
  6. package/dist/adapter.js.map +1 -1
  7. package/dist/bare-maps.d.ts +3 -0
  8. package/dist/bare-maps.d.ts.map +1 -0
  9. package/dist/bare-maps.js +44 -0
  10. package/dist/bare-maps.js.map +1 -0
  11. package/dist/identity.d.ts +26 -0
  12. package/dist/identity.d.ts.map +1 -0
  13. package/dist/identity.js +280 -0
  14. package/dist/identity.js.map +1 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/instance-pointer.d.ts +9 -0
  20. package/dist/instance-pointer.d.ts.map +1 -0
  21. package/dist/instance-pointer.js +115 -0
  22. package/dist/instance-pointer.js.map +1 -0
  23. package/dist/normalize.d.ts +3 -0
  24. package/dist/normalize.d.ts.map +1 -0
  25. package/dist/normalize.js +118 -0
  26. package/dist/normalize.js.map +1 -0
  27. package/dist/projection-shape.d.ts +25 -0
  28. package/dist/projection-shape.d.ts.map +1 -0
  29. package/dist/projection-shape.js +74 -0
  30. package/dist/projection-shape.js.map +1 -0
  31. package/dist/projection.d.ts +7 -1
  32. package/dist/projection.d.ts.map +1 -1
  33. package/dist/projection.js +202 -111
  34. package/dist/projection.js.map +1 -1
  35. package/dist/root-reference.d.ts +4 -0
  36. package/dist/root-reference.d.ts.map +1 -0
  37. package/dist/root-reference.js +86 -0
  38. package/dist/root-reference.js.map +1 -0
  39. package/dist/schema-graph.d.ts +29 -0
  40. package/dist/schema-graph.d.ts.map +1 -0
  41. package/dist/schema-graph.js +365 -0
  42. package/dist/schema-graph.js.map +1 -0
  43. package/package.json +3 -3
@@ -0,0 +1,74 @@
1
+ // Every family takes part in detecting a conflict, but only `object` and `array` are ever inferred:
2
+ // a wrong scalar guess selects the wrong widget. `format` is absent because it describes string contents.
3
+ const KEYWORD_FAMILIES = {
4
+ object: [
5
+ 'properties',
6
+ 'patternProperties',
7
+ 'additionalProperties',
8
+ 'propertyNames',
9
+ 'required',
10
+ 'minProperties',
11
+ 'maxProperties',
12
+ 'dependentSchemas',
13
+ 'dependentRequired',
14
+ 'dependencies',
15
+ 'unevaluatedProperties',
16
+ ],
17
+ array: [
18
+ 'items',
19
+ 'prefixItems',
20
+ 'additionalItems',
21
+ 'contains',
22
+ 'minItems',
23
+ 'maxItems',
24
+ 'uniqueItems',
25
+ 'minContains',
26
+ 'maxContains',
27
+ 'unevaluatedItems',
28
+ ],
29
+ string: ['minLength', 'maxLength', 'pattern'],
30
+ number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
31
+ };
32
+ export function projectionTypeFamilies(schema) {
33
+ const families = new Set();
34
+ for (const [family, keywords] of Object.entries(KEYWORD_FAMILIES)) {
35
+ if (keywords.some((keyword) => schema[keyword] !== undefined)) {
36
+ families.add(family);
37
+ }
38
+ }
39
+ return families;
40
+ }
41
+ export function shapeOfFamilies(families) {
42
+ if (families.size > 1)
43
+ return { kind: 'ambiguous', families: [...families].sort() };
44
+ if (families.size === 1) {
45
+ const [family] = families;
46
+ if (family === 'object' || family === 'array')
47
+ return { kind: 'resolved', type: family };
48
+ }
49
+ return { kind: 'none' };
50
+ }
51
+ export function inferProjectionShape(schema) {
52
+ return shapeOfFamilies(projectionTypeFamilies(schema));
53
+ }
54
+ export function shapeDiagnostic(pointer, shape, enumDeclared) {
55
+ if (shape.kind === 'ambiguous') {
56
+ return {
57
+ pointer,
58
+ code: 'ambiguous-projection-shape',
59
+ message: `No explicit "type", and keywords from more than one type apply ` +
60
+ `(${shape.families.join(', ')}), so the shape to render is undecidable. ` +
61
+ `Declare "type" on this schema to resolve it.`,
62
+ };
63
+ }
64
+ return {
65
+ pointer,
66
+ code: 'unresolved-projection-shape',
67
+ message: `No explicit "type", and no keyword that implies one, so there is no ` +
68
+ `shape to render. Declare "type" on this schema.` +
69
+ (enumDeclared
70
+ ? ` An "enum" alone does not imply a type, because its members may be of different types.`
71
+ : ''),
72
+ };
73
+ }
74
+ //# sourceMappingURL=projection-shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projection-shape.js","sourceRoot":"","sources":["../src/projection-shape.ts"],"names":[],"mappings":"AAEA,oGAAoG;AACpG,0GAA0G;AAC1G,MAAM,gBAAgB,GAAG;IACvB,MAAM,EAAE;QACN,YAAY;QACZ,mBAAmB;QACnB,sBAAsB;QACtB,eAAe;QACf,UAAU;QACV,eAAe;QACf,eAAe;QACf,kBAAkB;QAClB,mBAAmB;QACnB,cAAc;QACd,uBAAuB;KACxB;IACD,KAAK,EAAE;QACL,OAAO;QACP,aAAa;QACb,iBAAiB;QACjB,UAAU;QACV,UAAU;QACV,UAAU;QACV,aAAa;QACb,aAAa;QACb,aAAa;QACb,kBAAkB;KACnB;IACD,MAAM,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,CAAC;IAC7C,MAAM,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,YAAY,CAAC;CAChC,CAAA;AAStD,MAAM,UAAU,sBAAsB,CAAC,MAA+B;IACpE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAA;IACzC,KAAK,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAClE,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,SAAS,CAAC,EAAE,CAAC;YAC9D,QAAQ,CAAC,GAAG,CAAC,MAAuB,CAAC,CAAA;QACvC,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAoC;IAClE,IAAI,QAAQ,CAAC,IAAI,GAAG,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,CAAA;IAEnF,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAA;QACzB,IAAI,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IAC1F,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAA+B;IAClE,OAAO,eAAe,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAA;AACxD,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,OAAoB,EACpB,KAAqD,EACrD,YAAqB;IAErB,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC/B,OAAO;YACL,OAAO;YACP,IAAI,EAAE,4BAA4B;YAClC,OAAO,EACL,iEAAiE;gBACjE,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,4CAA4C;gBACzE,8CAA8C;SACjD,CAAA;IACH,CAAC;IACD,OAAO;QACL,OAAO;QACP,IAAI,EAAE,6BAA6B;QACnC,OAAO,EACL,sEAAsE;YACtE,iDAAiD;YACjD,CAAC,YAAY;gBACX,CAAC,CAAC,wFAAwF;gBAC1F,CAAC,CAAC,EAAE,CAAC;KACV,CAAA;AACH,CAAC"}
@@ -1,5 +1,11 @@
1
1
  import { type SchemaNode } from 'json-schema-library';
2
2
  import type { SchemaProjection } from '@texaryn/core';
3
+ import { type ProjectionCache } from './identity.js';
4
+ export interface ProjectionLimits {
5
+ readonly objects: number;
6
+ readonly nodes: number;
7
+ }
8
+ export declare const DEFAULT_LIMITS: ProjectionLimits;
3
9
  /**
4
10
  * Maps a compiled json-schema-library root node to a SchemaProjection.
5
11
  * Called by adapter.project() on every data change.
@@ -10,5 +16,5 @@ import type { SchemaProjection } from '@texaryn/core';
10
16
  * contract), while array items are walked from `data` (an array's length is a data-time
11
17
  * fact, not a schema-time one). $ref is resolved transparently via node.resolveRef().
12
18
  */
13
- export declare function buildProjection(root: SchemaNode, data: unknown): SchemaProjection;
19
+ export declare function buildProjection(root: SchemaNode, data: unknown, cache: ProjectionCache, limits?: ProjectionLimits): SchemaProjection;
14
20
  //# sourceMappingURL=projection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAChE,OAAO,KAAK,EACV,gBAAgB,EASjB,MAAM,eAAe,CAAA;AA4nCtB;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,GAAG,gBAAgB,CAKjF"}
1
+ {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAC9E,OAAO,KAAK,EACV,gBAAgB,EAUjB,MAAM,eAAe,CAAA;AACtB,OAAO,EAQL,KAAK,eAAe,EACrB,MAAM,eAAe,CAAA;AAwJtB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,cAAc,EAAE,gBAA8C,CAAA;AAqiC3E;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,eAAe,EACtB,MAAM,GAAE,gBAAiC,GACxC,gBAAgB,CAiDlB"}
@@ -1,4 +1,7 @@
1
- import { mergeNode } from 'json-schema-library';
1
+ import { mergeNode, isSchemaNode } from 'json-schema-library';
2
+ import { childDeclaring, followRef, itemDeclaring, locationInfo, onlyPoints, positionOf, } from './identity.js';
3
+ import { inferProjectionShape, shapeDiagnostic } from './projection-shape.js';
4
+ import { POSITION } from './schema-graph.js';
2
5
  const VALID_TYPES = new Set([
3
6
  'string',
4
7
  'number',
@@ -25,64 +28,6 @@ function resolveExplicitType(schema) {
25
28
  }
26
29
  return undefined;
27
30
  }
28
- /**
29
- * Keywords that apply to exactly one JSON type, grouped by that type.
30
- *
31
- * Two different jobs are done with these, and only one of them infers
32
- * anything. Every family takes part in detecting a conflict, because a schema
33
- * drawing keywords from two families implies no single shape. Only `object`
34
- * and `array` are shapes a form can be given, so those are the only two ever
35
- * inferred: `string` and `number` are here to be noticed, not chosen.
36
- *
37
- * Inferring a scalar would need a rule that is right rather than symmetrical,
38
- * and there is not one. `minimum` cannot tell `number` from `integer`, and a
39
- * wrong scalar guess selects the wrong widget, which is harder to notice than
40
- * a field that never appeared at all.
41
- *
42
- * `format` is deliberately absent: it annotates a string's contents rather
43
- * than describing structure, and schemas apply it to non-strings in practice.
44
- */
45
- const KEYWORD_FAMILIES = {
46
- object: [
47
- 'properties',
48
- 'patternProperties',
49
- 'additionalProperties',
50
- 'propertyNames',
51
- 'required',
52
- 'minProperties',
53
- 'maxProperties',
54
- 'dependentSchemas',
55
- 'dependentRequired',
56
- // json-schema-library normalises draft-07 `dependencies` into the two
57
- // above at parse time. Listed anyway, so the rule does not rely on that.
58
- 'dependencies',
59
- 'unevaluatedProperties',
60
- ],
61
- array: [
62
- 'items',
63
- 'prefixItems',
64
- 'additionalItems',
65
- 'contains',
66
- 'minItems',
67
- 'maxItems',
68
- 'uniqueItems',
69
- 'minContains',
70
- 'maxContains',
71
- 'unevaluatedItems',
72
- ],
73
- string: ['minLength', 'maxLength', 'pattern'],
74
- number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
75
- };
76
- /** Which families a schema draws keywords from, computed over all of them at once. */
77
- function projectionTypeFamilies(schema) {
78
- const families = new Set();
79
- for (const [family, keywords] of Object.entries(KEYWORD_FAMILIES)) {
80
- if (keywords.some((keyword) => schema[keyword] !== undefined)) {
81
- families.add(family);
82
- }
83
- }
84
- return families;
85
- }
86
31
  /**
87
32
  * Whether any branch of this node's `oneOf`/`anyOf` could be given a shape at
88
33
  * all, for some value.
@@ -104,17 +49,6 @@ function compositionHasRenderableAlternative(node) {
104
49
  return inferProjectionShape(schema).kind === 'resolved';
105
50
  });
106
51
  }
107
- function inferProjectionShape(schema) {
108
- const families = projectionTypeFamilies(schema);
109
- if (families.size > 1)
110
- return { kind: 'ambiguous', families: [...families].sort() };
111
- if (families.size === 1) {
112
- const [family] = families;
113
- if (family === 'object' || family === 'array')
114
- return { kind: 'resolved', type: family };
115
- }
116
- return { kind: 'none' };
117
- }
118
52
  function extractConstraints(schema) {
119
53
  const constraints = {};
120
54
  if (typeof schema.minLength === 'number')
@@ -174,9 +108,80 @@ function extractEnumValues(schema) {
174
108
  return undefined;
175
109
  return schema.enum.map((value) => ({ value }));
176
110
  }
111
+ function dereferenceChecked(node) {
112
+ let current = node;
113
+ const seen = new Set();
114
+ while (typeof current.$ref === 'string') {
115
+ const position = positionOf(current);
116
+ if (seen.has(position))
117
+ return { node: current, cycle: true };
118
+ seen.add(position);
119
+ const next = followRef(current);
120
+ if (!next) {
121
+ const raw = current.resolveRef();
122
+ return { node: (raw ?? current), cycle: false };
123
+ }
124
+ current = next;
125
+ if (!onlyPoints(current))
126
+ break;
127
+ }
128
+ return { node: current, cycle: false };
129
+ }
177
130
  /** Follows a $ref to the node it points at; returns the node unchanged otherwise. */
178
131
  function dereference(node) {
179
- return node.$ref ? node.resolveRef() : node;
132
+ return dereferenceChecked(node).node;
133
+ }
134
+ // A dialect whose reducer leaves $ref unresolved reduces a selected `{ $ref }` branch to `{}`.
135
+ function resolveSelectedBranch(original, reduced, data) {
136
+ const reducedSchema = reduced.schema;
137
+ if (reducedSchema && typeof reducedSchema === 'object') {
138
+ if (resolveExplicitType(reducedSchema) || inferProjectionShape(reducedSchema).kind === 'resolved') {
139
+ return reduced;
140
+ }
141
+ }
142
+ const selected = [];
143
+ if (original.oneOf) {
144
+ const matching = original.oneOf.filter((branch) => branchApplies(branch, data));
145
+ if (matching.length === 1)
146
+ selected.push(matching[0]);
147
+ }
148
+ for (const branch of original.anyOf ?? []) {
149
+ if (branchApplies(branch, data))
150
+ selected.push(branch);
151
+ }
152
+ if (!selected.some((branch) => typeof branch.$ref === 'string'))
153
+ return reduced;
154
+ let merged = reduced;
155
+ for (const branch of selected) {
156
+ const target = dereference(branch);
157
+ if (!isSchemaNode(target))
158
+ continue;
159
+ const targetReduced = target.reduceNode(data).node ?? target;
160
+ merged = mergeNode(merged, targetReduced) ?? merged;
161
+ }
162
+ return merged;
163
+ }
164
+ export const DEFAULT_LIMITS = { objects: 16, nodes: 512 };
165
+ function walkOrder(a, b) {
166
+ for (let i = 0; i < Math.min(a.length, b.length); i++)
167
+ if (a[i] !== b[i])
168
+ return a[i] - b[i];
169
+ return a.length - b.length;
170
+ }
171
+ function addBoundary(ctx, pointer, reason) {
172
+ const reasons = ctx.boundaries.get(pointer) ?? new Set();
173
+ reasons.add(reason);
174
+ ctx.boundaries.set(pointer, reasons);
175
+ }
176
+ // A leaf needs no reduction, so it rides with its parent's budget unit.
177
+ function isLeafSchema(schema) {
178
+ if (typeof schema !== 'object' || schema === null)
179
+ return true;
180
+ const record = schema;
181
+ const type = resolveExplicitType(record);
182
+ if (type !== undefined)
183
+ return type !== 'object';
184
+ return ('enum' in record || 'const' in record) && !('properties' in record);
180
185
  }
181
186
  /**
182
187
  * The position of a schema within the document it was authored in, as a JSON
@@ -334,7 +339,7 @@ function collectApplicableDefaults(roots, data) {
334
339
  return declarationsFrom((visitor) => eachApplicable(roots, data, visitor));
335
340
  }
336
341
  /** The positions that declare one property of `roots`, by both reachability rules. */
337
- function childPositions(roots, key, data) {
342
+ function childPositions(roots, key, data, declaring) {
338
343
  const unconditional = [];
339
344
  eachUnconditional(roots.unconditional, (node) => {
340
345
  const child = node.properties?.[key];
@@ -347,10 +352,10 @@ function childPositions(roots, key, data) {
347
352
  if (child)
348
353
  applicable.push(child);
349
354
  });
350
- return { unconditional, applicable };
355
+ return { unconditional, applicable, declaring };
351
356
  }
352
357
  /** The positions that declare the elements of `roots`, by both reachability rules. */
353
- function itemPositions(roots, data) {
358
+ function itemPositions(roots, data, declaring) {
354
359
  const unconditional = [];
355
360
  eachUnconditional(roots.unconditional, (node) => {
356
361
  if (node.items)
@@ -361,7 +366,7 @@ function itemPositions(roots, data) {
361
366
  if (node.items)
362
367
  applicable.push(node.items);
363
368
  });
364
- return { unconditional, applicable };
369
+ return { unconditional, applicable, declaring };
365
370
  }
366
371
  /**
367
372
  * The declarations to report, or nothing where the schema states one answer.
@@ -641,6 +646,8 @@ function computeRequiredSet(resolved, reducedSchema, dataRecord) {
641
646
  }
642
647
  return required;
643
648
  }
649
+ // An unreadable identity is never a repeat, and is budgeted so it cannot expand without end.
650
+ const budgeted = (info) => info.cyclic || info.key === '';
644
651
  /**
645
652
  * Walks the compiled schema statically (via node.properties/node.items plus every
646
653
  * conditional branch), only descending into array items that are actually present in
@@ -664,9 +671,57 @@ provisional, nodes, diagnostics,
664
671
  * supply: by the time the caller has a node to walk it holds the merge of
665
672
  * them, and the merge is what loses a disagreement.
666
673
  */
667
- declaredAt) {
668
- const original = dereference(node);
674
+ declaredAt, ctx,
675
+ /** Entered as an object member, which is the only way to be past the data. */
676
+ member,
677
+ /** The past-the-data ancestors, nearest first, up to the nearest location holding data. */
678
+ lineage, path) {
679
+ const info = locationInfo(declaredAt.declaring, ctx.cache);
680
+ const { node: original, cycle: referenceCycle } = dereferenceChecked(node);
681
+ if (info.cycle || referenceCycle) {
682
+ diagnostics.push({
683
+ pointer: toPointer(pointer),
684
+ code: 'unresolved-projection-shape',
685
+ message: `A "$ref" or in-place applicator cycle never leaves this location, so there is no ` +
686
+ `shape to render.`,
687
+ });
688
+ return;
689
+ }
690
+ const pastData = member && (data === undefined || data === null);
691
+ const recursive = pastData && ((lineage?.recursive ?? false) || budgeted(info));
692
+ for (let ancestor = pastData ? lineage : undefined; ancestor; ancestor = ancestor.parent) {
693
+ if (info.key !== '' && ancestor.key === info.key) {
694
+ addBoundary(ctx, ancestor.pointer, 'recursion');
695
+ ctx.removed.add(pointer);
696
+ return;
697
+ }
698
+ }
669
699
  const originalSchema = original.schema;
700
+ if (recursive && lineage !== undefined) {
701
+ const parent = pointer.slice(0, pointer.lastIndexOf('/'));
702
+ let admit;
703
+ if (isLeafSchema(originalSchema)) {
704
+ const covered = ctx.reserved.has(parent);
705
+ admit = covered || ctx.nodesUsed < ctx.limits.nodes;
706
+ if (admit && !covered)
707
+ ctx.nodesUsed += 1;
708
+ }
709
+ else {
710
+ const leaves = [...collectCandidateProperties(original).values()].filter((candidate) => isLeafSchema(dereference(candidatePrototype(candidate)).schema)).length;
711
+ admit = ctx.objectsUsed < ctx.limits.objects && ctx.nodesUsed + 1 + leaves <= ctx.limits.nodes;
712
+ if (admit) {
713
+ ctx.objectsUsed += 1;
714
+ ctx.nodesUsed += 1 + leaves;
715
+ ctx.reserved.add(pointer);
716
+ }
717
+ }
718
+ if (!admit) {
719
+ addBoundary(ctx, parent, 'budget');
720
+ ctx.removed.add(pointer);
721
+ return;
722
+ }
723
+ ctx.flagged.add(pointer);
724
+ }
670
725
  if (typeof originalSchema !== 'object' || originalSchema === null)
671
726
  return;
672
727
  let resolved = original;
@@ -697,8 +752,8 @@ declaredAt) {
697
752
  composedAgainstData = true;
698
753
  const { node: branchNode } = original.reduceNode(data);
699
754
  if (branchNode) {
700
- resolved = branchNode;
701
- schema = branchNode.schema;
755
+ resolved = resolveSelectedBranch(original, branchNode, data);
756
+ schema = resolved.schema;
702
757
  type = resolveExplicitType(schema);
703
758
  }
704
759
  else {
@@ -751,7 +806,7 @@ declaredAt) {
751
806
  * this adapter renders, because scalar shapes are deliberately not inferred.
752
807
  * That schema is genuinely unprojectable and has to be reported.
753
808
  */
754
- const isTransientBranchMiss = () => Object.keys(schema).length === 0 && compositionHasRenderableAlternative(original);
809
+ const isTransientBranchMiss = () => Object.keys(schema).every((keyword) => keyword === POSITION) && compositionHasRenderableAlternative(original);
755
810
  // Last resort, after the oneOf/anyOf branch above has had its chance: that
756
811
  // path handles a typeless wrapper whose type only exists once a branch is
757
812
  // chosen, and inferring first would take a schema carrying both `properties`
@@ -776,30 +831,14 @@ declaredAt) {
776
831
  // without a shape, so the caller is told which pointer was skipped and
777
832
  // why, instead of finding out from a form that never collected the
778
833
  // value.
779
- diagnostics.push(shape.kind === 'ambiguous'
780
- ? {
781
- pointer: toPointer(pointer),
782
- code: 'ambiguous-projection-shape',
783
- message: `No explicit "type", and keywords from more than one type apply ` +
784
- `(${shape.families.join(', ')}), so the shape to render is undecidable. ` +
785
- `Declare "type" on this schema to resolve it.`,
786
- }
787
- : {
788
- pointer: toPointer(pointer),
789
- code: 'unresolved-projection-shape',
790
- message: `No explicit "type", and no keyword that implies one, so there is no ` +
791
- `shape to render. Declare "type" on this schema.` +
792
- // Said only where it applies, because inferring `string` from
793
- // an enum is the tempting wrong rule and the reason deserves
794
- // to travel with the case rather than every message.
795
- (schema.enum !== undefined
796
- ? ` An "enum" alone does not imply a type, because its members may be of different types.`
797
- : ''),
798
- });
834
+ diagnostics.push(shapeDiagnostic(toPointer(pointer), shape, schema.enum !== undefined));
799
835
  }
800
836
  }
801
837
  if (!type)
802
838
  return;
839
+ const selfLineage = pastData
840
+ ? { key: info.key, pointer, recursive, depth: (lineage?.depth ?? 0) + 1, parent: lineage }
841
+ : undefined;
803
842
  // Reported at the location it is about, and after the shape gate above: a
804
843
  // pointer with no node has nothing to omit an annotation from, and saying
805
844
  // that its `default` is undecidable on top of saying it cannot be drawn at
@@ -819,9 +858,13 @@ declaredAt) {
819
858
  // selects. A superset of the diagnostic's: every unconditional disagreement
820
859
  // is also an applicable one, so the node carries both kinds and a consumer
821
860
  // reads one place.
822
- const applicableConflict = disagreeingDefaults(collectApplicableDefaults(declaredAt.applicable, data));
861
+ const applicableDeclarations = collectApplicableDefaults(declaredAt.applicable, data);
862
+ const applicableConflict = disagreeingDefaults(applicableDeclarations);
823
863
  const conflictedDefault = applicableConflict !== undefined || ambiguousDefault !== undefined;
824
864
  const defaultConflict = (applicableConflict ?? ambiguousDefault)?.map((declaration) => declaration.source);
865
+ const defaultSources = !conflictedDefault && 'default' in schema && info.cyclic
866
+ ? [...new Set(applicableDeclarations.map((declaration) => declaration.source))].sort()
867
+ : undefined;
825
868
  const nodeActive = active && branchResolved;
826
869
  // A node the schema applies is never also provisional; the two report
827
870
  // different facts and only the second is a choice this projection made. The
@@ -846,7 +889,13 @@ declaredAt) {
846
889
  // wrapper case), it has already been reduced against the real `data` at this
847
890
  // pointer; re-reducing it against `dataRecord ?? {}` here would be redundant (and,
848
891
  // for a branch with no dynamic keywords of its own, a no-op), so it is skipped.
849
- const reducedNode = resolved === original ? resolved.reduceNode(dataRecord ?? {}).node : resolved;
892
+ let reducedNode = resolved === original ? resolved.reduceNode(dataRecord ?? {}).node : resolved;
893
+ if (reducedNode &&
894
+ resolved === original &&
895
+ !resolveExplicitType(originalSchema) &&
896
+ (original.oneOf || original.anyOf)) {
897
+ reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {});
898
+ }
850
899
  const reducedSchema = reducedNode?.schema;
851
900
  const reducedProperties = reducedSchema?.properties ??
852
901
  schema.properties ??
@@ -903,8 +952,9 @@ declaredAt) {
903
952
  provisional: nodeProvisional ? true : undefined,
904
953
  defaultConflict,
905
954
  annotations: extractAnnotations(schema, conflictedDefault),
955
+ ...(defaultSources !== undefined ? { defaultSources } : {}),
906
956
  });
907
- for (const key of propKeys) {
957
+ for (const [index, key] of propKeys.entries()) {
908
958
  const childPointer = `${pointer}/${escapeSegment(key)}`;
909
959
  const childActive = nodeActive && activeKeys.has(key);
910
960
  // A descendant inherits exposure, not activity: under a provisionally
@@ -927,15 +977,23 @@ declaredAt) {
927
977
  const childNode = reducedChildNode ??
928
978
  composeChild(candidate.direct, provisionalChildNode) ??
929
979
  candidatePrototype(candidate);
930
- walk(childNode, childPointer, dataRecord?.[key], childActive, childProvisional, nodes, diagnostics,
980
+ const positions = childPositions(declaredAt, key,
931
981
  // This node's own data, not the child's: the branches being entered
932
982
  // are this location's, so they are decided by the instance here. The
933
983
  // child's own conditional edges are entered by the child's walk, with
934
984
  // the child's data.
935
- childPositions(declaredAt, key, data));
985
+ data, childDeclaring(info, key));
986
+ const enter = () => walk(childNode, childPointer, dataRecord !== undefined && Object.hasOwn(dataRecord, key) ? dataRecord[key] : undefined, childActive, childProvisional, nodes, diagnostics, positions, ctx, true, selfLineage, [...path, index]);
987
+ // Deferring the children the budget counts, by depth below the anchor, admits it breadth first.
988
+ const counted = selfLineage && (recursive || budgeted(locationInfo(positions.declaring, ctx.cache)));
989
+ if (counted)
990
+ (ctx.queue[selfLineage.depth] ??= []).push({ path: [...path, index], enter });
991
+ else
992
+ enter();
936
993
  }
937
994
  return;
938
995
  }
996
+ const itemSchema = type === 'array' && resolved.items ? dereference(resolved.items).schema : undefined;
939
997
  nodes.set(toPointer(pointer), {
940
998
  type,
941
999
  format: typeof schema.format === 'string' ? schema.format : undefined,
@@ -946,20 +1004,21 @@ declaredAt) {
946
1004
  provisional: nodeProvisional ? true : undefined,
947
1005
  defaultConflict,
948
1006
  annotations: extractAnnotations(schema, conflictedDefault),
1007
+ ...(defaultSources !== undefined ? { defaultSources } : {}),
949
1008
  // Deliberately not given the flag above. A conflict under `items` belongs
950
1009
  // to the element locations, and each of those reports its own; this field
951
1010
  // describes the item schema rather than being one of those locations, so
952
1011
  // omitting its `default` would be an omission no pointer could report. It
953
1012
  // is also the only annotation set nothing reads a `default` from, and the
954
1013
  // only one the other adapter does not produce at all.
955
- itemAnnotations: type === 'array' && resolved.items
956
- ? extractAnnotations(dereference(resolved.items).schema)
1014
+ itemAnnotations: typeof itemSchema === 'object' && itemSchema !== null
1015
+ ? extractAnnotations(itemSchema)
957
1016
  : undefined,
958
1017
  });
959
1018
  if (type === 'array' && resolved.items && Array.isArray(data)) {
960
- const elementPositions = itemPositions(declaredAt, data);
1019
+ const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info));
961
1020
  data.forEach((item, index) => {
962
- walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions);
1021
+ walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions, ctx, false, undefined, [...path, index]);
963
1022
  });
964
1023
  }
965
1024
  }
@@ -973,10 +1032,42 @@ declaredAt) {
973
1032
  * contract), while array items are walked from `data` (an array's length is a data-time
974
1033
  * fact, not a schema-time one). $ref is resolved transparently via node.resolveRef().
975
1034
  */
976
- export function buildProjection(root, data) {
1035
+ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
977
1036
  const nodes = new Map();
978
1037
  const diagnostics = [];
979
- walk(root, '', data, true, false, nodes, diagnostics, { unconditional: [root], applicable: [root] });
1038
+ const ctx = {
1039
+ cache,
1040
+ limits,
1041
+ boundaries: new Map(),
1042
+ removed: new Set(),
1043
+ flagged: new Set(),
1044
+ reserved: new Set(),
1045
+ queue: [],
1046
+ objectsUsed: 0,
1047
+ nodesUsed: 0,
1048
+ };
1049
+ walk(root, '', data, true, false, nodes, diagnostics, { unconditional: [root], applicable: [root], declaring: [root] }, ctx, false, undefined, []);
1050
+ for (const level of ctx.queue)
1051
+ for (const { enter } of (level ?? []).sort((a, b) => walkOrder(a.path, b.path)))
1052
+ enter();
1053
+ for (const node of nodes.values()) {
1054
+ if (node.children?.some((child) => ctx.removed.has(child.pointer))) {
1055
+ ;
1056
+ node.children = node.children.filter((child) => !ctx.removed.has(child.pointer));
1057
+ }
1058
+ }
1059
+ for (const pointer of ctx.flagged) {
1060
+ const node = nodes.get(toPointer(pointer));
1061
+ if (node)
1062
+ node.recursiveExpansion = true;
1063
+ }
1064
+ for (const [pointer, reasons] of ctx.boundaries) {
1065
+ const node = nodes.get(toPointer(pointer));
1066
+ if (node) {
1067
+ ;
1068
+ node.boundaries = ['recursion', 'budget'].filter((reason) => reasons.has(reason));
1069
+ }
1070
+ }
980
1071
  return { nodes, diagnostics };
981
1072
  }
982
1073
  //# sourceMappingURL=projection.js.map