1
.gitignore
vendored
1
.gitignore
vendored
@@ -71,6 +71,7 @@ subprojects/qhull/
|
||||
subprojects/libconstants/
|
||||
subprojects/liblogging/
|
||||
subprojects/packagecache/
|
||||
subprojects/glaze/
|
||||
|
||||
qhull.wrap
|
||||
|
||||
|
||||
16
build-config/glaze/meson.build
Normal file
16
build-config/glaze/meson.build
Normal file
@@ -0,0 +1,16 @@
|
||||
glaze_cmake_options = cmake.subproject_options()
|
||||
|
||||
glaze_cmake_options.add_cmake_defines({
|
||||
'BUILD_SHARED_LIBS': 'OFF',
|
||||
'BUILD_STATIC_LIBS': 'ON',
|
||||
'CMAKE_INSTALL_LIBDIR': get_option('libdir'),
|
||||
'CMAKE_INSTALL_INCLUDEDIR': get_option('includedir'),
|
||||
'CMAKE_POSITION_INDEPENDENT_CODE': 'ON',
|
||||
'galze_BUILD_EXAMPLES': 'OFF',
|
||||
})
|
||||
|
||||
glaze_sp = cmake.subproject(
|
||||
'glaze',
|
||||
options: glaze_cmake_options,
|
||||
)
|
||||
glaze_dep = glaze_sp.dependency('glaze_glaze')
|
||||
@@ -1,2 +1,2 @@
|
||||
cmake = import('cmake')
|
||||
subdir('yaml-cpp')
|
||||
subdir('glaze')
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
cmake = import('cmake')
|
||||
yaml_cpp_cmake_options = cmake.subproject_options()
|
||||
|
||||
yaml_cpp_cmake_options.add_cmake_defines({
|
||||
'BUILD_SHARED_LIBS': 'OFF',
|
||||
'BUILD_STATIC_LIBS': 'ON',
|
||||
'YAML_CPP_BUILD_TESTS': 'OFF',
|
||||
'CMAKE_CXX_FLAGS': '-Wno-shadow',
|
||||
'CMAKE_C_FLAGS': '-Wno-shadow',
|
||||
'CMAKE_INSTALL_LIBDIR': get_option('libdir'),
|
||||
'CMAKE_INSTALL_INCLUDEDIR': get_option('includedir'),
|
||||
'CMAKE_POLICY_VERSION_MINIMUM': '3.5',
|
||||
'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'
|
||||
})
|
||||
|
||||
yaml_cpp_sp = cmake.subproject(
|
||||
'yaml-cpp',
|
||||
options: yaml_cpp_cmake_options,
|
||||
)
|
||||
|
||||
|
||||
yaml_cpp_tgt = yaml_cpp_sp.target('yaml-cpp')
|
||||
yaml_cpp_inc = yaml_cpp_sp.include_directories('yaml-cpp')
|
||||
empty_yaml_cpp_file = configure_file(
|
||||
output: 'yaml_cpp_dummy_ar.cpp',
|
||||
command: ['echo'],
|
||||
capture: true
|
||||
)
|
||||
libyaml_static = static_library(
|
||||
'yaml_cpp-static',
|
||||
empty_yaml_cpp_file,
|
||||
objects: [yaml_cpp_tgt.extract_all_objects(recursive: true)],
|
||||
include_directories: yaml_cpp_inc,
|
||||
pic: true,
|
||||
install: false
|
||||
)
|
||||
|
||||
|
||||
yaml_cpp_dep = declare_dependency(
|
||||
link_with: libyaml_static,
|
||||
include_directories: yaml_cpp_inc,
|
||||
)
|
||||
4
examples/config_example.toml
Normal file
4
examples/config_example.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[main]
|
||||
x = 10
|
||||
y = 20
|
||||
person = {age = 30, name = "Alice"}
|
||||
@@ -1,4 +0,0 @@
|
||||
men: ["John Smith", "Bill Jones"]
|
||||
women:
|
||||
- Karol Boudreaux
|
||||
- Emily Boudreaux
|
||||
@@ -1,35 +1,46 @@
|
||||
#include "fourdst/config/config.h"
|
||||
#include "glaze/glaze.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
std::string get_validated_filename(int argc, char* argv[]) {
|
||||
if (argc != 2) {
|
||||
std::cout << "Usage: " << argv[0] << " <input_file>" << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
using namespace fourdst::config;
|
||||
|
||||
std::filesystem::path const file_path{ argv[1] };
|
||||
struct sub {
|
||||
double x;
|
||||
double y;
|
||||
};
|
||||
|
||||
if (not std::filesystem::exists(file_path) || not std::filesystem::is_regular_file(file_path)) {
|
||||
std::cout << "Error: File does not exist or is not a regular file." << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
struct BoundaryConditions {
|
||||
double pressure = 1e6;
|
||||
sub sub;
|
||||
};
|
||||
|
||||
return file_path.string();
|
||||
}
|
||||
struct ExampleConfig {
|
||||
double parameterA = 1.0;
|
||||
int parameterB = 1.0;
|
||||
std::string parameterC = "default_value";
|
||||
std::vector<double> parameterD = {0.1, 0.2, 0.3};
|
||||
BoundaryConditions boundaryConditions;
|
||||
};
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
fourdst::config::Config& config = fourdst::config::Config::getInstance();
|
||||
struct Person {
|
||||
int age;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
std::string filename = get_validated_filename(argc, argv);
|
||||
config.loadConfig(filename);
|
||||
struct AppConfig {
|
||||
double x;
|
||||
double y;
|
||||
Person person;
|
||||
};
|
||||
|
||||
auto men = config.get<std::vector<std::string>>("men", {});
|
||||
int main() {
|
||||
const Config<ExampleConfig> cfg;
|
||||
cfg.save();
|
||||
cfg.save_schema(".");
|
||||
|
||||
for (const auto& name : men) {
|
||||
std::cout << "men are " << name << std::endl;
|
||||
}
|
||||
|
||||
}
|
||||
Config<AppConfig> loaded;
|
||||
loaded.save_schema(".");
|
||||
loaded.load("config_example.toml");
|
||||
std::println("{}", loaded);
|
||||
}
|
||||
@@ -2,18 +2,22 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
struct AppConfig {
|
||||
double x;
|
||||
double y;
|
||||
std::vector<std::string> men;
|
||||
};
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
fourdst::config::Config& config = fourdst::config::Config::getInstance();
|
||||
fourdst::config::Config<AppConfig> config;
|
||||
config.load("/input.toml");
|
||||
|
||||
config.loadConfig("/input.yaml");
|
||||
auto x = config->x;
|
||||
const auto& men = config->men;
|
||||
|
||||
auto men = config.get<std::vector<std::string>>("men", {});
|
||||
|
||||
for (const auto& name : men) {
|
||||
std::cout << "men are " << name << std::endl;
|
||||
}
|
||||
for (const auto& name : men) {
|
||||
std::cout << "men are " << name << std::endl;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
28
meson.build
28
meson.build
@@ -18,7 +18,7 @@
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# *********************************************************************** #
|
||||
project('libconfig', 'cpp', version: 'v1.1.4', default_options: ['cpp_std=c++23'], meson_version: '>=1.5.0')
|
||||
project('libconfig', 'cpp', version: 'v2.0.0', default_options: ['cpp_std=c++23'], meson_version: '>=1.5.0')
|
||||
|
||||
# Add default visibility for all C++ targets
|
||||
add_project_arguments('-fvisibility=default', language: 'cpp')
|
||||
@@ -35,16 +35,16 @@ if get_option('build_examples')
|
||||
subdir('examples')
|
||||
endif
|
||||
|
||||
if get_option('pkg_config')
|
||||
message('Generating pkg-config file for libconfig...')
|
||||
pkg = import('pkgconfig')
|
||||
pkg.generate(
|
||||
name: 'libconfig',
|
||||
description: 'Configuration module for SERiF and related projects',
|
||||
version: meson.project_version(),
|
||||
libraries: [libconfig],
|
||||
subdirs: ['fourdst'],
|
||||
filebase: 'fourdst_config',
|
||||
install_dir: join_paths(get_option('libdir'), 'pkgconfig')
|
||||
)
|
||||
endif
|
||||
#if get_option('pkg_config')
|
||||
# message('Generating pkg-config file for libconfig...')
|
||||
# pkg = import('pkgconfig')
|
||||
# pkg.generate(
|
||||
# name: 'libconfig',
|
||||
# description: 'Configuration module for SERiF and related projects',
|
||||
# version: meson.project_version(),
|
||||
# libraries: [libconfig],
|
||||
# subdirs: ['fourdst'],
|
||||
# filebase: 'fourdst_config',
|
||||
# install_dir: join_paths(get_option('libdir'), 'pkgconfig')
|
||||
# )
|
||||
#endif
|
||||
|
||||
166
src/config/include/fourdst/config/base.h
Normal file
166
src/config/include/fourdst/config/base.h
Normal file
@@ -0,0 +1,166 @@
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
#include "glaze/toml.hpp"
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
#include "fourdst/config/registry.h"
|
||||
#include "fourdst/config/exceptions/exceptions.h"
|
||||
|
||||
namespace fourdst::config::utils {
|
||||
inline std::string extract_error_key(const std::string& buffer, const glz::error_ctx& pe) {
|
||||
if (pe.location >= buffer.size()) return "unknown";
|
||||
|
||||
size_t line_start = pe.location;
|
||||
while (line_start > 0 && buffer[line_start - 1] != '\n') {
|
||||
line_start--;
|
||||
}
|
||||
|
||||
size_t line_end = pe.location;
|
||||
while (line_end < buffer.size() && buffer[line_end] != '\n' && buffer[line_end] != '\r') {
|
||||
line_end++;
|
||||
}
|
||||
|
||||
std::string line = buffer.substr(line_start, line_end - line_start);
|
||||
|
||||
const size_t separator_pos = line.find_first_of("=:");
|
||||
|
||||
if (separator_pos != std::string::npos) {
|
||||
std::string key_part = line.substr(0, separator_pos);
|
||||
|
||||
while (!key_part.empty() && std::isspace(key_part.back())) {
|
||||
key_part.pop_back();
|
||||
}
|
||||
|
||||
size_t first_char = 0;
|
||||
while (first_char < key_part.size() && std::isspace(key_part[first_char])) {
|
||||
first_char++;
|
||||
}
|
||||
|
||||
if (first_char < key_part.size()) {
|
||||
return key_part.substr(first_char);
|
||||
}
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
namespace fourdst::config {
|
||||
enum class ConfigState {
|
||||
DEFAULT,
|
||||
LOADED_FROM_FILE
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class Config {
|
||||
public:
|
||||
Config() {
|
||||
(void)m_registrar;
|
||||
}
|
||||
|
||||
const T* operator->() const { return &m_content.main; }
|
||||
const T& main() const { return m_content.main; }
|
||||
|
||||
void save(std::optional<std::string_view> path = std::nullopt) const {
|
||||
if (!path) {
|
||||
path = std::string(glz::name_v<T>) + ".toml";
|
||||
}
|
||||
auto err = glz::write_file_toml(m_content, path.value(), std::string{});
|
||||
if (err) {
|
||||
throw exceptions::ConfigSaveError(
|
||||
std::format(
|
||||
"Config::save: Failed to save config to {} with glaze error {} ",
|
||||
std::string(path.value()),
|
||||
glz::format_error(err.ec)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void load(const std::string_view path) {
|
||||
std::string buffer;
|
||||
const auto ec = glz::file_to_buffer(buffer, path);
|
||||
if (ec != glz::error_code::none) {
|
||||
throw exceptions::ConfigLoadError(
|
||||
std::format(
|
||||
"Config::load: Failed to load config from {} with glaze error {} ",
|
||||
std::string(path),
|
||||
glz::format_error(ec)
|
||||
)
|
||||
);
|
||||
}
|
||||
auto err = glz::read<
|
||||
glz::opts{
|
||||
.format = glz::TOML,
|
||||
.error_on_unknown_keys = true
|
||||
}>(m_content, buffer);
|
||||
if (err) {
|
||||
throw exceptions::ConfigParseError(
|
||||
std::format(
|
||||
"Config::load: Failed to parse config from {} with glaze error {} (Key: {}) ",
|
||||
std::string(path),
|
||||
glz::format_error(err.ec),
|
||||
utils::extract_error_key(buffer, err)
|
||||
)
|
||||
);
|
||||
}
|
||||
m_state = ConfigState::LOADED_FROM_FILE;
|
||||
}
|
||||
|
||||
void save_schema(const std::string_view dir) const {
|
||||
Registry::generate_named(dir, std::string(glz::name_v<T>));
|
||||
}
|
||||
|
||||
ConfigState get_state() const { return m_state; }
|
||||
|
||||
std::string describe_state() const {
|
||||
switch (m_state) {
|
||||
case ConfigState::DEFAULT:
|
||||
return "DEFAULT";
|
||||
case ConfigState::LOADED_FROM_FILE:
|
||||
return "LOADED_FROM_FILE";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct Content {
|
||||
T main;
|
||||
};
|
||||
Content m_content;
|
||||
ConfigState m_state = ConfigState::DEFAULT;
|
||||
|
||||
|
||||
struct Registrar {
|
||||
Registrar() {
|
||||
const auto name = std::string(glz::name_v<T>);
|
||||
Registry::register_schema<Content>(name);
|
||||
}
|
||||
};
|
||||
static inline Registrar m_registrar;
|
||||
};
|
||||
}
|
||||
|
||||
template <typename T, typename CharT>
|
||||
struct std::formatter<fourdst::config::Config<T>, CharT> {
|
||||
|
||||
static constexpr auto parse(auto& ctx) { return ctx.begin(); }
|
||||
|
||||
auto format(const fourdst::config::Config<T>& config, auto& ctx) const {
|
||||
const T& inner_value = config.main();
|
||||
struct Content {
|
||||
T main;
|
||||
};
|
||||
Content content{inner_value};
|
||||
std::string buffer;
|
||||
const glz::error_ctx ec = glz::write<glz::opts{.format=glz::TOML, .prettify = true}>(content, buffer);
|
||||
if (ec) {
|
||||
return std::format_to(ctx.out(), "Error serializing config");
|
||||
}
|
||||
|
||||
return std::format_to(ctx.out(), "{}", buffer);
|
||||
}
|
||||
};
|
||||
@@ -20,227 +20,6 @@
|
||||
// *********************************************************************** */
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include "fourdst/config/base.h"
|
||||
#include "fourdst/config/exceptions/exceptions.h"
|
||||
|
||||
// Required for YAML parsing
|
||||
#include "yaml-cpp/yaml.h"
|
||||
|
||||
// -- Forward Def of Resource manager to let it act as a friend of Config --
|
||||
// Note this is for SERiF development
|
||||
namespace serif::resource { class ResourceManager; }
|
||||
|
||||
class configTestPrivateAccessor; // Forward declaration for test utility
|
||||
|
||||
namespace fourdst::config {
|
||||
|
||||
/**
|
||||
* @class Config
|
||||
* @brief Singleton class to manage configuration settings loaded from a YAML file.
|
||||
*/
|
||||
class Config {
|
||||
private:
|
||||
/**
|
||||
* @brief Private constructor to prevent instantiation.
|
||||
*/
|
||||
Config();
|
||||
|
||||
/**
|
||||
* @brief Destructor.
|
||||
*/
|
||||
~Config();
|
||||
|
||||
YAML::Node yamlRoot; ///< Root node of the YAML configuration.
|
||||
std::string configFilePath; ///< Path to the configuration file.
|
||||
bool debug = false; ///< Flag to enable debug output.
|
||||
bool loaded = false; ///< Flag to indicate if the configuration has been loaded.
|
||||
|
||||
std::map<std::string, YAML::Node> configMap; ///< Cache for the location of configuration settings.
|
||||
std::vector<std::string> unknownKeys; ///< Cache for the existence of configuration settings.
|
||||
|
||||
/**
|
||||
* @brief Get a value from the configuration cache.
|
||||
* @tparam T Type of the value to retrieve.
|
||||
* @param key Key of the configuration value.
|
||||
* @param defaultValue Default value to return if the key does not exist.
|
||||
* @return Configuration value of type T.
|
||||
*/
|
||||
template <typename T>
|
||||
T getFromCache(const std::string &key, T defaultValue) {
|
||||
if (configMap.find(key) != configMap.end()) {
|
||||
try {
|
||||
return configMap[key].as<T>();
|
||||
} catch (const YAML::Exception& e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Check if a key exists in the configuration cache.
|
||||
* @param key Key to check.
|
||||
* @return True if the key exists in the cache, false otherwise.
|
||||
*/
|
||||
bool isKeyInCache(const std::string &key);
|
||||
|
||||
/**
|
||||
* @brief Add a key-value pair to the configuration cache.
|
||||
* @param key Key of the configuration value.
|
||||
* @param node YAML node containing the configuration value.
|
||||
*/
|
||||
void addToCache(const std::string &key, const YAML::Node &node);
|
||||
|
||||
/**
|
||||
* @brief Register a key as not found in the configuration.
|
||||
* @param key Key that was not found.
|
||||
*/
|
||||
void registerUnknownKey(const std::string &key);
|
||||
|
||||
bool m_loaded = false;
|
||||
|
||||
// Only friends can access get without a default value
|
||||
template <typename T>
|
||||
T get(const std::string &key) {
|
||||
if (!m_loaded) {
|
||||
throw std::runtime_error("Error! Config file not loaded");
|
||||
}
|
||||
if (has(key)) {
|
||||
return getFromCache<T>(key, T());
|
||||
} else {
|
||||
throw std::runtime_error("Error! Key not found in config file");
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Get the singleton instance of the Config class.
|
||||
* @return Reference to the Config instance.
|
||||
*/
|
||||
static Config& getInstance();
|
||||
|
||||
Config (const Config&) = delete;
|
||||
Config& operator= (const Config&) = delete;
|
||||
Config (Config&&) = delete;
|
||||
Config& operator= (Config&&) = delete;
|
||||
|
||||
void setDebug(bool debug) { this->debug = debug; }
|
||||
|
||||
/**
|
||||
* @brief Load configuration from a YAML file.
|
||||
* @param configFilePath Path to the YAML configuration file.
|
||||
* @return True if the configuration was loaded successfully, false otherwise.
|
||||
*/
|
||||
bool loadConfig(const std::string& configFilePath);
|
||||
|
||||
/**
|
||||
* @brief Get the input table from the configuration.
|
||||
* @return Input table as a string.
|
||||
*/
|
||||
std::string getInputTable() const;
|
||||
|
||||
/**
|
||||
* @brief Get a configuration value by key.
|
||||
* @tparam T Type of the value to retrieve.
|
||||
* @param key Key of the configuration value.
|
||||
* @param defaultValue Default value to return if the key does not exist.
|
||||
* @return Configuration value of type T.
|
||||
*
|
||||
* @example
|
||||
* @code
|
||||
* Config& config = Config::getInstance();
|
||||
* config.loadConfig("example.yaml");
|
||||
* int maxIter = config.get<int>("opac:lowTemp:numeric:maxIter", 10);
|
||||
*/
|
||||
template <typename T>
|
||||
T get(const std::string &key, T defaultValue) {
|
||||
if (!m_loaded) {
|
||||
// ONLY THROW ERROR IF HARSH OR WARN CONFIGURATION
|
||||
#if defined(CONFIG_HARSH)
|
||||
throw std::runtime_error("Error! Config file not loaded. To disable this error, recompile with CONFIG_HARSH=0");
|
||||
#elif defined(CONFIG_WARN)
|
||||
std::cerr << "Warning! Config file not loaded. This instance of 4DSSE was compiled with CONFIG_WARN so the code will continue using only default values" << std::endl;
|
||||
#endif
|
||||
}
|
||||
// --- Check if the key has already been checked for existence
|
||||
if (std::find(unknownKeys.begin(), unknownKeys.end(), key) != unknownKeys.end()) {
|
||||
return defaultValue; // If the key has already been added to the unknown cache do not traverse the YAML tree or hit the cache
|
||||
}
|
||||
|
||||
// --- Check if the key is already in the cache (avoid traversing YAML nodes)
|
||||
if (isKeyInCache(key)) {
|
||||
return getFromCache<T>(key, defaultValue);
|
||||
}
|
||||
// --- If the key is not in the cache, check the YAML file
|
||||
else {
|
||||
YAML::Node node = YAML::Clone(yamlRoot);
|
||||
std::istringstream keyStream(key);
|
||||
std::string subKey;
|
||||
while (std::getline(keyStream, subKey, ':')) {
|
||||
if (!node[subKey]) {
|
||||
// Key does not exist
|
||||
registerUnknownKey(key);
|
||||
return defaultValue;
|
||||
}
|
||||
node = node[subKey]; // go deeper
|
||||
}
|
||||
|
||||
try {
|
||||
// Key exists and is of the requested type
|
||||
addToCache(key, node);
|
||||
return node.as<T>();
|
||||
} catch (const YAML::Exception& e) {
|
||||
// Key is not of the requested type
|
||||
registerUnknownKey(key);
|
||||
return defaultValue; // return default value if the key does not exist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the key exists in the given config file
|
||||
* @param key Key to check;
|
||||
* @return boolean true or false
|
||||
*/
|
||||
bool has(const std::string &key);
|
||||
|
||||
/**
|
||||
* @brief Get all keys defined in the configuration file.
|
||||
* @return Vector of all keys in the configuration file.
|
||||
*/
|
||||
std::vector<std::string> keys() const;
|
||||
|
||||
/**
|
||||
* @brief Print the configuration file path and the YAML root node.
|
||||
* @param os Output stream.
|
||||
* @param config Config object to print.
|
||||
* @return Output stream.
|
||||
*/
|
||||
friend std::ostream& operator<<(std::ostream& os, const Config& config) {
|
||||
if (!config.m_loaded) {
|
||||
os << "Config file not loaded" << std::endl;
|
||||
return os;
|
||||
}
|
||||
if (!config.debug) {
|
||||
os << "Config file: " << config.configFilePath << std::endl;
|
||||
} else{
|
||||
// Print entire YAML file from root
|
||||
os << "Config file: " << config.configFilePath << std::endl;
|
||||
os << config.yamlRoot << std::endl;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
// Setup gTest class as a friend
|
||||
friend class ::configTestPrivateAccessor; // Friend declaration for global test accessor
|
||||
// -- Resource Manager is a friend of config so it can create a seperate instance
|
||||
friend class serif::resource::ResourceManager; // Adjusted friend declaration
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
38
src/config/include/fourdst/config/exceptions/exceptions.h
Normal file
38
src/config/include/fourdst/config/exceptions/exceptions.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace fourdst::config::exceptions {
|
||||
class ConfigError : public std::exception {
|
||||
public:
|
||||
ConfigError(const std::string & what): m_msg(what) {}
|
||||
|
||||
const char* what() const noexcept override {
|
||||
return m_msg.c_str();
|
||||
}
|
||||
private:
|
||||
std::string m_msg;
|
||||
};
|
||||
|
||||
class ConfigSaveError final : public ConfigError {
|
||||
using ConfigError::ConfigError;
|
||||
};
|
||||
|
||||
class ConfigLoadError final : public ConfigError {
|
||||
using ConfigError::ConfigError;
|
||||
};
|
||||
|
||||
class ConfigParseError final : public ConfigError {
|
||||
using ConfigError::ConfigError;
|
||||
};
|
||||
|
||||
class SchemaGenerationError final : public ConfigError {
|
||||
using ConfigError::ConfigError;
|
||||
};
|
||||
|
||||
class SchemaNameError final : public ConfigError {
|
||||
using ConfigError::ConfigError;
|
||||
};
|
||||
|
||||
}
|
||||
58
src/config/include/fourdst/config/registry.h
Normal file
58
src/config/include/fourdst/config/registry.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <glaze/glaze.hpp>
|
||||
#include <exception>
|
||||
#include "fourdst/config/exceptions/exceptions.h"
|
||||
|
||||
namespace fourdst::config {
|
||||
|
||||
struct Registry {
|
||||
using SchemaWriter = std::function<void(std::filesystem::path)>;
|
||||
|
||||
static std::unordered_map<std::string, SchemaWriter>& get_writers() {
|
||||
static std::unordered_map<std::string, SchemaWriter> writers;
|
||||
return writers;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static void register_schema(const std::string& name) {
|
||||
auto& writers = get_writers();
|
||||
|
||||
writers.insert({name, [name](const std::filesystem::path &dir) {
|
||||
|
||||
auto schema_r = glz::write_json_schema<T>();
|
||||
if (!schema_r.has_value()) {
|
||||
throw std::runtime_error("Failed to generate schema for " + name);
|
||||
}
|
||||
std::string schema = schema_r.value();
|
||||
|
||||
const auto path = dir / (name + ".schema.json");
|
||||
const auto err = glz::buffer_to_file(schema, path.string());
|
||||
|
||||
if (err != glz::error_code::none) {
|
||||
throw exceptions::SchemaGenerationError("Failed to write schema for " + name + " to " + path.string());
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
static void generate_all(const std::filesystem::path& dir) {
|
||||
std::filesystem::create_directories(dir);
|
||||
for (const auto &writer: get_writers() | std::views::values) {
|
||||
writer(dir);
|
||||
}
|
||||
}
|
||||
|
||||
static void generate_named(const std::filesystem::path& dir, const std::string& name) {
|
||||
const auto& writers = get_writers();
|
||||
const auto it = writers.find(name);
|
||||
if (it == writers.end()) {
|
||||
throw exceptions::SchemaNameError("No schema registered with name: " + name);
|
||||
}
|
||||
it->second(dir);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -5,22 +5,23 @@ config_sources = files(
|
||||
|
||||
|
||||
# Define the libconfig library so it can be linked against by other parts of the build system
|
||||
libconfig = library('config',
|
||||
config_sources,
|
||||
include_directories: include_directories('include'),
|
||||
cpp_args: ['-fvisibility=default'],
|
||||
dependencies: [yaml_cpp_dep],
|
||||
install : true)
|
||||
#libconfig = library('config',
|
||||
# config_sources,
|
||||
# include_directories: include_directories('include'),
|
||||
# cpp_args: ['-fvisibility=default'],
|
||||
# dependencies: [yaml_cpp_dep, glaze_dep],
|
||||
# install : true)
|
||||
|
||||
config_dep = declare_dependency(
|
||||
include_directories: include_directories('include'),
|
||||
link_with: libconfig,
|
||||
sources: config_sources,
|
||||
dependencies: [yaml_cpp_dep],
|
||||
# link_with: libconfig,
|
||||
# sources: config_sources,
|
||||
dependencies: [glaze_dep],
|
||||
)
|
||||
|
||||
# Make headers accessible
|
||||
config_headers = files(
|
||||
'include/fourdst/config/config.h'
|
||||
'include/fourdst/config/config.h',
|
||||
'include/fourdst/config/registry.h',
|
||||
)
|
||||
install_headers(config_headers, subdir : 'fourdst/fourdst/config')
|
||||
|
||||
5
subprojects/glaze.wrap
Normal file
5
subprojects/glaze.wrap
Normal file
@@ -0,0 +1,5 @@
|
||||
[wrap-git]
|
||||
url = https://github.com/stephenberry/glaze.git
|
||||
revision = v6.1.0
|
||||
|
||||
[cmake]
|
||||
@@ -1,9 +0,0 @@
|
||||
--- yaml-cpp/src/emitterutils.cpp.bak 2025-07-29 08:42:40
|
||||
+++ yaml-cpp/src/emitterutils.cpp 2025-07-29 08:42:50
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
+#include <cstdint>
|
||||
#include <sstream>
|
||||
|
||||
#include "emitterutils.h"
|
||||
@@ -1,10 +0,0 @@
|
||||
--- yaml-cpp/CMakeLists.txt.orig 2025-07-24 08:18:01
|
||||
+++ yaml-cpp/CMakeLists.txt 2025-07-24 08:18:25
|
||||
@@ -93,7 +93,6 @@
|
||||
|
||||
target_compile_options(yaml-cpp
|
||||
PRIVATE
|
||||
- $<${not-msvc}:-Wall -Wextra -Wshadow -Weffc++ -Wno-long-long>
|
||||
$<${not-msvc}:-pedantic -pedantic-errors>
|
||||
|
||||
$<$<AND:${backport-msvc-runtime},${msvc-rt-mtd-static}>:-MTd>
|
||||
@@ -1,6 +0,0 @@
|
||||
[wrap-git]
|
||||
url = https://github.com/jbeder/yaml-cpp.git
|
||||
revision = yaml-cpp-0.7.0
|
||||
diff_files = yaml-cpp/disableShadowWarnings.patch, yaml-cpp/addCSTDINTToEmitterUtils.patch
|
||||
|
||||
[cmake]
|
||||
@@ -7,38 +7,46 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include "fourdst/config/config.h"
|
||||
#include "test_schema.h"
|
||||
|
||||
std::string EXAMPLE_FILENAME = std::string(getenv("MESON_SOURCE_ROOT")) + "/tests/config/example.yaml";
|
||||
|
||||
std::string get_good_example_file() {
|
||||
const char* source_root = getenv("MESON_SOURCE_ROOT");
|
||||
if (source_root == nullptr) {
|
||||
throw std::runtime_error("MESON_SOURCE_ROOT environment variable is not set.");
|
||||
}
|
||||
return std::string(source_root) + "/tests/config/example_config_files/example.good.toml";
|
||||
}
|
||||
|
||||
|
||||
enum class BAD_FILES {
|
||||
UNKNOWN_KEY,
|
||||
INVALID_TYPE,
|
||||
INCORRECT_ARRAY_SIZE
|
||||
};
|
||||
|
||||
std::string get_bad_example_file(BAD_FILES type) {
|
||||
const char* source_root = getenv("MESON_SOURCE_ROOT");
|
||||
if (source_root == nullptr) {
|
||||
throw std::runtime_error("MESON_SOURCE_ROOT environment variable is not set.");
|
||||
}
|
||||
switch (type) {
|
||||
case BAD_FILES::UNKNOWN_KEY:
|
||||
return std::string(source_root) + "/tests/config/example_config_files/example.unknownkey.toml";
|
||||
case BAD_FILES::INVALID_TYPE:
|
||||
return std::string(source_root) + "/tests/config/example_config_files/example.invalidtype.toml";
|
||||
case BAD_FILES::INCORRECT_ARRAY_SIZE:
|
||||
return std::string(source_root) + "/tests/config/example_config_files/example.incorrectarraysize.toml";
|
||||
}
|
||||
throw std::runtime_error("Invalid BAD_FILES type.");
|
||||
}
|
||||
|
||||
// std::string EXAMPLE_FILENAME = std::string(getenv("MESON_SOURCE_ROOT")) + "/tests/config/example.toml";
|
||||
/**
|
||||
* @file configTest.cpp
|
||||
* @brief Unit tests for the Config class.
|
||||
*/
|
||||
|
||||
class configTestPrivateAccessor {
|
||||
public:
|
||||
static bool callIsKeyInCache(fourdst::config::Config& config, const std::string& key) {
|
||||
return config.isKeyInCache(key);
|
||||
}
|
||||
|
||||
static int callCacheSize(fourdst::config::Config& config) {
|
||||
return config.configMap.size();
|
||||
}
|
||||
|
||||
static void callAddToCache(fourdst::config::Config& config, const std::string& key, const YAML::Node& node) {
|
||||
config.addToCache(key, node);
|
||||
}
|
||||
|
||||
static void callRegisterKeyNotFound(fourdst::config::Config& config, const std::string& key) {
|
||||
config.registerUnknownKey(key);
|
||||
}
|
||||
|
||||
static bool CheckIfKeyUnknown(fourdst::config::Config& config, const std::string& key) {
|
||||
if (std::find(config.unknownKeys.begin(), config.unknownKeys.end(), key) == config.unknownKeys.end()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Test suite for the Config class.
|
||||
@@ -49,64 +57,77 @@ class configTest : public ::testing::Test {};
|
||||
* @brief Test the constructor of the Config class.
|
||||
*/
|
||||
TEST_F(configTest, constructor) {
|
||||
EXPECT_NO_THROW(fourdst::config::Config::getInstance());
|
||||
EXPECT_NO_THROW(fourdst::config::Config<TestConfigSchema>());
|
||||
}
|
||||
|
||||
TEST_F(configTest, loadConfig) {
|
||||
fourdst::config::Config& config = fourdst::config::Config::getInstance();
|
||||
EXPECT_TRUE(config.loadConfig(EXAMPLE_FILENAME));
|
||||
TEST_F(configTest, load_good_file) {
|
||||
using namespace fourdst::config;
|
||||
Config<TestConfigSchema> cfg;
|
||||
EXPECT_NO_THROW(cfg.load(get_good_example_file()));
|
||||
}
|
||||
|
||||
TEST_F(configTest, singletonTest) {
|
||||
fourdst::config::Config& config1 = fourdst::config::Config::getInstance();
|
||||
fourdst::config::Config& config2 = fourdst::config::Config::getInstance();
|
||||
EXPECT_EQ(&config1, &config2);
|
||||
TEST_F(configTest, load_unknown_key_file) {
|
||||
using namespace fourdst::config;
|
||||
Config<TestConfigSchema> cfg;
|
||||
EXPECT_THROW(cfg.load(get_bad_example_file(BAD_FILES::UNKNOWN_KEY)), exceptions::ConfigParseError);
|
||||
}
|
||||
|
||||
TEST_F(configTest, getTest) {
|
||||
fourdst::config::Config& config = fourdst::config::Config::getInstance();
|
||||
config.loadConfig(EXAMPLE_FILENAME);
|
||||
int maxIter = config.get<int>("opac:lowTemp:numeric:maxIter", 10);
|
||||
EXPECT_EQ(maxIter, 100);
|
||||
EXPECT_NE(maxIter, 10);
|
||||
|
||||
std::string logLevel = config.get<std::string>("logLevel", "DEBUG");
|
||||
EXPECT_EQ(logLevel, "INFO");
|
||||
EXPECT_NE(logLevel, "DEBUG");
|
||||
|
||||
float polytropicIndex = config.get<float>("poly:physics:index", 2);
|
||||
EXPECT_EQ(polytropicIndex, 1.5);
|
||||
EXPECT_NE(polytropicIndex, 2);
|
||||
|
||||
float polytropicIndex2 = config.get<float>("poly:physics:index2", 2.0);
|
||||
EXPECT_EQ(polytropicIndex2, 2.0);
|
||||
TEST_F(configTest, load_invalid_type_file) {
|
||||
using namespace fourdst::config;
|
||||
Config<TestConfigSchema> cfg;
|
||||
EXPECT_THROW(cfg.load(get_bad_example_file(BAD_FILES::INVALID_TYPE)), exceptions::ConfigParseError);
|
||||
}
|
||||
|
||||
TEST_F(configTest, secondSingletonTest) {
|
||||
fourdst::config::Config& config = fourdst::config::Config::getInstance();
|
||||
EXPECT_EQ(config.get<int>("opac:lowTemp:numeric:maxIter", 10), 100);
|
||||
TEST_F(configTest, load_incorrect_array_size_file) {
|
||||
using namespace fourdst::config;
|
||||
Config<TestConfigSchema> cfg;
|
||||
EXPECT_THROW(cfg.load(get_bad_example_file(BAD_FILES::INCORRECT_ARRAY_SIZE)), exceptions::ConfigParseError);
|
||||
}
|
||||
|
||||
TEST_F(configTest, isKeyInCacheTest) {
|
||||
fourdst::config::Config& config = fourdst::config::Config::getInstance();
|
||||
config.loadConfig(EXAMPLE_FILENAME);
|
||||
EXPECT_TRUE(configTestPrivateAccessor::callIsKeyInCache(config, "opac:lowTemp:numeric:maxIter"));
|
||||
EXPECT_FALSE(configTestPrivateAccessor::callIsKeyInCache(config, "opac:lowTemp:numeric:maxIter2"));
|
||||
TEST_F(configTest, check_value) {
|
||||
using namespace fourdst::config;
|
||||
Config<TestConfigSchema> cfg;
|
||||
EXPECT_NO_THROW(cfg.load(get_good_example_file()));
|
||||
EXPECT_EQ(cfg->author, "Example Author");
|
||||
}
|
||||
|
||||
TEST_F(configTest, cacheSize) {
|
||||
fourdst::config::Config& config = fourdst::config::Config::getInstance();
|
||||
config.loadConfig(EXAMPLE_FILENAME);
|
||||
EXPECT_EQ(configTestPrivateAccessor::callCacheSize(config), 3);
|
||||
EXPECT_NE(configTestPrivateAccessor::callCacheSize(config), 4);
|
||||
config.get<std::string>("outputDir", "DEBUG");
|
||||
EXPECT_EQ(configTestPrivateAccessor::callCacheSize(config), 4);
|
||||
TEST_F(configTest, nested_values) {
|
||||
using namespace fourdst::config;
|
||||
Config<TestConfigSchema> cfg;
|
||||
EXPECT_NO_THROW(cfg.load(get_good_example_file()));
|
||||
EXPECT_EQ(cfg->physics.convection, false);
|
||||
}
|
||||
|
||||
TEST_F(configTest, unknownKeyTest) {
|
||||
fourdst::config::Config& config = fourdst::config::Config::getInstance();
|
||||
config.loadConfig(EXAMPLE_FILENAME);
|
||||
config.get<int>("opac:lowTemp:numeric:random", 10);
|
||||
EXPECT_FALSE(configTestPrivateAccessor::CheckIfKeyUnknown(config, "opac:lowTemp:numeric:maxIter"));
|
||||
EXPECT_TRUE(configTestPrivateAccessor::CheckIfKeyUnknown(config, "opac:lowTemp:numeric:random"));
|
||||
TEST_F(configTest, override_default) {
|
||||
using namespace fourdst::config;
|
||||
Config<TestConfigSchema> cfg;
|
||||
EXPECT_NO_THROW(cfg.load(get_good_example_file()));
|
||||
EXPECT_EQ(cfg->simulation.time_step, 0.01);
|
||||
}
|
||||
|
||||
TEST_F(configTest, array_values) {
|
||||
using namespace fourdst::config;
|
||||
Config<TestConfigSchema> cfg;
|
||||
EXPECT_NO_THROW(cfg.load(get_good_example_file()));
|
||||
constexpr std::array<int, 3> expected = {1, 0, 1};
|
||||
EXPECT_EQ(cfg->physics.flags, expected);
|
||||
}
|
||||
|
||||
TEST_F(configTest, string_values) {
|
||||
using namespace fourdst::config;
|
||||
Config<TestConfigSchema> cfg;
|
||||
EXPECT_NO_THROW(cfg.load(get_good_example_file()));
|
||||
EXPECT_EQ(cfg->output.format, "csv");
|
||||
}
|
||||
|
||||
TEST_F(configTest, save_default) {
|
||||
using namespace fourdst::config;
|
||||
const Config<TestConfigSchema> cfg;
|
||||
EXPECT_NO_THROW(cfg.save("TestConfigSchema.toml"));
|
||||
}
|
||||
|
||||
TEST_F(configTest, save_schema) {
|
||||
using namespace fourdst::config;
|
||||
const Config<TestConfigSchema> cfg;
|
||||
EXPECT_NO_THROW(cfg.save_schema("./"));
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# High level options
|
||||
logLevel: "INFO"
|
||||
outputDir: output
|
||||
|
||||
# Module options
|
||||
poly:
|
||||
numeric:
|
||||
newtonTol: 1e-6
|
||||
newtonMaxIter: 100
|
||||
gmresTol: 1e-6
|
||||
gmresMaxIter: 100
|
||||
physics:
|
||||
index: 1.5
|
||||
|
||||
# Module options
|
||||
opac:
|
||||
highTemp:
|
||||
physics:
|
||||
table: "/path/to/highTempTable.dat"
|
||||
numeric:
|
||||
tol: 1e-6
|
||||
maxIter: 100
|
||||
lowTemp:
|
||||
physics:
|
||||
table: "/path/to/lowTempTable.dat"
|
||||
numeric:
|
||||
tol: 1e-6
|
||||
maxIter: 100
|
||||
|
||||
mesh:
|
||||
structure:
|
||||
refine: 2
|
||||
19
tests/config/example_config_files/example.good.toml
Normal file
19
tests/config/example_config_files/example.good.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[main]
|
||||
description = "This is an example configuration file."
|
||||
author = "Example Author"
|
||||
|
||||
[main.physics]
|
||||
diffusion = true
|
||||
convection = false
|
||||
radiation = true
|
||||
flags = [1, 0, 1]
|
||||
|
||||
[main.simulation]
|
||||
time_step = 0.01
|
||||
total_time = 100.0
|
||||
output_frequency = 10
|
||||
|
||||
[main.output]
|
||||
format = "csv"
|
||||
directory = "results/"
|
||||
save_plots = true
|
||||
@@ -0,0 +1,19 @@
|
||||
[main]
|
||||
description = "This is an example configuration file."
|
||||
author = "Your Name"
|
||||
|
||||
[main.physics]
|
||||
diffusion = true
|
||||
convection = false
|
||||
radiation = true
|
||||
flags = [1, 2, 3, 5]
|
||||
|
||||
[main.simulation]
|
||||
time_step = 0.01
|
||||
total_time = 100.0
|
||||
output_frequency = 10
|
||||
|
||||
[main.output]
|
||||
format = "csv"
|
||||
directory = "results/"
|
||||
save_plots = true
|
||||
19
tests/config/example_config_files/example.invalidtype.toml
Normal file
19
tests/config/example_config_files/example.invalidtype.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[main]
|
||||
description = "This is an example configuration file."
|
||||
author = "Your Name"
|
||||
|
||||
[main.physics]
|
||||
diffusion = "HELLO"
|
||||
convection = false
|
||||
radiation = true
|
||||
flags = [1, 2, 3]
|
||||
|
||||
[main.simulation]
|
||||
time_step = 0.01
|
||||
total_time = 100.0
|
||||
output_frequency = 10
|
||||
|
||||
[main.output]
|
||||
format = "csv"
|
||||
directory = "results/"
|
||||
save_plots = true
|
||||
19
tests/config/example_config_files/example.unknownkey.toml
Normal file
19
tests/config/example_config_files/example.unknownkey.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[main]
|
||||
description = "This is an example configuration file."
|
||||
autor = "Your Name"
|
||||
|
||||
[main.physics]
|
||||
diffusion = true
|
||||
convection = false
|
||||
radiation = true
|
||||
flags = [1, 2, 3]
|
||||
|
||||
[main.simulation]
|
||||
time_step = 0.01
|
||||
total_time = 100.0
|
||||
output_frequency = 10
|
||||
|
||||
[main.output]
|
||||
format = "csv"
|
||||
directory = "results/"
|
||||
save_plots = true
|
||||
30
tests/config/test_schema.h
Normal file
30
tests/config/test_schema.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
struct PhysicsConfigOptions {
|
||||
bool diffusion;
|
||||
bool convection;
|
||||
bool radiation;
|
||||
std::array<int, 3> flags;
|
||||
};
|
||||
|
||||
struct SimulationConfigOptions {
|
||||
double time_step = 1;
|
||||
double total_time = 10;
|
||||
int output_frequency = 1;
|
||||
};
|
||||
|
||||
struct OutputConfigOptions {
|
||||
std::string directory = "./output";
|
||||
std::string format = "hdf5";
|
||||
bool save_plots = false;
|
||||
};
|
||||
|
||||
struct TestConfigSchema {
|
||||
std::string description;
|
||||
std::string author;
|
||||
|
||||
PhysicsConfigOptions physics;
|
||||
SimulationConfigOptions simulation;
|
||||
OutputConfigOptions output;
|
||||
};
|
||||
Reference in New Issue
Block a user