dynamic_reference_context.h 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. #pragma once
  2. #include <deque>
  3. #include <map>
  4. #include <optional>
  5. #include <jvalidate/detail/anchor.h>
  6. #include <jvalidate/detail/debug.h>
  7. #include <jvalidate/detail/on_block_exit.h>
  8. #include <jvalidate/detail/reference.h>
  9. #include <jvalidate/uri.h>
  10. namespace jvalidate::detail {
  11. /**
  12. * @brief Starting with Draft 2019-09, it is possible to set $recursiveAnchor
  13. * and $recursiveRef options in schemas. In Draft 2020-12 this changes to the
  14. * more powerful/generic $dynamicAnchor and $dynamicRef.
  15. *
  16. * The rules of handling these anchors is that we maintain a stack of all of the
  17. * loaded anchors for each given name in the order that they are loaded. But the
  18. * resolved reference that they point to is the first anchor by that name to be
  19. * loaded. This means that we can create recursive/self-referential chains.
  20. *
  21. * When we encounter the appropriate $dynamicRef/$recursiveRef tag, we fetch
  22. * the most appropriate anchored location - which is usually the prior mentioned
  23. * first path that registered the anchor.
  24. */
  25. class DynamicReferenceContext {
  26. private:
  27. std::deque<URI> sources_;
  28. std::map<Anchor, std::deque<std::optional<Reference>>> data_;
  29. public:
  30. /**
  31. * @brief Add all dynamic anchors contained in a given document (as defined
  32. * by a common URI) to the current stack, pointing them to the earliest loaded
  33. * parent reference and unregistering all of the anchors that are in context,
  34. * but not in this specific document.
  35. */
  36. OnBlockExit scope(URI const & source, std::map<Anchor, Reference> const & frame) {
  37. // No-Op loading, for convenience
  38. if (frame.empty() && data_.empty()) {
  39. return nullptr;
  40. }
  41. JVALIDATE_DEBUG("scope " << source << ": " << frame);
  42. sources_.push_back(source);
  43. for (auto const & [k, v] : frame) {
  44. // If we have not currently registered this anchor, use the input
  45. // reference path, else use the first reference path registered in the
  46. // stack.
  47. data_[k].push_back(data_[k].empty() ? v : data_[k].front());
  48. }
  49. // For all of the anchors that are not being pushed onto the stack, push a
  50. // nullopt onto the stack (as well as ensuring that all stacks are
  51. // equal-sized). This allows us to disable certain anchors in a given
  52. // document (i.e. that doc does not define a specific $dynamicAnchor).
  53. for (auto & [k, stack] : data_) {
  54. if (not frame.contains(k)) {
  55. stack.push_back(std::nullopt);
  56. }
  57. while (stack.size() < sources_.size()) {
  58. stack.push_front(std::nullopt);
  59. }
  60. }
  61. // Scope object the pops all of the elements on this object, due to the
  62. // equal-size promise of the above while loop, we can just blindly loop
  63. // through all elements to pop instead of dealing with
  64. return [this]() {
  65. sources_.pop_back();
  66. for (auto it = data_.begin(); it != data_.end();) {
  67. if (it->second.size() == 1) {
  68. it = data_.erase(it);
  69. } else {
  70. it->second.pop_back();
  71. ++it;
  72. }
  73. }
  74. };
  75. }
  76. /**
  77. * @brief Is the given anchor in the current $dynamicRef lookup context
  78. * (including suppressed anchors). This check is necessary in part because we
  79. * permit using $dynamicRef to refer regular $anchor objects if there is no
  80. * $dynamicAnchor in the current context.
  81. *
  82. * TODO(samjaffe): May be able to add a nullopt check...
  83. */
  84. bool contains(Anchor const & key) const { return data_.contains(key); }
  85. /**
  86. * @brief Safely fetch the closest matching $dynamicAnchor to the given
  87. * arguments. Because $dynamicRef permits including a URI, it is techinically
  88. * possible to "jump" to an anchor that is not the top-level one, this can
  89. * be useful if the current scope does not generate a bookmark $dynamicAnchor.
  90. *
  91. * @param source The owning source, which is either the URI of the currently
  92. * operating schema document, or a URI specified in the $dynamicRef value.
  93. * Using this information lets us jump past suppressed anchors by explicitly
  94. * stating the owning context.
  95. *
  96. * @param key The actual anchor being searched for.
  97. */
  98. std::optional<Reference> lookup(URI const & source, Anchor const & key) const {
  99. if (auto it = data_.find(key); it != data_.end()) {
  100. return it->second.at(index_of(source));
  101. }
  102. return std::nullopt;
  103. }
  104. /**
  105. * @brief Finds the (index of the) dynamic anchor directly associated with the
  106. * given URI; or the final registered anchor.
  107. */
  108. size_t index_of(URI const & source) const {
  109. // Start at the end because most commonly source will refer to the currently
  110. // operating schema, which is also going to be the top item in the sources
  111. // stack.
  112. for (size_t i = sources_.size(); i-- > 0;) {
  113. if (sources_[i] == source) {
  114. return i;
  115. }
  116. }
  117. return sources_.size() - 1;
  118. }
  119. bool empty() const { return data_.empty(); }
  120. };
  121. }