From a999d07b7b0c2a95e498d4545d16a740e64146ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Sun, 5 Jul 2026 12:10:01 +0200 Subject: [PATCH 01/22] Added the ToAllCaps preprocessor --- include/rfl.hpp | 1 + include/rfl/ToAllCaps.hpp | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 include/rfl/ToAllCaps.hpp diff --git a/include/rfl.hpp b/include/rfl.hpp index 1a399bc1..9e0d0f5d 100644 --- a/include/rfl.hpp +++ b/include/rfl.hpp @@ -49,6 +49,7 @@ #include "rfl/SnakeCaseToPascalCase.hpp" #include "rfl/TaggedUnion.hpp" #include "rfl/Timestamp.hpp" +#include "rfl/ToAllCaps.hpp" #include "rfl/UnderlyingEnums.hpp" #include "rfl/Validator.hpp" #include "rfl/Variant.hpp" diff --git a/include/rfl/ToAllCaps.hpp b/include/rfl/ToAllCaps.hpp new file mode 100644 index 00000000..4e130943 --- /dev/null +++ b/include/rfl/ToAllCaps.hpp @@ -0,0 +1,44 @@ +#ifndef RFL_TOALLCAPS_HPP_ +#define RFL_TOALLCAPS_HPP_ + +#include "Field.hpp" +#include "internal/is_rename.hpp" +#include "internal/transform_case.hpp" + +namespace rfl { + +struct ToAllCaps { + public: + /// Replaces all instances of field names with all uppercase letters. + /// A named tuple is a tuple-like structure where each element has a name/key. + /// @tparam StructType The type of the struct being processed + /// @param _named_tuple The named tuple containing the struct's fields + /// @return A new named tuple with field names converted to all uppercase + template + static auto process(const auto& _named_tuple) { + return _named_tuple.transform([](const FieldType& _f) { + if constexpr (FieldType::name() != "xml_content" && + !internal::is_rename_v) { + return handle_one_field(_f); + } else { + return _f; + } + }); + } + + private: + /// Applies the camelCase to snake_case transformation to a single field. + /// @tparam FieldType The type of the field being transformed + /// @param _f The field to transform + /// @return A new field with the transformed name and the same value + template + static auto handle_one_field(const FieldType& _f) { + using NewFieldType = Field(), + typename FieldType::Type>; + return NewFieldType(_f.value()); + } +}; + +} // namespace rfl + +#endif From 9795dcbafab6eacf59e4585ef88f056b21883c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Sun, 5 Jul 2026 12:10:13 +0200 Subject: [PATCH 02/22] Added missing modification --- include/rfl/internal/transform_case.hpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/include/rfl/internal/transform_case.hpp b/include/rfl/internal/transform_case.hpp index 0afa0524..082f27cc 100644 --- a/include/rfl/internal/transform_case.hpp +++ b/include/rfl/internal/transform_case.hpp @@ -10,10 +10,25 @@ namespace rfl::internal { constexpr bool is_upper(char c) { return c >= 'A' && c <= 'Z'; } -constexpr char to_upper(char c) { return c >= 'a' && c <= 'z' ? c + ('A' - 'a') : c; } +constexpr char to_upper(char c) { + return c >= 'a' && c <= 'z' ? c + ('A' - 'a') : c; +} constexpr char to_lower(char c) { return is_upper(c) ? c - ('A' - 'a') : c; } +/// Transforms the field name to all uppercase letters. +template +consteval auto to_all_caps() { + constexpr auto src = _name.string_view(); + constexpr auto len = src.size(); + + auto result = std::array{}; + for (size_t i = 0; i < len; ++i) { + result[i] = to_upper(src[i]); + } + return StringLiteral(result); +} + /// Transforms the field name from snake_case to camelCase or PascalCase. template consteval auto transform_snake_case() { @@ -47,8 +62,7 @@ consteval auto transform_camel_case() { if constexpr (!src.empty() && is_upper(src[0])) { return StringLiteral(result.data() + 1); - } - else { + } else { return StringLiteral(result); } } From 5aa0954f405d5ee50b8a9cb091242b24cd879eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Sun, 5 Jul 2026 12:14:41 +0200 Subject: [PATCH 03/22] Fixed typo --- include/rfl/ToAllCaps.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/rfl/ToAllCaps.hpp b/include/rfl/ToAllCaps.hpp index 4e130943..c86bad16 100644 --- a/include/rfl/ToAllCaps.hpp +++ b/include/rfl/ToAllCaps.hpp @@ -27,7 +27,7 @@ struct ToAllCaps { } private: - /// Applies the camelCase to snake_case transformation to a single field. + /// Applies the all-uppercase transformation to a single field. /// @tparam FieldType The type of the field being transformed /// @param _f The field to transform /// @return A new field with the transformed name and the same value From 5de8a6dc35aaf04b69b82257443d7fa39b832b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Sun, 5 Jul 2026 12:35:00 +0200 Subject: [PATCH 04/22] Wrote first draft --- include/rfl/env.hpp | 9 + include/rfl/env/Parser.hpp | 15 ++ include/rfl/env/Reader.hpp | 330 +++++++++++++++++++++++++++++++ include/rfl/env/parse_argv.hpp | 129 ++++++++++++ include/rfl/env/read.hpp | 37 ++++ include/rfl/env/resolve_args.hpp | 240 ++++++++++++++++++++++ 6 files changed, 760 insertions(+) create mode 100644 include/rfl/env.hpp create mode 100644 include/rfl/env/Parser.hpp create mode 100644 include/rfl/env/Reader.hpp create mode 100644 include/rfl/env/parse_argv.hpp create mode 100644 include/rfl/env/read.hpp create mode 100644 include/rfl/env/resolve_args.hpp diff --git a/include/rfl/env.hpp b/include/rfl/env.hpp new file mode 100644 index 00000000..a396a295 --- /dev/null +++ b/include/rfl/env.hpp @@ -0,0 +1,9 @@ +#ifndef RFL_ENV_HPP_ +#define RFL_ENV_HPP_ + +#include "../rfl.hpp" +#include "env/Parser.hpp" +#include "env/Reader.hpp" +#include "env/read.hpp" + +#endif diff --git a/include/rfl/env/Parser.hpp b/include/rfl/env/Parser.hpp new file mode 100644 index 00000000..d5c502fc --- /dev/null +++ b/include/rfl/env/Parser.hpp @@ -0,0 +1,15 @@ +#ifndef RFL_ENV_PARSER_HPP_ +#define RFL_ENV_PARSER_HPP_ + +#include "../generic/Writer.hpp" +#include "../parsing/Parser.hpp" +#include "Reader.hpp" + +namespace rfl::cli { + +template +using Parser = parsing::Parser; + +} // namespace rfl::cli + +#endif diff --git a/include/rfl/env/Reader.hpp b/include/rfl/env/Reader.hpp new file mode 100644 index 00000000..b93cd3a9 --- /dev/null +++ b/include/rfl/env/Reader.hpp @@ -0,0 +1,330 @@ +#ifndef RFL_ENV_READER_HPP_ +#define RFL_ENV_READER_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../Result.hpp" +#include "../always_false.hpp" + +namespace rfl::env { + +/// Represents a ENV variable that can be a direct value or a path in the +/// argument map. The `path` represents the hierarchical key (e.g., +/// "database.host"). The `direct_value` is used when parsing array elements +/// directly. +struct EnvVarType { + const std::map* const args = nullptr; + const std::string path; + const std::optional direct_value; +}; + +/// Represents a ENV object with a prefix path for accessing nested fields. +/// All child fields will be prefixed with this path. +struct EnvObjectType { + const std::map* const args = nullptr; + const std::string prefix; +}; + +/// Represents a ENV array containing multiple string values. +/// Typically created by splitting a comma-delimited argument value. +struct EnvArrayType { + const std::vector values; +}; + +// --- Constrained overloads for string-to-type parsing --- + +/// Parses a string value to std::string (identity function). +/// @tparam T Must be std::string +/// @param _str The string to parse +/// @param _path The ENV argument path (unused for strings) +/// @return The string unchanged +template + requires std::same_as +rfl::Result parse_value(const std::string& _str, + const std::string&) noexcept { + return _str; +} + +/// Parses a string value to boolean. +/// Accepts: empty/"true"/"1" → true, "false"/"0" → false. +/// @tparam T Must be bool +/// @param _str The string to parse +/// @param _path The ENV argument path (for error messages) +/// @return A Result containing the boolean value or an error +template + requires std::same_as +rfl::Result parse_value(const std::string& _str, + const std::string& _path) noexcept { + if (_str.empty() || _str == "true" || _str == "1") { + return true; + } + if (_str == "false" || _str == "0") { + return false; + } + return error("Could not cast '" + _str + "' to boolean for key '" + _path + + "'."); +} + +/// Parses a string value to a floating-point number. +/// Uses locale-independent parsing (C locale) to ensure "3.14" works +/// consistently. +/// @tparam T Must be a floating-point type (float, double, long double) +/// @param _str The string to parse +/// @param _path The ENV argument path (for error messages) +/// @return A Result containing the parsed number or an error +// std::from_chars for float/double is unavailable in Apple libc++. +// std::strtod depends on the C locale (LC_NUMERIC), so "3.14" can fail +// under locales that use comma as decimal separator. +// Use strtod_l with an explicit "C" locale on all platforms for consistency. +template + requires(std::is_floating_point_v) +rfl::Result parse_value(const std::string& _str, + const std::string& _path) noexcept { + char* end = nullptr; +#ifdef _WIN32 + const auto c_locale = _create_locale(LC_NUMERIC, "C"); + const double value = _strtod_l(_str.c_str(), &end, c_locale); + _free_locale(c_locale); +#else + const auto c_locale = newlocale(LC_NUMERIC_MASK, "C", nullptr); + const double value = strtod_l(_str.c_str(), &end, c_locale); + freelocale(c_locale); +#endif + if (end != _str.c_str() + _str.size()) { + return error("Could not cast '" + _str + "' to floating point for key '" + + _path + "'."); + } + return static_cast(value); +} + +/// Parses a string value to an integral type (excluding bool). +/// Uses std::from_chars for efficient and locale-independent parsing. +/// @tparam T Must be an integral type other than bool +/// @param _str The string to parse +/// @param _path The ENV argument path (for error messages) +/// @return A Result containing the parsed integer or an error +template + requires(std::is_integral_v && !std::same_as) +rfl::Result parse_value(const std::string& _str, + const std::string& _path) noexcept { + T value; + const auto [ptr, ec] = + std::from_chars(_str.data(), _str.data() + _str.size(), value); + if (ec != std::errc() || ptr != _str.data() + _str.size()) { + return error("Could not cast '" + _str + "' to integer for key '" + _path + + "'."); + } + return value; +} + +/// Parser for environment variables. Provides methods to read fields from ENV +/// arrays and objects, check for empty variables, and convert string values to +/// basic C++ types. +struct Reader { + using InputArrayType = EnvArrayType; + using InputObjectType = EnvObjectType; + using InputVarType = EnvVarType; + + template + static constexpr bool has_custom_constructor = false; + + /// Gets a specific element from a ENV array by index. + /// @param _idx The index of the element to retrieve + /// @param _arr The ENV array + /// @return A Result containing the element as a CliVarType or an error if out + /// of bounds + rfl::Result get_field_from_array( + const size_t _idx, const InputArrayType& _arr) const noexcept { + if (_idx >= _arr.values.size()) { + return error(std::string("Index ") + std::to_string(_idx) + + " out of bounds."); + } + return InputVarType{nullptr, "", _arr.values[_idx]}; + } + + /// Gets a specific field from a ENV object by name. + /// Constructs a child path by appending the field name to the object's + /// prefix. + /// @param _name The field name + /// @param _obj The ENV object + /// @return A Result containing a CliVarType for accessing the field + rfl::Result get_field_from_object( + const std::string& _name, const InputObjectType& _obj) const noexcept { + const auto child_path = _obj.prefix.empty() ? _name : _obj.prefix + _name; + return InputVarType{_obj.args, child_path, std::nullopt}; + } + + /// Checks if a ENV variable is empty (has no value). + /// A variable is empty if there's no direct value and no matching key in the + /// argument map. + /// @param _var The ENV variable to check + /// @return true if the variable is empty, false otherwise + bool is_empty(const InputVarType& _var) const noexcept { + if (_var.direct_value) { + return false; + } + if (!_var.args) { + return true; + } + if (_var.args->count(_var.path)) { + return false; + } + const auto prefix = _var.path + "_"; + const auto it = _var.args->lower_bound(prefix); + return it == _var.args->end() || + it->first.substr(0, prefix.size()) != prefix; + } + + /// Reads all elements from a ENV array using the provided array reader. + /// @tparam ArrayReader The type of reader that processes individual array + /// elements + /// @param _array_reader The reader object that processes each element + /// @param _arr The ENV array to read from + /// @return std::nullopt on success, or an Error if reading fails + template + std::optional read_array(const ArrayReader& _array_reader, + const InputArrayType& _arr) const noexcept { + for (const auto& val : _arr.values) { + const auto err = _array_reader.read(InputVarType{nullptr, "", val}); + if (err) { + return err; + } + } + return std::nullopt; + } + + /// Reads all fields from a ENV object using the provided object reader. + /// Iterates through all arguments with the object's prefix and extracts child + /// field names. + /// @tparam ObjectReader The type of reader that processes individual object + /// fields + /// @param _object_reader The reader object that processes each field + /// @param _obj The ENV object to read from + /// @return std::nullopt on success, or an Error if reading fails + template + std::optional read_object(const ObjectReader& _object_reader, + const InputObjectType& _obj) const noexcept { + std::set seen; + auto it = _obj.prefix.empty() ? _obj.args->begin() + : _obj.args->lower_bound(_obj.prefix); + while (it != _obj.args->end()) { + if (!_obj.prefix.empty() && + it->first.substr(0, _obj.prefix.size()) != _obj.prefix) { + break; + } + const auto rest = std::string_view(it->first).substr(_obj.prefix.size()); + const auto separator_pos = rest.find(path_separator); + const auto child = std::string(separator_pos == std::string_view::npos + ? rest + : rest.substr(0, separator_pos)); + if (!child.empty() && seen.insert(child).second) { + const auto child_path = _obj.prefix + child; + _object_reader.read(std::string_view(child), + InputVarType{_obj.args, child_path, std::nullopt}); + } + ++it; + } + return std::nullopt; + } + + /// Converts a ENV variable to a basic C++ type by parsing the string value. + /// @tparam T The target type to convert to + /// @param _var The ENV variable containing the string value + /// @return A Result containing the converted value or an error + template + rfl::Result to_basic_type(const InputVarType& _var) const noexcept { + const auto str = get_value(_var); + if (!str) { + return error("No value for key '" + _var.path + "'."); + } + return parse_value(*str, _var.path); + } + + /// Converts a ENV variable to an array by splitting the value on commas. + /// @param _var The ENV variable containing a comma-delimited string + /// @return A Result containing a CliArrayType with the split values + rfl::Result to_array( + const InputVarType& _var) const noexcept { + const auto str = get_value(_var); + if (!str) { + return InputArrayType{{}}; + } + return InputArrayType{split(*str, ",")}; + } + + /// Converts a ENV variable to an object for reading nested fields. + /// Creates a CliObjectType with an appropriate prefix for child field access. + /// @param _var The ENV variable representing the object + /// @return A Result containing a CliObjectType or an error + rfl::Result to_object( + const InputVarType& _var) const noexcept { + if (!_var.args) { + return error("Cannot convert to object: no argument map available" + + (_var.path.empty() ? std::string(".") + : " for key '" + _var.path + "'.")); + } + const auto prefix = _var.path.empty() ? std::string("") : _var.path + "_"; + return InputObjectType{_var.args, prefix}; + } + + /// Custom constructors are not supported for ENV parsing. + /// @tparam T The type to construct (unused) + /// @param _var The ENV variable (unused) + /// @return Always returns an error + template + rfl::Result use_custom_constructor(const InputVarType&) const noexcept { + return error("Custom constructors are not supported for ENV parsing."); + } + + private: + static std::optional get_value( + const InputVarType& _var) noexcept { + if (_var.direct_value) { + return *_var.direct_value; + } + if (!_var.args) { + return std::nullopt; + } + const auto it = _var.args->find(_var.path); + if (it == _var.args->end()) { + return std::nullopt; + } + return it->second; + } + + static std::vector split(const std::string& _str, char _delim) { + std::vector result; + if (_str.empty()) { + return result; + } + size_t start = 0; + while (true) { + const auto pos = _str.find(_delim, start); + if (pos == std::string::npos) { + if (start < _str.size()) { + result.emplace_back(_str.substr(start)); + } + break; + } + if (pos > start) { + result.emplace_back(_str.substr(start, pos - start)); + } + start = pos + 1; + } + return result; + } +}; + +} // namespace rfl::env + +#endif diff --git a/include/rfl/env/parse_argv.hpp b/include/rfl/env/parse_argv.hpp new file mode 100644 index 00000000..eb030afe --- /dev/null +++ b/include/rfl/env/parse_argv.hpp @@ -0,0 +1,129 @@ +#ifndef RFL_ENV_PARSE_ARGV_HPP_ +#define RFL_ENV_PARSE_ARGV_HPP_ + +#include +#include +#include +#include +#include +#include + +#include "../Result.hpp" +#include "resolve_args.hpp" + +namespace rfl::cli { + +/// Returns true if the token looks like a ENV option (starts with '-' +/// followed by a letter), as opposed to a negative number like "-42". +/// This helps distinguish option flags from negative numeric values. +/// @param _token The command-line token to check +/// @return true if it looks like an option, false otherwise +inline bool looks_like_option(std::string_view _token) noexcept { + return _token.size() >= 2 && _token[0] == '-' && + !std::isdigit(static_cast(_token[1])) && + _token[1] != '.'; +} + +/// Parses command-line arguments into categorized buckets. +/// Handles three types of arguments: +/// - Long options: --key=value or --flag (stored in `named`) +/// - Short options: -x value, -x=value, or -x (stored in `short_args`) +/// - Positional arguments: bare arguments not prefixed with dashes (stored in +/// `positional`) The special argument "--" forces all subsequent arguments to +/// be treated as positional. +/// @param argc Number of command-line arguments +/// @param argv Array of command-line argument strings +/// @return A Result containing ParsedArgs with categorized arguments or an +/// error +/// --key=value / --flag → named +/// -x value / -x=value / -x → short_args +/// bare arguments → positional +/// -- → everything after goes to positional +inline rfl::Result parse_argv(int argc, char* argv[]) { + if (argc < 0 || (argc > 0 && !argv)) { + return error("Invalid argc/argv."); + } + ParsedArgs result; + if (argc <= 1) { + return result; + } + + const auto add_short = [&](std::string _key, std::string _val, + std::string_view _raw) -> rfl::Result { + result.short_order.emplace_back(_key); + if (!result.short_args.emplace(std::move(_key), std::move(_val)).second) { + return error("Duplicate short argument: " + std::string(_raw)); + } + return true; + }; + + const auto args = std::span(argv + 1, argc - 1); + bool force_positional = false; + for (size_t i = 0; i < args.size(); ++i) { + const std::string_view arg_raw(args[i]); + + if (force_positional) { + result.positional.emplace_back(arg_raw); + continue; + } + + // "--" separator: everything after is positional. + if (arg_raw == "--") { + force_positional = true; + continue; + } + + // Long argument: --key=value or --flag. + if (arg_raw.starts_with("--")) { + const auto arg = arg_raw.substr(2); + const auto eq = arg.find('='); + auto key = + std::string(eq == std::string_view::npos ? arg : arg.substr(0, eq)); + auto val = + std::string(eq == std::string_view::npos ? "" : arg.substr(eq + 1)); + if (key.empty()) { + return error("Empty key in argument: " + std::string(arg_raw)); + } + if (!result.named.emplace(std::move(key), std::move(val)).second) { + return error("Duplicate argument: " + std::string(arg_raw)); + } + continue; + } + + // Short argument: -x value, -x=value, or -x (bool). + if (arg_raw.size() >= 2 && arg_raw[0] == '-') { + const auto arg = arg_raw.substr(1); + const auto eq = arg.find('='); + + // -x=value + if (eq != std::string_view::npos) { + const auto r = add_short(std::string(arg.substr(0, eq)), + std::string(arg.substr(eq + 1)), arg_raw); + if (!r) { + return error(r.error().what()); + } + continue; + } + + // -x value or -x (bool flag). + // Peek at next token: consume as value if it doesn't look like an option. + auto val = std::string(); + if (i + 1 < args.size() && !looks_like_option(args[i + 1])) { + val = std::string(args[++i]); + } + const auto r = add_short(std::string(arg), std::move(val), arg_raw); + if (!r) { + return error(r.error().what()); + } + continue; + } + + // Bare argument → positional. + result.positional.emplace_back(arg_raw); + } + return result; +} + +} // namespace rfl::cli + +#endif diff --git a/include/rfl/env/read.hpp b/include/rfl/env/read.hpp new file mode 100644 index 00000000..dd529388 --- /dev/null +++ b/include/rfl/env/read.hpp @@ -0,0 +1,37 @@ +#ifndef RFL_ENV_READ_HPP_ +#define RFL_ENV_READ_HPP_ + +#include "../Processors.hpp" +#include "../SnakeCaseToKebabCase.hpp" +#include "Parser.hpp" +#include "Reader.hpp" +#include "parse_argv.hpp" +#include "resolve_args.hpp" + +namespace rfl::cli { + +/// Parses command-line arguments into a struct using reflection. +/// Field names are automatically converted from snake_case to kebab-case for +/// ENV arguments. For example, a struct field named `host_name` will match the +/// CLI argument `--host-name`. Supports nested objects (e.g., --database.host), +/// arrays (e.g., --ports=8080,8081), positional arguments, and short flags. +/// @tparam T The struct type to parse into +/// @tparam Ps Optional processors to apply during parsing +/// @param argc Number of command-line arguments +/// @param argv Array of command-line argument strings +/// @return A Result containing the parsed struct or an error +template +rfl::Result read(int argc, char* argv[]) { + using ProcessorsType = Processors; + return parse_argv(argc, argv) + .and_then(resolve_args) + .and_then([](auto _args) -> rfl::Result { + const auto r = Reader(); + const auto var = CliVarType{&_args, "", std::nullopt}; + return Parser::read(r, var); + }); +} + +} // namespace rfl::cli + +#endif diff --git a/include/rfl/env/resolve_args.hpp b/include/rfl/env/resolve_args.hpp new file mode 100644 index 00000000..24ea9bdd --- /dev/null +++ b/include/rfl/env/resolve_args.hpp @@ -0,0 +1,240 @@ +#ifndef RFL_ENV_RESOLVE_ARGS_HPP_ +#define RFL_ENV_RESOLVE_ARGS_HPP_ + +#include +#include +#include +#include + +#include "../Result.hpp" +#include "../internal/is_positional.hpp" +#include "../internal/is_short.hpp" +#include "../internal/processed_t.hpp" + +namespace rfl::cli { + +/// Holds the parsed command-line arguments categorized by type. +/// Used as an intermediate structure before resolving to a flat key-value map. +struct ParsedArgs { + std::map named; + std::map short_args; + std::vector positional; + /// Insertion order of short arg keys (for stable reclaim ordering). + std::vector short_order; +}; + +namespace internal { + +template +consteval bool check_no_nested_wrappers() { + if constexpr (_is_positional) { + return !rfl::internal::is_short_v; + } else if constexpr (_is_short) { + return !rfl::internal::is_positional_v; + } else { + return true; + } +} + +template +struct field_info { + using FieldType = rfl::tuple_element_t<_i, typename NamedTupleType::Fields>; + using InnerType = typename FieldType::Type; + + static constexpr bool is_positional = + rfl::internal::is_positional_v; + static constexpr bool is_short = rfl::internal::is_short_v; + + static_assert( + check_no_nested_wrappers(), + "Nested wrappers (Positional> or Short<..., Positional<...>>) " + "are not allowed."); + + static constexpr std::string_view name() { return FieldType::name(); } +}; + +template +constexpr auto get_short_name() { + using Info = field_info; + if constexpr (Info::is_short) { + return Info::InnerType::short_name_; + } else { + return rfl::internal::StringLiteral<1>(""); + } +} + +template +void collect_positional_names(std::vector& _names, + std::index_sequence<_i, _is...>) { + using Info = field_info; + if constexpr (Info::is_positional) { + _names.emplace_back(Info::name()); + } + if constexpr (sizeof...(_is) > 0) { + collect_positional_names(_names, + std::index_sequence<_is...>{}); + } +} + +template +void collect_positional_names(std::vector&, + std::index_sequence<>) {} + +template +rfl::Result collect_short_mapping( + std::map& _mapping, + std::index_sequence<_i, _is...>) { + using Info = field_info; + if constexpr (Info::is_short) { + constexpr auto short_lit = get_short_name(); + const auto short_str = std::string(short_lit.string_view()); + const auto long_str = std::string(Info::name()); + if (!_mapping.emplace(short_str, long_str).second) { + return rfl::error("Duplicate short name '-" + short_str + + "' in struct definition."); + } + } + if constexpr (sizeof...(_is) > 0) { + return collect_short_mapping(_mapping, + std::index_sequence<_is...>{}); + } + return true; +} + +template +rfl::Result collect_short_mapping(std::map&, + std::index_sequence<>) { + return true; +} + +template +consteval bool is_short_bool() { + using Info = field_info; + if constexpr (Info::is_short) { + return std::is_same_v; + } else { + return false; + } +} + +template +void collect_short_bool_names(std::set& _names, + std::index_sequence<_i, _is...>) { + if constexpr (is_short_bool()) { + constexpr auto short_lit = get_short_name(); + _names.emplace(short_lit.string_view()); + } + if constexpr (sizeof...(_is) > 0) { + collect_short_bool_names(_names, + std::index_sequence<_is...>{}); + } +} + +template +void collect_short_bool_names(std::set&, std::index_sequence<>) {} + +} // namespace internal + +/// Resolves ParsedArgs into a flat key-value map using compile-time +/// metadata from the target struct T. +/// This function: +/// 1. Maps positional arguments to their corresponding field names based on +/// declaration order +/// 2. Expands short arguments (e.g., -p) to long names (e.g., --port) using +/// Short<> annotations +/// 3. Handles special cases like boolean short flags that don't consume values +/// 4. Detects conflicts between different ways of specifying the same field +/// @tparam T The target struct type +/// @tparam ProcessorsType The processors applied to the struct +/// @param _parsed The parsed command-line arguments +/// @return A Result containing a flat map of field names to values or an error +template +rfl::Result> resolve_args( + ParsedArgs _parsed) { + using NT = rfl::internal::processed_t; + constexpr auto sz = NT::size(); + using Indices = std::make_index_sequence; + + // Collect positional field names (in declaration order). + std::vector positional_names; + internal::collect_positional_names(positional_names, Indices{}); + + // Collect short-to-long mapping. + std::map short_to_long; + const auto short_result = + internal::collect_short_mapping(short_to_long, Indices{}); + if (!short_result) { + return rfl::error(short_result.error().what()); + } + + // Reclaim non-boolean values from short bool flags. + // parse_argv doesn't know types, so `-v somefile` consumes "somefile" as + // the value of -v. Since -v is bool, "somefile" is not a valid bool value + // and must be returned to the positional list. + // We iterate in insertion order (short_order) to preserve argv ordering. + std::set short_bool_names; + internal::collect_short_bool_names(short_bool_names, Indices{}); + std::vector reclaimed; + for (const auto& short_name : _parsed.short_order) { + if (!short_bool_names.count(short_name)) { + continue; + } + auto it = _parsed.short_args.find(short_name); + if (it == _parsed.short_args.end()) { + continue; + } + auto& value = it->second; + if (value != "true" && value != "false" && value != "0" && value != "1" && + !value.empty()) { + reclaimed.push_back(value); + value.clear(); + } + } + // Insert reclaimed values before existing positional args to preserve + // the original argv order: `-v file1 file2` → positional ["file1", "file2"]. + if (!reclaimed.empty()) { + _parsed.positional.insert(_parsed.positional.begin(), + std::make_move_iterator(reclaimed.begin()), + std::make_move_iterator(reclaimed.end())); + } + + auto& result = _parsed.named; + + // Merge positional args. + if (_parsed.positional.size() > positional_names.size()) { + auto msg = std::string("Too many positional arguments: expected at most "); + msg += std::to_string(positional_names.size()); + msg += ", got "; + msg += std::to_string(_parsed.positional.size()); + msg += "."; + return rfl::error(std::move(msg)); + } + for (size_t i = 0; i < _parsed.positional.size(); ++i) { + const auto& long_name = positional_names[i]; + if (result.count(long_name)) { + return rfl::error("Conflict: positional argument and '--" + long_name + + "' both provided."); + } + result.emplace(long_name, std::move(_parsed.positional[i])); + } + + // Merge short args. + for (auto& [short_name, value] : _parsed.short_args) { + const auto it = short_to_long.find(short_name); + if (it == short_to_long.end()) { + return rfl::error("Unknown short argument: -" + short_name); + } + const auto& long_name = it->second; + if (result.count(long_name)) { + return rfl::error("Conflict: '-" + short_name + "' and '--" + long_name + + "' both provided."); + } + result.emplace(long_name, std::move(value)); + } + + return std::move(result); +} + +} // namespace rfl::cli + +#endif From 4e7f6136bb995c7ff523c0905b70147dfb9433ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Wed, 8 Jul 2026 00:02:16 +0200 Subject: [PATCH 05/22] First draft for read_object --- include/rfl/env/Parser.hpp | 11 ++- include/rfl/env/Reader.hpp | 73 +++++++------------ include/rfl/parsing/ViewReader.hpp | 11 +-- include/rfl/parsing/ViewReaderWithDefault.hpp | 8 +- ...ReaderWithDefaultAndStrippedFieldNames.hpp | 7 +- .../ViewReaderWithStrippedFieldNames.hpp | 10 ++- 6 files changed, 53 insertions(+), 67 deletions(-) diff --git a/include/rfl/env/Parser.hpp b/include/rfl/env/Parser.hpp index d5c502fc..e6075d03 100644 --- a/include/rfl/env/Parser.hpp +++ b/include/rfl/env/Parser.hpp @@ -1,15 +1,20 @@ #ifndef RFL_ENV_PARSER_HPP_ #define RFL_ENV_PARSER_HPP_ +#include "../NamedTuple.hpp" #include "../generic/Writer.hpp" +#include "../internal/no_field_names_v.hpp" +#include "../internal/no_optionals_v.hpp" +#include "../parsing/NamedTupleParser.hpp" #include "../parsing/Parser.hpp" +#include "../parsing/Parser_base.hpp" #include "Reader.hpp" -namespace rfl::cli { +namespace rfl::env { template -using Parser = parsing::Parser; +using Parser = parsing::Parser; -} // namespace rfl::cli +} // namespace rfl::env #endif diff --git a/include/rfl/env/Reader.hpp b/include/rfl/env/Reader.hpp index b93cd3a9..976742b6 100644 --- a/include/rfl/env/Reader.hpp +++ b/include/rfl/env/Reader.hpp @@ -20,10 +20,8 @@ namespace rfl::env { /// Represents a ENV variable that can be a direct value or a path in the /// argument map. The `path` represents the hierarchical key (e.g., -/// "database.host"). The `direct_value` is used when parsing array elements -/// directly. +/// "DATABASE_HOST"). The `direct_value` is used when parsing array directly. struct EnvVarType { - const std::map* const args = nullptr; const std::string path; const std::optional direct_value; }; @@ -31,7 +29,6 @@ struct EnvVarType { /// Represents a ENV object with a prefix path for accessing nested fields. /// All child fields will be prefixed with this path. struct EnvObjectType { - const std::map* const args = nullptr; const std::string prefix; }; @@ -149,7 +146,7 @@ struct Reader { return error(std::string("Index ") + std::to_string(_idx) + " out of bounds."); } - return InputVarType{nullptr, "", _arr.values[_idx]}; + return InputVarType{.path = "", .direct_value = _arr.values[_idx]}; } /// Gets a specific field from a ENV object by name. @@ -161,7 +158,7 @@ struct Reader { rfl::Result get_field_from_object( const std::string& _name, const InputObjectType& _obj) const noexcept { const auto child_path = _obj.prefix.empty() ? _name : _obj.prefix + _name; - return InputVarType{_obj.args, child_path, std::nullopt}; + return InputVarType{.path = child_path, .direct_value = std::nullopt}; } /// Checks if a ENV variable is empty (has no value). @@ -173,16 +170,9 @@ struct Reader { if (_var.direct_value) { return false; } - if (!_var.args) { - return true; - } - if (_var.args->count(_var.path)) { + if (std::getenv(_var.path.c_str()) != nullptr) { return false; } - const auto prefix = _var.path + "_"; - const auto it = _var.args->lower_bound(prefix); - return it == _var.args->end() || - it->first.substr(0, prefix.size()) != prefix; } /// Reads all elements from a ENV array using the provided array reader. @@ -195,7 +185,8 @@ struct Reader { std::optional read_array(const ArrayReader& _array_reader, const InputArrayType& _arr) const noexcept { for (const auto& val : _arr.values) { - const auto err = _array_reader.read(InputVarType{nullptr, "", val}); + const auto err = + _array_reader.read(InputVarType{.path = "", .direct_value = val}); if (err) { return err; } @@ -214,25 +205,18 @@ struct Reader { template std::optional read_object(const ObjectReader& _object_reader, const InputObjectType& _obj) const noexcept { - std::set seen; - auto it = _obj.prefix.empty() ? _obj.args->begin() - : _obj.args->lower_bound(_obj.prefix); - while (it != _obj.args->end()) { - if (!_obj.prefix.empty() && - it->first.substr(0, _obj.prefix.size()) != _obj.prefix) { - break; - } - const auto rest = std::string_view(it->first).substr(_obj.prefix.size()); - const auto separator_pos = rest.find(path_separator); - const auto child = std::string(separator_pos == std::string_view::npos - ? rest - : rest.substr(0, separator_pos)); - if (!child.empty() && seen.insert(child).second) { - const auto child_path = _obj.prefix + child; - _object_reader.read(std::string_view(child), - InputVarType{_obj.args, child_path, std::nullopt}); + using ViewType = typename std::remove_cvref_t::ViewType; + const auto names = typename ViewType::Names::names(); + for (const auto& name : names) { + const auto child_path = _obj.prefix.empty() ? name : _obj.prefix + name; + const auto var = + InputVarType{.path = child_path, .direct_value = std::nullopt}; + if (!is_empty(var)) { + const auto err = _object_reader.read(std::string_view(name), var); + if (err) { + return err; + } } - ++it; } return std::nullopt; } @@ -257,24 +241,19 @@ struct Reader { const InputVarType& _var) const noexcept { const auto str = get_value(_var); if (!str) { - return InputArrayType{{}}; + return InputArrayType{.values = {}}; } - return InputArrayType{split(*str, ",")}; + return InputArrayType{.values = split(*str, ',')}; } /// Converts a ENV variable to an object for reading nested fields. - /// Creates a CliObjectType with an appropriate prefix for child field access. + /// Creates a EnvObjectType with an appropriate prefix for child field access. /// @param _var The ENV variable representing the object /// @return A Result containing a CliObjectType or an error rfl::Result to_object( const InputVarType& _var) const noexcept { - if (!_var.args) { - return error("Cannot convert to object: no argument map available" + - (_var.path.empty() ? std::string(".") - : " for key '" + _var.path + "'.")); - } const auto prefix = _var.path.empty() ? std::string("") : _var.path + "_"; - return InputObjectType{_var.args, prefix}; + return InputObjectType{.prefix = prefix}; } /// Custom constructors are not supported for ENV parsing. @@ -292,14 +271,12 @@ struct Reader { if (_var.direct_value) { return *_var.direct_value; } - if (!_var.args) { - return std::nullopt; - } - const auto it = _var.args->find(_var.path); - if (it == _var.args->end()) { + const char* env_value = std::getenv(_var.path.c_str()); + if (env_value) { + return std::string(env_value); + } else { return std::nullopt; } - return it->second; } static std::vector split(const std::string& _str, char _delim) { diff --git a/include/rfl/parsing/ViewReader.hpp b/include/rfl/parsing/ViewReader.hpp index 928bf672..c901adab 100644 --- a/include/rfl/parsing/ViewReader.hpp +++ b/include/rfl/parsing/ViewReader.hpp @@ -8,7 +8,6 @@ #include #include -#include "../Result.hpp" #include "../Tuple.hpp" #include "../internal/is_array.hpp" #include "../internal/no_extra_fields_v.hpp" @@ -17,20 +16,22 @@ namespace rfl::parsing { -template +template class ViewReader { - private: + public: using InputVarType = typename R::InputVarType; + using ViewType = _ViewType; + static constexpr size_t size_ = ViewType::size(); - public: /** * @brief Constructor. * * @param _r The reader to use. * @param _view The view to read into. * @param _found A boolean array indicating which fields have been found. - * @param _set A boolean array indicating which fields have been successfully set. + * @param _set A boolean array indicating which fields have been successfully + * set. * @param _errors The vector to collect errors in. */ ViewReader(const R* _r, ViewType* _view, std::array* _found, diff --git a/include/rfl/parsing/ViewReaderWithDefault.hpp b/include/rfl/parsing/ViewReaderWithDefault.hpp index bb5fd2af..654f38eb 100644 --- a/include/rfl/parsing/ViewReaderWithDefault.hpp +++ b/include/rfl/parsing/ViewReaderWithDefault.hpp @@ -9,7 +9,6 @@ #include #include "../Ref.hpp" -#include "../Result.hpp" #include "../Tuple.hpp" #include "../internal/is_array.hpp" #include "../internal/no_extra_fields_v.hpp" @@ -17,13 +16,14 @@ namespace rfl::parsing { -template +template class ViewReaderWithDefault { - private: + public: using InputVarType = typename R::InputVarType; + using ViewType = _ViewType; + static constexpr size_t size_ = ViewType::size(); - public: /** * @brief Constructor. * diff --git a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp index 6abd2520..de57b25d 100644 --- a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp @@ -14,13 +14,14 @@ namespace rfl::parsing { -template +template class ViewReaderWithDefaultAndStrippedFieldNames { - private: + public: using InputVarType = typename R::InputVarType; + using ViewType = _ViewType; + static constexpr size_t size_ = ViewType::size(); - public: /** * @brief Constructor. * diff --git a/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp index 18f54377..cc776beb 100644 --- a/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp @@ -15,20 +15,22 @@ namespace rfl::parsing { -template +template class ViewReaderWithStrippedFieldNames { - private: + public: using InputVarType = typename R::InputVarType; + using ViewType = _ViewType; + static constexpr size_t size_ = ViewType::size(); - public: /** * @brief Constructor. * * @param _r The reader to use. * @param _view The view to read into. * @param _found A boolean array indicating which fields have been found. - * @param _set A boolean array indicating which fields have been successfully set. + * @param _set A boolean array indicating which fields have been successfully + * set. * @param _errors The vector to collect errors in. */ ViewReaderWithStrippedFieldNames(const R* _r, ViewType* _view, From 7d656be7480373cd523d5d1a85190aa92008f4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Wed, 8 Jul 2026 21:38:01 +0200 Subject: [PATCH 06/22] Fixed exists --- include/rfl/env/Reader.hpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/include/rfl/env/Reader.hpp b/include/rfl/env/Reader.hpp index 976742b6..4a4b0f13 100644 --- a/include/rfl/env/Reader.hpp +++ b/include/rfl/env/Reader.hpp @@ -16,6 +16,8 @@ #include "../Result.hpp" #include "../always_false.hpp" +extern char** environ; + namespace rfl::env { /// Represents a ENV variable that can be a direct value or a path in the @@ -173,6 +175,20 @@ struct Reader { if (std::getenv(_var.path.c_str()) != nullptr) { return false; } + if (_var.path.empty()) { + return true; + } + const auto prefix = _var.path.empty() ? std::string("") : _var.path + "_"; + for (char** env = environ; *env != nullptr; ++env) { + const std::string env_entry(*env); + if (env_entry.size() <= prefix.size()) { + continue; + } + if (env_entry.compare(0, prefix.size(), prefix) == 0) { + return false; + } + } + return true; } /// Reads all elements from a ENV array using the provided array reader. From 994f2c85e76344c3ab8f41fad6fe74e7ce89d5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Wed, 8 Jul 2026 21:38:09 +0200 Subject: [PATCH 07/22] Removed obsolete files --- include/rfl/env/parse_argv.hpp | 129 ----------------- include/rfl/env/resolve_args.hpp | 240 ------------------------------- 2 files changed, 369 deletions(-) delete mode 100644 include/rfl/env/parse_argv.hpp delete mode 100644 include/rfl/env/resolve_args.hpp diff --git a/include/rfl/env/parse_argv.hpp b/include/rfl/env/parse_argv.hpp deleted file mode 100644 index eb030afe..00000000 --- a/include/rfl/env/parse_argv.hpp +++ /dev/null @@ -1,129 +0,0 @@ -#ifndef RFL_ENV_PARSE_ARGV_HPP_ -#define RFL_ENV_PARSE_ARGV_HPP_ - -#include -#include -#include -#include -#include -#include - -#include "../Result.hpp" -#include "resolve_args.hpp" - -namespace rfl::cli { - -/// Returns true if the token looks like a ENV option (starts with '-' -/// followed by a letter), as opposed to a negative number like "-42". -/// This helps distinguish option flags from negative numeric values. -/// @param _token The command-line token to check -/// @return true if it looks like an option, false otherwise -inline bool looks_like_option(std::string_view _token) noexcept { - return _token.size() >= 2 && _token[0] == '-' && - !std::isdigit(static_cast(_token[1])) && - _token[1] != '.'; -} - -/// Parses command-line arguments into categorized buckets. -/// Handles three types of arguments: -/// - Long options: --key=value or --flag (stored in `named`) -/// - Short options: -x value, -x=value, or -x (stored in `short_args`) -/// - Positional arguments: bare arguments not prefixed with dashes (stored in -/// `positional`) The special argument "--" forces all subsequent arguments to -/// be treated as positional. -/// @param argc Number of command-line arguments -/// @param argv Array of command-line argument strings -/// @return A Result containing ParsedArgs with categorized arguments or an -/// error -/// --key=value / --flag → named -/// -x value / -x=value / -x → short_args -/// bare arguments → positional -/// -- → everything after goes to positional -inline rfl::Result parse_argv(int argc, char* argv[]) { - if (argc < 0 || (argc > 0 && !argv)) { - return error("Invalid argc/argv."); - } - ParsedArgs result; - if (argc <= 1) { - return result; - } - - const auto add_short = [&](std::string _key, std::string _val, - std::string_view _raw) -> rfl::Result { - result.short_order.emplace_back(_key); - if (!result.short_args.emplace(std::move(_key), std::move(_val)).second) { - return error("Duplicate short argument: " + std::string(_raw)); - } - return true; - }; - - const auto args = std::span(argv + 1, argc - 1); - bool force_positional = false; - for (size_t i = 0; i < args.size(); ++i) { - const std::string_view arg_raw(args[i]); - - if (force_positional) { - result.positional.emplace_back(arg_raw); - continue; - } - - // "--" separator: everything after is positional. - if (arg_raw == "--") { - force_positional = true; - continue; - } - - // Long argument: --key=value or --flag. - if (arg_raw.starts_with("--")) { - const auto arg = arg_raw.substr(2); - const auto eq = arg.find('='); - auto key = - std::string(eq == std::string_view::npos ? arg : arg.substr(0, eq)); - auto val = - std::string(eq == std::string_view::npos ? "" : arg.substr(eq + 1)); - if (key.empty()) { - return error("Empty key in argument: " + std::string(arg_raw)); - } - if (!result.named.emplace(std::move(key), std::move(val)).second) { - return error("Duplicate argument: " + std::string(arg_raw)); - } - continue; - } - - // Short argument: -x value, -x=value, or -x (bool). - if (arg_raw.size() >= 2 && arg_raw[0] == '-') { - const auto arg = arg_raw.substr(1); - const auto eq = arg.find('='); - - // -x=value - if (eq != std::string_view::npos) { - const auto r = add_short(std::string(arg.substr(0, eq)), - std::string(arg.substr(eq + 1)), arg_raw); - if (!r) { - return error(r.error().what()); - } - continue; - } - - // -x value or -x (bool flag). - // Peek at next token: consume as value if it doesn't look like an option. - auto val = std::string(); - if (i + 1 < args.size() && !looks_like_option(args[i + 1])) { - val = std::string(args[++i]); - } - const auto r = add_short(std::string(arg), std::move(val), arg_raw); - if (!r) { - return error(r.error().what()); - } - continue; - } - - // Bare argument → positional. - result.positional.emplace_back(arg_raw); - } - return result; -} - -} // namespace rfl::cli - -#endif diff --git a/include/rfl/env/resolve_args.hpp b/include/rfl/env/resolve_args.hpp deleted file mode 100644 index 24ea9bdd..00000000 --- a/include/rfl/env/resolve_args.hpp +++ /dev/null @@ -1,240 +0,0 @@ -#ifndef RFL_ENV_RESOLVE_ARGS_HPP_ -#define RFL_ENV_RESOLVE_ARGS_HPP_ - -#include -#include -#include -#include - -#include "../Result.hpp" -#include "../internal/is_positional.hpp" -#include "../internal/is_short.hpp" -#include "../internal/processed_t.hpp" - -namespace rfl::cli { - -/// Holds the parsed command-line arguments categorized by type. -/// Used as an intermediate structure before resolving to a flat key-value map. -struct ParsedArgs { - std::map named; - std::map short_args; - std::vector positional; - /// Insertion order of short arg keys (for stable reclaim ordering). - std::vector short_order; -}; - -namespace internal { - -template -consteval bool check_no_nested_wrappers() { - if constexpr (_is_positional) { - return !rfl::internal::is_short_v; - } else if constexpr (_is_short) { - return !rfl::internal::is_positional_v; - } else { - return true; - } -} - -template -struct field_info { - using FieldType = rfl::tuple_element_t<_i, typename NamedTupleType::Fields>; - using InnerType = typename FieldType::Type; - - static constexpr bool is_positional = - rfl::internal::is_positional_v; - static constexpr bool is_short = rfl::internal::is_short_v; - - static_assert( - check_no_nested_wrappers(), - "Nested wrappers (Positional> or Short<..., Positional<...>>) " - "are not allowed."); - - static constexpr std::string_view name() { return FieldType::name(); } -}; - -template -constexpr auto get_short_name() { - using Info = field_info; - if constexpr (Info::is_short) { - return Info::InnerType::short_name_; - } else { - return rfl::internal::StringLiteral<1>(""); - } -} - -template -void collect_positional_names(std::vector& _names, - std::index_sequence<_i, _is...>) { - using Info = field_info; - if constexpr (Info::is_positional) { - _names.emplace_back(Info::name()); - } - if constexpr (sizeof...(_is) > 0) { - collect_positional_names(_names, - std::index_sequence<_is...>{}); - } -} - -template -void collect_positional_names(std::vector&, - std::index_sequence<>) {} - -template -rfl::Result collect_short_mapping( - std::map& _mapping, - std::index_sequence<_i, _is...>) { - using Info = field_info; - if constexpr (Info::is_short) { - constexpr auto short_lit = get_short_name(); - const auto short_str = std::string(short_lit.string_view()); - const auto long_str = std::string(Info::name()); - if (!_mapping.emplace(short_str, long_str).second) { - return rfl::error("Duplicate short name '-" + short_str + - "' in struct definition."); - } - } - if constexpr (sizeof...(_is) > 0) { - return collect_short_mapping(_mapping, - std::index_sequence<_is...>{}); - } - return true; -} - -template -rfl::Result collect_short_mapping(std::map&, - std::index_sequence<>) { - return true; -} - -template -consteval bool is_short_bool() { - using Info = field_info; - if constexpr (Info::is_short) { - return std::is_same_v; - } else { - return false; - } -} - -template -void collect_short_bool_names(std::set& _names, - std::index_sequence<_i, _is...>) { - if constexpr (is_short_bool()) { - constexpr auto short_lit = get_short_name(); - _names.emplace(short_lit.string_view()); - } - if constexpr (sizeof...(_is) > 0) { - collect_short_bool_names(_names, - std::index_sequence<_is...>{}); - } -} - -template -void collect_short_bool_names(std::set&, std::index_sequence<>) {} - -} // namespace internal - -/// Resolves ParsedArgs into a flat key-value map using compile-time -/// metadata from the target struct T. -/// This function: -/// 1. Maps positional arguments to their corresponding field names based on -/// declaration order -/// 2. Expands short arguments (e.g., -p) to long names (e.g., --port) using -/// Short<> annotations -/// 3. Handles special cases like boolean short flags that don't consume values -/// 4. Detects conflicts between different ways of specifying the same field -/// @tparam T The target struct type -/// @tparam ProcessorsType The processors applied to the struct -/// @param _parsed The parsed command-line arguments -/// @return A Result containing a flat map of field names to values or an error -template -rfl::Result> resolve_args( - ParsedArgs _parsed) { - using NT = rfl::internal::processed_t; - constexpr auto sz = NT::size(); - using Indices = std::make_index_sequence; - - // Collect positional field names (in declaration order). - std::vector positional_names; - internal::collect_positional_names(positional_names, Indices{}); - - // Collect short-to-long mapping. - std::map short_to_long; - const auto short_result = - internal::collect_short_mapping(short_to_long, Indices{}); - if (!short_result) { - return rfl::error(short_result.error().what()); - } - - // Reclaim non-boolean values from short bool flags. - // parse_argv doesn't know types, so `-v somefile` consumes "somefile" as - // the value of -v. Since -v is bool, "somefile" is not a valid bool value - // and must be returned to the positional list. - // We iterate in insertion order (short_order) to preserve argv ordering. - std::set short_bool_names; - internal::collect_short_bool_names(short_bool_names, Indices{}); - std::vector reclaimed; - for (const auto& short_name : _parsed.short_order) { - if (!short_bool_names.count(short_name)) { - continue; - } - auto it = _parsed.short_args.find(short_name); - if (it == _parsed.short_args.end()) { - continue; - } - auto& value = it->second; - if (value != "true" && value != "false" && value != "0" && value != "1" && - !value.empty()) { - reclaimed.push_back(value); - value.clear(); - } - } - // Insert reclaimed values before existing positional args to preserve - // the original argv order: `-v file1 file2` → positional ["file1", "file2"]. - if (!reclaimed.empty()) { - _parsed.positional.insert(_parsed.positional.begin(), - std::make_move_iterator(reclaimed.begin()), - std::make_move_iterator(reclaimed.end())); - } - - auto& result = _parsed.named; - - // Merge positional args. - if (_parsed.positional.size() > positional_names.size()) { - auto msg = std::string("Too many positional arguments: expected at most "); - msg += std::to_string(positional_names.size()); - msg += ", got "; - msg += std::to_string(_parsed.positional.size()); - msg += "."; - return rfl::error(std::move(msg)); - } - for (size_t i = 0; i < _parsed.positional.size(); ++i) { - const auto& long_name = positional_names[i]; - if (result.count(long_name)) { - return rfl::error("Conflict: positional argument and '--" + long_name + - "' both provided."); - } - result.emplace(long_name, std::move(_parsed.positional[i])); - } - - // Merge short args. - for (auto& [short_name, value] : _parsed.short_args) { - const auto it = short_to_long.find(short_name); - if (it == short_to_long.end()) { - return rfl::error("Unknown short argument: -" + short_name); - } - const auto& long_name = it->second; - if (result.count(long_name)) { - return rfl::error("Conflict: '-" + short_name + "' and '--" + long_name + - "' both provided."); - } - result.emplace(long_name, std::move(value)); - } - - return std::move(result); -} - -} // namespace rfl::cli - -#endif From 4bb69bb739d0767acbd475fec805b95358125210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Wed, 8 Jul 2026 22:46:45 +0200 Subject: [PATCH 08/22] Added Writer --- include/rfl/env.hpp | 1 + include/rfl/env/Reader.hpp | 18 ++-- include/rfl/env/Writer.hpp | 188 +++++++++++++++++++++++++++++++++++++ src/rfl/env/Writer.cpp | 112 ++++++++++++++++++++++ 4 files changed, 310 insertions(+), 9 deletions(-) create mode 100644 include/rfl/env/Writer.hpp create mode 100644 src/rfl/env/Writer.cpp diff --git a/include/rfl/env.hpp b/include/rfl/env.hpp index a396a295..12e256f4 100644 --- a/include/rfl/env.hpp +++ b/include/rfl/env.hpp @@ -4,6 +4,7 @@ #include "../rfl.hpp" #include "env/Parser.hpp" #include "env/Reader.hpp" +#include "env/Writer.hpp" #include "env/read.hpp" #endif diff --git a/include/rfl/env/Reader.hpp b/include/rfl/env/Reader.hpp index 4a4b0f13..7073c895 100644 --- a/include/rfl/env/Reader.hpp +++ b/include/rfl/env/Reader.hpp @@ -23,20 +23,20 @@ namespace rfl::env { /// Represents a ENV variable that can be a direct value or a path in the /// argument map. The `path` represents the hierarchical key (e.g., /// "DATABASE_HOST"). The `direct_value` is used when parsing array directly. -struct EnvVarType { +struct InputEnvVarType { const std::string path; const std::optional direct_value; }; /// Represents a ENV object with a prefix path for accessing nested fields. /// All child fields will be prefixed with this path. -struct EnvObjectType { +struct InputEnvObjectType { const std::string prefix; }; /// Represents a ENV array containing multiple string values. /// Typically created by splitting a comma-delimited argument value. -struct EnvArrayType { +struct InputEnvArrayType { const std::vector values; }; @@ -129,10 +129,10 @@ rfl::Result parse_value(const std::string& _str, /// Parser for environment variables. Provides methods to read fields from ENV /// arrays and objects, check for empty variables, and convert string values to /// basic C++ types. -struct Reader { - using InputArrayType = EnvArrayType; - using InputObjectType = EnvObjectType; - using InputVarType = EnvVarType; +struct RFL_API Reader { + using InputArrayType = InputEnvArrayType; + using InputObjectType = InputEnvObjectType; + using InputVarType = InputEnvVarType; template static constexpr bool has_custom_constructor = false; @@ -172,8 +172,8 @@ struct Reader { if (_var.direct_value) { return false; } - if (std::getenv(_var.path.c_str()) != nullptr) { - return false; + if (const auto str = std::getenv(_var.path.c_str()) != nullptr) { + return str.empty(); } if (_var.path.empty()) { return true; diff --git a/include/rfl/env/Writer.hpp b/include/rfl/env/Writer.hpp new file mode 100644 index 00000000..ec8546f5 --- /dev/null +++ b/include/rfl/env/Writer.hpp @@ -0,0 +1,188 @@ +#ifndef RFL_ENV_WRITER_HPP_ +#define RFL_ENV_WRITER_HPP_ + +#include +#include +#include +#include +#include + +#include "../Ref.hpp" +#include "../Variant.hpp" +#include "../always_false.hpp" +#include "../common.hpp" + +namespace rfl::env { + +struct OutputEnvVarType { + std::string value; +}; + +struct OutputEnvArrayType { + Ref> values; +}; + +struct OutputEnvObjectType { + std::string prefix; + Ref>> + fields; +}; + +/// A Writer class for serializing data into ENV format. It provides methods to +/// create arrays, objects, and values, and to add nested elements. +class RFL_API Writer { + public: + using OutputArrayType = OutputEnvArrayType; + using OutputObjectType = OutputEnvObjectType; + using OutputVarType = OutputEnvVarType; + + Writer(); + + template + OutputArrayType array_as_root(const size_t) const noexcept { + static_assert(always_false_v, + "Writing arrays to ENV as root variables is not supported."); + return OutputArrayType{}; + } + + OutputObjectType object_as_root(const size_t) const noexcept; + + template + OutputVarType null_as_root() const noexcept { + static_assert(always_false_v, + "Writing null to ENV as root variables is not supported."); + return OutputVarType{}; + } + + template + OutputVarType value_as_root(const T& _var) const noexcept { + static_assert( + always_false_v, + "Writing basic types to ENV as root variables is not supported."); + return OutputVarType{}; + } + + /// Adds a nested array to a parent array. + /// @param The expected size (unused, reserved for future optimization) + /// @param _parent Pointer to the parent array to add to + /// @return An output array that can be populated with elements + template + OutputArrayType add_array_to_array(const size_t, + OutputArrayType* _parent) const { + static_assert(always_false_v, + "Writing nested arrays to ENV is not supported."); + return OutputArrayType{}; + } + + /// Adds a nested array to a parent object with the specified field name. + /// @param _name The name of the field in the parent object + /// @param The expected size (unused, reserved for future optimization) + /// @param _parent Pointer to the parent object to add to + /// @return An output array that can be populated with elements + OutputArrayType add_array_to_object(const std::string_view& _name, + const size_t, + OutputObjectType* _parent) const; + + /// Adds a nested object to a parent array. + /// @param The expected size (unused, reserved for future optimization) + /// @param _parent Pointer to the parent array to add to + /// @return An output object that can be populated with key-value pairs + template + OutputObjectType add_object_to_array(const size_t, + OutputArrayType* _parent) const { + static_assert(always_false_v, + "Writing nested objects to ENV is not supported."); + return OutputObjectType{}; + } + + /// Adds a nested object to a parent object with the specified field name. + /// @param _name The name of the field in the parent object + /// @param The expected size (unused, reserved for future optimization) + /// @param _parent Pointer to the parent object to add to + /// @return An output object that can be populated with key-value pairs + OutputObjectType add_object_to_object(const std::string_view& _name, + const size_t, + OutputObjectType* _parent) const; + + /// Adds a value to a parent array. + /// Supports basic types like strings, numbers, and booleans. + /// @tparam T The type of the value to add + /// @param _var The value to add to the array + /// @param _parent Pointer to the parent array to add to + /// @return An output variable representing the added value + /// @throws std::runtime_error if the value cannot be added + template + OutputVarType add_value_to_array(const T& _var, + OutputArrayType* _parent) const { + const auto val = from_basic_type(_var); + _parent->values->emplace_back(val); + return OutputVarType(val); + } + + /// Adds a value to a parent object with the specified field name. + /// Supports basic types like strings, numbers, and booleans. + /// @tparam T The type of the value to add + /// @param _name The name of the field in the parent object + /// @param _var The value to add to the object + /// @param _parent Pointer to the parent object to add to + /// @return An output variable representing the added value + /// @throws std::runtime_error if the field cannot be added + template + OutputVarType add_value_to_object(const std::string_view& _name, + const T& _var, + OutputObjectType* _parent) const { + const auto val = from_basic_type(_var); + _parent->fields->emplace(std::string(_name), Ref(val)); + return OutputVarType(val); + } + + /// Adds a null value to a parent array. + /// @param _parent Pointer to the parent array to add to + /// @return An output variable representing the null value + OutputVarType add_null_to_array(OutputArrayType* _parent) const; + + /// Adds a null value to a parent object with the specified field name. + /// @param _name The name of the field in the parent object + /// @param _parent Pointer to the parent object to add to + /// @return An output variable representing the null value + OutputVarType add_null_to_object(const std::string_view& _name, + OutputObjectType* _parent) const; + + /// Finalizes a JSON array after all elements have been added. + /// This is a no-op for yyjson as arrays don't need explicit finalization. + /// @param Pointer to the array (unused) + void end_array(OutputArrayType*) const noexcept; + + /// Finalizes a JSON object after all fields have been added. + /// This is a no-op for yyjson as objects don't need explicit finalization. + /// @param Pointer to the object (unused) + void end_object(OutputObjectType*) const noexcept; + + private: + template + OutputVarType from_basic_type(const T& _var) const { + using U = std::remove_cvref_t; + + if constexpr (std::same_as) { + return OutputVarType{.value = _var}; + + } else if constexpr (std::same_as) { + return OutputVarType{.value = _var ? "true" : "false"}; + + } else if constexpr (std::is_integral_v) { + return OutputVarType{.value = std::to_string(_var)}; + + } else if constexpr (std::is_floating_point_v) { + return OutputVarType{.value = std::to_string(_var)}; + + } else { + static_assert(always_false_v, + "Unsupported type for ENV serialization."); + } + } +}; + +} // namespace rfl::env + +#endif diff --git a/src/rfl/env/Writer.cpp b/src/rfl/env/Writer.cpp new file mode 100644 index 00000000..e39e9a85 --- /dev/null +++ b/src/rfl/env/Writer.cpp @@ -0,0 +1,112 @@ +/* + +MIT License + +Copyright (c) 2023-2024 Code17 GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#include "rfl/env/Writer.hpp" + +#include +#include +#include +#include +#include + +namespace rfl::env { + +static constexpr const char* XML_CONTENT = "xml_content"; + +Writer::Writer() {} + +Writer::OutputObjectType Writer::object_as_root( + const size_t /*_size*/) const noexcept { + return OutputObjectType{.prefix = ""}; +} + +Writer::OutputArrayType Writer::add_array_to_object( + const std::string_view& _name, const size_t /*_size*/, + OutputObjectType* _parent) const { + const auto arr = OutputArrayType{}; + _parent->fields->emplace(std::string(_name), arr); + return arr; +} + +Writer::OutputObjectType Writer::add_object_to_object( + const std::string_view& _name, const size_t /*_size*/, + OutputObjectType* _parent) const { + const auto obj = + OutputObjectType{.prefix = _parent->prefix + std::string(_name) + "_"}; + _parent->fields->emplace(std::string(_name), obj); + return obj; +} + +Writer::OutputVarType Writer::add_null_to_array( + OutputArrayType* _parent) const { + const auto var = OutputVarType{}; + _parent->values->emplace_back(var); + return var; +} + +Writer::OutputVarType Writer::add_null_to_object( + const std::string_view& _name, OutputObjectType* _parent) const { + const auto var = OutputVarType{}; + _parent->fields->emplace(std::string(_name), var); + return var; +} + +void Writer::end_array(OutputArrayType* /*_arr*/) const {} + +void Writer::end_object(OutputObjectType* _obj) const { + const auto& obj = *_obj; + for (const auto& [key, value] : *obj.fields) { + value.visit([&](const auto& _v) { + using T = std::remove_cvref_t; + if constexpr (std::is_same_v) { + std::setenv((obj.prefix + key).c_str(), _v.value.c_str(), 1); + + } else if constexpr (std::is_same_v) { + const auto arr_str = [&]() { + std::string result; + for (const auto& val : *_v.values) { + if (!result.empty()) { + result += ","; + } + result += val.value; + } + return result; + }(); + std::setenv((obj.prefix + key).c_str(), arr_str.c_str(), 1); + + } else if constexpr (std::is_same_v) { + // Do nothing for objects, as end_object should have already processed + // their fields recursively. + + } else { + static_assert(always_false_v, + "Unsupported type in OutputObjectType."); + } + }); + } +} + +} // namespace rfl::env From dcf647e476aec06d47edb7b6fc010a39bcaf443c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Thu, 9 Jul 2026 23:21:10 +0200 Subject: [PATCH 09/22] Built my first working test --- include/rfl/env.hpp | 1 + include/rfl/env/Parser.hpp | 6 +----- include/rfl/env/Reader.hpp | 13 ++++++------- include/rfl/env/Writer.hpp | 4 ++-- include/rfl/env/read.hpp | 31 +++++++---------------------- include/rfl/env/write.hpp | 23 ++++++++++++++++++++++ src/reflectcpp.cpp | 1 + src/rfl/env/Writer.cpp | 12 ++++++------ tests/CMakeLists.txt | 1 + tests/env/CMakeLists.txt | 21 ++++++++++++++++++++ tests/env/test_basic.cpp | 40 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 109 insertions(+), 44 deletions(-) create mode 100644 include/rfl/env/write.hpp create mode 100644 tests/env/CMakeLists.txt create mode 100644 tests/env/test_basic.cpp diff --git a/include/rfl/env.hpp b/include/rfl/env.hpp index 12e256f4..06dfaeb4 100644 --- a/include/rfl/env.hpp +++ b/include/rfl/env.hpp @@ -6,5 +6,6 @@ #include "env/Reader.hpp" #include "env/Writer.hpp" #include "env/read.hpp" +#include "env/write.hpp" #endif diff --git a/include/rfl/env/Parser.hpp b/include/rfl/env/Parser.hpp index e6075d03..aeb57ded 100644 --- a/include/rfl/env/Parser.hpp +++ b/include/rfl/env/Parser.hpp @@ -1,14 +1,10 @@ #ifndef RFL_ENV_PARSER_HPP_ #define RFL_ENV_PARSER_HPP_ -#include "../NamedTuple.hpp" -#include "../generic/Writer.hpp" -#include "../internal/no_field_names_v.hpp" -#include "../internal/no_optionals_v.hpp" -#include "../parsing/NamedTupleParser.hpp" #include "../parsing/Parser.hpp" #include "../parsing/Parser_base.hpp" #include "Reader.hpp" +#include "Writer.hpp" namespace rfl::env { diff --git a/include/rfl/env/Reader.hpp b/include/rfl/env/Reader.hpp index 7073c895..aa765de6 100644 --- a/include/rfl/env/Reader.hpp +++ b/include/rfl/env/Reader.hpp @@ -172,8 +172,9 @@ struct RFL_API Reader { if (_var.direct_value) { return false; } - if (const auto str = std::getenv(_var.path.c_str()) != nullptr) { - return str.empty(); + const auto str = std::getenv(_var.path.c_str()); + if (str != nullptr) { + return std::string(str).empty(); } if (_var.path.empty()) { return true; @@ -222,16 +223,14 @@ struct RFL_API Reader { std::optional read_object(const ObjectReader& _object_reader, const InputObjectType& _obj) const noexcept { using ViewType = typename std::remove_cvref_t::ViewType; - const auto names = typename ViewType::Names::names(); + using NamesType = typename ViewType::Names; + const auto names = NamesType::names(); for (const auto& name : names) { const auto child_path = _obj.prefix.empty() ? name : _obj.prefix + name; const auto var = InputVarType{.path = child_path, .direct_value = std::nullopt}; if (!is_empty(var)) { - const auto err = _object_reader.read(std::string_view(name), var); - if (err) { - return err; - } + _object_reader.read(std::string_view(name), var); } } return std::nullopt; diff --git a/include/rfl/env/Writer.hpp b/include/rfl/env/Writer.hpp index ec8546f5..d8a33391 100644 --- a/include/rfl/env/Writer.hpp +++ b/include/rfl/env/Writer.hpp @@ -133,8 +133,8 @@ class RFL_API Writer { const T& _var, OutputObjectType* _parent) const { const auto val = from_basic_type(_var); - _parent->fields->emplace(std::string(_name), Ref(val)); - return OutputVarType(val); + _parent->fields->emplace(std::string(_name), val); + return val; } /// Adds a null value to a parent array. diff --git a/include/rfl/env/read.hpp b/include/rfl/env/read.hpp index dd529388..6854f5b7 100644 --- a/include/rfl/env/read.hpp +++ b/include/rfl/env/read.hpp @@ -2,36 +2,19 @@ #define RFL_ENV_READ_HPP_ #include "../Processors.hpp" -#include "../SnakeCaseToKebabCase.hpp" +#include "../ToAllCaps.hpp" #include "Parser.hpp" #include "Reader.hpp" -#include "parse_argv.hpp" -#include "resolve_args.hpp" -namespace rfl::cli { +namespace rfl::env { -/// Parses command-line arguments into a struct using reflection. -/// Field names are automatically converted from snake_case to kebab-case for -/// ENV arguments. For example, a struct field named `host_name` will match the -/// CLI argument `--host-name`. Supports nested objects (e.g., --database.host), -/// arrays (e.g., --ports=8080,8081), positional arguments, and short flags. -/// @tparam T The struct type to parse into -/// @tparam Ps Optional processors to apply during parsing -/// @param argc Number of command-line arguments -/// @param argv Array of command-line argument strings -/// @return A Result containing the parsed struct or an error template -rfl::Result read(int argc, char* argv[]) { - using ProcessorsType = Processors; - return parse_argv(argc, argv) - .and_then(resolve_args) - .and_then([](auto _args) -> rfl::Result { - const auto r = Reader(); - const auto var = CliVarType{&_args, "", std::nullopt}; - return Parser::read(r, var); - }); +rfl::Result read() { + using InputVarType = typename Reader::InputVarType; + const auto r = Reader(); + return Parser>::read(r, InputVarType{}); } -} // namespace rfl::cli +} // namespace rfl::env #endif diff --git a/include/rfl/env/write.hpp b/include/rfl/env/write.hpp new file mode 100644 index 00000000..af7bf633 --- /dev/null +++ b/include/rfl/env/write.hpp @@ -0,0 +1,23 @@ +#ifndef RFL_ENV_WRITE_HPP_ +#define RFL_ENV_WRITE_HPP_ + +#include "../Processors.hpp" +#include "../ToAllCaps.hpp" +#include "../parsing/Parent.hpp" +#include "Parser.hpp" +#include "Writer.hpp" + +namespace rfl::env { + +template +void write(const auto& _obj) { + using T = std::remove_cvref_t; + using ParentType = parsing::Parent; + auto w = Writer(); + Parser>::write(w, _obj, + typename ParentType::Root{}); +} + +} // namespace rfl::env + +#endif diff --git a/src/reflectcpp.cpp b/src/reflectcpp.cpp index ae3c4b3b..ea76c27c 100644 --- a/src/reflectcpp.cpp +++ b/src/reflectcpp.cpp @@ -30,6 +30,7 @@ SOFTWARE. // compilation. #include "rfl/Generic.cpp" +#include "rfl/env/Writer.cpp" #include "rfl/generic/Writer.cpp" #include "rfl/internal/strings/strings.cpp" #include "rfl/parsing/schema/Type.cpp" diff --git a/src/rfl/env/Writer.cpp b/src/rfl/env/Writer.cpp index e39e9a85..1bae92d6 100644 --- a/src/rfl/env/Writer.cpp +++ b/src/rfl/env/Writer.cpp @@ -74,15 +74,15 @@ Writer::OutputVarType Writer::add_null_to_object( return var; } -void Writer::end_array(OutputArrayType* /*_arr*/) const {} +void Writer::end_array(OutputArrayType* /*_arr*/) const noexcept {} -void Writer::end_object(OutputObjectType* _obj) const { +void Writer::end_object(OutputObjectType* _obj) const noexcept { const auto& obj = *_obj; for (const auto& [key, value] : *obj.fields) { value.visit([&](const auto& _v) { using T = std::remove_cvref_t; if constexpr (std::is_same_v) { - std::setenv((obj.prefix + key).c_str(), _v.value.c_str(), 1); + setenv((obj.prefix + key).c_str(), _v.value.c_str(), 1); } else if constexpr (std::is_same_v) { const auto arr_str = [&]() { @@ -95,11 +95,11 @@ void Writer::end_object(OutputObjectType* _obj) const { } return result; }(); - std::setenv((obj.prefix + key).c_str(), arr_str.c_str(), 1); + setenv((obj.prefix + key).c_str(), arr_str.c_str(), 1); } else if constexpr (std::is_same_v) { - // Do nothing for objects, as end_object should have already processed - // their fields recursively. + // Do nothing for objects, as end_object should have already + // processed their fields recursively. } else { static_assert(always_false_v, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d68b3a0f..553a5088 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,6 +15,7 @@ if (REFLECTCPP_JSON) add_subdirectory(json) add_subdirectory(json_c_arrays_and_inheritance) add_subdirectory(cli) + add_subdirectory(env) endif () if (REFLECTCPP_AVRO) diff --git a/tests/env/CMakeLists.txt b/tests/env/CMakeLists.txt new file mode 100644 index 00000000..8fe3e031 --- /dev/null +++ b/tests/env/CMakeLists.txt @@ -0,0 +1,21 @@ +project(reflect-cpp-env-tests) + +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "*.cpp") + +add_executable( + reflect-cpp-env-tests + ${SOURCES} +) +target_precompile_headers(reflect-cpp-env-tests PRIVATE [["rfl.hpp"]] ) + +target_include_directories(reflect-cpp-env-tests SYSTEM PRIVATE "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include") + +target_link_libraries(reflect-cpp-env-tests PRIVATE reflectcpp_tests_crt) + +add_custom_command(TARGET reflect-cpp-env-tests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy -t $ $ + COMMAND_EXPAND_LISTS +) + +find_package(GTest) +gtest_discover_tests(reflect-cpp-env-tests) diff --git a/tests/env/test_basic.cpp b/tests/env/test_basic.cpp new file mode 100644 index 00000000..eb7b4c48 --- /dev/null +++ b/tests/env/test_basic.cpp @@ -0,0 +1,40 @@ +#include + +#include +#include +#include + +namespace test_basic { + +struct Settings { + std::string host; + int port; + double rate; + bool verbose; +}; + +TEST(env, test_basic) { + const auto settings = Settings{ + .host = "localhost", + .port = 8080, + .rate = 1.5, + .verbose = true, + }; + + rfl::env::write(settings); + + ASSERT_EQ(std::getenv("HOST"), std::string("localhost")); + ASSERT_EQ(std::getenv("PORT"), std::string("8080")); + ASSERT_EQ(std::getenv("RATE"), std::string("1.500000")); + ASSERT_EQ(std::getenv("VERBOSE"), std::string("true")); + + const auto read_settings = rfl::env::read(); + + ASSERT_TRUE(read_settings); + ASSERT_EQ(read_settings->host, "localhost"); + ASSERT_EQ(read_settings->port, 8080); + ASSERT_DOUBLE_EQ(read_settings->rate, 1.5); + ASSERT_EQ(read_settings->verbose, true); +} + +} // namespace test_basic From 4eb0f9df21f81c0c085cdb87eb2b382d4dff61cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Thu, 9 Jul 2026 23:39:25 +0200 Subject: [PATCH 10/22] Added test for nested vars --- tests/env/test_nested.cpp | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/env/test_nested.cpp diff --git a/tests/env/test_nested.cpp b/tests/env/test_nested.cpp new file mode 100644 index 00000000..0db0c668 --- /dev/null +++ b/tests/env/test_nested.cpp @@ -0,0 +1,52 @@ +#include + +#include +#include +#include + +namespace test_nested { + +struct NestedSettings { + std::string database_host; + int database_port; +}; + +struct Settings { + std::string host; + int port; + double rate; + bool verbose; + NestedSettings nested; +}; + +TEST(env, test_nested) { + const auto settings = Settings{.host = "localhost", + .port = 8080, + .rate = 1.5, + .verbose = true, + .nested = NestedSettings{ + .database_host = "localhost", + .database_port = 8080, + }}; + + rfl::env::write(settings); + + ASSERT_EQ(std::getenv("HOST"), std::string("localhost")); + ASSERT_EQ(std::getenv("PORT"), std::string("8080")); + ASSERT_EQ(std::getenv("RATE"), std::string("1.500000")); + ASSERT_EQ(std::getenv("VERBOSE"), std::string("true")); + ASSERT_EQ(std::getenv("NESTED_DATABASE_HOST"), std::string("localhost")); + ASSERT_EQ(std::getenv("NESTED_DATABASE_PORT"), std::string("8080")); + + const auto read_settings = rfl::env::read(); + + ASSERT_TRUE(read_settings); + ASSERT_EQ(read_settings->host, "localhost"); + ASSERT_EQ(read_settings->port, 8080); + ASSERT_DOUBLE_EQ(read_settings->rate, 1.5); + ASSERT_EQ(read_settings->verbose, true); + ASSERT_EQ(read_settings->nested.database_host, "localhost"); + ASSERT_EQ(read_settings->nested.database_port, 8080); +} + +} // namespace test_nested From b730a41e6063fa4ea6771ae34a453660bdc54076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Sun, 12 Jul 2026 18:48:01 +0200 Subject: [PATCH 11/22] Better support for arrays --- include/rfl/env/Reader.hpp | 109 +++++++++++++----------------- include/rfl/env/Writer.hpp | 30 +++----- src/rfl/env/Writer.cpp | 64 ++++++++++++++---- tests/env/test_readme_example.cpp | 61 +++++++++++++++++ tests/env/test_vector.cpp | 53 +++++++++++++++ 5 files changed, 222 insertions(+), 95 deletions(-) create mode 100644 tests/env/test_readme_example.cpp create mode 100644 tests/env/test_vector.cpp diff --git a/include/rfl/env/Reader.hpp b/include/rfl/env/Reader.hpp index aa765de6..9524777a 100644 --- a/include/rfl/env/Reader.hpp +++ b/include/rfl/env/Reader.hpp @@ -2,19 +2,14 @@ #define RFL_ENV_READER_HPP_ #include -#include #include #include -#include #include -#include #include #include #include -#include #include "../Result.hpp" -#include "../always_false.hpp" extern char** environ; @@ -22,22 +17,20 @@ namespace rfl::env { /// Represents a ENV variable that can be a direct value or a path in the /// argument map. The `path` represents the hierarchical key (e.g., -/// "DATABASE_HOST"). The `direct_value` is used when parsing array directly. +/// "DATABASE_HOST"). struct InputEnvVarType { - const std::string path; - const std::optional direct_value; + std::string path; }; /// Represents a ENV object with a prefix path for accessing nested fields. /// All child fields will be prefixed with this path. struct InputEnvObjectType { - const std::string prefix; + std::string prefix; }; /// Represents a ENV array containing multiple string values. -/// Typically created by splitting a comma-delimited argument value. struct InputEnvArrayType { - const std::vector values; + std::string prefix; }; // --- Constrained overloads for string-to-type parsing --- @@ -144,11 +137,7 @@ struct RFL_API Reader { /// of bounds rfl::Result get_field_from_array( const size_t _idx, const InputArrayType& _arr) const noexcept { - if (_idx >= _arr.values.size()) { - return error(std::string("Index ") + std::to_string(_idx) + - " out of bounds."); - } - return InputVarType{.path = "", .direct_value = _arr.values[_idx]}; + return InputVarType{.path = _arr.prefix + std::to_string(_idx)}; } /// Gets a specific field from a ENV object by name. @@ -160,7 +149,7 @@ struct RFL_API Reader { rfl::Result get_field_from_object( const std::string& _name, const InputObjectType& _obj) const noexcept { const auto child_path = _obj.prefix.empty() ? _name : _obj.prefix + _name; - return InputVarType{.path = child_path, .direct_value = std::nullopt}; + return InputVarType{.path = child_path}; } /// Checks if a ENV variable is empty (has no value). @@ -169,9 +158,6 @@ struct RFL_API Reader { /// @param _var The ENV variable to check /// @return true if the variable is empty, false otherwise bool is_empty(const InputVarType& _var) const noexcept { - if (_var.direct_value) { - return false; - } const auto str = std::getenv(_var.path.c_str()); if (str != nullptr) { return std::string(str).empty(); @@ -201,12 +187,17 @@ struct RFL_API Reader { template std::optional read_array(const ArrayReader& _array_reader, const InputArrayType& _arr) const noexcept { - for (const auto& val : _arr.values) { - const auto err = - _array_reader.read(InputVarType{.path = "", .direct_value = val}); + size_t idx = 0; + while (true) { + const auto var = InputVarType{.path = _arr.prefix + std::to_string(idx)}; + if (is_empty(var)) { + break; + } + const auto err = _array_reader.read(var); if (err) { return err; } + ++idx; } return std::nullopt; } @@ -222,17 +213,39 @@ struct RFL_API Reader { template std::optional read_object(const ObjectReader& _object_reader, const InputObjectType& _obj) const noexcept { - using ViewType = typename std::remove_cvref_t::ViewType; - using NamesType = typename ViewType::Names; - const auto names = NamesType::names(); - for (const auto& name : names) { - const auto child_path = _obj.prefix.empty() ? name : _obj.prefix + name; - const auto var = - InputVarType{.path = child_path, .direct_value = std::nullopt}; - if (!is_empty(var)) { - _object_reader.read(std::string_view(name), var); + constexpr bool has_view_type = + requires { typename std::remove_cvref_t::ViewType; }; + + if constexpr (has_view_type) { + using ViewType = typename std::remove_cvref_t::ViewType; + using NamesType = typename ViewType::Names; + const auto names = NamesType::names(); + for (const auto& name : names) { + const auto child_path = _obj.prefix.empty() ? name : _obj.prefix + name; + const auto var = InputVarType{.path = child_path}; + if (!is_empty(var)) { + _object_reader.read(std::string_view(name), var); + } + } + + } else { + for (char** env = environ; *env != nullptr; ++env) { + const std::string env_entry(*env); + if (env_entry.size() <= _obj.prefix.size()) { + continue; + } + if (env_entry.compare(0, _obj.prefix.size(), _obj.prefix) == 0) { + const auto pos_eq = env_entry.find('='); + if (pos_eq == std::string::npos) { + continue; + } + const auto name = env_entry.substr(_obj.prefix.size(), pos_eq); + const auto var = InputVarType{.path = env_entry.substr(0, pos_eq)}; + _object_reader.read(std::string_view(name), var); + } } } + return std::nullopt; } @@ -254,11 +267,8 @@ struct RFL_API Reader { /// @return A Result containing a CliArrayType with the split values rfl::Result to_array( const InputVarType& _var) const noexcept { - const auto str = get_value(_var); - if (!str) { - return InputArrayType{.values = {}}; - } - return InputArrayType{.values = split(*str, ',')}; + const auto prefix = _var.path.empty() ? std::string("") : _var.path + "_"; + return InputArrayType{.prefix = prefix}; } /// Converts a ENV variable to an object for reading nested fields. @@ -283,9 +293,6 @@ struct RFL_API Reader { private: static std::optional get_value( const InputVarType& _var) noexcept { - if (_var.direct_value) { - return *_var.direct_value; - } const char* env_value = std::getenv(_var.path.c_str()); if (env_value) { return std::string(env_value); @@ -293,28 +300,6 @@ struct RFL_API Reader { return std::nullopt; } } - - static std::vector split(const std::string& _str, char _delim) { - std::vector result; - if (_str.empty()) { - return result; - } - size_t start = 0; - while (true) { - const auto pos = _str.find(_delim, start); - if (pos == std::string::npos) { - if (start < _str.size()) { - result.emplace_back(_str.substr(start)); - } - break; - } - if (pos > start) { - result.emplace_back(_str.substr(start, pos - start)); - } - start = pos + 1; - } - return result; - } }; } // namespace rfl::env diff --git a/include/rfl/env/Writer.hpp b/include/rfl/env/Writer.hpp index d8a33391..23512bb7 100644 --- a/include/rfl/env/Writer.hpp +++ b/include/rfl/env/Writer.hpp @@ -18,8 +18,13 @@ struct OutputEnvVarType { std::string value; }; +struct OutputEnvObjectType; + struct OutputEnvArrayType { - Ref> values; + std::string prefix; + Ref>> + values; }; struct OutputEnvObjectType { @@ -39,12 +44,7 @@ class RFL_API Writer { Writer(); - template - OutputArrayType array_as_root(const size_t) const noexcept { - static_assert(always_false_v, - "Writing arrays to ENV as root variables is not supported."); - return OutputArrayType{}; - } + OutputArrayType array_as_root(const size_t) const noexcept; OutputObjectType object_as_root(const size_t) const noexcept; @@ -67,13 +67,8 @@ class RFL_API Writer { /// @param The expected size (unused, reserved for future optimization) /// @param _parent Pointer to the parent array to add to /// @return An output array that can be populated with elements - template OutputArrayType add_array_to_array(const size_t, - OutputArrayType* _parent) const { - static_assert(always_false_v, - "Writing nested arrays to ENV is not supported."); - return OutputArrayType{}; - } + OutputArrayType* _parent) const; /// Adds a nested array to a parent object with the specified field name. /// @param _name The name of the field in the parent object @@ -88,13 +83,8 @@ class RFL_API Writer { /// @param The expected size (unused, reserved for future optimization) /// @param _parent Pointer to the parent array to add to /// @return An output object that can be populated with key-value pairs - template OutputObjectType add_object_to_array(const size_t, - OutputArrayType* _parent) const { - static_assert(always_false_v, - "Writing nested objects to ENV is not supported."); - return OutputObjectType{}; - } + OutputArrayType* _parent) const; /// Adds a nested object to a parent object with the specified field name. /// @param _name The name of the field in the parent object @@ -117,7 +107,7 @@ class RFL_API Writer { OutputArrayType* _parent) const { const auto val = from_basic_type(_var); _parent->values->emplace_back(val); - return OutputVarType(val); + return val; } /// Adds a value to a parent object with the specified field name. diff --git a/src/rfl/env/Writer.cpp b/src/rfl/env/Writer.cpp index 1bae92d6..7ff21a3a 100644 --- a/src/rfl/env/Writer.cpp +++ b/src/rfl/env/Writer.cpp @@ -38,15 +38,30 @@ static constexpr const char* XML_CONTENT = "xml_content"; Writer::Writer() {} +Writer::OutputArrayType Writer::array_as_root( + const size_t /*_size*/) const noexcept { + return OutputArrayType{.prefix = ""}; +} + Writer::OutputObjectType Writer::object_as_root( const size_t /*_size*/) const noexcept { return OutputObjectType{.prefix = ""}; } +Writer::OutputArrayType Writer::add_array_to_array( + const size_t /*_size*/, OutputArrayType* _parent) const { + const auto arr = + OutputArrayType{.prefix = _parent->prefix + + std::to_string(_parent->values->size()) + "_"}; + _parent->values->emplace_back(arr); + return arr; +} + Writer::OutputArrayType Writer::add_array_to_object( const std::string_view& _name, const size_t /*_size*/, OutputObjectType* _parent) const { - const auto arr = OutputArrayType{}; + const auto arr = + OutputArrayType{.prefix = _parent->prefix + std::string(_name) + "_"}; _parent->fields->emplace(std::string(_name), arr); return arr; } @@ -60,6 +75,15 @@ Writer::OutputObjectType Writer::add_object_to_object( return obj; } +Writer::OutputObjectType Writer::add_object_to_array( + const size_t /*_size*/, OutputArrayType* _parent) const { + const auto obj = + OutputObjectType{.prefix = _parent->prefix + + std::to_string(_parent->values->size()) + "_"}; + _parent->values->emplace_back(obj); + return obj; +} + Writer::OutputVarType Writer::add_null_to_array( OutputArrayType* _parent) const { const auto var = OutputVarType{}; @@ -74,7 +98,30 @@ Writer::OutputVarType Writer::add_null_to_object( return var; } -void Writer::end_array(OutputArrayType* /*_arr*/) const noexcept {} +void Writer::end_array(OutputArrayType* _arr) const noexcept { + const auto& arr = *_arr; + for (size_t i = 0; i < arr.values->size(); ++i) { + const auto& value = (*arr.values)[i]; + value.visit([&](const auto& _v) { + using T = std::remove_cvref_t; + if constexpr (std::is_same_v) { + setenv((arr.prefix + std::to_string(i)).c_str(), _v.value.c_str(), 1); + + } else if constexpr (std::is_same_v) { + // Do nothing for arrays, as end_array should have already + // processed their fields recursively. + + } else if constexpr (std::is_same_v) { + // Do nothing for objects, as end_object should have already + // processed their fields recursively. + + } else { + static_assert(always_false_v, + "Unsupported type in OutputArrayType."); + } + }); + } +} void Writer::end_object(OutputObjectType* _obj) const noexcept { const auto& obj = *_obj; @@ -85,17 +132,8 @@ void Writer::end_object(OutputObjectType* _obj) const noexcept { setenv((obj.prefix + key).c_str(), _v.value.c_str(), 1); } else if constexpr (std::is_same_v) { - const auto arr_str = [&]() { - std::string result; - for (const auto& val : *_v.values) { - if (!result.empty()) { - result += ","; - } - result += val.value; - } - return result; - }(); - setenv((obj.prefix + key).c_str(), arr_str.c_str(), 1); + // Do nothing for arrays, as end_array should have already + // processed their fields recursively. } else if constexpr (std::is_same_v) { // Do nothing for objects, as end_object should have already diff --git a/tests/env/test_readme_example.cpp b/tests/env/test_readme_example.cpp new file mode 100644 index 00000000..babbe4ce --- /dev/null +++ b/tests/env/test_readme_example.cpp @@ -0,0 +1,61 @@ +#include + +#include +#include +#include +#include + +namespace test_readme_example { + +using Age = rfl::Validator, rfl::Maximum<130>>; + +struct Person { + std::string first_name; + std::string last_name = "Simpson"; + std::string town = "Springfield"; + rfl::Timestamp<"%Y-%m-%d"> birthday; + Age age; + rfl::Email email; + std::vector children; +}; + +TEST(env, test_readme_example) { + const auto bart = Person{.first_name = "Bart", + .birthday = "1987-04-19", + .age = 10, + .email = "bart@simpson.com"}; + + const auto lisa = Person{.first_name = "Lisa", + .birthday = "1987-04-19", + .age = 8, + .email = "lisa@simpson.com"}; + + const auto maggie = Person{.first_name = "Maggie", + .birthday = "1987-04-19", + .age = 0, + .email = "maggie@simpson.com"}; + + const auto homer = + Person{.first_name = "Homer", + .birthday = "1987-04-19", + .age = 45, + .email = "homer@simpson.com", + .children = std::vector({bart, lisa, maggie})}; + + rfl::env::write(homer); + + ASSERT_EQ(std::getenv("FIRST_NAME"), std::string("Homer")); + ASSERT_EQ(std::getenv("LAST_NAME"), std::string("Simpson")); + ASSERT_EQ(std::getenv("TOWN"), std::string("Springfield")); + ASSERT_EQ(std::getenv("BIRTHDAY"), std::string("1987-04-19")); + ASSERT_EQ(std::getenv("AGE"), std::string("45")); + ASSERT_EQ(std::getenv("EMAIL"), std::string("homer@simpson.com")); + + ASSERT_EQ(std::getenv("CHILDREN_0_FIRST_NAME"), std::string("Bart")); + ASSERT_EQ(std::getenv("CHILDREN_0_LAST_NAME"), std::string("Simpson")); + ASSERT_EQ(std::getenv("CHILDREN_0_TOWN"), std::string("Springfield")); + ASSERT_EQ(std::getenv("CHILDREN_0_BIRTHDAY"), std::string("1987-04-19")); + ASSERT_EQ(std::getenv("CHILDREN_0_AGE"), std::string("10")); +} + +} // namespace test_readme_example diff --git a/tests/env/test_vector.cpp b/tests/env/test_vector.cpp new file mode 100644 index 00000000..32939b96 --- /dev/null +++ b/tests/env/test_vector.cpp @@ -0,0 +1,53 @@ +#include + +#include +#include +#include + +namespace test_vector { + +struct Settings { + std::string host; + int port; + double rate; + bool verbose; + std::vector tags; +}; + +TEST(env, test_vector) { + const auto settings = Settings{.host = "localhost", + .port = 8080, + .rate = 1.5, + .verbose = true, + .tags = {"tag1", "tag2", "tag3"}}; + + rfl::env::write(settings); + + ASSERT_TRUE(std::getenv("HOST")); + ASSERT_EQ(std::getenv("HOST"), std::string("localhost")); + ASSERT_TRUE(std::getenv("PORT")); + ASSERT_EQ(std::getenv("PORT"), std::string("8080")); + ASSERT_TRUE(std::getenv("RATE")); + ASSERT_EQ(std::getenv("RATE"), std::string("1.500000")); + ASSERT_TRUE(std::getenv("VERBOSE")); + ASSERT_EQ(std::getenv("VERBOSE"), std::string("true")); + ASSERT_TRUE(std::getenv("TAGS_0")); + ASSERT_EQ(std::getenv("TAGS_0"), std::string("tag1")); + ASSERT_EQ(std::getenv("TAGS_0"), std::string("tag1")); + ASSERT_EQ(std::getenv("TAGS_1"), std::string("tag2")); + ASSERT_EQ(std::getenv("TAGS_2"), std::string("tag3")); + + const auto read_settings = rfl::env::read(); + + ASSERT_TRUE(read_settings); + ASSERT_EQ(read_settings->host, "localhost"); + ASSERT_EQ(read_settings->port, 8080); + ASSERT_DOUBLE_EQ(read_settings->rate, 1.5); + ASSERT_EQ(read_settings->verbose, true); + ASSERT_EQ(read_settings->tags.size(), 3); + ASSERT_EQ(read_settings->tags[0], "tag1"); + ASSERT_EQ(read_settings->tags[1], "tag2"); + ASSERT_EQ(read_settings->tags[2], "tag3"); +} + +} // namespace test_vector From 8b27f1711a58eeee4578b7aba995834dbf297f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Sun, 12 Jul 2026 20:31:50 +0200 Subject: [PATCH 12/22] Make sure that empty arrays are handled properly --- include/rfl/env/Parser.hpp | 17 ++++++++++++++++- tests/env/test_readme_example.cpp | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/include/rfl/env/Parser.hpp b/include/rfl/env/Parser.hpp index aeb57ded..e58574e9 100644 --- a/include/rfl/env/Parser.hpp +++ b/include/rfl/env/Parser.hpp @@ -1,11 +1,26 @@ #ifndef RFL_ENV_PARSER_HPP_ #define RFL_ENV_PARSER_HPP_ -#include "../parsing/Parser.hpp" +#include "../parsing/AreReaderAndWriter.hpp" #include "../parsing/Parser_base.hpp" #include "Reader.hpp" #include "Writer.hpp" +namespace rfl::parsing { + +template + requires AreReaderAndWriter> +struct Parser, + ProcessorsType> + : public NamedTupleParser< + env::Reader, env::Writer, + /*_ignore_empty_containers=*/true, + /*_all_required=*/internal::no_optionals_v, + /*_no_field_names_=*/false, ProcessorsType, FieldTypes...> {}; + +} // namespace rfl::parsing + namespace rfl::env { template diff --git a/tests/env/test_readme_example.cpp b/tests/env/test_readme_example.cpp index babbe4ce..d33dfa25 100644 --- a/tests/env/test_readme_example.cpp +++ b/tests/env/test_readme_example.cpp @@ -56,6 +56,22 @@ TEST(env, test_readme_example) { ASSERT_EQ(std::getenv("CHILDREN_0_TOWN"), std::string("Springfield")); ASSERT_EQ(std::getenv("CHILDREN_0_BIRTHDAY"), std::string("1987-04-19")); ASSERT_EQ(std::getenv("CHILDREN_0_AGE"), std::string("10")); + + const auto read_homer = rfl::env::read(); + + if (!read_homer) { + std::cout << "Error reading Person from environment: " + << read_homer.error().what() << std::endl; + } + + ASSERT_TRUE(read_homer); + + ASSERT_EQ(read_homer->first_name, "Homer"); + ASSERT_EQ(read_homer->last_name, "Simpson"); + ASSERT_EQ(read_homer->town, "Springfield"); + ASSERT_EQ(read_homer->birthday.str(), "1987-04-19"); + ASSERT_EQ(read_homer->age, 45); + ASSERT_EQ(read_homer->email, "homer@simpson.com"); } } // namespace test_readme_example From 71c82d35ceb5ac34b3c077a8e511d3789013e4cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Mon, 13 Jul 2026 23:47:41 +0200 Subject: [PATCH 13/22] Add more tests --- tests/env/test_add_struct_name.cpp | 45 ++++++++++++++++ tests/env/test_array.cpp | 30 +++++++++++ tests/env/test_box.cpp | 39 ++++++++++++++ tests/env/test_combined_processors.cpp | 48 +++++++++++++++++ tests/env/test_custom_class1.cpp | 32 +++++++++++ tests/env/test_custom_class3.cpp | 61 +++++++++++++++++++++ tests/env/test_custom_class4.cpp | 62 ++++++++++++++++++++++ tests/env/test_default_values.cpp | 25 +++++++++ tests/env/test_deque.cpp | 24 +++++++++ tests/env/test_enum.cpp | 20 +++++++ tests/env/test_extra_fields.cpp | 22 ++++++++ tests/env/test_field_variant.cpp | 30 +++++++++++ tests/env/test_flag_enum.cpp | 31 +++++++++++ tests/env/test_flag_enum_with_int.cpp | 30 +++++++++++ tests/env/test_flatten.cpp | 30 +++++++++++ tests/env/test_flatten_anonymous.cpp | 31 +++++++++++ tests/env/test_forward_list.cpp | 24 +++++++++ tests/env/test_literal.cpp | 22 ++++++++ tests/env/test_literal_map.cpp | 17 ++++++ tests/env/test_map.cpp | 25 +++++++++ tests/env/test_map_with_key_validation.cpp | 26 +++++++++ tests/env/test_monster_example.cpp | 57 ++++++++++++++++++++ tests/env/test_readme_example.cpp | 37 +++++++++++++ tests/env/test_readme_example2.cpp | 19 +++++++ tests/env/test_ref.cpp | 39 ++++++++++++++ tests/env/test_set.cpp | 22 ++++++++ tests/env/test_size.cpp | 35 ++++++++++++ tests/env/test_skip.cpp | 24 +++++++++ tests/env/test_string_map.cpp | 15 ++++++ tests/env/test_tagged_union.cpp | 27 ++++++++++ tests/env/test_tagged_union2.cpp | 27 ++++++++++ tests/env/test_timestamp.cpp | 27 ++++++++++ tests/env/test_unique_ptr.cpp | 26 +++++++++ tests/env/test_unique_ptr2.cpp | 39 ++++++++++++++ tests/env/test_variant.cpp | 27 ++++++++++ 35 files changed, 1095 insertions(+) create mode 100644 tests/env/test_add_struct_name.cpp create mode 100644 tests/env/test_array.cpp create mode 100644 tests/env/test_box.cpp create mode 100644 tests/env/test_combined_processors.cpp create mode 100644 tests/env/test_custom_class1.cpp create mode 100644 tests/env/test_custom_class3.cpp create mode 100644 tests/env/test_custom_class4.cpp create mode 100644 tests/env/test_default_values.cpp create mode 100644 tests/env/test_deque.cpp create mode 100644 tests/env/test_enum.cpp create mode 100644 tests/env/test_extra_fields.cpp create mode 100644 tests/env/test_field_variant.cpp create mode 100644 tests/env/test_flag_enum.cpp create mode 100644 tests/env/test_flag_enum_with_int.cpp create mode 100644 tests/env/test_flatten.cpp create mode 100644 tests/env/test_flatten_anonymous.cpp create mode 100644 tests/env/test_forward_list.cpp create mode 100644 tests/env/test_literal.cpp create mode 100644 tests/env/test_literal_map.cpp create mode 100644 tests/env/test_map.cpp create mode 100644 tests/env/test_map_with_key_validation.cpp create mode 100644 tests/env/test_monster_example.cpp create mode 100644 tests/env/test_readme_example2.cpp create mode 100644 tests/env/test_ref.cpp create mode 100644 tests/env/test_set.cpp create mode 100644 tests/env/test_size.cpp create mode 100644 tests/env/test_skip.cpp create mode 100644 tests/env/test_string_map.cpp create mode 100644 tests/env/test_tagged_union.cpp create mode 100644 tests/env/test_tagged_union2.cpp create mode 100644 tests/env/test_timestamp.cpp create mode 100644 tests/env/test_unique_ptr.cpp create mode 100644 tests/env/test_unique_ptr2.cpp create mode 100644 tests/env/test_variant.cpp diff --git a/tests/env/test_add_struct_name.cpp b/tests/env/test_add_struct_name.cpp new file mode 100644 index 00000000..25f30c6b --- /dev/null +++ b/tests/env/test_add_struct_name.cpp @@ -0,0 +1,45 @@ +#include + +#include +#include +#include +#include + +namespace test_add_struct_name { + +using Age = rfl::Validator, rfl::Maximum<130>>; + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::string town = "Springfield"; + rfl::Timestamp<"%Y-%m-%d"> birthday; + Age age; + rfl::Email email; + std::vector children; +}; + +TEST(env, test_add_struct_name) { + const auto bart = Person{.first_name = "Bart", + .birthday = "1987-04-19", + .age = 10, + .email = "bart@simpson.com"}; + + const auto lisa = Person{.first_name = "Lisa", + .birthday = "1987-04-19", + .age = 8, + .email = "lisa@simpson.com"}; + + const auto maggie = Person{.first_name = "Maggie", + .birthday = "1987-04-19", + .age = 0, + .email = "maggie@simpson.com"}; + + const auto homer = + Person{.first_name = "Homer", + .birthday = "1987-04-19", + .age = 45, + .email = "homer@simpson.com", + .children = std::vector({bart, lisa, maggie})}; +} +} // namespace test_add_struct_name diff --git a/tests/env/test_array.cpp b/tests/env/test_array.cpp new file mode 100644 index 00000000..622c28b9 --- /dev/null +++ b/tests/env/test_array.cpp @@ -0,0 +1,30 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_array { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children = nullptr; +}; + +TEST(env, test_array) { + auto bart = Person{.first_name = "Bart"}; + + auto lisa = Person{.first_name = "Lisa"}; + + auto maggie = Person{.first_name = "Maggie"}; + + const auto homer = Person{ + .first_name = "Homer", + .children = std::make_unique>(std::array{ + std::move(bart), std::move(lisa), std::move(maggie)})}; +} + +} // namespace test_array diff --git a/tests/env/test_box.cpp b/tests/env/test_box.cpp new file mode 100644 index 00000000..446a1151 --- /dev/null +++ b/tests/env/test_box.cpp @@ -0,0 +1,39 @@ +#include + +#include +#include +#include + +namespace test_box { + +struct DecisionTree { + struct Leaf { + using Tag = rfl::Literal<"Leaf">; + double value; + }; + + struct Node { + using Tag = rfl::Literal<"Node">; + rfl::Rename<"criticalValue", double> critical_value; + rfl::Box lesser; + rfl::Box greater; + }; + + using LeafOrNode = rfl::TaggedUnion<"type", Leaf, Node>; + + rfl::Field<"leafOrNode", LeafOrNode> leaf_or_node; +}; + +TEST(env, test_box) { + auto leaf1 = DecisionTree::Leaf{.value = 3.0}; + + auto leaf2 = DecisionTree::Leaf{.value = 5.0}; + + auto node = DecisionTree::Node{ + .critical_value = 10.0, + .lesser = rfl::make_box(DecisionTree{leaf1}), + .greater = rfl::make_box(DecisionTree{leaf2})}; + + const DecisionTree tree{.leaf_or_node = std::move(node)}; +} +} // namespace test_box diff --git a/tests/env/test_combined_processors.cpp b/tests/env/test_combined_processors.cpp new file mode 100644 index 00000000..c6a063fb --- /dev/null +++ b/tests/env/test_combined_processors.cpp @@ -0,0 +1,48 @@ +#include + +#include +#include +#include +#include + +namespace test_combined_processors { + +using Age = rfl::Validator, rfl::Maximum<130>>; + +struct Person { + std::string first_name; + std::string last_name = "Simpson"; + std::string town = "Springfield"; + rfl::Timestamp<"%Y-%m-%d"> birthday; + Age age; + rfl::Email email; + std::vector children; +}; + +TEST(env, test_combined_processors) { + const auto bart = Person{.first_name = "Bart", + .birthday = "1987-04-19", + .age = 10, + .email = "bart@simpson.com"}; + + const auto lisa = Person{.first_name = "Lisa", + .birthday = "1987-04-19", + .age = 8, + .email = "lisa@simpson.com"}; + + const auto maggie = Person{.first_name = "Maggie", + .birthday = "1987-04-19", + .age = 0, + .email = "maggie@simpson.com"}; + + const auto homer = + Person{.first_name = "Homer", + .birthday = "1987-04-19", + .age = 45, + .email = "homer@simpson.com", + .children = std::vector({bart, lisa, maggie})}; + + using Processors = + rfl::Processors>; +} +} // namespace test_combined_processors diff --git a/tests/env/test_custom_class1.cpp b/tests/env/test_custom_class1.cpp new file mode 100644 index 00000000..0e38ed88 --- /dev/null +++ b/tests/env/test_custom_class1.cpp @@ -0,0 +1,32 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_custom_class1 { + +struct Person { + struct PersonImpl { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::vector children; + }; + + using ReflectionType = PersonImpl; + + Person(const PersonImpl& _impl) : impl(_impl) {} + + Person(const std::string& _first_name) + : impl(PersonImpl{.first_name = _first_name}) {} + + const ReflectionType& reflection() const { return impl; }; + + private: + PersonImpl impl; +}; + +TEST(env, test_custom_class1) { const auto bart = Person("Bart"); } +} // namespace test_custom_class1 diff --git a/tests/env/test_custom_class3.cpp b/tests/env/test_custom_class3.cpp new file mode 100644 index 00000000..575b01c0 --- /dev/null +++ b/tests/env/test_custom_class3.cpp @@ -0,0 +1,61 @@ +#include + +#include +#include +#include +#include + +namespace test_custom_class3 { + +struct Person { + Person(const std::string& _first_name, const std::string& _last_name, + const int _age) + : first_name_(_first_name), last_name_(_last_name), age_(_age) {} + + const auto& first_name() const { return first_name_; } + + const auto& last_name() const { return last_name_; } + + auto age() const { return age_; } + + private: + std::string first_name_; + std::string last_name_; + int age_; +}; + +struct PersonImpl { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name; + int age; + + static PersonImpl from_class(const Person& _p) noexcept { + return PersonImpl{.first_name = _p.first_name(), + .last_name = _p.last_name(), + .age = _p.age()}; + } + + Person to_class() const { return Person(first_name(), last_name(), age); } +}; +} // namespace test_custom_class3 + +namespace rfl { +namespace parsing { + +template +struct Parser + : public CustomParser {}; + +} // namespace parsing +} // namespace rfl + +namespace test_custom_class3 { + +TEST(env, test_custom_class3) { + const auto bart = Person("Bart", "Simpson", 10); +} + +} // namespace test_custom_class3 diff --git a/tests/env/test_custom_class4.cpp b/tests/env/test_custom_class4.cpp new file mode 100644 index 00000000..85ce0254 --- /dev/null +++ b/tests/env/test_custom_class4.cpp @@ -0,0 +1,62 @@ +#include + +#include +#include +#include +#include + +namespace test_custom_class4 { + +struct Person { + Person(const std::string& _first_name, + const rfl::Box& _last_name, int _age) + : first_name_(_first_name), + last_name_(rfl::make_box(*_last_name)), + age_(_age) {} + + const auto& first_name() const { return first_name_; } + + const auto& last_name() const { return last_name_; } + + auto age() const { return age_; } + + private: + std::string first_name_; + rfl::Box last_name_; + int age_; +}; + +struct PersonImpl { + rfl::Field<"firstName", std::string> first_name; + rfl::Field<"lastName", rfl::Box> last_name; + rfl::Field<"age", int> age; + + static PersonImpl from_class(const Person& _p) noexcept { + return PersonImpl{.first_name = _p.first_name(), + .last_name = rfl::make_box(*_p.last_name()), + .age = _p.age()}; + } +}; + +} // namespace test_custom_class4 + +namespace rfl { +namespace parsing { + +template +struct Parser + : public CustomParser {}; + +} // namespace parsing +} // namespace rfl + +namespace test_custom_class4 { + +TEST(msgpack, test_custom_class4) { + const auto bart = test_custom_class4::Person( + "Bart", rfl::make_box("Simpson"), 10); +} +} // namespace test_custom_class4 diff --git a/tests/env/test_default_values.cpp b/tests/env/test_default_values.cpp new file mode 100644 index 00000000..d54e76ad --- /dev/null +++ b/tests/env/test_default_values.cpp @@ -0,0 +1,25 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_default_values { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::vector children; +}; + +TEST(env, test_default_values) { + const auto bart = Person{.first_name = "Bart"}; + const auto lisa = Person{.first_name = "Lisa"}; + const auto maggie = Person{.first_name = "Maggie"}; + const auto homer = + Person{.first_name = "Homer", + .children = std::vector({bart, lisa, maggie})}; +} +} // namespace test_default_values diff --git a/tests/env/test_deque.cpp b/tests/env/test_deque.cpp new file mode 100644 index 00000000..4133cbdd --- /dev/null +++ b/tests/env/test_deque.cpp @@ -0,0 +1,24 @@ +#include + +#include +#include +#include + +namespace test_deque { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children; +}; + +TEST(env, test_default_values) { + auto children = std::make_unique>(); + children->emplace_back(Person{.first_name = "Bart"}); + children->emplace_back(Person{.first_name = "Lisa"}); + children->emplace_back(Person{.first_name = "Maggie"}); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; +} +} // namespace test_deque diff --git a/tests/env/test_enum.cpp b/tests/env/test_enum.cpp new file mode 100644 index 00000000..39e88bc6 --- /dev/null +++ b/tests/env/test_enum.cpp @@ -0,0 +1,20 @@ +#include + +#include +#include +#include + +namespace test_enum { + +enum class Color { red, green, blue, yellow }; + +struct Circle { + float radius; + Color color; +}; + +TEST(env, test_enum) { + const auto circle = Circle{.radius = 2.0, .color = Color::green}; +} + +} // namespace test_enum diff --git a/tests/env/test_extra_fields.cpp b/tests/env/test_extra_fields.cpp new file mode 100644 index 00000000..2d987fa6 --- /dev/null +++ b/tests/env/test_extra_fields.cpp @@ -0,0 +1,22 @@ +#include + +#include +#include +#include + +namespace test_extra_fields { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + rfl::ExtraFields extra_fields; +}; + +TEST(env, test_extra_fields) { + auto homer = Person{.first_name = "Homer"}; + + homer.extra_fields["age"] = 45; + homer.extra_fields["email"] = "homer@simpson.com"; + homer.extra_fields["town"] = "Springfield"; +} +} // namespace test_extra_fields diff --git a/tests/env/test_field_variant.cpp b/tests/env/test_field_variant.cpp new file mode 100644 index 00000000..1fa81066 --- /dev/null +++ b/tests/env/test_field_variant.cpp @@ -0,0 +1,30 @@ +#include + +#include +#include +#include + +namespace test_field_variant { + +struct Circle { + double radius; +}; + +struct Rectangle { + double height; + double width; +}; + +struct Square { + double width; +}; + +using Shapes = rfl::Variant, + rfl::Field<"rectangle", Rectangle>, + rfl::Field<"square", rfl::Box>>; + +TEST(env, test_field_variant) { + const Shapes r = + rfl::make_field<"rectangle">(Rectangle{.height = 10, .width = 5}); +} +} // namespace test_field_variant diff --git a/tests/env/test_flag_enum.cpp b/tests/env/test_flag_enum.cpp new file mode 100644 index 00000000..418f6101 --- /dev/null +++ b/tests/env/test_flag_enum.cpp @@ -0,0 +1,31 @@ +#include + +#include +#include +#include + +namespace test_flag_enum { + +enum class Color { + red = 256, + green = 512, + blue = 1024, + yellow = 2048, + orange = (256 | 2048) // red + yellow = orange +}; + +inline Color operator|(Color c1, Color c2) { + return static_cast(static_cast(c1) | static_cast(c2)); +} + +struct Circle { + float radius; + Color color; +}; + +TEST(env, test_flag_enum) { + const auto circle = + Circle{.radius = 2.0, .color = Color::blue | Color::orange}; +} + +} // namespace test_flag_enum diff --git a/tests/env/test_flag_enum_with_int.cpp b/tests/env/test_flag_enum_with_int.cpp new file mode 100644 index 00000000..db0120ff --- /dev/null +++ b/tests/env/test_flag_enum_with_int.cpp @@ -0,0 +1,30 @@ +#include + +#include +#include +#include + +namespace test_flag_enum_with_int { + +enum class Color { + red = 256, + green = 512, + blue = 1024, + yellow = 2048, + orange = (256 | 2048) // red + yellow = orange +}; + +inline Color operator|(Color c1, Color c2) { + return static_cast(static_cast(c1) | static_cast(c2)); +} + +struct Circle { + float radius; + Color color; +}; + +TEST(env, test_flag_enum_with_int) { + const auto circle = Circle{.radius = 2.0, .color = static_cast(10000)}; +} + +} // namespace test_flag_enum_with_int diff --git a/tests/env/test_flatten.cpp b/tests/env/test_flatten.cpp new file mode 100644 index 00000000..9681ebfe --- /dev/null +++ b/tests/env/test_flatten.cpp @@ -0,0 +1,30 @@ +#include + +#include +#include +#include +#include + +namespace test_flatten { + +struct Person { + rfl::Field<"firstName", std::string> first_name; + rfl::Field<"lastName", rfl::Box> last_name; + rfl::Field<"age", int> age; +}; + +struct Employee { + rfl::Flatten person; + rfl::Field<"employer", rfl::Box> employer; + rfl::Field<"salary", float> salary; +}; + +TEST(env, test_flatten) { + const auto employee = Employee{ + .person = Person{.first_name = "Homer", + .last_name = rfl::make_box("Simpson"), + .age = 45}, + .employer = rfl::make_box("Mr. Burns"), + .salary = 60000.0}; +} +} // namespace test_flatten diff --git a/tests/env/test_flatten_anonymous.cpp b/tests/env/test_flatten_anonymous.cpp new file mode 100644 index 00000000..8f3e6563 --- /dev/null +++ b/tests/env/test_flatten_anonymous.cpp @@ -0,0 +1,31 @@ +#include + +#include +#include +#include +#include + +namespace test_flatten_anonymous { + +struct Person { + std::string first_name; + rfl::Box last_name; + int age; +}; + +struct Employee { + rfl::Flatten person; + rfl::Box employer; + float salary; +}; + +TEST(env, test_flatten_anonymous) { + const auto employee = Employee{ + .person = Person{.first_name = "Homer", + .last_name = rfl::make_box("Simpson"), + .age = 45}, + .employer = rfl::make_box("Mr. Burns"), + .salary = 60000.0}; +} + +} // namespace test_flatten_anonymous diff --git a/tests/env/test_forward_list.cpp b/tests/env/test_forward_list.cpp new file mode 100644 index 00000000..8f6bafa1 --- /dev/null +++ b/tests/env/test_forward_list.cpp @@ -0,0 +1,24 @@ +#include + +#include +#include +#include + +namespace test_forward_list { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children; +}; + +TEST(env, test_forward_list) { + auto children = std::make_unique>(); + children->emplace_front(Person{.first_name = "Maggie"}); + children->emplace_front(Person{.first_name = "Lisa"}); + children->emplace_front(Person{.first_name = "Bart"}); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; +} +} // namespace test_forward_list diff --git a/tests/env/test_literal.cpp b/tests/env/test_literal.cpp new file mode 100644 index 00000000..232b04c5 --- /dev/null +++ b/tests/env/test_literal.cpp @@ -0,0 +1,22 @@ +#include + +#include +#include +#include +#include + +namespace test_literal { + +using FirstName = rfl::Literal<"Homer", "Marge", "Bart", "Lisa", "Maggie">; +using LastName = rfl::Literal<"Simpson">; + +struct Person { + rfl::Rename<"firstName", FirstName> first_name; + rfl::Rename<"lastName", LastName> last_name; + std::vector children; +}; + +TEST(env, test_literal) { + const auto bart = Person{.first_name = FirstName::make<"Bart">()}; +} +} // namespace test_literal diff --git a/tests/env/test_literal_map.cpp b/tests/env/test_literal_map.cpp new file mode 100644 index 00000000..ab4a5640 --- /dev/null +++ b/tests/env/test_literal_map.cpp @@ -0,0 +1,17 @@ +#include + +#include +#include + +namespace test_literal_map { + +using FieldName = rfl::Literal<"firstName", "lastName">; + +TEST(env, test_literal_map) { + std::map> homer; + homer.insert(std::make_pair(FieldName::make<"firstName">(), + std::make_unique("Homer"))); + homer.insert(std::make_pair(FieldName::make<"lastName">(), + std::make_unique("Simpson"))); +} +} // namespace test_literal_map diff --git a/tests/env/test_map.cpp b/tests/env/test_map.cpp new file mode 100644 index 00000000..d48a00d7 --- /dev/null +++ b/tests/env/test_map.cpp @@ -0,0 +1,25 @@ +#include + +#include +#include +#include +#include + +namespace test_map { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::map children; +}; + +TEST(env, test_map) { + auto children = std::map(); + children.insert(std::make_pair("child1", Person{.first_name = "Bart"})); + children.insert(std::make_pair("child2", Person{.first_name = "Lisa"})); + children.insert(std::make_pair("child3", Person{.first_name = "Maggie"})); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; +} +} // namespace test_map diff --git a/tests/env/test_map_with_key_validation.cpp b/tests/env/test_map_with_key_validation.cpp new file mode 100644 index 00000000..351f4d97 --- /dev/null +++ b/tests/env/test_map_with_key_validation.cpp @@ -0,0 +1,26 @@ +#include + +#include +#include +#include +#include + +namespace test_map_with_key_validation { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children; +}; + +TEST(env, test_map_with_key_validation) { + auto children = std::make_unique>(); + + children->insert(std::make_pair("Bart", Person{.first_name = "Bart"})); + children->insert(std::make_pair("Lisa", Person{.first_name = "Lisa"})); + children->insert(std::make_pair("Maggie", Person{.first_name = "Maggie"})); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; +} +} // namespace test_map_with_key_validation diff --git a/tests/env/test_monster_example.cpp b/tests/env/test_monster_example.cpp new file mode 100644 index 00000000..1d6fa1bc --- /dev/null +++ b/tests/env/test_monster_example.cpp @@ -0,0 +1,57 @@ +#include + +#include +#include +#include +#include + +namespace test_monster_example { + +using Color = rfl::Literal<"Red", "Green", "Blue">; + +struct Weapon { + std::string name; + short damage; +}; + +using Equipment = rfl::Variant>; + +struct Vec3 { + float x; + float y; + float z; +}; + +struct Monster { + Vec3 pos; + short mana = 150; + short hp = 100; + std::string name; + bool friendly = false; + std::vector inventory; + Color color = Color::make<"Blue">(); + std::vector weapons; + Equipment equipped; + std::vector path; +}; + +TEST(env, test_monster_example) { + const auto sword = Weapon{.name = "Sword", .damage = 3}; + const auto axe = Weapon{.name = "Axe", .damage = 5}; + + const auto weapons = std::vector({sword, axe}); + + const auto position = Vec3{1.0f, 2.0f, 3.0f}; + + const auto inventory = std::vector({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); + + const auto orc = Monster{.pos = position, + .mana = 150, + .hp = 80, + .name = "MyMonster", + .inventory = inventory, + .color = Color::make<"Red">(), + .weapons = weapons, + .equipped = rfl::make_field<"weapon">(axe)}; +} +} // namespace test_monster_example diff --git a/tests/env/test_readme_example.cpp b/tests/env/test_readme_example.cpp index d33dfa25..d8332b12 100644 --- a/tests/env/test_readme_example.cpp +++ b/tests/env/test_readme_example.cpp @@ -56,6 +56,21 @@ TEST(env, test_readme_example) { ASSERT_EQ(std::getenv("CHILDREN_0_TOWN"), std::string("Springfield")); ASSERT_EQ(std::getenv("CHILDREN_0_BIRTHDAY"), std::string("1987-04-19")); ASSERT_EQ(std::getenv("CHILDREN_0_AGE"), std::string("10")); + ASSERT_EQ(std::getenv("CHILDREN_0_EMAIL"), std::string("bart@simpson.com")); + + ASSERT_EQ(std::getenv("CHILDREN_1_FIRST_NAME"), std::string("Lisa")); + ASSERT_EQ(std::getenv("CHILDREN_1_LAST_NAME"), std::string("Simpson")); + ASSERT_EQ(std::getenv("CHILDREN_1_TOWN"), std::string("Springfield")); + ASSERT_EQ(std::getenv("CHILDREN_1_BIRTHDAY"), std::string("1987-04-19")); + ASSERT_EQ(std::getenv("CHILDREN_1_AGE"), std::string("8")); + ASSERT_EQ(std::getenv("CHILDREN_1_EMAIL"), std::string("lisa@simpson.com")); + + ASSERT_EQ(std::getenv("CHILDREN_2_FIRST_NAME"), std::string("Maggie")); + ASSERT_EQ(std::getenv("CHILDREN_2_LAST_NAME"), std::string("Simpson")); + ASSERT_EQ(std::getenv("CHILDREN_2_TOWN"), std::string("Springfield")); + ASSERT_EQ(std::getenv("CHILDREN_2_BIRTHDAY"), std::string("1987-04-19")); + ASSERT_EQ(std::getenv("CHILDREN_2_AGE"), std::string("0")); + ASSERT_EQ(std::getenv("CHILDREN_2_EMAIL"), std::string("maggie@simpson.com")); const auto read_homer = rfl::env::read(); @@ -72,6 +87,28 @@ TEST(env, test_readme_example) { ASSERT_EQ(read_homer->birthday.str(), "1987-04-19"); ASSERT_EQ(read_homer->age, 45); ASSERT_EQ(read_homer->email, "homer@simpson.com"); + ASSERT_EQ(read_homer->children.size(), 3); + + ASSERT_EQ(read_homer->children[0].first_name, "Bart"); + ASSERT_EQ(read_homer->children[0].last_name, "Simpson"); + ASSERT_EQ(read_homer->children[0].town, "Springfield"); + ASSERT_EQ(read_homer->children[0].birthday.str(), "1987-04-19"); + ASSERT_EQ(read_homer->children[0].age, 10); + ASSERT_EQ(read_homer->children[0].email, "bart@simpson.com"); + + ASSERT_EQ(read_homer->children[1].first_name, "Lisa"); + ASSERT_EQ(read_homer->children[1].last_name, "Simpson"); + ASSERT_EQ(read_homer->children[1].town, "Springfield"); + ASSERT_EQ(read_homer->children[1].birthday.str(), "1987-04-19"); + ASSERT_EQ(read_homer->children[1].age, 8); + ASSERT_EQ(read_homer->children[1].email, "lisa@simpson.com"); + + ASSERT_EQ(read_homer->children[2].first_name, "Maggie"); + ASSERT_EQ(read_homer->children[2].last_name, "Simpson"); + ASSERT_EQ(read_homer->children[2].town, "Springfield"); + ASSERT_EQ(read_homer->children[2].birthday.str(), "1987-04-19"); + ASSERT_EQ(read_homer->children[2].age, 0); + ASSERT_EQ(read_homer->children[2].email, "maggie@simpson.com"); } } // namespace test_readme_example diff --git a/tests/env/test_readme_example2.cpp b/tests/env/test_readme_example2.cpp new file mode 100644 index 00000000..85a929c0 --- /dev/null +++ b/tests/env/test_readme_example2.cpp @@ -0,0 +1,19 @@ +#include + +#include +#include +#include + +namespace test_readme_example2 { + +struct Person { + std::string first_name; + std::string last_name; + int age; +}; + +TEST(env, test_readme_example2) { + const auto homer = + Person{.first_name = "Homer", .last_name = "Simpson", .age = 45}; +} +} // namespace test_readme_example2 diff --git a/tests/env/test_ref.cpp b/tests/env/test_ref.cpp new file mode 100644 index 00000000..d4eb694f --- /dev/null +++ b/tests/env/test_ref.cpp @@ -0,0 +1,39 @@ +#include + +#include +#include +#include + +namespace test_ref { + +struct DecisionTree { + struct Leaf { + using Tag = rfl::Literal<"Leaf">; + double value; + }; + + struct Node { + using Tag = rfl::Literal<"Node">; + rfl::Rename<"criticalValue", double> critical_value; + rfl::Ref lesser; + rfl::Ref greater; + }; + + using LeafOrNode = rfl::TaggedUnion<"type", Leaf, Node>; + + rfl::Field<"leafOrNode", LeafOrNode> leaf_or_node; +}; + +TEST(env, test_ref) { + const auto leaf1 = DecisionTree::Leaf{.value = 3.0}; + + const auto leaf2 = DecisionTree::Leaf{.value = 5.0}; + + auto node = DecisionTree::Node{ + .critical_value = 10.0, + .lesser = rfl::make_ref(DecisionTree{leaf1}), + .greater = rfl::make_ref(DecisionTree{leaf2})}; + + const DecisionTree tree{.leaf_or_node = std::move(node)}; +} +} // namespace test_ref diff --git a/tests/env/test_set.cpp b/tests/env/test_set.cpp new file mode 100644 index 00000000..23ca7f26 --- /dev/null +++ b/tests/env/test_set.cpp @@ -0,0 +1,22 @@ +#include + +#include +#include +#include + +namespace test_set { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children; +}; + +TEST(env, test_set) { + auto children = std::make_unique>( + std::set({"Bart", "Lisa", "Maggie"})); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; +} +} // namespace test_set diff --git a/tests/env/test_size.cpp b/tests/env/test_size.cpp new file mode 100644 index 00000000..03916afb --- /dev/null +++ b/tests/env/test_size.cpp @@ -0,0 +1,35 @@ +#include + +#include +#include +#include +#include + +namespace test_size { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name; + rfl::Timestamp<"%Y-%m-%d"> birthday; + rfl::Validator, + rfl::Size, rfl::EqualTo<3>>>> + children; +}; + +TEST(env, test_size) { + const auto bart = Person{ + .first_name = "Bart", .last_name = "Simpson", .birthday = "1987-04-19"}; + + const auto lisa = Person{ + .first_name = "Lisa", .last_name = "Simpson", .birthday = "1987-04-19"}; + + const auto maggie = Person{ + .first_name = "Maggie", .last_name = "Simpson", .birthday = "1987-04-19"}; + + const auto homer = + Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = "1987-04-19", + .children = std::vector({bart, lisa, maggie})}; +} +} // namespace test_size diff --git a/tests/env/test_skip.cpp b/tests/env/test_skip.cpp new file mode 100644 index 00000000..3824f68d --- /dev/null +++ b/tests/env/test_skip.cpp @@ -0,0 +1,24 @@ +#include + +#include +#include +#include + +namespace test_skip { + +using Age = rfl::Validator, rfl::Maximum<130>>; + +struct Person { + rfl::Skip town; + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name; + Age age; +}; + +TEST(env, test_skip) { + const auto homer = Person{.town = "Springfield", + .first_name = "Homer", + .last_name = "Simpson", + .age = 45}; +} +} // namespace test_skip diff --git a/tests/env/test_string_map.cpp b/tests/env/test_string_map.cpp new file mode 100644 index 00000000..73e97d52 --- /dev/null +++ b/tests/env/test_string_map.cpp @@ -0,0 +1,15 @@ +#include + +#include +#include + +namespace test_string_map { + +TEST(env, test_string_map) { + std::map> homer; + homer.insert( + std::make_pair("firstName", std::make_unique("Homer"))); + homer.insert( + std::make_pair("lastName", std::make_unique("Simpson"))); +} +} // namespace test_string_map diff --git a/tests/env/test_tagged_union.cpp b/tests/env/test_tagged_union.cpp new file mode 100644 index 00000000..5f42b069 --- /dev/null +++ b/tests/env/test_tagged_union.cpp @@ -0,0 +1,27 @@ +#include + +#include +#include +#include + +namespace test_tagged_union { + +struct Circle { + double radius; +}; + +struct Rectangle { + double height; + double width; +}; + +struct Square { + double width; +}; + +using Shapes = rfl::TaggedUnion<"shape", Circle, Square, Rectangle>; + +TEST(env, test_tagged_union) { + const Shapes r = Rectangle{.height = 10, .width = 5}; +} +} // namespace test_tagged_union diff --git a/tests/env/test_tagged_union2.cpp b/tests/env/test_tagged_union2.cpp new file mode 100644 index 00000000..f0092d7e --- /dev/null +++ b/tests/env/test_tagged_union2.cpp @@ -0,0 +1,27 @@ +#include + +#include +#include +#include + +namespace test_tagged_union2 { + +struct Circle { + double radius; +}; + +struct Rectangle { + double height; + double width; +}; + +struct Square { + double width; +}; + +using Shapes = rfl::TaggedUnion<"shape", Circle, Square, Rectangle>; + +TEST(env, test_tagged_union2) { + const Shapes r = Rectangle{.height = 10, .width = 5}; +} +} // namespace test_tagged_union2 diff --git a/tests/env/test_timestamp.cpp b/tests/env/test_timestamp.cpp new file mode 100644 index 00000000..1c5b4522 --- /dev/null +++ b/tests/env/test_timestamp.cpp @@ -0,0 +1,27 @@ +#include + +#include +#include +#include + +namespace test_timestamp { + +using TS = rfl::Timestamp<"%Y-%m-%d">; + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + TS birthday; +}; + +TEST(env, test_timestamp) { + const auto result = TS::from_string("nonsense"); + + if (result) { + std::cout << "Failed: Expected an error, but got none." << std::endl; + return; + } + + const auto bart = Person{.first_name = "Bart", .birthday = "1987-04-19"}; +} +} // namespace test_timestamp diff --git a/tests/env/test_unique_ptr.cpp b/tests/env/test_unique_ptr.cpp new file mode 100644 index 00000000..30399588 --- /dev/null +++ b/tests/env/test_unique_ptr.cpp @@ -0,0 +1,26 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_unique_ptr { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children; +}; + +TEST(env, test_unique_ptr) { + auto children = std::make_unique>(); + children->emplace_back(Person{.first_name = "Bart"}); + children->emplace_back(Person{.first_name = "Lisa"}); + children->emplace_back(Person{.first_name = "Maggie"}); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; +} +} // namespace test_unique_ptr diff --git a/tests/env/test_unique_ptr2.cpp b/tests/env/test_unique_ptr2.cpp new file mode 100644 index 00000000..137fd40a --- /dev/null +++ b/tests/env/test_unique_ptr2.cpp @@ -0,0 +1,39 @@ +#include + +#include +#include +#include + +namespace test_unique_ptr2 { + +struct DecisionTree { + struct Leaf { + using Tag = rfl::Literal<"Leaf">; + double value; + }; + + struct Node { + using Tag = rfl::Literal<"Node">; + rfl::Rename<"criticalValue", double> critical_value; + std::unique_ptr lesser; + std::unique_ptr greater; + }; + + using LeafOrNode = rfl::TaggedUnion<"type", Leaf, Node>; + + rfl::Field<"leafOrNode", LeafOrNode> leaf_or_node; +}; + +TEST(env, test_unique_ptr2) { + auto leaf1 = DecisionTree::Leaf{.value = 3.0}; + + auto leaf2 = DecisionTree::Leaf{.value = 5.0}; + + auto node = DecisionTree::Node{ + .critical_value = 10.0, + .lesser = std::make_unique(DecisionTree{leaf1}), + .greater = std::make_unique(DecisionTree{leaf2})}; + + const DecisionTree tree{.leaf_or_node = std::move(node)}; +} +} // namespace test_unique_ptr2 diff --git a/tests/env/test_variant.cpp b/tests/env/test_variant.cpp new file mode 100644 index 00000000..ae86ed80 --- /dev/null +++ b/tests/env/test_variant.cpp @@ -0,0 +1,27 @@ +#include + +#include +#include +#include + +namespace test_variant { + +struct Circle { + double radius; +}; + +struct Rectangle { + double height; + double width; +}; + +struct Square { + double width; +}; + +using Shapes = std::variant>; + +TEST(env, test_variant) { + const Shapes r = Rectangle{.height = 10, .width = 5}; +} +} // namespace test_variant From 057d801fbd0c174e64479cddf698db12d52f01ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Tue, 14 Jul 2026 00:17:30 +0200 Subject: [PATCH 14/22] =?UTF-8?q?Added=C2=A0more=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/env/test_add_struct_name.cpp | 4 + tests/env/test_array.cpp | 4 + tests/env/test_basic.cpp | 22 ++--- tests/env/test_box.cpp | 4 + tests/env/test_combined_processors.cpp | 4 + tests/env/test_custom_class1.cpp | 8 +- tests/env/test_custom_class3.cpp | 4 + tests/env/test_custom_class4.cpp | 6 +- tests/env/test_default_values.cpp | 4 + tests/env/test_deque.cpp | 4 + tests/env/test_enum.cpp | 4 + tests/env/test_extra_fields.cpp | 13 +++ tests/env/test_field_variant.cpp | 10 ++ tests/env/test_flag_enum.cpp | 4 + tests/env/test_flag_enum_with_int.cpp | 4 + tests/env/test_flatten.cpp | 4 + tests/env/test_flatten_anonymous.cpp | 4 + tests/env/test_forward_list.cpp | 4 + tests/env/test_literal.cpp | 4 + tests/env/test_literal_map.cpp | 4 + tests/env/test_map.cpp | 10 ++ tests/env/test_map_with_key_validation.cpp | 10 ++ tests/env/test_monster_example.cpp | 10 ++ tests/env/test_nested.cpp | 28 ++---- tests/env/test_readme_example.cpp | 98 +++++++------------- tests/env/test_readme_example2.cpp | 4 + tests/env/test_ref.cpp | 4 + tests/env/test_set.cpp | 4 + tests/env/test_size.cpp | 4 + tests/env/test_skip.cpp | 4 + tests/env/test_string_map.cpp | 4 + tests/env/test_tagged_union.cpp | 4 + tests/env/test_tagged_union2.cpp | 4 + tests/env/test_timestamp.cpp | 9 +- tests/env/test_unique_ptr.cpp | 4 + tests/env/test_unique_ptr2.cpp | 4 + tests/env/test_variant.cpp | 6 +- tests/env/test_vector.cpp | 44 ++++----- tests/env/write_and_read.hpp | 101 +++++++++++++++++++++ 39 files changed, 342 insertions(+), 133 deletions(-) create mode 100644 tests/env/write_and_read.hpp diff --git a/tests/env/test_add_struct_name.cpp b/tests/env/test_add_struct_name.cpp index 25f30c6b..3d971c42 100644 --- a/tests/env/test_add_struct_name.cpp +++ b/tests/env/test_add_struct_name.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_add_struct_name { using Age = rfl::Validator, rfl::Maximum<130>>; @@ -41,5 +43,7 @@ TEST(env, test_add_struct_name) { .age = 45, .email = "homer@simpson.com", .children = std::vector({bart, lisa, maggie})}; + + write_and_read>(homer); } } // namespace test_add_struct_name diff --git a/tests/env/test_array.cpp b/tests/env/test_array.cpp index 622c28b9..57c68fd5 100644 --- a/tests/env/test_array.cpp +++ b/tests/env/test_array.cpp @@ -6,6 +6,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_array { struct Person { @@ -25,6 +27,8 @@ TEST(env, test_array) { .first_name = "Homer", .children = std::make_unique>(std::array{ std::move(bart), std::move(lisa), std::move(maggie)})}; + + write_and_read(homer); } } // namespace test_array diff --git a/tests/env/test_basic.cpp b/tests/env/test_basic.cpp index eb7b4c48..30cf2144 100644 --- a/tests/env/test_basic.cpp +++ b/tests/env/test_basic.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_basic { struct Settings { @@ -21,20 +23,12 @@ TEST(env, test_basic) { .verbose = true, }; - rfl::env::write(settings); - - ASSERT_EQ(std::getenv("HOST"), std::string("localhost")); - ASSERT_EQ(std::getenv("PORT"), std::string("8080")); - ASSERT_EQ(std::getenv("RATE"), std::string("1.500000")); - ASSERT_EQ(std::getenv("VERBOSE"), std::string("true")); - - const auto read_settings = rfl::env::read(); - - ASSERT_TRUE(read_settings); - ASSERT_EQ(read_settings->host, "localhost"); - ASSERT_EQ(read_settings->port, 8080); - ASSERT_DOUBLE_EQ(read_settings->rate, 1.5); - ASSERT_EQ(read_settings->verbose, true); + write_and_read(settings, []() { + ASSERT_EQ(std::getenv("HOST"), std::string("localhost")); + ASSERT_EQ(std::getenv("PORT"), std::string("8080")); + ASSERT_EQ(std::getenv("RATE"), std::string("1.500000")); + ASSERT_EQ(std::getenv("VERBOSE"), std::string("true")); + }); } } // namespace test_basic diff --git a/tests/env/test_box.cpp b/tests/env/test_box.cpp index 446a1151..10729848 100644 --- a/tests/env/test_box.cpp +++ b/tests/env/test_box.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_box { struct DecisionTree { @@ -35,5 +37,7 @@ TEST(env, test_box) { .greater = rfl::make_box(DecisionTree{leaf2})}; const DecisionTree tree{.leaf_or_node = std::move(node)}; + + write_and_read(tree); } } // namespace test_box diff --git a/tests/env/test_combined_processors.cpp b/tests/env/test_combined_processors.cpp index c6a063fb..a9c467f5 100644 --- a/tests/env/test_combined_processors.cpp +++ b/tests/env/test_combined_processors.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_combined_processors { using Age = rfl::Validator, rfl::Maximum<130>>; @@ -44,5 +46,7 @@ TEST(env, test_combined_processors) { using Processors = rfl::Processors>; + + write_and_read(homer); } } // namespace test_combined_processors diff --git a/tests/env/test_custom_class1.cpp b/tests/env/test_custom_class1.cpp index 0e38ed88..abc9ab55 100644 --- a/tests/env/test_custom_class1.cpp +++ b/tests/env/test_custom_class1.cpp @@ -6,6 +6,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_custom_class1 { struct Person { @@ -28,5 +30,9 @@ struct Person { PersonImpl impl; }; -TEST(env, test_custom_class1) { const auto bart = Person("Bart"); } +TEST(env, test_custom_class1) { + const auto bart = Person("Bart"); + + write_and_read(bart); +} } // namespace test_custom_class1 diff --git a/tests/env/test_custom_class3.cpp b/tests/env/test_custom_class3.cpp index 575b01c0..485fe94a 100644 --- a/tests/env/test_custom_class3.cpp +++ b/tests/env/test_custom_class3.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_custom_class3 { struct Person { @@ -56,6 +58,8 @@ namespace test_custom_class3 { TEST(env, test_custom_class3) { const auto bart = Person("Bart", "Simpson", 10); + + write_and_read(bart); } } // namespace test_custom_class3 diff --git a/tests/env/test_custom_class4.cpp b/tests/env/test_custom_class4.cpp index 85ce0254..9bac945c 100644 --- a/tests/env/test_custom_class4.cpp +++ b/tests/env/test_custom_class4.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_custom_class4 { struct Person { @@ -55,8 +57,10 @@ struct Parser("Simpson"), 10); + + write_and_read(bart); } } // namespace test_custom_class4 diff --git a/tests/env/test_default_values.cpp b/tests/env/test_default_values.cpp index d54e76ad..1ea63e22 100644 --- a/tests/env/test_default_values.cpp +++ b/tests/env/test_default_values.cpp @@ -6,6 +6,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_default_values { struct Person { @@ -21,5 +23,7 @@ TEST(env, test_default_values) { const auto homer = Person{.first_name = "Homer", .children = std::vector({bart, lisa, maggie})}; + + write_and_read(homer); } } // namespace test_default_values diff --git a/tests/env/test_deque.cpp b/tests/env/test_deque.cpp index 4133cbdd..d6f02725 100644 --- a/tests/env/test_deque.cpp +++ b/tests/env/test_deque.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_deque { struct Person { @@ -20,5 +22,7 @@ TEST(env, test_default_values) { const auto homer = Person{.first_name = "Homer", .children = std::move(children)}; + + write_and_read(homer); } } // namespace test_deque diff --git a/tests/env/test_enum.cpp b/tests/env/test_enum.cpp index 39e88bc6..a63243ea 100644 --- a/tests/env/test_enum.cpp +++ b/tests/env/test_enum.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_enum { enum class Color { red, green, blue, yellow }; @@ -15,6 +17,8 @@ struct Circle { TEST(env, test_enum) { const auto circle = Circle{.radius = 2.0, .color = Color::green}; + + write_and_read(circle); } } // namespace test_enum diff --git a/tests/env/test_extra_fields.cpp b/tests/env/test_extra_fields.cpp index 2d987fa6..1aa2a045 100644 --- a/tests/env/test_extra_fields.cpp +++ b/tests/env/test_extra_fields.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_extra_fields { struct Person { @@ -13,10 +15,21 @@ struct Person { }; TEST(env, test_extra_fields) { + const auto scoped_environment = test_env::ScopedEnvironment(); + auto homer = Person{.first_name = "Homer"}; homer.extra_fields["age"] = 45; homer.extra_fields["email"] = "homer@simpson.com"; homer.extra_fields["town"] = "Springfield"; + + rfl::env::write(homer); + + const auto read_homer = rfl::env::read(); + + ASSERT_TRUE(read_homer); + ASSERT_EQ(read_homer->first_name(), "Homer"); + ASSERT_EQ(read_homer->last_name(), "Simpson"); + ASSERT_EQ(read_homer->extra_fields.size(), 0); } } // namespace test_extra_fields diff --git a/tests/env/test_field_variant.cpp b/tests/env/test_field_variant.cpp index 1fa81066..162d32ad 100644 --- a/tests/env/test_field_variant.cpp +++ b/tests/env/test_field_variant.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_field_variant { struct Circle { @@ -24,7 +26,15 @@ using Shapes = rfl::Variant, rfl::Field<"square", rfl::Box>>; TEST(env, test_field_variant) { + const auto scoped_environment = test_env::ScopedEnvironment(); + const Shapes r = rfl::make_field<"rectangle">(Rectangle{.height = 10, .width = 5}); + + rfl::env::write(r); + + const auto read_shape = rfl::env::read(); + + ASSERT_FALSE(read_shape); } } // namespace test_field_variant diff --git a/tests/env/test_flag_enum.cpp b/tests/env/test_flag_enum.cpp index 418f6101..9184a080 100644 --- a/tests/env/test_flag_enum.cpp +++ b/tests/env/test_flag_enum.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_flag_enum { enum class Color { @@ -26,6 +28,8 @@ struct Circle { TEST(env, test_flag_enum) { const auto circle = Circle{.radius = 2.0, .color = Color::blue | Color::orange}; + + write_and_read(circle); } } // namespace test_flag_enum diff --git a/tests/env/test_flag_enum_with_int.cpp b/tests/env/test_flag_enum_with_int.cpp index db0120ff..5bf842c0 100644 --- a/tests/env/test_flag_enum_with_int.cpp +++ b/tests/env/test_flag_enum_with_int.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_flag_enum_with_int { enum class Color { @@ -25,6 +27,8 @@ struct Circle { TEST(env, test_flag_enum_with_int) { const auto circle = Circle{.radius = 2.0, .color = static_cast(10000)}; + + write_and_read(circle); } } // namespace test_flag_enum_with_int diff --git a/tests/env/test_flatten.cpp b/tests/env/test_flatten.cpp index 9681ebfe..4280cb9b 100644 --- a/tests/env/test_flatten.cpp +++ b/tests/env/test_flatten.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_flatten { struct Person { @@ -26,5 +28,7 @@ TEST(env, test_flatten) { .age = 45}, .employer = rfl::make_box("Mr. Burns"), .salary = 60000.0}; + + write_and_read(employee); } } // namespace test_flatten diff --git a/tests/env/test_flatten_anonymous.cpp b/tests/env/test_flatten_anonymous.cpp index 8f3e6563..ee8430f8 100644 --- a/tests/env/test_flatten_anonymous.cpp +++ b/tests/env/test_flatten_anonymous.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_flatten_anonymous { struct Person { @@ -26,6 +28,8 @@ TEST(env, test_flatten_anonymous) { .age = 45}, .employer = rfl::make_box("Mr. Burns"), .salary = 60000.0}; + + write_and_read(employee); } } // namespace test_flatten_anonymous diff --git a/tests/env/test_forward_list.cpp b/tests/env/test_forward_list.cpp index 8f6bafa1..2f3f10c5 100644 --- a/tests/env/test_forward_list.cpp +++ b/tests/env/test_forward_list.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_forward_list { struct Person { @@ -20,5 +22,7 @@ TEST(env, test_forward_list) { const auto homer = Person{.first_name = "Homer", .children = std::move(children)}; + + write_and_read(homer); } } // namespace test_forward_list diff --git a/tests/env/test_literal.cpp b/tests/env/test_literal.cpp index 232b04c5..498ebffc 100644 --- a/tests/env/test_literal.cpp +++ b/tests/env/test_literal.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_literal { using FirstName = rfl::Literal<"Homer", "Marge", "Bart", "Lisa", "Maggie">; @@ -18,5 +20,7 @@ struct Person { TEST(env, test_literal) { const auto bart = Person{.first_name = FirstName::make<"Bart">()}; + + write_and_read(bart); } } // namespace test_literal diff --git a/tests/env/test_literal_map.cpp b/tests/env/test_literal_map.cpp index ab4a5640..0735dd62 100644 --- a/tests/env/test_literal_map.cpp +++ b/tests/env/test_literal_map.cpp @@ -3,6 +3,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_literal_map { using FieldName = rfl::Literal<"firstName", "lastName">; @@ -13,5 +15,7 @@ TEST(env, test_literal_map) { std::make_unique("Homer"))); homer.insert(std::make_pair(FieldName::make<"lastName">(), std::make_unique("Simpson"))); + + write_and_read(homer); } } // namespace test_literal_map diff --git a/tests/env/test_map.cpp b/tests/env/test_map.cpp index d48a00d7..30cc597c 100644 --- a/tests/env/test_map.cpp +++ b/tests/env/test_map.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_map { struct Person { @@ -14,6 +16,8 @@ struct Person { }; TEST(env, test_map) { + const auto scoped_environment = test_env::ScopedEnvironment(); + auto children = std::map(); children.insert(std::make_pair("child1", Person{.first_name = "Bart"})); children.insert(std::make_pair("child2", Person{.first_name = "Lisa"})); @@ -21,5 +25,11 @@ TEST(env, test_map) { const auto homer = Person{.first_name = "Homer", .children = std::move(children)}; + + rfl::env::write(homer); + + const auto read_homer = rfl::env::read(); + + ASSERT_FALSE(read_homer); } } // namespace test_map diff --git a/tests/env/test_map_with_key_validation.cpp b/tests/env/test_map_with_key_validation.cpp index 351f4d97..0f1f1e63 100644 --- a/tests/env/test_map_with_key_validation.cpp +++ b/tests/env/test_map_with_key_validation.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_map_with_key_validation { struct Person { @@ -14,6 +16,8 @@ struct Person { }; TEST(env, test_map_with_key_validation) { + const auto scoped_environment = test_env::ScopedEnvironment(); + auto children = std::make_unique>(); children->insert(std::make_pair("Bart", Person{.first_name = "Bart"})); @@ -22,5 +26,11 @@ TEST(env, test_map_with_key_validation) { const auto homer = Person{.first_name = "Homer", .children = std::move(children)}; + + rfl::env::write(homer); + + const auto read_homer = rfl::env::read(); + + ASSERT_FALSE(read_homer); } } // namespace test_map_with_key_validation diff --git a/tests/env/test_monster_example.cpp b/tests/env/test_monster_example.cpp index 1d6fa1bc..771d8f95 100644 --- a/tests/env/test_monster_example.cpp +++ b/tests/env/test_monster_example.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_monster_example { using Color = rfl::Literal<"Red", "Green", "Blue">; @@ -36,6 +38,8 @@ struct Monster { }; TEST(env, test_monster_example) { + const auto scoped_environment = test_env::ScopedEnvironment(); + const auto sword = Weapon{.name = "Sword", .damage = 3}; const auto axe = Weapon{.name = "Axe", .damage = 5}; @@ -53,5 +57,11 @@ TEST(env, test_monster_example) { .color = Color::make<"Red">(), .weapons = weapons, .equipped = rfl::make_field<"weapon">(axe)}; + + rfl::env::write(orc); + + const auto read_orc = rfl::env::read(); + + ASSERT_FALSE(read_orc); } } // namespace test_monster_example diff --git a/tests/env/test_nested.cpp b/tests/env/test_nested.cpp index 0db0c668..168d9206 100644 --- a/tests/env/test_nested.cpp +++ b/tests/env/test_nested.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_nested { struct NestedSettings { @@ -29,24 +31,14 @@ TEST(env, test_nested) { .database_port = 8080, }}; - rfl::env::write(settings); - - ASSERT_EQ(std::getenv("HOST"), std::string("localhost")); - ASSERT_EQ(std::getenv("PORT"), std::string("8080")); - ASSERT_EQ(std::getenv("RATE"), std::string("1.500000")); - ASSERT_EQ(std::getenv("VERBOSE"), std::string("true")); - ASSERT_EQ(std::getenv("NESTED_DATABASE_HOST"), std::string("localhost")); - ASSERT_EQ(std::getenv("NESTED_DATABASE_PORT"), std::string("8080")); - - const auto read_settings = rfl::env::read(); - - ASSERT_TRUE(read_settings); - ASSERT_EQ(read_settings->host, "localhost"); - ASSERT_EQ(read_settings->port, 8080); - ASSERT_DOUBLE_EQ(read_settings->rate, 1.5); - ASSERT_EQ(read_settings->verbose, true); - ASSERT_EQ(read_settings->nested.database_host, "localhost"); - ASSERT_EQ(read_settings->nested.database_port, 8080); + write_and_read(settings, []() { + ASSERT_EQ(std::getenv("HOST"), std::string("localhost")); + ASSERT_EQ(std::getenv("PORT"), std::string("8080")); + ASSERT_EQ(std::getenv("RATE"), std::string("1.500000")); + ASSERT_EQ(std::getenv("VERBOSE"), std::string("true")); + ASSERT_EQ(std::getenv("NESTED_DATABASE_HOST"), std::string("localhost")); + ASSERT_EQ(std::getenv("NESTED_DATABASE_PORT"), std::string("8080")); + }); } } // namespace test_nested diff --git a/tests/env/test_readme_example.cpp b/tests/env/test_readme_example.cpp index d8332b12..35bf6fb3 100644 --- a/tests/env/test_readme_example.cpp +++ b/tests/env/test_readme_example.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_readme_example { using Age = rfl::Validator, rfl::Maximum<130>>; @@ -42,73 +44,35 @@ TEST(env, test_readme_example) { .email = "homer@simpson.com", .children = std::vector({bart, lisa, maggie})}; - rfl::env::write(homer); - - ASSERT_EQ(std::getenv("FIRST_NAME"), std::string("Homer")); - ASSERT_EQ(std::getenv("LAST_NAME"), std::string("Simpson")); - ASSERT_EQ(std::getenv("TOWN"), std::string("Springfield")); - ASSERT_EQ(std::getenv("BIRTHDAY"), std::string("1987-04-19")); - ASSERT_EQ(std::getenv("AGE"), std::string("45")); - ASSERT_EQ(std::getenv("EMAIL"), std::string("homer@simpson.com")); - - ASSERT_EQ(std::getenv("CHILDREN_0_FIRST_NAME"), std::string("Bart")); - ASSERT_EQ(std::getenv("CHILDREN_0_LAST_NAME"), std::string("Simpson")); - ASSERT_EQ(std::getenv("CHILDREN_0_TOWN"), std::string("Springfield")); - ASSERT_EQ(std::getenv("CHILDREN_0_BIRTHDAY"), std::string("1987-04-19")); - ASSERT_EQ(std::getenv("CHILDREN_0_AGE"), std::string("10")); - ASSERT_EQ(std::getenv("CHILDREN_0_EMAIL"), std::string("bart@simpson.com")); - - ASSERT_EQ(std::getenv("CHILDREN_1_FIRST_NAME"), std::string("Lisa")); - ASSERT_EQ(std::getenv("CHILDREN_1_LAST_NAME"), std::string("Simpson")); - ASSERT_EQ(std::getenv("CHILDREN_1_TOWN"), std::string("Springfield")); - ASSERT_EQ(std::getenv("CHILDREN_1_BIRTHDAY"), std::string("1987-04-19")); - ASSERT_EQ(std::getenv("CHILDREN_1_AGE"), std::string("8")); - ASSERT_EQ(std::getenv("CHILDREN_1_EMAIL"), std::string("lisa@simpson.com")); - - ASSERT_EQ(std::getenv("CHILDREN_2_FIRST_NAME"), std::string("Maggie")); - ASSERT_EQ(std::getenv("CHILDREN_2_LAST_NAME"), std::string("Simpson")); - ASSERT_EQ(std::getenv("CHILDREN_2_TOWN"), std::string("Springfield")); - ASSERT_EQ(std::getenv("CHILDREN_2_BIRTHDAY"), std::string("1987-04-19")); - ASSERT_EQ(std::getenv("CHILDREN_2_AGE"), std::string("0")); - ASSERT_EQ(std::getenv("CHILDREN_2_EMAIL"), std::string("maggie@simpson.com")); - - const auto read_homer = rfl::env::read(); - - if (!read_homer) { - std::cout << "Error reading Person from environment: " - << read_homer.error().what() << std::endl; - } - - ASSERT_TRUE(read_homer); - - ASSERT_EQ(read_homer->first_name, "Homer"); - ASSERT_EQ(read_homer->last_name, "Simpson"); - ASSERT_EQ(read_homer->town, "Springfield"); - ASSERT_EQ(read_homer->birthday.str(), "1987-04-19"); - ASSERT_EQ(read_homer->age, 45); - ASSERT_EQ(read_homer->email, "homer@simpson.com"); - ASSERT_EQ(read_homer->children.size(), 3); - - ASSERT_EQ(read_homer->children[0].first_name, "Bart"); - ASSERT_EQ(read_homer->children[0].last_name, "Simpson"); - ASSERT_EQ(read_homer->children[0].town, "Springfield"); - ASSERT_EQ(read_homer->children[0].birthday.str(), "1987-04-19"); - ASSERT_EQ(read_homer->children[0].age, 10); - ASSERT_EQ(read_homer->children[0].email, "bart@simpson.com"); - - ASSERT_EQ(read_homer->children[1].first_name, "Lisa"); - ASSERT_EQ(read_homer->children[1].last_name, "Simpson"); - ASSERT_EQ(read_homer->children[1].town, "Springfield"); - ASSERT_EQ(read_homer->children[1].birthday.str(), "1987-04-19"); - ASSERT_EQ(read_homer->children[1].age, 8); - ASSERT_EQ(read_homer->children[1].email, "lisa@simpson.com"); - - ASSERT_EQ(read_homer->children[2].first_name, "Maggie"); - ASSERT_EQ(read_homer->children[2].last_name, "Simpson"); - ASSERT_EQ(read_homer->children[2].town, "Springfield"); - ASSERT_EQ(read_homer->children[2].birthday.str(), "1987-04-19"); - ASSERT_EQ(read_homer->children[2].age, 0); - ASSERT_EQ(read_homer->children[2].email, "maggie@simpson.com"); + write_and_read(homer, []() { + ASSERT_EQ(std::getenv("FIRST_NAME"), std::string("Homer")); + ASSERT_EQ(std::getenv("LAST_NAME"), std::string("Simpson")); + ASSERT_EQ(std::getenv("TOWN"), std::string("Springfield")); + ASSERT_EQ(std::getenv("BIRTHDAY"), std::string("1987-04-19")); + ASSERT_EQ(std::getenv("AGE"), std::string("45")); + ASSERT_EQ(std::getenv("EMAIL"), std::string("homer@simpson.com")); + + ASSERT_EQ(std::getenv("CHILDREN_0_FIRST_NAME"), std::string("Bart")); + ASSERT_EQ(std::getenv("CHILDREN_0_LAST_NAME"), std::string("Simpson")); + ASSERT_EQ(std::getenv("CHILDREN_0_TOWN"), std::string("Springfield")); + ASSERT_EQ(std::getenv("CHILDREN_0_BIRTHDAY"), std::string("1987-04-19")); + ASSERT_EQ(std::getenv("CHILDREN_0_AGE"), std::string("10")); + ASSERT_EQ(std::getenv("CHILDREN_0_EMAIL"), std::string("bart@simpson.com")); + + ASSERT_EQ(std::getenv("CHILDREN_1_FIRST_NAME"), std::string("Lisa")); + ASSERT_EQ(std::getenv("CHILDREN_1_LAST_NAME"), std::string("Simpson")); + ASSERT_EQ(std::getenv("CHILDREN_1_TOWN"), std::string("Springfield")); + ASSERT_EQ(std::getenv("CHILDREN_1_BIRTHDAY"), std::string("1987-04-19")); + ASSERT_EQ(std::getenv("CHILDREN_1_AGE"), std::string("8")); + ASSERT_EQ(std::getenv("CHILDREN_1_EMAIL"), std::string("lisa@simpson.com")); + + ASSERT_EQ(std::getenv("CHILDREN_2_FIRST_NAME"), std::string("Maggie")); + ASSERT_EQ(std::getenv("CHILDREN_2_LAST_NAME"), std::string("Simpson")); + ASSERT_EQ(std::getenv("CHILDREN_2_TOWN"), std::string("Springfield")); + ASSERT_EQ(std::getenv("CHILDREN_2_BIRTHDAY"), std::string("1987-04-19")); + ASSERT_EQ(std::getenv("CHILDREN_2_AGE"), std::string("0")); + ASSERT_EQ(std::getenv("CHILDREN_2_EMAIL"), std::string("maggie@simpson.com")); + }); } } // namespace test_readme_example diff --git a/tests/env/test_readme_example2.cpp b/tests/env/test_readme_example2.cpp index 85a929c0..96536eec 100644 --- a/tests/env/test_readme_example2.cpp +++ b/tests/env/test_readme_example2.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_readme_example2 { struct Person { @@ -15,5 +17,7 @@ struct Person { TEST(env, test_readme_example2) { const auto homer = Person{.first_name = "Homer", .last_name = "Simpson", .age = 45}; + + write_and_read(homer); } } // namespace test_readme_example2 diff --git a/tests/env/test_ref.cpp b/tests/env/test_ref.cpp index d4eb694f..3f2364f1 100644 --- a/tests/env/test_ref.cpp +++ b/tests/env/test_ref.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_ref { struct DecisionTree { @@ -35,5 +37,7 @@ TEST(env, test_ref) { .greater = rfl::make_ref(DecisionTree{leaf2})}; const DecisionTree tree{.leaf_or_node = std::move(node)}; + + write_and_read(tree); } } // namespace test_ref diff --git a/tests/env/test_set.cpp b/tests/env/test_set.cpp index 23ca7f26..b2e0a73b 100644 --- a/tests/env/test_set.cpp +++ b/tests/env/test_set.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_set { struct Person { @@ -18,5 +20,7 @@ TEST(env, test_set) { const auto homer = Person{.first_name = "Homer", .children = std::move(children)}; + + write_and_read(homer); } } // namespace test_set diff --git a/tests/env/test_size.cpp b/tests/env/test_size.cpp index 03916afb..8e83d5bf 100644 --- a/tests/env/test_size.cpp +++ b/tests/env/test_size.cpp @@ -5,6 +5,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_size { struct Person { @@ -31,5 +33,7 @@ TEST(env, test_size) { .last_name = "Simpson", .birthday = "1987-04-19", .children = std::vector({bart, lisa, maggie})}; + + write_and_read(homer); } } // namespace test_size diff --git a/tests/env/test_skip.cpp b/tests/env/test_skip.cpp index 3824f68d..d90f10fa 100644 --- a/tests/env/test_skip.cpp +++ b/tests/env/test_skip.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_skip { using Age = rfl::Validator, rfl::Maximum<130>>; @@ -20,5 +22,7 @@ TEST(env, test_skip) { .first_name = "Homer", .last_name = "Simpson", .age = 45}; + + write_and_read(homer); } } // namespace test_skip diff --git a/tests/env/test_string_map.cpp b/tests/env/test_string_map.cpp index 73e97d52..014ac3f5 100644 --- a/tests/env/test_string_map.cpp +++ b/tests/env/test_string_map.cpp @@ -3,6 +3,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_string_map { TEST(env, test_string_map) { @@ -11,5 +13,7 @@ TEST(env, test_string_map) { std::make_pair("firstName", std::make_unique("Homer"))); homer.insert( std::make_pair("lastName", std::make_unique("Simpson"))); + + write_and_read(homer); } } // namespace test_string_map diff --git a/tests/env/test_tagged_union.cpp b/tests/env/test_tagged_union.cpp index 5f42b069..e507cf96 100644 --- a/tests/env/test_tagged_union.cpp +++ b/tests/env/test_tagged_union.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_tagged_union { struct Circle { @@ -23,5 +25,7 @@ using Shapes = rfl::TaggedUnion<"shape", Circle, Square, Rectangle>; TEST(env, test_tagged_union) { const Shapes r = Rectangle{.height = 10, .width = 5}; + + write_and_read(r); } } // namespace test_tagged_union diff --git a/tests/env/test_tagged_union2.cpp b/tests/env/test_tagged_union2.cpp index f0092d7e..6e541a96 100644 --- a/tests/env/test_tagged_union2.cpp +++ b/tests/env/test_tagged_union2.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_tagged_union2 { struct Circle { @@ -23,5 +25,7 @@ using Shapes = rfl::TaggedUnion<"shape", Circle, Square, Rectangle>; TEST(env, test_tagged_union2) { const Shapes r = Rectangle{.height = 10, .width = 5}; + + write_and_read(r); } } // namespace test_tagged_union2 diff --git a/tests/env/test_timestamp.cpp b/tests/env/test_timestamp.cpp index 1c5b4522..7ad83d81 100644 --- a/tests/env/test_timestamp.cpp +++ b/tests/env/test_timestamp.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_timestamp { using TS = rfl::Timestamp<"%Y-%m-%d">; @@ -17,11 +19,10 @@ struct Person { TEST(env, test_timestamp) { const auto result = TS::from_string("nonsense"); - if (result) { - std::cout << "Failed: Expected an error, but got none." << std::endl; - return; - } + ASSERT_FALSE(result); const auto bart = Person{.first_name = "Bart", .birthday = "1987-04-19"}; + + write_and_read(bart); } } // namespace test_timestamp diff --git a/tests/env/test_unique_ptr.cpp b/tests/env/test_unique_ptr.cpp index 30399588..761e9d06 100644 --- a/tests/env/test_unique_ptr.cpp +++ b/tests/env/test_unique_ptr.cpp @@ -6,6 +6,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_unique_ptr { struct Person { @@ -22,5 +24,7 @@ TEST(env, test_unique_ptr) { const auto homer = Person{.first_name = "Homer", .children = std::move(children)}; + + write_and_read(homer); } } // namespace test_unique_ptr diff --git a/tests/env/test_unique_ptr2.cpp b/tests/env/test_unique_ptr2.cpp index 137fd40a..9ba51c7f 100644 --- a/tests/env/test_unique_ptr2.cpp +++ b/tests/env/test_unique_ptr2.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_unique_ptr2 { struct DecisionTree { @@ -35,5 +37,7 @@ TEST(env, test_unique_ptr2) { .greater = std::make_unique(DecisionTree{leaf2})}; const DecisionTree tree{.leaf_or_node = std::move(node)}; + + write_and_read(tree); } } // namespace test_unique_ptr2 diff --git a/tests/env/test_variant.cpp b/tests/env/test_variant.cpp index ae86ed80..68aa17db 100644 --- a/tests/env/test_variant.cpp +++ b/tests/env/test_variant.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_variant { struct Circle { @@ -19,9 +21,11 @@ struct Square { double width; }; -using Shapes = std::variant>; +using Shapes = std::variant; TEST(env, test_variant) { const Shapes r = Rectangle{.height = 10, .width = 5}; + + write_and_read(r); } } // namespace test_variant diff --git a/tests/env/test_vector.cpp b/tests/env/test_vector.cpp index 32939b96..099474de 100644 --- a/tests/env/test_vector.cpp +++ b/tests/env/test_vector.cpp @@ -4,6 +4,8 @@ #include #include +#include "write_and_read.hpp" + namespace test_vector { struct Settings { @@ -21,33 +23,21 @@ TEST(env, test_vector) { .verbose = true, .tags = {"tag1", "tag2", "tag3"}}; - rfl::env::write(settings); - - ASSERT_TRUE(std::getenv("HOST")); - ASSERT_EQ(std::getenv("HOST"), std::string("localhost")); - ASSERT_TRUE(std::getenv("PORT")); - ASSERT_EQ(std::getenv("PORT"), std::string("8080")); - ASSERT_TRUE(std::getenv("RATE")); - ASSERT_EQ(std::getenv("RATE"), std::string("1.500000")); - ASSERT_TRUE(std::getenv("VERBOSE")); - ASSERT_EQ(std::getenv("VERBOSE"), std::string("true")); - ASSERT_TRUE(std::getenv("TAGS_0")); - ASSERT_EQ(std::getenv("TAGS_0"), std::string("tag1")); - ASSERT_EQ(std::getenv("TAGS_0"), std::string("tag1")); - ASSERT_EQ(std::getenv("TAGS_1"), std::string("tag2")); - ASSERT_EQ(std::getenv("TAGS_2"), std::string("tag3")); - - const auto read_settings = rfl::env::read(); - - ASSERT_TRUE(read_settings); - ASSERT_EQ(read_settings->host, "localhost"); - ASSERT_EQ(read_settings->port, 8080); - ASSERT_DOUBLE_EQ(read_settings->rate, 1.5); - ASSERT_EQ(read_settings->verbose, true); - ASSERT_EQ(read_settings->tags.size(), 3); - ASSERT_EQ(read_settings->tags[0], "tag1"); - ASSERT_EQ(read_settings->tags[1], "tag2"); - ASSERT_EQ(read_settings->tags[2], "tag3"); + write_and_read(settings, []() { + ASSERT_TRUE(std::getenv("HOST")); + ASSERT_EQ(std::getenv("HOST"), std::string("localhost")); + ASSERT_TRUE(std::getenv("PORT")); + ASSERT_EQ(std::getenv("PORT"), std::string("8080")); + ASSERT_TRUE(std::getenv("RATE")); + ASSERT_EQ(std::getenv("RATE"), std::string("1.500000")); + ASSERT_TRUE(std::getenv("VERBOSE")); + ASSERT_EQ(std::getenv("VERBOSE"), std::string("true")); + ASSERT_TRUE(std::getenv("TAGS_0")); + ASSERT_EQ(std::getenv("TAGS_0"), std::string("tag1")); + ASSERT_EQ(std::getenv("TAGS_0"), std::string("tag1")); + ASSERT_EQ(std::getenv("TAGS_1"), std::string("tag2")); + ASSERT_EQ(std::getenv("TAGS_2"), std::string("tag3")); + }); } } // namespace test_vector diff --git a/tests/env/write_and_read.hpp b/tests/env/write_and_read.hpp new file mode 100644 index 00000000..addc7b51 --- /dev/null +++ b/tests/env/write_and_read.hpp @@ -0,0 +1,101 @@ +#ifndef WRITE_AND_READ_ +#define WRITE_AND_READ_ + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace test_env { + +class ScopedEnvironment { + public: + ScopedEnvironment() : snapshot_(capture()) { clear(); } + + ~ScopedEnvironment() { restore(snapshot_); } + + private: + using Environment = std::map; + + static Environment capture() { + auto env = Environment{}; + + for (char** current = environ; *current != nullptr; ++current) { + const std::string entry(*current); + const auto pos = entry.find('='); + + if (pos == std::string::npos) { + continue; + } + + env.emplace(entry.substr(0, pos), entry.substr(pos + 1)); + } + + return env; + } + + static void restore(const Environment& _snapshot) { + const auto current = capture(); + + for (const auto& [name, value] : current) { + if (!_snapshot.contains(name)) { + unsetenv(name.c_str()); + } + } + + for (const auto& [name, value] : _snapshot) { + setenv(name.c_str(), value.c_str(), 1); + } + } + + static void clear() { + const auto current = capture(); + + for (const auto& [name, value] : current) { + unsetenv(name.c_str()); + } + } + + Environment snapshot_; +}; + +template +void write_and_read(const auto& _obj, Assertions&& _assertions = nullptr) { + using T = std::remove_cvref_t; + + const auto scoped_environment = ScopedEnvironment(); + + rfl::env::write(_obj); + + if constexpr (!std::is_same_v, + std::nullptr_t>) { + _assertions(); + } + + const auto res = rfl::env::read(); + + EXPECT_TRUE(res && true) << "Test failed on read. Error: " + << res.error().what(); + + if (!res) { + return; + } + + const auto serialized1 = rfl::json::write(_obj); + const auto serialized2 = rfl::json::write(res.value()); + + EXPECT_EQ(serialized1, serialized2); +} + +} // namespace test_env + +using test_env::write_and_read; + +#endif From cc0b6d711e869837b2a42769609db4b537475c36 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 19 Jul 2026 01:04:16 +0200 Subject: [PATCH 15/22] Added documentation for environment variables --- README.md | 44 +++++++++--- docs/supported_formats/env.md | 124 ++++++++++++++++++++++++++++++++++ mkdocs.yaml | 7 +- 3 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 docs/supported_formats/env.md diff --git a/README.md b/README.md index 921bed8c..ef797cd8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ **reflect-cpp** is a C++-20 library for **fast serialization, deserialization and validation** using reflection, similar to [pydantic](https://github.com/pydantic/pydantic) in Python, [serde](https://github.com/serde-rs) in Rust, [encoding](https://github.com/golang/go/tree/master/src/encoding) in Go or [aeson](https://github.com/haskell/aeson/tree/master) in Haskell. -Moreover, reflect-cpp is the basis for [sqlgen](https://github.com/getml/sqlgen), a **modern, type-safe ORM and SQL query generator** for C++20, inspired by Python's SQLAlchemy/SQLModel and Rust's Diesel. It provides a fluent, composable interface for database operations with compile-time type checking and SQL injection protection. +Moreover, reflect-cpp is the basis for [sqlgen](https://github.com/getml/sqlgen), a **modern, type-safe ORM and SQL query generator** for C++20, inspired by Python's SQLAlchemy/SQLModel and Rust's Diesel. It provides a fluent, composable interface for database operations with compile-time type checking and SQL injection protection. reflect-cpp and sqlgen fill important gaps in C++ development. They reduce boilerplate code and increase code safety. Together, they enable reliable and efficient ETL pipelines. @@ -45,14 +45,14 @@ reflect-cpp and sqlgen fill important gaps in C++ development. They reduce boile - [Algebraic data types](#algebraic-data-types) - [Extra fields](#extra-fields) - [Reflective programming](#reflective-programming) - - [Standard Library Integration](#support-for-containers) + - [Standard Library Integration](#support-for-containers) - [The team behind reflect-cpp](#the-team-behind-reflect-cpp) - [License](#license) ### More in our [documentation](https://rfl.getml.com): - [Installation ↗](https://rfl.getml.com/install/#option-2-compilation-using-cmake) - [Benchmarks ↗](https://rfl.getml.com/benchmarks) - - [How to contribute ↗](https://rfl.getml.com/contributing) + - [How to contribute ↗](https://rfl.getml.com/contributing) - [Compiling and running the tests ↗](https://rfl.getml.com/contributing/#compiling-and-running-the-tests) @@ -70,6 +70,8 @@ The following table lists the serialization formats currently supported by refle | BSON | [libbson](https://github.com/mongodb/mongo-c-driver) | >= 1.25.1 | Apache 2.0 | JSON-like binary format | | Cap'n Proto | [capnproto](https://capnproto.org) | >= 1.0.2 | MIT | Schemaful binary format | | CBOR | [jsoncons](https://github.com/danielaparker/jsoncons)| >= 0.176.0 | BSL 1.0 | JSON-like binary format | +| cli | *(none)* | *(none)* | MIT | Command line interface | +| env | *(none)* | *(none)* | MIT | Environment variables | | Cereal | [Cereal](https://uscilab.github.io/cereal/) | >= 1.3.2 | BSD | C++ serialization library with multiple formats | | CSV | [Apache Arrow](https://arrow.apache.org/) | >= 21.0.0 | Apache 2.0 | Tabular textual format | | flexbuffers | [flatbuffers](https://github.com/google/flatbuffers) | >= 23.5.26 | Apache 2.0 | Schema-less version of flatbuffers, binary format | @@ -119,9 +121,9 @@ The resulting JSON string looks like this: You can transform the field names from `snake_case` to `camelCase` like this: ```cpp -const std::string json_string = +const std::string json_string = rfl::json::write(homer); -auto homer2 = +auto homer2 = rfl::json::read(json_string).value(); ``` @@ -150,7 +152,7 @@ last_name: Simpson age: 45 ``` -This will work for just about any example in the entire documentation +This will work for just about any example in the entire documentation and any of the following formats, except where explicitly noted otherwise: ```cpp @@ -300,6 +302,32 @@ This will resulting CSV will look like this: "Homer","Simpson","Springfield",1987-04-19,45,"homer@simpson.com" ``` +### Environment variables + +reflect-cpp can also read from and write to environment variables using `rfl::env::read` and `rfl::env::write`: + +```cpp +#include + +struct Config { + std::string host; + int port; + bool verbose; +}; + +const auto config = Config{.host = "localhost", .port = 8080, .verbose = true}; + +rfl::env::write(config); +// Sets HOST=localhost, PORT=8080, VERBOSE=true + +const auto config2 = rfl::env::read().value(); +``` + +Nested structs are flattened with `_` as a separator (e.g., `DATABASE_HOST`). +Arrays use `_INDEX` suffix (e.g., `TAGS_0`, `TAGS_1`). +Enums are serialized via their enumerator name (e.g., `"green"` for `Color::green`). +All field names are converted to uppercase by default. + ### CLI argument parsing reflect-cpp can also parse command-line arguments directly into structs using `rfl::cli::read`: @@ -404,7 +432,7 @@ struct Item { Color color; }; -const auto item = Item{.pos_x = 2.0, +const auto item = Item{.pos_x = 2.0, .pos_y = 3.0, .shape = Shape::square, .color = Color::red | Color::blue}; @@ -616,7 +644,7 @@ In addition, it supports the following custom containers: - `rfl::Binary`: Used to express numbers in binary format. - `rfl::Box`: Similar to `std::unique_ptr`, but (almost) guaranteed to never be null. -- `rfl::Bytestring`: An alias for `std::vector`. Supported by Avro, BSON, Cap'n Proto, CBOR, flexbuffers, msgpack and UBJSON. +- `rfl::Bytestring`: An alias for `std::vector`. Supported by Avro, BSON, Cap'n Proto, CBOR, flexbuffers, msgpack and UBJSON. - `rfl::Commented`: Allows you to add comments to fields (supported by YAML and XML). - `rfl::Generic`: A catch-all type that can represent (almost) anything. - `rfl::Hex`: Used to express numbers in hex format. diff --git a/docs/supported_formats/env.md b/docs/supported_formats/env.md new file mode 100644 index 00000000..47297a98 --- /dev/null +++ b/docs/supported_formats/env.md @@ -0,0 +1,124 @@ +# Environment Variables + +For environment variable support, you must also include the header ``. + +Environment variables provide a flat, hierarchical key-value store for configuration. This module serializes nested structs into environment variable names using `_` as a separator (e.g., `DATABASE_HOST`) and parses them back. + +All field names are converted to uppercase by default. + +## Reading and writing + +Suppose you have a struct like this: + +```cpp +struct Settings { + std::string host; + int port; + double rate; + bool verbose; +}; +``` + +You can serialize to environment variables like this: + +```cpp +const auto settings = Settings{...}; +rfl::env::write(settings); +``` + +You can parse from environment variables like this: + +```cpp +const rfl::Result result = rfl::env::read(); +``` + +## Nested structs + +Nested structs are serialized using `_` as a separator between field names: + +```cpp +struct DatabaseSettings { + std::string host; + int port; +}; + +struct Settings { + std::string host; + DatabaseSettings database; +}; +``` + +This produces the following environment variables: + +- `HOST` — the value of `host` +- `DATABASE_HOST` — the value of `database.host` +- `DATABASE_PORT` — the value of `database.port` + +## Arrays + +Arrays are serialized with `_INDEX` suffix on the field name: + +```cpp +struct Settings { + std::string host; + std::vector tags; +}; +``` + +This produces: + +- `HOST` — the value of `host` +- `TAGS_0` — the first element +- `TAGS_1` — the second element +- ... + +## Enums + +Enums are serialized using `rfl::enum_to_string()`, which produces the enumerator name (e.g., `"green"` for `Color::green`). If the enumerator has no name, the underlying integer value is used. + +```cpp +enum class Color { red, green, blue }; + +struct Circle { + float radius; + Color color; +}; +``` + +`color = Color::green` produces `COLOR=green`. + +### Flag enums + +Flag enums (detected via `enchantum::is_bitflag`) are serialized by joining matching flag names with `|`: + +```cpp +enum class Permissions { read = 1, write = 2, execute = 4 }; + +inline Permissions operator|(Permissions a, Permissions b) { + return static_cast(static_cast(a) | static_cast(b)); +} +``` + +`permissions = Permissions::read | Permissions::write` produces `PERMISSIONS=read|write`. + +## Tagged unions + +Tagged unions work as expected, with the discriminant field serialized as a regular field: + +```cpp +using Shapes = rfl::TaggedUnion<"shape", Circle, Square, Rectangle>; +``` + +This produces a `SHAPE` field with the variant name (e.g., `SHAPE=RECTANGLE`). + +## Processors + +You can pass processors to `read` and `write` to customize behavior. By default, `ToAllCaps` is applied, which converts all field names to uppercase. You can add additional processors to the template parameter list: + +```cpp +rfl::env::write(settings); +``` + +## Custom constructors + +Custom constructors are not supported for environment variable parsing. diff --git a/mkdocs.yaml b/mkdocs.yaml index bbe743ce..9c2e9fcd 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -5,8 +5,8 @@ strict: false site_url: https://rfl.getml.com/ site_author: Code17 GmbH site_description: >- - A C++20 library for fast serialization, deserialization and validation using reflection. - Supports JSON, AVRO, BSON, Boost Serialization, Cap'n Proto, CBOR, Cereal, CSV, flexbuffers, msgpack, parquet, TOML, UBJSON, XML, YAML, yas + A C++20 library for fast serialization, deserialization and validation using reflection. + Supports JSON, AVRO, BSON, Boost Serialization, Cap'n Proto, CBOR, Cereal, CSV, flexbuffers, msgpack, parquet, TOML, UBJSON, XML, YAML, yas theme: name: "material" @@ -99,6 +99,7 @@ nav: - CBOR: supported_formats/cbor.md - Cereal: supported_formats/cereal.md - CSV: supported_formats/csv.md + - Environment variables: supported_formats/env.md - FlexBuffers: supported_formats/flexbuffers.md - JSON: supported_formats/json.md - MessagePack: supported_formats/msgpack.md @@ -111,7 +112,7 @@ nav: - Custom Format: supported_formats/supporting_your_own_format.md # - Reflective Programming: ./reflective_programming.md - Installation: install.md - - Documentation: + - Documentation: - docs-readme.md - The basics: - Structs: concepts/structs.md From ce659dc55b726d98bfff9f9a8d46f3b5909ce26a Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 19 Jul 2026 22:19:48 +0200 Subject: [PATCH 16/22] Fixed a number of minor issues --- include/rfl/env/Reader.hpp | 19 ++++++++++++++++--- src/rfl/env/Writer.cpp | 13 ++++++++++--- tests/env/write_and_read.hpp | 27 +++++++++++++++++++++------ 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/include/rfl/env/Reader.hpp b/include/rfl/env/Reader.hpp index 9524777a..33e3a832 100644 --- a/include/rfl/env/Reader.hpp +++ b/include/rfl/env/Reader.hpp @@ -1,7 +1,10 @@ #ifndef RFL_ENV_READER_HPP_ #define RFL_ENV_READER_HPP_ +#include + #include +#include #include #include #include @@ -11,7 +14,16 @@ #include "../Result.hpp" +#if defined(__APPLE__) +#include +inline char** get_environ() noexcept { return *_NSGetEnviron(); } +#elif defined(_WIN32) +#include +inline char** get_environ() noexcept { return _environ; } +#else extern char** environ; +inline char** get_environ() noexcept { return environ; } +#endif namespace rfl::env { @@ -166,7 +178,7 @@ struct RFL_API Reader { return true; } const auto prefix = _var.path.empty() ? std::string("") : _var.path + "_"; - for (char** env = environ; *env != nullptr; ++env) { + for (char** env = get_environ(); *env != nullptr; ++env) { const std::string env_entry(*env); if (env_entry.size() <= prefix.size()) { continue; @@ -229,7 +241,7 @@ struct RFL_API Reader { } } else { - for (char** env = environ; *env != nullptr; ++env) { + for (char** env = get_environ(); *env != nullptr; ++env) { const std::string env_entry(*env); if (env_entry.size() <= _obj.prefix.size()) { continue; @@ -239,7 +251,8 @@ struct RFL_API Reader { if (pos_eq == std::string::npos) { continue; } - const auto name = env_entry.substr(_obj.prefix.size(), pos_eq); + const auto name = + env_entry.substr(_obj.prefix.size(), pos_eq - _obj.prefix.size()); const auto var = InputVarType{.path = env_entry.substr(0, pos_eq)}; _object_reader.read(std::string_view(name), var); } diff --git a/src/rfl/env/Writer.cpp b/src/rfl/env/Writer.cpp index 7ff21a3a..ba54a3b1 100644 --- a/src/rfl/env/Writer.cpp +++ b/src/rfl/env/Writer.cpp @@ -34,7 +34,13 @@ SOFTWARE. namespace rfl::env { -static constexpr const char* XML_CONTENT = "xml_content"; +int portable_setenv(const char* name, const char* value) { +#ifdef _WIN32 + return _putenv_s(name, value); +#else + return setenv(name, value, 1); +#endif +} Writer::Writer() {} @@ -105,7 +111,8 @@ void Writer::end_array(OutputArrayType* _arr) const noexcept { value.visit([&](const auto& _v) { using T = std::remove_cvref_t; if constexpr (std::is_same_v) { - setenv((arr.prefix + std::to_string(i)).c_str(), _v.value.c_str(), 1); + portable_setenv((arr.prefix + std::to_string(i)).c_str(), + _v.value.c_str()); } else if constexpr (std::is_same_v) { // Do nothing for arrays, as end_array should have already @@ -129,7 +136,7 @@ void Writer::end_object(OutputObjectType* _obj) const noexcept { value.visit([&](const auto& _v) { using T = std::remove_cvref_t; if constexpr (std::is_same_v) { - setenv((obj.prefix + key).c_str(), _v.value.c_str(), 1); + portable_setenv((obj.prefix + key).c_str(), _v.value.c_str()); } else if constexpr (std::is_same_v) { // Do nothing for arrays, as end_array should have already diff --git a/tests/env/write_and_read.hpp b/tests/env/write_and_read.hpp index addc7b51..c2105d3f 100644 --- a/tests/env/write_and_read.hpp +++ b/tests/env/write_and_read.hpp @@ -3,18 +3,33 @@ #include -#include #include +#include #include +#include +#include #include #include #include -#include -#include - namespace test_env { +inline void portable_setenv(const std::string& name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name.c_str(), value.c_str()); +#else + setenv(name.c_str(), value.c_str(), 1); +#endif +} + +inline void portable_unsetenv(const std::string& name) { +#ifdef _WIN32 + _putenv_s(name.c_str(), ""); +#else + unsetenv(name.c_str()); +#endif +} + class ScopedEnvironment { public: ScopedEnvironment() : snapshot_(capture()) { clear(); } @@ -51,7 +66,7 @@ class ScopedEnvironment { } for (const auto& [name, value] : _snapshot) { - setenv(name.c_str(), value.c_str(), 1); + portable_setenv(name.c_str(), value.c_str()); } } @@ -59,7 +74,7 @@ class ScopedEnvironment { const auto current = capture(); for (const auto& [name, value] : current) { - unsetenv(name.c_str()); + portable_unsetenv(name.c_str()); } } From 314830c02c701d88cbf664ff08bcab8c075a7293 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 19 Jul 2026 22:23:23 +0200 Subject: [PATCH 17/22] Only empty if it is nullptr --- include/rfl/env/Reader.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/rfl/env/Reader.hpp b/include/rfl/env/Reader.hpp index 33e3a832..924faba5 100644 --- a/include/rfl/env/Reader.hpp +++ b/include/rfl/env/Reader.hpp @@ -172,7 +172,7 @@ struct RFL_API Reader { bool is_empty(const InputVarType& _var) const noexcept { const auto str = std::getenv(_var.path.c_str()); if (str != nullptr) { - return std::string(str).empty(); + return false; } if (_var.path.empty()) { return true; From 4eba9d34909374b64bdd792fc9a2440d9e9a703a Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 19 Jul 2026 22:34:58 +0200 Subject: [PATCH 18/22] Added an explicit test for maps --- tests/env/test_map_field.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/env/test_map_field.cpp diff --git a/tests/env/test_map_field.cpp b/tests/env/test_map_field.cpp new file mode 100644 index 00000000..9b9598c0 --- /dev/null +++ b/tests/env/test_map_field.cpp @@ -0,0 +1,26 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_map_field { + +struct Config { + std::map settings; +}; + +TEST(env, test_map_field) { + auto settings = std::map(); + settings["host"] = "localhost"; + settings["port"] = "8080"; + settings["debug"] = "true"; + + const auto config = Config{.settings = std::move(settings)}; + + write_and_read(config); +} + +} // namespace test_map_field From 5f8ae2482fc49ebcb7919a43b4a4a73a2ed441d2 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Mon, 20 Jul 2026 01:11:22 +0200 Subject: [PATCH 19/22] Fixed typo --- tests/env/write_and_read.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/env/write_and_read.hpp b/tests/env/write_and_read.hpp index c2105d3f..54aa941c 100644 --- a/tests/env/write_and_read.hpp +++ b/tests/env/write_and_read.hpp @@ -61,7 +61,7 @@ class ScopedEnvironment { for (const auto& [name, value] : current) { if (!_snapshot.contains(name)) { - unsetenv(name.c_str()); + portable_unsetenv(name.c_str()); } } From 6fb2c1025b0534a68771aa81a51c4dcbb98acf6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Mon, 20 Jul 2026 21:24:38 +0200 Subject: [PATCH 20/22] Remove RFL_API --- include/rfl/env/Reader.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/rfl/env/Reader.hpp b/include/rfl/env/Reader.hpp index 924faba5..ae4d28a1 100644 --- a/include/rfl/env/Reader.hpp +++ b/include/rfl/env/Reader.hpp @@ -134,7 +134,7 @@ rfl::Result parse_value(const std::string& _str, /// Parser for environment variables. Provides methods to read fields from ENV /// arrays and objects, check for empty variables, and convert string values to /// basic C++ types. -struct RFL_API Reader { +struct Reader { using InputArrayType = InputEnvArrayType; using InputObjectType = InputEnvObjectType; using InputVarType = InputEnvVarType; From fa222361688bbedc2200d97783cedc54f07b272f Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Mon, 20 Jul 2026 21:53:55 +0200 Subject: [PATCH 21/22] Fixed headers --- include/rfl/env/Parser.hpp | 17 +++++++++++++++++ include/rfl/env/read.hpp | 7 +++++++ include/rfl/env/write.hpp | 6 ++++++ 3 files changed, 30 insertions(+) diff --git a/include/rfl/env/Parser.hpp b/include/rfl/env/Parser.hpp index e58574e9..741711df 100644 --- a/include/rfl/env/Parser.hpp +++ b/include/rfl/env/Parser.hpp @@ -1,13 +1,30 @@ #ifndef RFL_ENV_PARSER_HPP_ #define RFL_ENV_PARSER_HPP_ +#include "../NamedTuple.hpp" +#include "../internal/no_optionals_v.hpp" #include "../parsing/AreReaderAndWriter.hpp" +#include "../parsing/NamedTupleParser.hpp" #include "../parsing/Parser_base.hpp" #include "Reader.hpp" #include "Writer.hpp" namespace rfl::parsing { +/// Specializes the Parser template for NamedTuple types within the env +/// namespace. This specialization inherits from NamedTupleParser to handle +/// serialization and deserialization of NamedTuple structures into/from +/// environment variables. +/// +/// Key configuration: +/// - _ignore_empty_containers: true (empty containers are ignored during +/// parsing) +/// - _all_required: determined by no_optionals_v (fields are +/// required if the processors do not contain the NoOptional preprocessor) +/// - _no_field_names: false (field names are preserved in the output) +/// +/// The AreReaderAndWriter concept ensures that env::Reader and env::Writer can +/// handle NamedTuple types. template requires AreReaderAndWriter> diff --git a/include/rfl/env/read.hpp b/include/rfl/env/read.hpp index 6854f5b7..8e05149d 100644 --- a/include/rfl/env/read.hpp +++ b/include/rfl/env/read.hpp @@ -8,6 +8,13 @@ namespace rfl::env { +/// This function provides a convenient way to read environment variables into a +/// type T. +/// It automatically applies the ToAllCaps processor to the environment variable +/// keys, allowing the user to specify field names in snake_case or camelCase in +/// their struct, and they will automatically match the corresponding uppercase +/// environment variables. The Processors pack allows for additional custom +/// processing to be applied. template rfl::Result read() { using InputVarType = typename Reader::InputVarType; diff --git a/include/rfl/env/write.hpp b/include/rfl/env/write.hpp index af7bf633..bdc9f91b 100644 --- a/include/rfl/env/write.hpp +++ b/include/rfl/env/write.hpp @@ -9,6 +9,12 @@ namespace rfl::env { +/// Serializes the given object `_obj` into the environment variables. +/// +/// This function acts as a high-level entry point for serialization. It: +/// +/// @tparam Ps Variadic list of additional processors to apply during parsing. +/// @param _obj The object to serialize. template void write(const auto& _obj) { using T = std::remove_cvref_t; From d92d1e7d892837a0a723da8f3d1934dfb271ce4d Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Mon, 20 Jul 2026 23:34:59 +0200 Subject: [PATCH 22/22] Use generic get_environ() --- tests/env/write_and_read.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/env/write_and_read.hpp b/tests/env/write_and_read.hpp index 54aa941c..37c85738 100644 --- a/tests/env/write_and_read.hpp +++ b/tests/env/write_and_read.hpp @@ -42,7 +42,7 @@ class ScopedEnvironment { static Environment capture() { auto env = Environment{}; - for (char** current = environ; *current != nullptr; ++current) { + for (char** current = get_environ(); *current != nullptr; ++current) { const std::string entry(*current); const auto pos = entry.find('=');