@speclynx/apidom-reference 5.2.4 → 5.2.6

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 (53) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +37 -14
  3. package/dist/apidom-reference.browser.js +1234 -596
  4. package/dist/apidom-reference.browser.min.js +1 -1
  5. package/package.json +29 -29
  6. package/src/bundle/strategies/arazzo-1/visitor.cjs +105 -22
  7. package/src/bundle/strategies/arazzo-1/visitor.mjs +107 -24
  8. package/src/bundle/strategies/openapi-3-1/visitor.cjs +197 -47
  9. package/src/bundle/strategies/openapi-3-1/visitor.mjs +200 -50
  10. package/src/bundle/util.cjs +57 -2
  11. package/src/bundle/util.mjs +52 -0
  12. package/src/dereference/strategies/arazzo-1/index.cjs +20 -17
  13. package/src/dereference/strategies/arazzo-1/index.mjs +14 -11
  14. package/src/dereference/strategies/arazzo-1/selectors/$anchor.cjs +6 -0
  15. package/src/dereference/strategies/arazzo-1/selectors/$anchor.mjs +1 -1
  16. package/src/dereference/strategies/arazzo-1/selectors/uri.cjs +6 -0
  17. package/src/dereference/strategies/arazzo-1/selectors/uri.mjs +1 -1
  18. package/src/dereference/strategies/arazzo-1/source-descriptions.cjs +65 -42
  19. package/src/dereference/strategies/arazzo-1/source-descriptions.mjs +64 -42
  20. package/src/dereference/strategies/arazzo-1/util.cjs +6 -0
  21. package/src/dereference/strategies/arazzo-1/util.mjs +1 -1
  22. package/src/dereference/strategies/arazzo-1/visitor.cjs +97 -35
  23. package/src/dereference/strategies/arazzo-1/visitor.mjs +101 -39
  24. package/src/dereference/strategies/openapi-3-1/index.cjs +2 -1
  25. package/src/dereference/strategies/openapi-3-1/index.mjs +3 -1
  26. package/src/dereference/strategies/openapi-3-1/selectors/$anchor.cjs +16 -4
  27. package/src/dereference/strategies/openapi-3-1/selectors/$anchor.mjs +14 -3
  28. package/src/dereference/strategies/openapi-3-1/selectors/json-pointer.cjs +29 -0
  29. package/src/dereference/strategies/openapi-3-1/selectors/json-pointer.mjs +22 -0
  30. package/src/dereference/strategies/openapi-3-1/selectors/uri.cjs +54 -27
  31. package/src/dereference/strategies/openapi-3-1/selectors/uri.mjs +51 -27
  32. package/src/dereference/strategies/openapi-3-1/util.cjs +75 -14
  33. package/src/dereference/strategies/openapi-3-1/util.mjs +68 -14
  34. package/src/dereference/strategies/openapi-3-1/visitor.cjs +85 -9
  35. package/src/dereference/strategies/openapi-3-1/visitor.mjs +89 -13
  36. package/src/util/url.cjs +35 -2
  37. package/src/util/url.mjs +32 -0
  38. package/types/apidom-reference.d.ts +12 -0
  39. package/types/bundle/strategies/arazzo-1/index.d.ts +1 -1
  40. package/types/bundle/strategies/arazzo-1/visitor.d.ts +51 -6
  41. package/types/bundle/strategies/openapi-3-1/index.d.ts +1 -1
  42. package/types/bundle/strategies/openapi-3-1/visitor.d.ts +59 -3
  43. package/types/bundle/util.d.ts +39 -0
  44. package/types/dereference/strategies/arazzo-1/selectors/$anchor.d.ts +1 -1
  45. package/types/dereference/strategies/arazzo-1/selectors/uri.d.ts +1 -1
  46. package/types/dereference/strategies/arazzo-1/util.d.ts +1 -1
  47. package/types/dereference/strategies/arazzo-1/visitor.d.ts +17 -1
  48. package/types/dereference/strategies/openapi-3-1/selectors/$anchor.d.ts +8 -0
  49. package/types/dereference/strategies/openapi-3-1/selectors/json-pointer.d.ts +9 -0
  50. package/types/dereference/strategies/openapi-3-1/selectors/uri.d.ts +37 -4
  51. package/types/dereference/strategies/openapi-3-1/util.d.ts +53 -2
  52. package/types/dereference/strategies/openapi-3-1/visitor.d.ts +17 -1
  53. package/types/util/url.d.ts +10 -0
