feat(reflect-cpp): Switched from glaze -> reflect cpp

A bug was discovered in glaze which prevented valid toml output. We have
switched to toml++ and reflect-cpp. The interface has remained the same
so this should not break any code
This commit is contained in:
2025-12-06 10:55:46 -05:00
parent 2b5abeae58
commit ec13264050
365 changed files with 63946 additions and 357 deletions

View File

@@ -1,53 +1,22 @@
#pragma once
#include <filesystem>
#include "glaze/toml.hpp"
#include <string>
#include <map>
#include <optional>
#include <format>
#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;
}
}
#include "rfl.hpp"
#include "rfl/toml.hpp"
#include "rfl/json.hpp"
namespace fourdst::config {
enum class RootNameLoadPolicy {
FROM_FILE,
KEEP_CURRENT
};
enum class ConfigState {
DEFAULT,
LOADED_FROM_FILE
@@ -56,66 +25,110 @@ namespace fourdst::config {
template <typename T>
class Config {
public:
Config() {
(void)m_registrar;
Config() = default;
const T* operator->() const { return &m_content; }
const T& main() const { return m_content; }
void save(std::string_view path) const {
T default_instance{};
std::unordered_map<std::string, T> wrapper;
wrapper[m_root_name] = m_content;
const std::string toml_string = rfl::toml::write(wrapper);
std::ofstream ofs{std::string(path)};
if (!ofs.is_open()) {
throw exceptions::ConfigSaveError(
std::format("Failed to open file for writing config: {}", path)
);
}
ofs << toml_string;
ofs.close();
}
const T* operator->() const { return &m_content.main; }
const T& main() const { return m_content.main; }
void set_root_name(const std::string_view name) {
m_root_name = name;
}
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)
)
);
[[nodiscard]] std::string_view get_root_name() const {
return m_root_name;
}
void set_root_name_load_policy(const RootNameLoadPolicy policy) {
m_root_name_load_policy = policy;
}
RootNameLoadPolicy get_root_name_load_policy() const {
return m_root_name_load_policy;
}
std::string describe_root_name_load_policy() const {
switch (m_root_name_load_policy) {
case RootNameLoadPolicy::FROM_FILE:
return "FROM_FILE";
case RootNameLoadPolicy::KEEP_CURRENT:
return "KEEP_CURRENT";
default:
return "UNKNOWN";
}
}
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) {
if (m_state == ConfigState::LOADED_FROM_FILE) {
throw exceptions::ConfigLoadError(
"Config has already been loaded from file. Reloading is not supported.");
}
if (!std::filesystem::exists(path)) {
throw exceptions::ConfigLoadError(
std::format("Config file does not exist: {}", path));
}
using wrapper = std::unordered_map<std::string, T>;
const rfl::Result<wrapper> result = rfl::toml::load<wrapper>(std::string(path));
if (!result) {
throw exceptions::ConfigParseError(
std::format("Failed to load config from file: {}", path));
}
std::string loaded_root_name = result.value().begin()->first;
if (m_root_name_load_policy == RootNameLoadPolicy::KEEP_CURRENT && m_root_name != loaded_root_name) {
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)
"Root name mismatch when loading config from file. Current root name is '{}', but file root name is '{}'. If you want to use the root name from the file, set the root name load policy to FROM_FILE using set_root_name_load_policy().",
m_root_name,
loaded_root_name
)
);
}
m_root_name = loaded_root_name;
m_content = result.value().at(loaded_root_name);
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>));
static void save_schema(const std::string& path) {
using wrapper = std::unordered_map<std::string, T>;
const std::string json_schema = rfl::json::to_schema<wrapper>(rfl::json::pretty);
std::ofstream ofs{std::string(path)};
if (!ofs.is_open()) {
throw exceptions::SchemaSaveError(
std::format("Failed to open file for writing schema: {}", path)
);
}
ofs << json_schema;
ofs.close();
}
ConfigState get_state() const { return m_state; }
[[nodiscard]] ConfigState get_state() const { return m_state; }
std::string describe_state() const {
[[nodiscard]] std::string describe_state() const {
switch (m_state) {
case ConfigState::DEFAULT:
return "DEFAULT";
@@ -127,20 +140,10 @@ namespace fourdst::config {
}
private:
struct Content {
T main;
};
Content m_content;
T m_content;
std::string m_root_name = "main";
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;
RootNameLoadPolicy m_root_name_load_policy = RootNameLoadPolicy::KEEP_CURRENT;
};
}
@@ -151,16 +154,9 @@ struct std::formatter<fourdst::config::Config<T>, CharT> {
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::map<std::string, T> wrapper;
wrapper[std::string(config.get_root_name())] = 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);
return buffer;
}
};

