diff --git a/include/yaml-cpp/exceptions.h b/include/yaml-cpp/exceptions.h index f6b2602ae..9c8da4ce9 100644 --- a/include/yaml-cpp/exceptions.h +++ b/include/yaml-cpp/exceptions.h @@ -87,6 +87,8 @@ const char* const INVALID_ANCHOR = "invalid anchor"; const char* const INVALID_ALIAS = "invalid alias"; const char* const INVALID_TAG = "invalid tag"; const char* const BAD_FILE = "bad file"; +const char* const MERGE_KEY_NEEDS_SINGLE_OR_SEQUENCE_OF_MAPS = + "merge key needs either single map or sequence of maps"; template inline const std::string KEY_NOT_FOUND_WITH_KEY( diff --git a/src/nodebuilder.cpp b/src/nodebuilder.cpp index bbaefac8a..cc1765005 100644 --- a/src/nodebuilder.cpp +++ b/src/nodebuilder.cpp @@ -1,3 +1,4 @@ +#include #include #include "nodebuilder.h" @@ -15,6 +16,7 @@ NodeBuilder::NodeBuilder() m_stack{}, m_anchors{}, m_keys{}, + m_mergeDicts{}, m_mapDepth(0) { m_anchors.push_back(nullptr); // since the anchors start at 1 } @@ -69,11 +71,38 @@ void NodeBuilder::OnMapStart(const Mark& mark, const std::string& tag, node.set_tag(tag); node.set_style(style); m_mapDepth++; + m_mergeDicts.emplace_back(); +} + +static void MergeMapCollection(detail::node& map_to, detail::node& map_from, + detail::shared_memory_holder& pMemory) { + for (auto j = map_from.begin(); j != map_from.end(); j++) { + const auto from_key = j->first; + /// NOTE: const_map_to.get(*j->first) cannot be used here, since it + /// compares only the shared_ptr's, while we need to compare the key + /// itself. + /// + /// NOTE: get() also iterates over elements + bool found = std::any_of(map_to.begin(), map_to.end(), [&](const detail::node_iterator_value & kv) + { + const auto key_node = kv.first; + return key_node->scalar() == from_key->scalar(); + }); + if (!found) + map_to.insert(*from_key, *j->second, pMemory); + } } void NodeBuilder::OnMapEnd() { assert(m_mapDepth > 0); + detail::node& collection = *m_stack.back(); + auto& toMerge = *m_mergeDicts.rbegin(); + /// The elements for merging should be traversed in reverse order to prefer last values. + for (auto it = toMerge.rbegin(); it != toMerge.rend(); ++it) { + MergeMapCollection(collection, **it, m_pMemory); + } m_mapDepth--; + m_mergeDicts.pop_back(); Pop(); } @@ -107,15 +136,40 @@ void NodeBuilder::Pop() { m_stack.pop_back(); detail::node& collection = *m_stack.back(); - if (collection.type() == NodeType::Sequence) { collection.push_back(node, m_pMemory); } else if (collection.type() == NodeType::Map) { assert(!m_keys.empty()); PushedKey& key = m_keys.back(); if (key.second) { - collection.insert(*key.first, node, m_pMemory); - m_keys.pop_back(); + detail::node& nk = *key.first; + if (nk.type() == NodeType::Scalar && + ((nk.tag() == "tag:yaml.org,2002:merge" && nk.scalar() == "<<") || + (nk.tag() == "?" && nk.scalar() == "<<"))) { + if (node.type() == NodeType::Map) { + m_mergeDicts.rbegin()->emplace_back(&node); + m_keys.pop_back(); + } else if (node.type() == NodeType::Sequence) { + for (auto i = node.begin(); i != node.end(); i++) { + auto v = *i; + if ((*v).type() == NodeType::Map) { + m_mergeDicts.rbegin()->emplace_back(&(*v)); + } else { + throw ParserException( + node.mark(), + ErrorMsg::MERGE_KEY_NEEDS_SINGLE_OR_SEQUENCE_OF_MAPS); + } + } + m_keys.pop_back(); + } else { + throw ParserException( + node.mark(), + ErrorMsg::MERGE_KEY_NEEDS_SINGLE_OR_SEQUENCE_OF_MAPS); + } + } else { + collection.insert(*key.first, node, m_pMemory); + m_keys.pop_back(); + } } else { key.second = true; } diff --git a/src/nodebuilder.h b/src/nodebuilder.h index c580d40e2..b053d58e4 100644 --- a/src/nodebuilder.h +++ b/src/nodebuilder.h @@ -67,6 +67,7 @@ class NodeBuilder : public EventHandler { using PushedKey = std::pair; std::vector m_keys; + std::vector m_mergeDicts; std::size_t m_mapDepth; }; } // namespace YAML diff --git a/test/integration/load_node_test.cpp b/test/integration/load_node_test.cpp index 9d0c790fd..f6dab4945 100644 --- a/test/integration/load_node_test.cpp +++ b/test/integration/load_node_test.cpp @@ -1,6 +1,7 @@ #include "yaml-cpp/yaml.h" // IWYU pragma: keep #include "gtest/gtest.h" +#include namespace YAML { namespace { @@ -173,6 +174,78 @@ TEST(LoadNodeTest, CloneAlias) { EXPECT_EQ(clone[0], clone); } +TEST(LoadNodeTest, MergeKeyA) { + Node node = Load( + "{x: &foo {a : 1,b : 1,c : 1}, y: &bar {d: 2, e : 2, f : 2, a : 2}, z: " + "&stuff { << : *foo, b : 3} }"); + EXPECT_EQ(NodeType::Map, node["z"].Type()); + EXPECT_FALSE(node["z"]["<<"]); + EXPECT_EQ(1, node["z"]["a"].as()); + EXPECT_EQ(3, node["z"]["b"].as()); + EXPECT_EQ(1, node["z"]["c"].as()); +} + +TEST(LoadNodeTest, MergeKeyAIterator) { + Node node = Load( + "{x: &foo {a : 1,b : 1,c : 1}, y: &bar {d: 2, e : 2, f : 2, a : 2}, z: " + "&stuff { << : *foo, b : 3} }"); + EXPECT_EQ(NodeType::Map, node["z"].Type()); + + const auto& z = node["z"]; + size_t z_b_keys = std::count_if(z.begin(), z.end(), [&](const detail::iterator_value & kv) + { + return kv.first.as() == "b"; + }); + ASSERT_EQ(z_b_keys, 1); +} + +TEST(LoadNodeTest, MergeKeyTwoOverrides) { + Node node = Load(R"( +trait1: &t1 + foo: 1 + +trait2: &t2 + foo: 2 + +merged: + <<: *t1 + <<: *t2 +)"); + EXPECT_EQ(NodeType::Map, node["merged"].Type()); + EXPECT_FALSE(node["merged"]["<<"]); + EXPECT_EQ(2, node["merged"]["foo"].as()); +} + +TEST(LoadNodeTest, MergeKeyB) { + Node node = Load( + "{x: &foo {a : 1,b : 1,c : 1}, y: &bar {d: 2, e : 2, f : 2, a : 2}, z: " + "&stuff { << : *foo, b : 3}, w: { << : [*stuff, *bar], c: 4 }, v: { '<<' " + ": *foo } , u : {!!merge << : *bar}, t: {!!merge << : *bar, h: 3} }"); + EXPECT_EQ(NodeType::Map, node["z"].Type()); + EXPECT_EQ(NodeType::Map, node["w"].Type()); + EXPECT_FALSE(node["z"]["<<"]); + EXPECT_EQ(1, node["z"]["a"].as()); + EXPECT_EQ(3, node["z"]["b"].as()); + EXPECT_EQ(1, node["z"]["c"].as()); + + EXPECT_EQ(2, node["w"]["a"].as()); + EXPECT_EQ(3, node["w"]["b"].as()); + EXPECT_EQ(4, node["w"]["c"].as()); + EXPECT_EQ(2, node["w"]["d"].as()); + EXPECT_EQ(2, node["w"]["e"].as()); + EXPECT_EQ(2, node["w"]["f"].as()); + + EXPECT_TRUE(node["v"]["<<"]); + EXPECT_EQ(1, node["v"]["<<"]["a"].as()); + + EXPECT_FALSE(node["u"]["<<"]); + EXPECT_EQ(2, node["u"]["d"].as()); + + EXPECT_FALSE(node["t"]["<<"]); + EXPECT_EQ(2, node["t"]["d"].as()); + EXPECT_EQ(3, node["t"]["h"].as()); +} + TEST(LoadNodeTest, ForceInsertIntoMap) { Node node; node["a"] = "b";