dynamic_reference_context.h 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. #pragma once
  2. #include <cstdlib>
  3. #include <deque>
  4. #include <map>
  5. #include <optional>
  6. #include <jvalidate/detail/anchor.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. sources_.push_back(source);
  42. for (auto const & [k, v] : frame) {
  43. // If we have not currently registered this anchor, use the input
  44. // reference path, else use the first reference path registered in the
  45. // stack.
  46. data_[k].push_back(data_[k].empty() ? v : data_[k].front());
  47. }
  48. // For all of the anchors that are not being pushed onto the stack, push a
  49. // nullopt onto the stack (as well as ensuring that all stacks are
  50. // equal-sized). This allows us to disable certain anchors in a given
  51. // document (i.e. that doc does not define a specific $dynamicAnchor).
  52. for (auto & [k, stack] : data_) {
  53. if (not frame.contains(k)) {
  54. stack.emplace_back(std::nullopt);
  55. }
  56. while (stack.size() < sources_.size()) {
  57. stack.emplace_front(std::nullopt);
  58. }
  59. }
  60. // Scope object the pops all of the elements on this object, due to the
  61. // equal-size promise of the above while loop, we can just blindly loop
  62. // through all elements to pop instead of dealing with
  63. return [this]() {
  64. sources_.pop_back();
  65. for (auto it = data_.begin(); it != data_.end();) {
  66. if (it->second.size() == 1) {
  67. it = data_.erase(it);
  68. } else {
  69. it->second.pop_back();
  70. ++it;
  71. }
  72. }
  73. };
  74. }
  75. /**
  76. * @brief Is the given anchor in the current $dynamicRef lookup context
  77. * (including suppressed anchors). This check is necessary in part because we
  78. * permit using $dynamicRef to refer regular $anchor objects if there is no
  79. * $dynamicAnchor in the current context.
  80. *
  81. * TODO(samjaffe): May be able to add a nullopt check...
  82. */
  83. bool contains(Anchor const & key) const { return data_.contains(key); }
  84. /**
  85. * @brief Safely fetch the closest matching $dynamicAnchor to the given
  86. * arguments. Because $dynamicRef permits including a URI, it is techinically
  87. * possible to "jump" to an anchor that is not the top-level one, this can
  88. * be useful if the current scope does not generate a bookmark $dynamicAnchor.
  89. *
  90. * @param source The owning source, which is either the URI of the currently
  91. * operating schema document, or a URI specified in the $dynamicRef value.
  92. * Using this information lets us jump past suppressed anchors by explicitly
  93. * stating the owning context.
  94. *
  95. * @param key The actual anchor being searched for.
  96. */
  97. std::optional<Reference> lookup(URI const & source, Anchor const & key) const {
  98. if (auto it = data_.find(key); it != data_.end()) {
  99. return it->second.at(index_of(source));
  100. }
  101. return std::nullopt;
  102. }
  103. /**
  104. * @brief Finds the (index of the) dynamic anchor directly associated with the
  105. * given URI; or the final registered anchor.
  106. */
  107. size_t index_of(URI const & source) const {
  108. // Start at the end because most commonly source will refer to the currently
  109. // operating schema, which is also going to be the top item in the sources
  110. // stack.
  111. for (size_t i = sources_.size(); i-- > 0;) {
  112. if (sources_[i] == source) {
  113. return i;
  114. }
  115. }
  116. return sources_.size() - 1;
  117. }
  118. bool empty() const { return data_.empty(); }
  119. };
  120. }