Skip to content

Commit

Permalink
feat: Add YAML-to-struct conversion utilities and improve logging setup
Browse files Browse the repository at this point in the history
- Added YAML converters for `ModelSettings`, `PopulationDemographic`,
`TransmissionSettings`, and `date::year_month_day` structs.
- Introduced `ConfigData` struct to group configuration data sections.
- Updated `Config` class to load, reload, and notify observers for
configuration changes using the new `ConfigData` structure.
- Integrated `sol2` and `Lua` for dynamic configuration validation.
- Added `Logger` class for initializing loggers using `spdlog`, with a
dedicated logger for `ConfigValidator` and network operations.
- Updated CMakeLists to include dependencies: Sol2, Lua, spdlog, and
date libraries.
- Added unit tests for YAML converters, including date and
configuration-related structs, using Google Test.
- Removed example tests and added relevant configuration tests.
- Added prompts for generating GTest test cases and YAML struct
converters for `ModelSettings`.
  • Loading branch information
merlinvn committed Oct 2, 2024
1 parent 545698a commit 2f6bdaa
Show file tree
Hide file tree
Showing 23 changed files with 942 additions and 84 deletions.
147 changes: 147 additions & 0 deletions not_used/ConfigValidator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#include "ConfigValidator.h"

#include <iostream>

#include "Config.h"

bool ConfigValidator::Validate(const ConfigData &config) {
try {
// Add more validations as needed
return true;
} catch (const std::exception &e) {
std::cerr << "Validation failed: " << e.what() << std::endl;
return false;
}
}

bool ConfigValidator::ValidateAgainstSchema(const ConfigData &config,
const YAML::Node &schema) {
// Implement dynamic validation logic here (e.g., using Lua or schema-based
// validation)
return true; // Placeholder
}

// Recursive function to convert YAML node to Lua table using Sol2
sol::table ConfigValidator::PushYamlToLua(sol::state &lua,
const YAML::Node &node) {
sol::table lua_table = lua.create_table();

for (auto it = node.begin(); it != node.end(); ++it) {
std::string key;
try {
key = it->first.as<std::string>();
} catch (const YAML::BadConversion &e) {
// Handle invalid key conversion
key = "invalid_key"; // Or handle appropriately
}

const YAML::Node &value = it->second;

try {
if (value.IsScalar()) {
if (value.Tag() == "tag:yaml.org,2002:int") {
lua_table[key] = value.as<int>();
} else if (value.Tag() == "tag:yaml.org,2002:float") {
lua_table[key] = value.as<double>();
} else if (value.Tag() == "tag:yaml.org,2002:bool") {
lua_table[key] = value.as<bool>();
} else if (value.IsNull()) {
lua_table[key] = sol::lua_nil;
} else {
lua_table[key] = value.as<std::string>();
}
} else if (value.IsMap()) {
lua_table[key] = PushYamlToLua(lua, value);
} else if (value.IsSequence()) {
sol::table array_table = lua.create_table();
int index = 1;
for (const auto &element : value) {
if (element.IsScalar()) {
if (element.Tag() == "tag:yaml.org,2002:int") {
array_table[index++] = element.as<int>();
} else if (element.Tag() == "tag:yaml.org,2002:float") {
array_table[index++] = element.as<double>();
} else if (element.Tag() == "tag:yaml.org,2002:bool") {
array_table[index++] = element.as<bool>();
} else if (element.IsNull()) {
array_table[index++] = sol::lua_nil;
} else {
array_table[index++] = element.as<std::string>();
}
} else {
array_table[index++] = PushYamlToLua(lua, element);
}
}
lua_table[key] = array_table;
}
} catch (const YAML::BadConversion &e) {
// Handle conversion error, possibly logging and setting Lua to nil or a
// default value
lua_table[key] = sol::lua_nil;
}
}

return lua_table;
}

// Load YAML configuration into Lua
void ConfigValidator::LoadConfigToLua(sol::state &lua,
const YAML::Node &config) {
sol::table lua_config = PushYamlToLua(lua, config);
lua["config"] = lua_config;
// Debugging: Print Lua table contents
std::cout << "Lua 'config' table contents:\n";
for (const auto &pair : lua_config) {
std::string key = pair.first.as<std::string>();
sol::object value = pair.second;
std::cout << key << " = ";
switch (value.get_type()) {
case sol::type::lua_nil:
std::cout << "nil";
break;
case sol::type::boolean:
std::cout << (value.as<bool>() ? "true" : "false");
break;
case sol::type::number:
std::cout << value.as<double>();
break;
case sol::type::string:
std::cout << "\"" << value.as<std::string>() << "\"";
break;
case sol::type::table:
std::cout << "table";
break;
default:
std::cout << "other";
break;
}
std::cout << "\n";
}
}