@@ -67,6 +67,12 @@ const componentFieldElementByField = {
67
67
  [cf.pathItems.name]: _apidomNsOpenapi.ComponentsPathItemsElement
68
68
  };
69
69
 
70
+ /**
71
+ * A bundled element awaiting placement into the entry document's Components
72
+ * Object field.
73
+ * @public
74
+ */
75
+
70
76
  /**
71
77
  * @public
72
78
  */
@@ -124,18 +130,50 @@ class OpenAPI3_1BundleVisitor {
124
130
  * generic object referenced once as a Parameter and once as a Response).
125
131
  */
126
132
  refractCache;
133
+
134
+ /**
135
+ * Index of `$id`-bearing schemas per document, shared across the entry document
136
+ * and every external document visitor of a single bundle run so it never
137
+ * outlives mutations of the documents (e.g. the final placement of components).
138
+ */
139
+ schema$idIndex;
140
+
141
+ /**
142
+ * Bundled fragments waiting to be placed into the entry document's
143
+ * Components Object, shared across the entry document and every external
144
+ * document visitor. Placement is deferred until the entry document traversal
145
+ * completes: the traversal walks live content, so placing a fragment mid-way
146
+ * would make it descend into the fragment and re-bundle its `$ref`s against
147
+ * the entry document's base URI instead of the fragment's own.
148
+ */
149
+ placements;
150
+
151
+ /**
152
+ * The base URI the traversed content will have once placed into the entry
153
+ * document, when placement changes it: content hoisted out of a non-schema
154
+ * document (a Response, Parameter, Path Item, ...) trades that document's
155
+ * base URI for the entry document's. Unset for the entry document itself and
156
+ * for embedded schema resources, whose `$id` carries their base along.
157
+ */
158
+ relocationBaseURI;
127
159
  constructor({
128
160
  reference,
129
161
  options,
130
162
  assignments = new Map(),
131
163
  reservedNames = new Map(),
132
- refractCache = new WeakMap()
164
+ refractCache = new WeakMap(),
165
+ schema$idIndex = new WeakMap(),
166
+ placements = [],
167
+ relocationBaseURI
133
168
  }) {
134
169
  this.reference = reference;
135
170
  this.options = options;
136
171
  this.assignments = assignments;
137
172
  this.reservedNames = reservedNames;
138
173
  this.refractCache = refractCache;
174
+ this.schema$idIndex = schema$idIndex;
175
+ this.placements = placements;
176
+ this.relocationBaseURI = relocationBaseURI;
139
177
  }
