Skip to content

Commit

Permalink
Adding support for handling YAML Merge Key (#41)
Browse files Browse the repository at this point in the history
Support for YAML Merge keys ( <<: [*dict1, *dict2] ) is added. The merge
key is a specific scalar with value << (and tag !!merge) that implies
that during node construction, the map (or sequence of maps) are merged
into the current map. The priority rules are that each key from maps
within the value associated with  << are added iff the key is not yet
present in the current map (and first map gets higher priority). Test
cases have been added accordingly.
  • Loading branch information
nlescoua committed Nov 4, 2023
1 parent 51adc5f commit 6bbc603
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 3 deletions.
2 changes: 2 additions & 0 deletions include/yaml-cpp/exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename T>
inline const std::string KEY_NOT_FOUND_WITH_KEY(
Expand Down
48 changes: 45 additions & 3 deletions src/nodebuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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
}
Expand Down Expand Up @@ -71,8 +72,24 @@ void NodeBuilder::OnMapStart(const Mark& mark, const std::string& tag,
m_mapDepth++;
}

void MergeMapCollection(detail::node& map_to, detail::node& map_from,
detail::shared_memory_holder& pMemory) {
const detail::node& const_map_to = map_to;
for (auto j = map_from.begin(); j != map_from.end(); j++) {
detail::node* s = const_map_to.get(*j->first, pMemory);
if (s == nullptr) {
map_to.insert(*j->first, *j->second, pMemory);
}
}
}

void NodeBuilder::OnMapEnd() {
assert(m_mapDepth > 0);
detail::node& collection = *m_stack.back();
for (detail::node* n : m_mergeDicts) {
MergeMapCollection(collection, *n, m_pMemory);
}
m_mergeDicts.clear();
m_mapDepth--;
Pop();
}
Expand Down Expand Up @@ -107,15 +124,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.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.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;
}
Expand Down
1 change: 1 addition & 0 deletions src/nodebuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class NodeBuilder : public EventHandler {

using PushedKey = std::pair<detail::node*, bool>;
std::vector<PushedKey> m_keys;
Nodes m_mergeDicts;
std::size_t m_mapDepth;
};
} // namespace YAML
Expand Down
37 changes: 37 additions & 0 deletions test/integration/load_node_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,43 @@ 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<int>());
EXPECT_EQ(3, node["z"]["b"].as<int>());
EXPECT_EQ(1, node["z"]["c"].as<int>());
}

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} }");
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<int>());
EXPECT_EQ(3, node["z"]["b"].as<int>());
EXPECT_EQ(1, node["z"]["c"].as<int>());

EXPECT_EQ(1, node["w"]["a"].as<int>());
EXPECT_EQ(3, node["w"]["b"].as<int>());
EXPECT_EQ(4, node["w"]["c"].as<int>());
EXPECT_EQ(2, node["w"]["d"].as<int>());
EXPECT_EQ(2, node["w"]["e"].as<int>());
EXPECT_EQ(2, node["w"]["f"].as<int>());

EXPECT_TRUE(node["v"]["<<"]);
EXPECT_EQ(1, node["v"]["<<"]["a"].as<int>());

EXPECT_FALSE(node["u"]["<<"]);
EXPECT_EQ(2, node["u"]["d"].as<int>());
}

TEST(LoadNodeTest, ForceInsertIntoMap) {
Node node;
node["a"] = "b";
Expand Down

0 comments on commit 6bbc603

Please sign in to comment.