// Example validation function using Lua for dynamic rules
bool ConfigValidator::ValidateAgainstLua(const ConfigData &config,
const YAML::Node &schema,
sol::state &lua) {
LoadConfigToLua(lua, schema);

// You can define or load Lua conditions here based on the schema
std::string condition = R"(
if physics.gravity > 9.8 and simulation.duration > 1000 then
return false
else
return true
end
)";

sol::protected_function_result result =
lua.safe_script(condition, sol::script_pass_on_error);
if (!result.valid()) {
sol::error err = result;
std::cerr << "Lua validation error: " << err.what() << std::endl;
return false;
}

return result;
}

46 changes: 46 additions & 0 deletions not_used/ConfigValidator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// ConfigValidator.h
#ifndef CONFIGVALIDATOR_H
#define CONFIGVALIDATOR_H

#include <yaml-cpp/yaml.h>

#include <sol/sol.hpp>

class ConfigData;

class ConfigValidator {
public:
ConfigValidator() = default;
~ConfigValidator() = default;

// Prevent copying and moving
ConfigValidator(const ConfigValidator &) = delete;
ConfigValidator(ConfigValidator &&) = delete;
ConfigValidator &operator=(const ConfigValidator &) = delete;
ConfigValidator &operator=(ConfigValidator &&) = delete;

// Validate the config data
bool Validate(const ConfigData &config);

bool ValidateAgainstLua(const ConfigData &config, const YAML::Node &schema,
sol::state &lua);

// Optionally, validate against YAML schema rules
bool ValidateAgainstSchema(const ConfigData &config,
const YAML::Node &schema);

// Helper method to load entire config into Lua
void LoadConfigToLua(sol::state &lua, const YAML::Node &config);

// Helper method to convert YAML to Lua tables
sol::table PushYamlToLua(sol::state &lua, const YAML::Node &node);

private:
// Helper functions for different validation rules
// void ValidateTimestep(double timestep);
// void ValidateGravity(double gravity);
// Other validation logic for fields
};

#endif // CONFIGVALIDATOR_H

37 changes: 37 additions & 0 deletions not_used/ConfigValidator_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "Configuration/ConfigValidator.h"

#include <gtest/gtest.h>
#include <spdlog/spdlog.h>

class ConfigValidatorTest : public ::testing::Test {
protected:
ConfigValidator validator;

void SetUp() override {
// Optional: Initialize any shared resources
}
void TearDown() override {
// Optional: Clean up any shared resources
}
};

TEST_F(ConfigValidatorTest, Validate) {
YAML::Node node = YAML::Load("{ settings: { pi: 3.14159}, alpha: 0.5 }");
// YAML::Emitter out;
// out << node;
// spdlog::info("node: {}", out.c_str());

// Create a Lua state and load Sol2's standard libraries
sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::package);

validator.LoadConfigToLua(lua, node);

sol::table config = lua["config"];

sol::table settings = config["settings"];

EXPECT_DOUBLE_EQ(settings.get<double>("pi"), 3.14159);

