|
| 1 | +class AllOfMergeCompatibleBranches final : public SchemaTransformRule { |
| 2 | +public: |
| 3 | + using mutates = std::true_type; |
| 4 | + using reframe_after_transform = std::true_type; |
| 5 | + AllOfMergeCompatibleBranches() |
| 6 | + : SchemaTransformRule{"allof_merge_compatible_branches", ""} {}; |
| 7 | + |
| 8 | + [[nodiscard]] auto |
| 9 | + condition(const sourcemeta::core::JSON &schema, |
| 10 | + const sourcemeta::core::JSON &, |
| 11 | + const sourcemeta::core::Vocabularies &vocabularies, |
| 12 | + const sourcemeta::core::SchemaFrame &frame, |
| 13 | + const sourcemeta::core::SchemaFrame::Location &location, |
| 14 | + const sourcemeta::core::SchemaWalker &walker, |
| 15 | + const sourcemeta::core::SchemaResolver &) const |
| 16 | + -> SchemaTransformRule::Result override { |
| 17 | + static const JSON::String KEYWORD{"allOf"}; |
| 18 | + ONLY_CONTINUE_IF(vocabularies.contains_any( |
| 19 | + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, |
| 20 | + Vocabularies::Known::JSON_Schema_2019_09_Applicator, |
| 21 | + Vocabularies::Known::JSON_Schema_Draft_7, |
| 22 | + Vocabularies::Known::JSON_Schema_Draft_6, |
| 23 | + Vocabularies::Known::JSON_Schema_Draft_4}) && |
| 24 | + schema.is_object() && schema.defines(KEYWORD) && |
| 25 | + schema.at(KEYWORD).is_array() && |
| 26 | + schema.at(KEYWORD).size() >= 2); |
| 27 | + |
| 28 | + ONLY_CONTINUE_IF(!frame.has_references_through( |
| 29 | + location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); |
| 30 | + |
| 31 | + const auto &branches{schema.at(KEYWORD)}; |
| 32 | + |
| 33 | + for (std::size_t index_a = 0; index_a < branches.size(); ++index_a) { |
| 34 | + const auto &branch_a{branches.at(index_a)}; |
| 35 | + if (!is_mergeable_branch(branch_a)) { |
| 36 | + continue; |
| 37 | + } |
| 38 | + |
| 39 | + for (std::size_t index_b = index_a + 1; index_b < branches.size(); |
| 40 | + ++index_b) { |
| 41 | + const auto &branch_b{branches.at(index_b)}; |
| 42 | + if (!is_mergeable_branch(branch_b)) { |
| 43 | + continue; |
| 44 | + } |
| 45 | + |
| 46 | + const auto a_is_type_only{is_type_only_branch(branch_a)}; |
| 47 | + const auto b_is_type_only{is_type_only_branch(branch_b)}; |
| 48 | + if (!a_is_type_only && !b_is_type_only) { |
| 49 | + continue; |
| 50 | + } |
| 51 | + |
| 52 | + const auto &non_type_branch{a_is_type_only ? branch_b : branch_a}; |
| 53 | + if (has_in_place_applicators(non_type_branch)) { |
| 54 | + continue; |
| 55 | + } |
| 56 | + |
| 57 | + if (has_overlapping_keywords(branch_a, branch_b)) { |
| 58 | + continue; |
| 59 | + } |
| 60 | + |
| 61 | + if (has_cross_dependencies(branch_a, branch_b, walker, vocabularies)) { |
| 62 | + continue; |
| 63 | + } |
| 64 | + |
| 65 | + this->merge_into_ = index_a; |
| 66 | + this->merge_from_ = index_b; |
| 67 | + return true; |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + return false; |
| 72 | + } |
| 73 | + |
| 74 | + auto transform(JSON &schema, const Result &) const -> void override { |
| 75 | + static const JSON::String KEYWORD{"allOf"}; |
| 76 | + auto &target{schema.at(KEYWORD).at(this->merge_into_)}; |
| 77 | + const auto &source{schema.at(KEYWORD).at(this->merge_from_)}; |
| 78 | + target.merge(source.as_object()); |
| 79 | + schema.at(KEYWORD).erase( |
| 80 | + std::next(schema.at(KEYWORD).as_array().begin(), |
| 81 | + static_cast<std::ptrdiff_t>(this->merge_from_))); |
| 82 | + } |
| 83 | + |
| 84 | + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, |
| 85 | + const Pointer &target, |
| 86 | + const Pointer ¤t) const |
| 87 | + -> Pointer override { |
| 88 | + static const JSON::String KEYWORD{"allOf"}; |
| 89 | + const auto relative{target.resolve_from(current)}; |
| 90 | + if (relative.size() < 2 || !relative.at(0).is_property() || |
| 91 | + relative.at(0).to_property() != KEYWORD || !relative.at(1).is_index()) { |
| 92 | + return target; |
| 93 | + } |
| 94 | + |
| 95 | + const auto index{relative.at(1).to_index()}; |
| 96 | + if (index == this->merge_from_) { |
| 97 | + const Pointer old_prefix{current.concat({KEYWORD, this->merge_from_})}; |
| 98 | + const Pointer new_prefix{current.concat({KEYWORD, this->merge_into_})}; |
| 99 | + return target.rebase(old_prefix, new_prefix); |
| 100 | + } |
| 101 | + |
| 102 | + if (index > this->merge_from_) { |
| 103 | + const Pointer old_prefix{current.concat({KEYWORD, index})}; |
| 104 | + const Pointer new_prefix{current.concat({KEYWORD, index - 1})}; |
| 105 | + return target.rebase(old_prefix, new_prefix); |
| 106 | + } |
| 107 | + |
| 108 | + return target; |
| 109 | + } |
| 110 | + |
| 111 | +private: |
| 112 | + static auto is_type_only_branch(const JSON &branch) -> bool { |
| 113 | + return branch.size() == 1 && branch.defines("type"); |
| 114 | + } |
| 115 | + |
| 116 | + static auto has_in_place_applicators(const JSON &branch) -> bool { |
| 117 | + return branch.defines("anyOf") || branch.defines("oneOf") || |
| 118 | + branch.defines("allOf") || branch.defines("not") || |
| 119 | + branch.defines("if"); |
| 120 | + } |
| 121 | + |
| 122 | + static auto is_mergeable_branch(const JSON &branch) -> bool { |
| 123 | + if (!branch.is_object()) { |
| 124 | + return false; |
| 125 | + } |
| 126 | + return !branch.defines("$ref") && !branch.defines("$dynamicRef") && |
| 127 | + !branch.defines("$recursiveRef") && !branch.defines("$id") && |
| 128 | + !branch.defines("$schema") && !branch.defines("id") && |
| 129 | + !branch.defines("$anchor") && !branch.defines("$dynamicAnchor") && |
| 130 | + !branch.defines("$recursiveAnchor"); |
| 131 | + } |
| 132 | + |
| 133 | + static auto has_overlapping_keywords(const JSON &branch_a, |
| 134 | + const JSON &branch_b) -> bool { |
| 135 | + for (const auto &entry : branch_a.as_object()) { |
| 136 | + if (branch_b.defines(entry.first)) { |
| 137 | + return true; |
| 138 | + } |
| 139 | + } |
| 140 | + return false; |
| 141 | + } |
| 142 | + |
| 143 | + static auto |
| 144 | + has_cross_dependencies(const JSON &branch_a, const JSON &branch_b, |
| 145 | + const sourcemeta::core::SchemaWalker &walker, |
| 146 | + const sourcemeta::core::Vocabularies &vocabularies) |
| 147 | + -> bool { |
| 148 | + for (const auto &entry_a : branch_a.as_object()) { |
| 149 | + const auto &metadata{walker(entry_a.first, vocabularies)}; |
| 150 | + for (const auto &dependency : metadata.dependencies) { |
| 151 | + if (branch_b.defines(JSON::String{dependency})) { |
| 152 | + return true; |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + for (const auto &entry_b : branch_b.as_object()) { |
| 158 | + const auto &metadata{walker(entry_b.first, vocabularies)}; |
| 159 | + for (const auto &dependency : metadata.dependencies) { |
| 160 | + if (branch_a.defines(JSON::String{dependency})) { |
| 161 | + return true; |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + return false; |
| 167 | + } |
| 168 | + |
| 169 | + mutable std::size_t merge_into_{0}; |
| 170 | + mutable std::size_t merge_from_{0}; |
| 171 | +}; |
0 commit comments