140
178
  getRefracted(source, type) {
141
179
  return this.refractCache.get(source)?.get(type);
@@ -193,6 +231,22 @@ class OpenAPI3_1BundleVisitor {
193
231
  return ref.startsWith('#') ? ref : url.getHash(ref);
194
232
  }
195
233
 
234
+ /**
235
+ * The base URI the given Schema Object's `$ref` will be resolved against once
236
+ * the content is placed into the entry document, or `undefined` when the
237
+ * `$ref` as written resolves to `$refBaseURI` from there as well: the content
238
+ * isn't relocated, an absolute `$id` on the schema's ancestor chain pins its
239
+ * base, or the hoisted document sits next to the entry document.
240
+ */
241
+ relocatedSchemaBaseURI(path, $refBaseURI) {
242
+ if (this.relocationBaseURI === undefined) return undefined;
243
+ const relocatedBaseURI = (0, _util.resolveSchemaBaseURI)(this.relocationBaseURI, path);
244
+ if ((0, _util.resolveSchema$refField)(relocatedBaseURI, path.node) === $refBaseURI) {
245
+ return undefined;
246
+ }
247
+ return relocatedBaseURI;
248
+ }
249
+
196
250
  /**
197
251
  * Handles an error according to the `bundle.continueOnError` option.
198
252
  *
@@ -311,13 +365,16 @@ class OpenAPI3_1BundleVisitor {
311
365
 
312
366
  /**
313
367
  * Computes a collision-free component name within the target field. A name is
314
- * taken if it's already placed in components or reserved by a component that
315
- * is still being bundled (reserved before recursion).
368
+ * taken if it's already present in the entry document's components or reserved
369
+ * by a component that is still being bundled (reserved before recursion).
316
370
  */
317
371
  uniqueName(candidate, field) {
318
- const fieldElement = this.ensureComponentsField(field);
372
+ const {
373
+ components
374
+ } = this.entryResult;
375
+ const fieldElement = (0, _apidomDatamodel.isObjectElement)(components) ? components.get(field) : undefined;
319
376
  const reserved = this.reservedNames.get(field) ?? new Set();
320
- return (0, _util2.uniqueName)(candidate, name => fieldElement.hasKey(name) || reserved.has(name));
377
+ return (0, _util2.uniqueName)(candidate, name => (0, _apidomDatamodel.isObjectElement)(fieldElement) && fieldElement.hasKey(name) || reserved.has(name));
321
378
  }
322
379
 
323
380
  /**
@@ -341,6 +398,28 @@ class OpenAPI3_1BundleVisitor {
341
398
  // append (never prepend) so the result element stays ahead of annotations
342
399
  this.entryParseResult.content.push(annotation);
343
400
  }
401
+
402
+ /**
403
+ * The entry document traversal is rooted at its Parse Result (nested
404
+ * traversals are rooted at fragments), so leaving it marks the moment the
405
+ * whole entry document has been visited and the collected fragments can be
406
+ * placed into its Components Object without the traversal descending into them.
407
+ * Guarded on the node so that a traversal rooted at any other Parse Result
408
+ * never flushes the shared placements early.
409
+ */
410
+ ParseResultElement = {
411
+ leave: path => {
412
+ if (path.node !== this.entryParseResult) return;
413
+ for (const {
414
+ field,
415
+ name,
416
+ element
417
+ } of this.placements) {
418
+ this.ensureComponentsField(field).set(name, element);
419
+ }
420
+ this.placements.length = 0;
421
+ }
422
+ };
344
423
  async ReferenceElement(path) {
345
424
  const referencingElement = path.node;
346
425
  const $ref = (0, _apidomCore.toValue)(referencingElement.$ref);
@@ -428,14 +507,19 @@ class OpenAPI3_1BundleVisitor {
428
507
  if (!this.reservedNames.has(field)) this.reservedNames.set(field, new Set());
429
508
  this.reservedNames.get(field).add(componentName);
430
509
 
431
- // own a copy of the fragment and bundle its own external references
510
+ // own a copy of the fragment and bundle its own external references; the
511
+ // fragment ends up in the entry document, so that is where its content
512
+ // resolves relative references from
432
513
  const hoistedElement = (0, _apidomDatamodel.cloneDeep)(referencedElement);
433
514
  const visitor = new OpenAPI3_1BundleVisitor({
434
515
  reference,
435
516
  options: this.options,
436
517
  assignments: this.assignments,
437
518
  reservedNames: this.reservedNames,
438
- refractCache: this.refractCache
519
+ refractCache: this.refractCache,
520
+ schema$idIndex: this.schema$idIndex,
521
+ placements: this.placements,
522
+ relocationBaseURI: this.entryURI
439
523
  });
440
524
  const bundledElement = await (0, _apidomTraverse.traverseAsync)(hoistedElement, visitor, {
441
525
  mutable: true
@@ -446,8 +530,12 @@ class OpenAPI3_1BundleVisitor {
446
530
  bundledElement.meta.set('ref-origin', reference.uri);
447
531
  }
448
532
 
449
- // place the bundled fragment into the entry document's components
450
- this.ensureComponentsField(field).set(componentName, bundledElement);
533
+ // queue the bundled fragment for placement into the entry document's components
534
+ this.placements.push({
535
+ field,
536
+ name: componentName,
537
+ element: bundledElement
538
+ });
451
539
 
452
540
  // rewrite the referencing element to point at the hoisted fragment
453
541
  referencingElement.set('$ref', internalPointer);
@@ -536,14 +624,19 @@ class OpenAPI3_1BundleVisitor {
536
624
  if (!this.reservedNames.has(field)) this.reservedNames.set(field, new Set());
537
625
  this.reservedNames.get(field).add(componentName);
538
626
 
539
- // own a copy of the fragment and bundle its own external references
627
+ // own a copy of the fragment and bundle its own external references; the
628
+ // Path Item ends up in the entry document, so that is where its content
629
+ // resolves relative references from
540
630
  const hoistedElement = (0, _apidomDatamodel.cloneDeep)(referencedElement);
541
631
  const visitor = new OpenAPI3_1BundleVisitor({
542
632
  reference,
543
633
  options: this.options,
544
634
  assignments: this.assignments,
545
635
  reservedNames: this.reservedNames,
546
- refractCache: this.refractCache
636
+ refractCache: this.refractCache,
637
+ schema$idIndex: this.schema$idIndex,
638
+ placements: this.placements,
639
+ relocationBaseURI: this.entryURI
547
640
  });
548
641
  const bundledElement = await (0, _apidomTraverse.traverseAsync)(hoistedElement, visitor, {
549
642
  mutable: true
@@ -554,8 +647,12 @@ class OpenAPI3_1BundleVisitor {
554
647
  // internal `#/components/pathItems/...` pointer by the child traversal
555
648
  bundledElement.meta.set('ref-origin', reference.uri);
556
649
 
557
- // place the bundled Path Item into the entry document's components
558
- this.ensureComponentsField(field).set(componentName, bundledElement);
650
+ // queue the bundled Path Item for placement into the entry document's components
651
+ this.placements.push({
652
+ field,
653
+ name: componentName,
654
+ element: bundledElement
655
+ });
559
656
 
560
657
  // rewrite the referencing Path Item to point at the hoisted one, keeping
561
658
  // any sibling fields (e.g. summary, description) next to the $ref
@@ -590,7 +687,8 @@ class OpenAPI3_1BundleVisitor {
590
687
  // toReference) so an internal $ref needs no resolution and never trips the
591
688
  // resolve.maxDepth guard, matching how ReferenceElement returns early.
592
689
  const retrievalURI = this.reference.uri;
593
- const $refBaseURI = (0, _util.resolveSchema$refField)(retrievalURI, referencingElement);
690
+ const schemaBaseURI = (0, _util.resolveSchemaBaseURI)(retrievalURI, path);
691
+ const $refBaseURI = (0, _util.resolveSchema$refField)(schemaBaseURI, referencingElement);
594
692
  const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
595
693
  const file = new _File.default({
596
694
  uri: $refBaseURIStrippedHash
@@ -626,59 +724,95 @@ class OpenAPI3_1BundleVisitor {
626
724
  // as a canonical URI / URL against the current document's $id graph; if
627
725
  // that fails, fall back to fetching the external document and resolving
628
726
  // the fragment there as a $anchor or JSON Pointer.
727
+ //
728
+ // the fragment being traversed is searched before the whole document:
729
+ // content hoisted out of an external document (a Response, Parameter, Path
730
+ // Item, ...) is refracted with its own semantics, so the Schema Objects it
731
+ // contains are indexed, whereas the external document as a whole is parsed
732
+ // generically and refracted as a bare JSON Schema, which leaves those Schema
733
+ // Objects (under `content`, `schema`, ...) unrecognized and their `$id`s out
734
+ // of the index
735
+ const documentElement = this.reference.value.result;
736
+ const rootNode = path.getAncestorNodes().at(-1) ?? path.node;
737
+ const fragmentElement = (0, _apidomDatamodel.isParseResultElement)(rootNode) ? rootNode.result : rootNode;
738
+ const uriEvaluateOptions = {
739
+ baseURI: retrievalURI,
740
+ index: this.schema$idIndex
741
+ };
629
742
  let schemaReference = this.reference;
743
+ let resourceRoot;
744
+ let resourceBaseURI;
630
745
  try {
631
- const referenceAsSchema = (0, _util.maybeRefractToSchemaElement)(schemaReference.value.result);
632
- (0, _uri.evaluate)($refBaseURI, referenceAsSchema);
746
+ try {
747
+ (0, _uri.evaluate)($refBaseURI, (0, _util.maybeRefractToSchemaElement)(fragmentElement), uriEvaluateOptions);
748
+ // a $ref that resolves (by $id/$anchor/pointer) within the traversed
749
+ // content is internal even when its URI form (e.g. a URN or absolute
750
+ // $id) is not a bare fragment: the $id resource travels with the content
751
+ // wherever it is placed. Leave it untouched — it resolves against the
752
+ // in-content $id graph, and embedding the content into itself would be
753
+ // wrong.
754
+ return;
755
+ } catch (error) {
756
+ if (!(error instanceof _EvaluationJsonSchemaUriError.default) || fragmentElement === documentElement) {
757
+ throw error;
758
+ }
759
+ const documentAsSchema = (0, _util.maybeRefractToSchemaElement)(documentElement);
760
+ (0, _uri.evaluate)($refBaseURI, documentAsSchema, uriEvaluateOptions);
761
+ // the $id resource sits in the current document but outside the
762
+ // traversed fragment, so it stays behind once the fragment is placed
763
+ // into the entry document. The resource to embed is that $id-bearing
764
+ // schema, addressed by its canonical URI (the $ref without fragment).
765
+ resourceRoot = (0, _uri.evaluate)($refBaseURIStrippedHash, documentAsSchema, uriEvaluateOptions);
766
+ resourceBaseURI = $refBaseURIStrippedHash;
767
+ }
633
768
  } catch (error) {
634
769
  if (isURL && error instanceof _EvaluationJsonSchemaUriError.default) {
770
+ schemaReference = await this.toReference(url.unsanitize($refBaseURI));
771
+ const referenceAsSchema = (0, _util.maybeRefractToSchemaElement)(schemaReference.value.result);
635
772
  if ((0, _$anchor.isAnchor)((0, _$anchor.uriToAnchor)($refBaseURI))) {
636
- schemaReference = await this.toReference(url.unsanitize($refBaseURI));
637
773
  const selector = (0, _$anchor.uriToAnchor)($refBaseURI);
638
- const referenceAsSchema = (0, _util.maybeRefractToSchemaElement)(schemaReference.value.result);
639
774
  (0, _$anchor.evaluate)(selector, referenceAsSchema);
640
775
  } else {
641
- schemaReference = await this.toReference(url.unsanitize($refBaseURI));
642
776
  const selector = _apidomJsonPointer.URIFragmentIdentifier.fromURIReference($refBaseURI);
643
- const referenceAsSchema = (0, _util.maybeRefractToSchemaElement)(schemaReference.value.result);
644
777
  (0, _apidomJsonPointer.evaluate)(referenceAsSchema, selector);
645
778
  }
779
+
780
+ // the resource to embed is the WHOLE external document (its root
781
+ // schema), identified by its $id. A $ref into a fragment of the
782
+ // document embeds the entire document resource once; the fragment
783
+ // keeps resolving against the embedded $id.
784
+ resourceRoot = referenceAsSchema;
785
+ resourceBaseURI = (0, _util.resolveSchema$idField)(url.stripHash(schemaReference.uri), resourceRoot) ?? url.stripHash(schemaReference.uri);
646
786
  } else {
647
787
  throw error;
648
788
  }
649
789
  }
650
-
651
- // a $ref that resolves (by $id/$anchor/pointer) within the current document
652
- // is internal even when its URI form (e.g. a URN or absolute $id) is not a
653
- // bare fragment. Leave it untouched — it resolves against the in-document
654
- // $id graph, and embedding the current document into itself would be wrong.
655
- if (url.stripHash(schemaReference.uri) === url.stripHash(this.reference.uri)) {
656
- return;
657
- }
658
-
659
- // the resource to embed is the WHOLE external document (its root schema),
660
- // identified by its $id. A $ref into a fragment of the document embeds the
661
- // entire document resource once; the fragment keeps resolving against the
662
- // embedded $id.
663
- const resourceRoot = (0, _util.maybeRefractToSchemaElement)(schemaReference.value.result);
664
- const resourceBaseURI = (0, _util.resolveSchema$idField)(url.stripHash(schemaReference.uri), resourceRoot) ?? url.stripHash(schemaReference.uri);
665
790
  const field = cf.schemas.name;
666
791
  const canonicalKey = `${field}\t${resourceBaseURI}`;
667
792
 
668
- // resource already embedded (or being embedded) — leave the referencing
669
- // $ref untouched and stop. This also terminates circular external schema
793
+ // a location-based $ref to a self-identifying resource is rebased onto the
794
+ // resource's $id (keeping its fragment) — once embedded, the resource is
795
+ // reachable only by its $id. A $ref already resolving to that $id is left
796
+ // as written. See rebaseSchema$ref for the spec trade-off.
797
+ //
798
+ // a $ref whose base URI changes on relocation into the entry document
799
+ // (inside a hoisted Response, Parameter, Path Item, ...) is instead
800
+ // rewritten to address the embedded resource from the base it will have
801
+ // there, otherwise it dangles once placed. See relocateSchema$ref.
802
+ const relocatedBaseURI = this.relocatedSchemaBaseURI(path, $refBaseURI);
803
+ const rebased$ref = relocatedBaseURI === undefined ? (0, _util2.rebaseSchema$ref)($refBaseURI, resourceBaseURI) : (0, _util2.relocateSchema$ref)($refBaseURI, resourceBaseURI, relocatedBaseURI, (0, _apidomDatamodel.isStringElement)(resourceRoot.$id));
804
+
805
+ // resource already embedded (or being embedded) — point the referencing
806
+ // $ref at it and stop. This also terminates circular external schema
670
807
  // references, since the assignment is reserved BEFORE recursion.
671
808
  if (this.assignments.has(canonicalKey)) {
809
+ if (rebased$ref !== undefined) referencingElement.set('$ref', rebased$ref);
672
810
  path.skip();
673
811
  return;
674
812
  }
675
813
 
676
- // own a copy of the resource and ensure it carries a $id so the unchanged
677
- // $ref still resolves against it once embedded
814
+ // own a copy of the resource
678
815
  const embeddedElement = (0, _apidomDatamodel.cloneDeep)(resourceRoot);
679
- if (!(0, _apidomDatamodel.isStringElement)(embeddedElement.$id)) {
680
- embeddedElement.set('$id', resourceBaseURI);
681
- }
682
816
 
683
817
  // the embedded thing is the WHOLE resource (keyed by its $id), so its name
684
818
  // is derived from the resource URI, not the $ref fragment — passing a root
@@ -700,20 +834,36 @@ class OpenAPI3_1BundleVisitor {
700
834
  options: this.options,
701
835
  assignments: this.assignments,
702
836
  reservedNames: this.reservedNames,
703
- refractCache: this.refractCache
837
+ refractCache: this.refractCache,
838
+ schema$idIndex: this.schema$idIndex,
839
+ placements: this.placements
704
840
  });
705
841
  const bundledElement = await (0, _apidomTraverse.traverseAsync)(embeddedElement, visitor, {
706
842
  mutable: true
707
843
  });
708
844
 
845
+ // a resource without its own $id is identified by its retrieval URI; write
846
+ // it relative to the entry document (the base of components.schemas) so the
847
+ // bundle carries no absolute filesystem path and stays relocatable. Assigned
848
+ // after the nested traversal so a reader deriving a schema's base from live
849
+ // $ids (see #540) never resolves the resource's own $refs against the entry
850
+ // document.
851
+ if (!(0, _apidomDatamodel.isStringElement)(bundledElement.$id)) {
852
+ bundledElement.set('$id', url.relative(this.entryURI, resourceBaseURI));
853
+ }
854
+
709
855
  // annotate the embedded resource with info about its origin
710
856
  bundledElement.meta.set('ref-origin', schemaReference.uri);
711
857
 
712
- // place the embedded resource into the entry document's components
713
- this.ensureComponentsField(field).set(componentName, bundledElement);
858
+ // queue the embedded resource for placement into the entry document's components
859
+ this.placements.push({
860
+ field,
861
+ name: componentName,
862
+ element: bundledElement
863
+ });
714
864
 
715
- // the referencing $ref is left UNCHANGED — it resolves against the
716
- // embedded resource's $id
865
+ // point the referencing $ref at the embedded resource's $id
866
+ if (rebased$ref !== undefined) referencingElement.set('$ref', rebased$ref);
717
867
  path.skip();
718
868
  } catch (error) {
719
869
  this.handleError(`Error while bundling Schema Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);