View File

@@ -6,9 +6,9 @@
namespace fourdst::config::exceptions {
class ConfigError : public std::exception {
public:
ConfigError(const std::string & what): m_msg(what) {}
explicit ConfigError(const std::string & what): m_msg(what) {}
const char* what() const noexcept override {
[[nodiscard]] const char* what() const noexcept override {
return m_msg.c_str();
}
private:
@@ -27,12 +27,9 @@ namespace fourdst::config::exceptions {
using ConfigError::ConfigError;
};
class SchemaGenerationError final : public ConfigError {
class SchemaSaveError final : public ConfigError {
using ConfigError::ConfigError;
};
class SchemaNameError final : public ConfigError {
using ConfigError::ConfigError;
};
}

View File

@@ -1,58 +0,0 @@
#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);
}
};
}

View File

@@ -1,109 +0,0 @@
/* ***********************************************************************
//
// Copyright (C) 2025 -- The 4D-STAR Collaboration
// File Author: Emily Boudreaux
// Last Modified: March 20, 2025
//
// 4DSSE is free software; you can use it and/or modify
// it under the terms and restrictions the GNU General Library Public
// License version 3 (GPLv3) as published by the Free Software Foundation.
//
// 4DSSE is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Library General Public License for more details.
//
// You should have received a copy of the GNU Library General Public License
// along with this software; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// *********************************************************************** */
#include <string>
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <map>
#include "yaml-cpp/yaml.h"
#include "fourdst/config/config.h"
namespace fourdst::config {
Config::Config() {}
Config::~Config() {}
Config& Config::getInstance() {
static Config instance;
return instance;
}
bool Config::loadConfig(const std::string& configFile) {
configFilePath = configFile;
try {
yamlRoot = YAML::LoadFile(configFile);
} catch (YAML::BadFile& e) {
std::cerr << "Error: " << e.what() << std::endl;
return false;
}
m_loaded = true;
return true;
}
bool Config::isKeyInCache(const std::string &key) {
return configMap.find(key) != configMap.end();
}
void Config::addToCache(const std::string &key, const YAML::Node &node) {
configMap[key] = node;
}
void Config::registerUnknownKey(const std::string &key) {
unknownKeys.push_back(key);
}
bool Config::has(const std::string &key) {
if (!m_loaded) {
throw std::runtime_error("Error! Config file not loaded");
}
if (isKeyInCache(key)) { return true; }
YAML::Node node = YAML::Clone(yamlRoot);
std::istringstream keyStream(key);
std::string subKey;
while (std::getline(keyStream, subKey, ':')) {
if (!node[subKey]) {
registerUnknownKey(key);
return false;
}
node = node[subKey]; // go deeper
}
// Key exists and is of the requested type
addToCache(key, node);
return true;
}
void recurse_keys(const YAML::Node& node, std::vector<std::string>& keyList, const std::string& path = "") {
if (node.IsMap()) {
for (const auto& it : node) {
auto key = it.first.as<std::string>();
auto new_path = path.empty() ? key : path + ":" + key;
recurse_keys(it.second, keyList, new_path);
}
} else {
keyList.push_back(path);
}
}
std::vector<std::string> Config::keys() const {
std::vector<std::string> keyList;
YAML::Node node = YAML::Clone(yamlRoot);
recurse_keys(node, keyList);
return keyList;
}
}

View File

@@ -1,27 +1,11 @@
# Define the library
config_sources = files(
'lib/config.cpp',
)
# 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, glaze_dep],
# install : true)
config_dep = declare_dependency(
include_directories: include_directories('include'),
# link_with: libconfig,
# sources: config_sources,
dependencies: [glaze_dep],
dependencies: [reflect_cpp_dep],
)
# Make headers accessible
config_headers = files(
'include/fourdst/config/config.h',
'include/fourdst/config/registry.h',
'include/fourdst/config/exceptions/exceptions.h',
'include/fourdst/config/base.h',
)
install_headers(config_headers, subdir : 'fourdst/fourdst/config')