Skip to content

Commit

Permalink
feat(configurationParser): moves the MergeYamlNodes method to an exte…
Browse files Browse the repository at this point in the history
…rnal function, added some UTs for the new MergeYamlNodes function
  • Loading branch information
Nicogp committed Dec 4, 2024
1 parent 77ab5a1 commit bd69dcf
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 123 deletions.
2 changes: 1 addition & 1 deletion src/agent/configuration_parser/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ set_common_settings()

find_package(yaml-cpp CONFIG REQUIRED)

add_library(ConfigurationParser src/configuration_parser.cpp)
add_library(ConfigurationParser src/configuration_parser.cpp src/configuration_parser_utils.cpp)
target_include_directories(ConfigurationParser PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(ConfigurationParser PUBLIC yaml-cpp::yaml-cpp Logger Config)

Expand Down
13 changes: 0 additions & 13 deletions src/agent/configuration_parser/include/configuration_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,6 @@ namespace configuration
/// @brief The groups information
std::function<std::vector<std::string>()> m_getGroups;

/// @brief Merges two YAML nodes, modifying the base node to include or override values from the
/// override node.
///
/// This function traverses the two YAML nodes. If a key exists in both nodes:
/// - If both values are maps, the function recurses to merge their content.
/// - If both values are sequences, their elements are concatenated.
/// - In all other cases (scalars, aliases, null values), the value from the override node replaces the value in
/// the base node. If a key only exists in the override node, it is added to the base node.
///
/// @param base Reference to the base YAML::Node that will be modified.
/// @param override Const reference to the YAML::Node containing values to merge into the base.
void MergeYamlNodes(YAML::Node& base, const YAML::Node& override);

/// @brief Method for loading the configuration from local file
void LoadLocalConfig();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include <yaml-cpp/yaml.h>

/// @brief Merges two YAML nodes, modifying the baseYaml node to include or additionalYaml values from the
/// additionalYaml node.
///
/// This function traverses the two YAML nodes. If a key exists in both nodes:
/// - If both values are maps, the function recurses to merge their content.
/// - If both values are sequences, their elements are concatenated.
/// - In all other cases (scalars, aliases, null values), the value from the additionalYaml node replaces the value in
/// the baseYaml node. If a key only exists in the additionalYaml node, it is added to the baseYaml node.
///
/// @param baseYaml Reference to the baseYaml YAML::Node that will be modified.
/// @param additionalYaml Const reference to the YAML::Node containing values to merge into the baseYaml.
void MergeYamlNodes(YAML::Node& baseYaml, const YAML::Node& additionalYaml);
110 changes: 1 addition & 109 deletions src/agent/configuration_parser/src/configuration_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <algorithm>
#include <cctype>
#include <config.h>
#include <configuration_parser_utils.hpp>

#include <queue>
#include <unordered_set>
Expand Down Expand Up @@ -124,115 +125,6 @@ namespace configuration
}
}

void ConfigurationParser::MergeYamlNodes(YAML::Node& base, const YAML::Node& override)
{
// Queue to manage nodes to be merged. Pairs of nodes are handled directly.
std::queue<std::pair<YAML::Node, YAML::Node>> nodesToProcess;
nodesToProcess.emplace(base, override);

while (!nodesToProcess.empty())
{
auto [baseNode, overrideNode] = nodesToProcess.front();
nodesToProcess.pop();

// Traverse each key-value pair in the override node.
for (auto it = overrideNode.begin(); it != overrideNode.end(); ++it)
{
const auto key = it->first.as<std::string>();
YAML::Node value = it->second;

if (baseNode[key])
{
// Key exists in the base node.
if (value.IsMap() && baseNode[key].IsMap())
{
// Both values are maps: enqueue for further merging.
nodesToProcess.emplace(baseNode[key], value);
}
else if (value.IsSequence() && baseNode[key].IsSequence())
{
// Merge sequences while preserving the order.
YAML::Node mergedSequence = YAML::Node(YAML::NodeType::Sequence);

// Collect elements from 'override' sequence to preserve insertion order.
std::vector<std::pair<std::string, YAML::Node>> overrideElements;
for (const YAML::Node& elem : value)
{
if (elem.IsScalar())
{
overrideElements.emplace_back(elem.as<std::string>(), elem);
}
else if (elem.IsMap() && elem.begin() != elem.end())
{
overrideElements.emplace_back(elem.begin()->first.as<std::string>(), elem);
}
}

// Track which keys from 'override' sequence are merged.
std::unordered_set<std::string> mergedKeys;

for (const YAML::Node& elem : baseNode[key])
{
std::string elemKey;

// Extract the key based on the type of element.
if (elem.IsScalar())
{
elemKey = elem.as<std::string>();
}
else if (elem.IsMap() && elem.begin() != elem.end())
{
elemKey = elem.begin()->first.as<std::string>();
}
else
{
// Skip elements that don't fit the expected types.
mergedSequence.push_back(elem);
continue;
}

// Common logic for merging elements.
auto overrideItem =
std::find_if(overrideElements.begin(),
overrideElements.end(),
[&elemKey](const auto& pair) { return pair.first == elemKey; });
if (overrideItem != overrideElements.end())
{
mergedSequence.push_back(overrideItem->second);
mergedKeys.insert(overrideItem->first);
}
else
{
mergedSequence.push_back(elem);
}
}

// Add remaining elements from 'override' sequence in order.
for (const auto& [itemKey, itemNode] : overrideElements)
{
if (mergedKeys.find(itemKey) == mergedKeys.end())
{
mergedSequence.push_back(itemNode);
}
}

baseNode[key] = mergedSequence;
}
else
{
// Other cases (scalar, alias, null): overwrite the value.
baseNode[key] = value;
}
}
else
{
// Key does not exist in the base node: add it directly.
baseNode[key] = value;
}
}
}
}

