reference_cache.h 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. #pragma once
  2. #include <functional>
  3. #include <map>
  4. #include <optional>
  5. #include <jvalidate/detail/pointer.h>
  6. #include <jvalidate/detail/reference.h>
  7. namespace jvalidate::detail {
  8. /**
  9. * @brief An bidirectional cache of absolute references
  10. * (URI + Anchor + JSON-Pointer) to root references (URI + Anchor). An object of
  11. * this sort is necessary for a couple of reasons:
  12. *
  13. * 1. It is possible to have more than one absolute reference map to the same
  14. * root reference.
  15. * 2. We need to employ some special handling to properly identify the nearest
  16. * RootReference that owns a given absolute Reference - given that that ref
  17. * may not actually be anchored to the RootReference at the time.
  18. */
  19. class ReferenceCache {
  20. private:
  21. std::map<Reference, RootReference, std::greater<>> to_anchor_;
  22. std::map<RootReference, Reference, std::greater<>> to_absolute_;
  23. public:
  24. /**
  25. * @brief Register the entry-point of a given schema-document. This function
  26. * should be called exactly one time for each "$id" tag in a given schema that
  27. * is being loaded.
  28. *
  29. * In principle, we should also perform a uniqueness check when calling
  30. * to_absolute_.emplace.
  31. */
  32. void emplace(URI const & root) {
  33. to_absolute_.emplace(root, root);
  34. to_anchor_.emplace(root, root);
  35. }
  36. /**
  37. * @brief Link together the absolute and root references of a document, as
  38. * well as recursively walk through all possible parent URIs that are already
  39. * stored in this cache.
  40. *
  41. * Therefore, this function will add "exactly 1" mapping to the to_absolute_
  42. * map, and "at least 1, but no more than to_absolute_.size()" mappings to
  43. * to_anchor_, representing all of the parent reference paths that link to the
  44. * newly added RootReference
  45. *
  46. * @param absolute An absolute JSON Reference, which either contains no
  47. * RootReference component, or contains the previous traversed RootReference,
  48. * as defined by the $id, $anchor, $recursiveAnchor, or $dynamicAnchor tags.
  49. *
  50. * @param canonical The current RootReference being operated on from the
  51. * tags listed above.
  52. *
  53. * For example, if we have a json document like:
  54. * @code{.json}
  55. * {
  56. * "$id": "A",
  57. * "$defs": {
  58. * "Example": {
  59. * "$id": "B"
  60. * }
  61. * }
  62. * }
  63. * @endcode
  64. *
  65. * then we would end up calling this function with the arguments:
  66. * absolute="#", canonical="A#"
  67. * absolute="A#/$defs/Example", canonical="B#"
  68. */
  69. Reference emplace(Reference const & absolute, RootReference const & canonical) {
  70. for (Reference where = absolute; not where.pointer().empty();) {
  71. // Recursively add all possible alternative paths that are equivalent to
  72. // this absolute path as valid mappings to this canonical one.
  73. auto it = to_absolute_.lower_bound(where.root());
  74. if (it == to_absolute_.end() || where.root() == it->second.root()) {
  75. break;
  76. }
  77. if (auto ptr = it->second.pointer(); where.pointer().starts_with(ptr)) {
  78. where = it->second / where.pointer().relative_to(ptr);
  79. to_anchor_.emplace(where, canonical);
  80. } else {
  81. break;
  82. }
  83. }
  84. to_anchor_.emplace(absolute, canonical);
  85. to_absolute_.emplace(canonical, absolute);
  86. return Reference(canonical);
  87. }
  88. /**
  89. * @brief Identifies the nearest RootReference that is associated with the
  90. * input.
  91. *
  92. * @param ref An arbitrary reference that we want to locate the nearest root
  93. * for.
  94. *
  95. * @param for_parent_reference A flag indicating if we should prohibit exact
  96. * matches of reference. For example, suppose that we have the same bindings
  97. * as in the above method comment:
  98. * absolute="#", canonical="A#"
  99. * absolute="A#/$defs/Example", canonical="B#"
  100. * If I request `relative_to_nearest_anchor("A#/$defs/Example", false)` then
  101. * it will return `B#` as the associated RootReference, because we have stored
  102. * that exact mapping in our absolute path to anchor cache.
  103. *
  104. * On the other hand - suppose that we want to ensure that we've acquired
  105. * strict parent of the current reference.
  106. * `relative_to_nearest_anchor("A#/$defs/Example", true)` would say "an anchor
  107. * cannot be its own parent, therefore we cannot resolve to B#".
  108. *
  109. * @returns ref, recalculated to be relative_to its nearest parent root, if
  110. * one is available.
  111. */
  112. Reference relative_to_nearest_anchor(Reference const & ref,
  113. bool for_parent_reference = false) const {
  114. auto it = for_parent_reference ? to_anchor_.upper_bound(ref) : to_anchor_.lower_bound(ref);
  115. if (it == to_anchor_.end()) {
  116. return ref;
  117. }
  118. auto const & [absolute, anchor] = *it;
  119. if (not ref.pointer().starts_with(absolute.pointer())) {
  120. // We've undershot our reference and landed at a cousin/neighbor node
  121. return ref;
  122. }
  123. return Reference(anchor, ref.pointer().relative_to(absolute.pointer()));
  124. }
  125. /**
  126. * @brief Deduces the URI part of the actual parent of this node, utilizing
  127. * the "an anchor cannot be its own parent" rule described above.
  128. *
  129. * @param parent An arbitrarily constructed reference to the parent context
  130. * of some other reference we are operating on.
  131. *
  132. * @returns The URI of the nearest non-equal parent if it exists and/or there
  133. * is a URI part in parent. If there is no URI part, we check if there is a
  134. * URI for the root (input) schema.
  135. */
  136. URI actual_parent_uri(detail::Reference const & parent) const {
  137. // TODO(samjaffe): Think about this some more - there's something awkward here
  138. URI uri = relative_to_nearest_anchor(parent, true).uri();
  139. if (not uri.empty() || not parent.uri().empty()) {
  140. return uri;
  141. }
  142. // This is a special case because we prohibit exact matches in the above
  143. // relative_to_nearest_anchor call. Since we can only reach this line if
  144. // BOTH uri and parent.uri() are empty - that means that the appropriate
  145. // parent is the root document, which might have marked its URI with an
  146. // $id tag.
  147. if (auto it = to_anchor_.find(Reference()); it != to_anchor_.end()) {
  148. return it->second.uri();
  149. }
  150. return uri;
  151. }
  152. };
  153. }