dynamic_reference_context.h 4.7 KB

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