EXPECT_DOUBLE_EQ(config["alpha"], 0.5);
}
6 changes: 6 additions & 0 deletions promts/generate_gtest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
```
As an expert in C++ programming, would you mind help me to write a gtest for the following function.
Suggest me a test file name and using Test Fixture class.
```
46 changes: 46 additions & 0 deletions promts/struct_to_yaml_converter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
```
Here is the example of using yaml-cpp to convert yaml to Vec3 class
namespace YAML {
template<>
struct convert<Vec3> {
static Node encode(const Vec3& rhs) {
Node node;
node.push_back(rhs.x);
node.push_back(rhs.y);
node.push_back(rhs.z);
return node;
}
static bool decode(const Node& node, Vec3& rhs) {
if(!node.IsSequence() || node.size() != 3) {
return false;
}
rhs.x = node[0].as<double>();
rhs.y = node[1].as<double>();
rhs.z = node[2].as<double>();
return true;
}
};
}
Then you could use Vec3 wherever you could use any other type:
YAML::Node node = YAML::Load("start: [1, 3, 0]");
Vec3 v = node["start"].as<Vec3>();
node["end"] = Vec3(2, -1, 0);
As an expert in C++ developer, would you mind make the similar convert function for the following struct:
struct ModelSettings {
int days_between_stdout_output; // Frequency of stdout output, in days
int initial_seed_number; // Seed for random number generator
bool record_genome_db; // Flag to record genomic data
date::year_month_day starting_date; // Simulation start date (YYYY/MM/DD)
date::year_month_day
start_of_comparison_period; // Start of comparison period (YYYY/MM/DD)
date::year_month_day ending_date; // Simulation end date (YYYY/MM/DD)
int start_collect_data_day; // Day to start collecting data
};
```
7 changes: 7 additions & 0 deletions promts/yaml_to_struct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```
As an expret in C++ programming, would you mind help me to convert the yaml which is from the input file to C++ struct.
For date type, you can use the date library from HowardHinnant.
```
15 changes: 13 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
find_package(fmt CONFIG REQUIRED)
find_package(GSL REQUIRED)
find_package(yaml-cpp CONFIG REQUIRED)

include_directories(${PROJECT_SOURCE_DIR}/src)
find_package(sol2 CONFIG REQUIRED)
find_package(Lua REQUIRED)
find_package(spdlog REQUIRED)
find_package(date CONFIG REQUIRED)

include_directories(
${PROJECT_SOURCE_DIR}/src
${LUA_INCLUDE_DIR}
)

# craete source files
# Add source files for the core library
Expand All @@ -23,6 +30,10 @@ target_link_libraries(MalaSimCore PUBLIC
fmt::fmt
GSL::gsl GSL::gslcblas
yaml-cpp::yaml-cpp
${LUA_LIBRARIES}
sol2
spdlog::spdlog
date::date date::date-tz
)

set_property(TARGET MalaSimCore PROPERTY CXX_STANDARD 20)
Expand Down
70 changes: 29 additions & 41 deletions src/Configuration/Config.cpp
Original file line number Diff line number Diff line change
@@ -1,44 +1,32 @@

#include "Config.h"

#include <iostream>

bool Config::ValidateNode(const YAML::Node &node, const YAML::Node &schema) {
for (auto it = schema.begin(); it != schema.end(); ++it) {
std::string key = it->first.as<std::string>();
const YAML::Node &schema_field = it->second;

// Check if the field is required and present
if (schema_field["required"] && schema_field["required"].as<bool>()
&& !node[key]) {
std::cerr << "Missing required field: " << key << std::endl;
return false;
}

// If the field exists, check the type
if (node[key]) {
std::string expected_type = schema_field["type"].as<std::string>();
if (expected_type == "double" && !node[key].IsScalar()) {
std::cerr << "Invalid type for field: " << key << " (expected double)"
<< std::endl;
return false;
}
if (expected_type == "string" && !node[key].IsScalar()) {
std::cerr << "Invalid type for field: " << key << " (expected string)"
<< std::endl;
return false;
}

// Additional checks like min, max can be added
if (expected_type == "double" && schema_field["min"]) {
double value = node[key].as<double>();
if (value < schema_field["min"].as<double>()) {
std::cerr << "Value for " << key
<< " is less than the minimum allowed: "
<< schema_field["min"].as<double>() << std::endl;
return false;
}
}
}
}
return true;
#include <yaml-cpp/yaml.h>

#include <mutex>

#include "YAMLConverters.h"

void Config::Load(const std::string &filename) {
std::shared_lock lock(mutex_);
config_file_path_ = filename;
YAML::Node config = YAML::LoadFile(filename);
config_data_.model_settings = config["ModelSettings"].as<ModelSettings>();
config_data_.transmission_settings =
config["TransmissionSettings"].as<TransmissionSettings>();
config_data_.population_demographic =
config["PopulationDemographic"].as<PopulationDemographic>();
NotifyObservers();
}

void Config::Reload() { Load(config_file_path_); }

void Config::RegisterObserver(ConfigObserver observer) {
std::unique_lock lock(mutex_);
observers_.push_back(observer);
}

void Config::NotifyObservers() {
for (const auto &observer : observers_) { observer(config_data_); }
}

Loading

0 comments on commit 2f6bdaa

Please sign in to comment.