void ConfigurationParser::LoadSharedConfig()
{
LogDebug("Loading shared configuration.");
Expand Down
112 changes: 112 additions & 0 deletions src/agent/configuration_parser/src/configuration_parser_utils.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#include <configuration_parser_utils.hpp>
#include <queue>
#include <unordered_set>

void MergeYamlNodes(YAML::Node& baseYaml, const YAML::Node& additionalYaml)
{
// Queue to manage nodes to be merged. Pairs of nodes are handled directly.
std::queue<std::pair<YAML::Node, YAML::Node>> nodesToProcess;
nodesToProcess.emplace(baseYaml, additionalYaml);

while (!nodesToProcess.empty())
{
auto [baseNode, additionalNode] = nodesToProcess.front();
nodesToProcess.pop();

// Traverse each key-value pair in the additionalYaml node.
for (auto it = additionalNode.begin(); it != additionalNode.end(); ++it)
{
const auto key = it->first.as<std::string>();
YAML::Node value = it->second;

if (baseNode[key])
{
// Key exists in the baseYaml node.
if (value.IsMap() && baseNode[key].IsMap())
{
// Both values are maps: enqueue for further merging.
nodesToProcess.emplace(baseNode[key], value);
}
else if (value.IsSequence() && baseNode[key].IsSequence())
{
// Merge sequences while preserving the order.
YAML::Node mergedSequence = YAML::Node(YAML::NodeType::Sequence);

// Collect elements from 'additionalYaml' sequence to preserve insertion order.
std::vector<std::pair<std::string, YAML::Node>> additionalElements;
for (const YAML::Node& elem : value)
{
if (elem.IsScalar())
{
additionalElements.emplace_back(elem.as<std::string>(), elem);
}
else if (elem.IsMap() && elem.begin() != elem.end())
{
additionalElements.emplace_back(elem.begin()->first.as<std::string>(), elem);
}
}

// Track which keys from 'additionalYaml' sequence are merged.
std::unordered_set<std::string> mergedKeys;

for (const YAML::Node& elem : baseNode[key])
{
std::string elemKey;

// Extract the key based on the type of element.
if (elem.IsScalar())
{
elemKey = elem.as<std::string>();
}
else if (elem.IsMap() && elem.begin() != elem.end())
{
elemKey = elem.begin()->first.as<std::string>();
}
else
{
// Skip elements that don't fit the expected types.
mergedSequence.push_back(elem);
continue;
}

// Common logic for merging elements.
auto additionalItem =
std::find_if(additionalElements.begin(),
additionalElements.end(),
[&elemKey](const auto& pair) { return pair.first == elemKey; });
if (additionalItem != additionalElements.end())
{
mergedSequence.push_back(additionalItem->second);
mergedKeys.insert(additionalItem->first);
}
else
{
mergedSequence.push_back(elem);
}
}

// Add remaining elements from 'additionalYaml' sequence in order.
for (const auto& [itemKey, itemNode] : additionalElements)
{
if (mergedKeys.find(itemKey) == mergedKeys.end())
{
mergedSequence.push_back(itemNode);
}
}

baseNode[key] = mergedSequence;
}
else
{
// Other cases (scalar, alias, null): overwrite the value.
baseNode[key] = value;
}
}
else
{
// Key does not exist in the baseYaml node: add it directly.
baseNode[key] = value;
}
}
}
}
7 changes: 7 additions & 0 deletions src/agent/configuration_parser/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
find_package(GTest CONFIG REQUIRED)
find_package(yaml-cpp CONFIG REQUIRED)

add_executable(ConfigurationParser_test configuration_parser_test.cpp)
configure_target(ConfigurationParser_test)
target_link_libraries(ConfigurationParser_test PUBLIC ConfigurationParser Config GTest::gtest GTest::gtest_main GTest::gmock GTest::gmock_main Logger)
add_test(NAME ConfigParserTest COMMAND ConfigurationParser_test)

add_executable(ConfigurationParserUtils_test configuration_parser_utils_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../src/configuration_parser_utils.cpp)
configure_target(ConfigurationParserUtils_test)
target_include_directories(ConfigurationParserUtils_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)
target_link_libraries(ConfigurationParserUtils_test PUBLIC GTest::gtest GTest::gtest_main GTest::gmock GTest::gmock_main yaml-cpp::yaml-cpp)
add_test(NAME ConfigParserUtilsTest COMMAND ConfigurationParserUtils_test)
Loading

0 comments on commit bd69dcf

Please sign in to comment.