diff --git a/README.md b/README.md index 921bed8ce..ef797cd82 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 000000000..47297a98d --- /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/include/rfl.hpp b/include/rfl.hpp index 1a399bc17..9e0d0f5d2 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 000000000..c86bad16b --- /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 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 + template + static auto handle_one_field(const FieldType& _f) { + using NewFieldType = Field(), + typename FieldType::Type>; + return NewFieldType(_f.value()); + } +}; + +} // namespace rfl + +#endif diff --git a/include/rfl/env.hpp b/include/rfl/env.hpp new file mode 100644 index 000000000..06dfaeb40 --- /dev/null +++ b/include/rfl/env.hpp @@ -0,0 +1,11 @@ +#ifndef RFL_ENV_HPP_ +#define RFL_ENV_HPP_ + +#include "../rfl.hpp" +#include "env/Parser.hpp" +#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 new file mode 100644 index 000000000..741711df9 --- /dev/null +++ b/include/rfl/env/Parser.hpp @@ -0,0 +1,48 @@ +#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> +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 +using Parser = parsing::Parser; + +} // namespace rfl::env + +#endif diff --git a/include/rfl/env/Reader.hpp b/include/rfl/env/Reader.hpp new file mode 100644 index 000000000..ae4d28a18 --- /dev/null +++ b/include/rfl/env/Reader.hpp @@ -0,0 +1,320 @@ +#ifndef RFL_ENV_READER_HPP_ +#define RFL_ENV_READER_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 { + +/// 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"). +struct InputEnvVarType { + 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 { + std::string prefix; +}; + +/// Represents a ENV array containing multiple string values. +struct InputEnvArrayType { + std::string prefix; +}; + +// --- 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 = InputEnvArrayType; + using InputObjectType = InputEnvObjectType; + using InputVarType = InputEnvVarType; + + 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 { + return InputVarType{.path = _arr.prefix + std::to_string(_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{.path = child_path}; + } + + /// 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 { + const auto str = std::getenv(_var.path.c_str()); + if (str != nullptr) { + return false; + } + if (_var.path.empty()) { + return true; + } + const auto prefix = _var.path.empty() ? std::string("") : _var.path + "_"; + for (char** env = get_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. + /// @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 { + 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; + } + + /// 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 { + 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 = get_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 - _obj.prefix.size()); + const auto var = InputVarType{.path = env_entry.substr(0, pos_eq)}; + _object_reader.read(std::string_view(name), var); + } + } + } + + 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 prefix = _var.path.empty() ? std::string("") : _var.path + "_"; + return InputArrayType{.prefix = prefix}; + } + + /// Converts a ENV variable to an object for reading nested fields. + /// 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 { + const auto prefix = _var.path.empty() ? std::string("") : _var.path + "_"; + return InputObjectType{.prefix = 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 { + const char* env_value = std::getenv(_var.path.c_str()); + if (env_value) { + return std::string(env_value); + } else { + return std::nullopt; + } + } +}; + +} // namespace rfl::env + +#endif diff --git a/include/rfl/env/Writer.hpp b/include/rfl/env/Writer.hpp new file mode 100644 index 000000000..23512bb72 --- /dev/null +++ b/include/rfl/env/Writer.hpp @@ -0,0 +1,178 @@ +#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 OutputEnvObjectType; + +struct OutputEnvArrayType { + std::string prefix; + 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(); + + OutputArrayType array_as_root(const size_t) const noexcept; + + 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 + OutputArrayType add_array_to_array(const size_t, + 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 + /// @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 + OutputObjectType add_object_to_array(const size_t, + 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 + /// @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 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), val); + return 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/include/rfl/env/read.hpp b/include/rfl/env/read.hpp new file mode 100644 index 000000000..8e05149df --- /dev/null +++ b/include/rfl/env/read.hpp @@ -0,0 +1,27 @@ +#ifndef RFL_ENV_READ_HPP_ +#define RFL_ENV_READ_HPP_ + +#include "../Processors.hpp" +#include "../ToAllCaps.hpp" +#include "Parser.hpp" +#include "Reader.hpp" + +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; + const auto r = Reader(); + return Parser>::read(r, InputVarType{}); +} + +} // namespace rfl::env + +#endif diff --git a/include/rfl/env/write.hpp b/include/rfl/env/write.hpp new file mode 100644 index 000000000..bdc9f91bd --- /dev/null +++ b/include/rfl/env/write.hpp @@ -0,0 +1,29 @@ +#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 { + +/// 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; + using ParentType = parsing::Parent; + auto w = Writer(); + Parser>::write(w, _obj, + typename ParentType::Root{}); +} + +} // namespace rfl::env + +#endif diff --git a/include/rfl/internal/transform_case.hpp b/include/rfl/internal/transform_case.hpp index 0afa0524a..082f27cc6 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); } } diff --git a/include/rfl/parsing/ViewReader.hpp b/include/rfl/parsing/ViewReader.hpp index 928bf672e..c901adab7 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 bb5fd2aff..654f38eb3 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 6abd25202..de57b25d3 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 18f54377d..cc776bebc 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, diff --git a/mkdocs.yaml b/mkdocs.yaml index bbe743ce4..9c2e9fcd7 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 diff --git a/src/reflectcpp.cpp b/src/reflectcpp.cpp index ae3c4b3bb..ea76c27ca 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 new file mode 100644 index 000000000..ba54a3b1d --- /dev/null +++ b/src/rfl/env/Writer.cpp @@ -0,0 +1,157 @@ +/* + +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 { + +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() {} + +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{.prefix = _parent->prefix + std::string(_name) + "_"}; + _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::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{}; + _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 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) { + 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 + // 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; + for (const auto& [key, value] : *obj.fields) { + value.visit([&](const auto& _v) { + using T = std::remove_cvref_t; + if constexpr (std::is_same_v) { + 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 + // 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 OutputObjectType."); + } + }); + } +} + +} // namespace rfl::env diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d68b3a0fa..553a5088c 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 000000000..8fe3e0312 --- /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_add_struct_name.cpp b/tests/env/test_add_struct_name.cpp new file mode 100644 index 000000000..3d971c42a --- /dev/null +++ b/tests/env/test_add_struct_name.cpp @@ -0,0 +1,49 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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})}; + + write_and_read>(homer); +} +} // namespace test_add_struct_name diff --git a/tests/env/test_array.cpp b/tests/env/test_array.cpp new file mode 100644 index 000000000..57c68fd5f --- /dev/null +++ b/tests/env/test_array.cpp @@ -0,0 +1,34 @@ +#include + +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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)})}; + + write_and_read(homer); +} + +} // namespace test_array diff --git a/tests/env/test_basic.cpp b/tests/env/test_basic.cpp new file mode 100644 index 000000000..30cf2144e --- /dev/null +++ b/tests/env/test_basic.cpp @@ -0,0 +1,34 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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, + }; + + 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 new file mode 100644 index 000000000..10729848f --- /dev/null +++ b/tests/env/test_box.cpp @@ -0,0 +1,43 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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)}; + + write_and_read(tree); +} +} // namespace test_box diff --git a/tests/env/test_combined_processors.cpp b/tests/env/test_combined_processors.cpp new file mode 100644 index 000000000..a9c467f52 --- /dev/null +++ b/tests/env/test_combined_processors.cpp @@ -0,0 +1,52 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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>; + + write_and_read(homer); +} +} // 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 000000000..abc9ab55b --- /dev/null +++ b/tests/env/test_custom_class1.cpp @@ -0,0 +1,38 @@ +#include + +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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"); + + write_and_read(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 000000000..485fe94a7 --- /dev/null +++ b/tests/env/test_custom_class3.cpp @@ -0,0 +1,65 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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); + + write_and_read(bart); +} + +} // 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 000000000..9bac945c5 --- /dev/null +++ b/tests/env/test_custom_class4.cpp @@ -0,0 +1,66 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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(env, test_custom_class4) { + const auto bart = test_custom_class4::Person( + "Bart", rfl::make_box("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 new file mode 100644 index 000000000..1ea63e22a --- /dev/null +++ b/tests/env/test_default_values.cpp @@ -0,0 +1,29 @@ +#include + +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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})}; + + write_and_read(homer); +} +} // namespace test_default_values diff --git a/tests/env/test_deque.cpp b/tests/env/test_deque.cpp new file mode 100644 index 000000000..d6f027257 --- /dev/null +++ b/tests/env/test_deque.cpp @@ -0,0 +1,28 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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)}; + + write_and_read(homer); +} +} // namespace test_deque diff --git a/tests/env/test_enum.cpp b/tests/env/test_enum.cpp new file mode 100644 index 000000000..a63243ea8 --- /dev/null +++ b/tests/env/test_enum.cpp @@ -0,0 +1,24 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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}; + + write_and_read(circle); +} + +} // namespace test_enum diff --git a/tests/env/test_extra_fields.cpp b/tests/env/test_extra_fields.cpp new file mode 100644 index 000000000..1aa2a0459 --- /dev/null +++ b/tests/env/test_extra_fields.cpp @@ -0,0 +1,35 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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) { + 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 new file mode 100644 index 000000000..162d32ade --- /dev/null +++ b/tests/env/test_field_variant.cpp @@ -0,0 +1,40 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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 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 new file mode 100644 index 000000000..9184a080c --- /dev/null +++ b/tests/env/test_flag_enum.cpp @@ -0,0 +1,35 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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}; + + 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 new file mode 100644 index 000000000..5bf842c00 --- /dev/null +++ b/tests/env/test_flag_enum_with_int.cpp @@ -0,0 +1,34 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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)}; + + write_and_read(circle); +} + +} // 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 000000000..4280cb9b1 --- /dev/null +++ b/tests/env/test_flatten.cpp @@ -0,0 +1,34 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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}; + + write_and_read(employee); +} +} // namespace test_flatten diff --git a/tests/env/test_flatten_anonymous.cpp b/tests/env/test_flatten_anonymous.cpp new file mode 100644 index 000000000..ee8430f81 --- /dev/null +++ b/tests/env/test_flatten_anonymous.cpp @@ -0,0 +1,35 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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}; + + write_and_read(employee); +} + +} // 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 000000000..2f3f10c55 --- /dev/null +++ b/tests/env/test_forward_list.cpp @@ -0,0 +1,28 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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)}; + + write_and_read(homer); +} +} // namespace test_forward_list diff --git a/tests/env/test_literal.cpp b/tests/env/test_literal.cpp new file mode 100644 index 000000000..498ebffcc --- /dev/null +++ b/tests/env/test_literal.cpp @@ -0,0 +1,26 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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">()}; + + write_and_read(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 000000000..0735dd627 --- /dev/null +++ b/tests/env/test_literal_map.cpp @@ -0,0 +1,21 @@ +#include + +#include +#include + +#include "write_and_read.hpp" + +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"))); + + write_and_read(homer); +} +} // namespace test_literal_map diff --git a/tests/env/test_map.cpp b/tests/env/test_map.cpp new file mode 100644 index 000000000..30cc597c2 --- /dev/null +++ b/tests/env/test_map.cpp @@ -0,0 +1,35 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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) { + 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"})); + children.insert(std::make_pair("child3", Person{.first_name = "Maggie"})); + + 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_field.cpp b/tests/env/test_map_field.cpp new file mode 100644 index 000000000..9b9598c08 --- /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 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 000000000..0f1f1e638 --- /dev/null +++ b/tests/env/test_map_with_key_validation.cpp @@ -0,0 +1,36 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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) { + const auto scoped_environment = test_env::ScopedEnvironment(); + + 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)}; + + 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 new file mode 100644 index 000000000..771d8f956 --- /dev/null +++ b/tests/env/test_monster_example.cpp @@ -0,0 +1,67 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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 scoped_environment = test_env::ScopedEnvironment(); + + 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)}; + + 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 new file mode 100644 index 000000000..168d92069 --- /dev/null +++ b/tests/env/test_nested.cpp @@ -0,0 +1,44 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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, + }}; + + 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 new file mode 100644 index 000000000..35bf6fb32 --- /dev/null +++ b/tests/env/test_readme_example.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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})}; + + 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 new file mode 100644 index 000000000..96536eec2 --- /dev/null +++ b/tests/env/test_readme_example2.cpp @@ -0,0 +1,23 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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}; + + write_and_read(homer); +} +} // namespace test_readme_example2 diff --git a/tests/env/test_ref.cpp b/tests/env/test_ref.cpp new file mode 100644 index 000000000..3f2364f1f --- /dev/null +++ b/tests/env/test_ref.cpp @@ -0,0 +1,43 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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)}; + + write_and_read(tree); +} +} // namespace test_ref diff --git a/tests/env/test_set.cpp b/tests/env/test_set.cpp new file mode 100644 index 000000000..b2e0a73b5 --- /dev/null +++ b/tests/env/test_set.cpp @@ -0,0 +1,26 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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)}; + + write_and_read(homer); +} +} // namespace test_set diff --git a/tests/env/test_size.cpp b/tests/env/test_size.cpp new file mode 100644 index 000000000..8e83d5bf5 --- /dev/null +++ b/tests/env/test_size.cpp @@ -0,0 +1,39 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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})}; + + write_and_read(homer); +} +} // namespace test_size diff --git a/tests/env/test_skip.cpp b/tests/env/test_skip.cpp new file mode 100644 index 000000000..d90f10fac --- /dev/null +++ b/tests/env/test_skip.cpp @@ -0,0 +1,28 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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}; + + write_and_read(homer); +} +} // namespace test_skip diff --git a/tests/env/test_string_map.cpp b/tests/env/test_string_map.cpp new file mode 100644 index 000000000..014ac3f5c --- /dev/null +++ b/tests/env/test_string_map.cpp @@ -0,0 +1,19 @@ +#include + +#include +#include + +#include "write_and_read.hpp" + +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"))); + + write_and_read(homer); +} +} // 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 000000000..e507cf961 --- /dev/null +++ b/tests/env/test_tagged_union.cpp @@ -0,0 +1,31 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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}; + + write_and_read(r); +} +} // 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 000000000..6e541a96c --- /dev/null +++ b/tests/env/test_tagged_union2.cpp @@ -0,0 +1,31 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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}; + + write_and_read(r); +} +} // namespace test_tagged_union2 diff --git a/tests/env/test_timestamp.cpp b/tests/env/test_timestamp.cpp new file mode 100644 index 000000000..7ad83d81e --- /dev/null +++ b/tests/env/test_timestamp.cpp @@ -0,0 +1,28 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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"); + + 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 new file mode 100644 index 000000000..761e9d067 --- /dev/null +++ b/tests/env/test_unique_ptr.cpp @@ -0,0 +1,30 @@ +#include + +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +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)}; + + write_and_read(homer); +} +} // 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 000000000..9ba51c7f8 --- /dev/null +++ b/tests/env/test_unique_ptr2.cpp @@ -0,0 +1,43 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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)}; + + write_and_read(tree); +} +} // namespace test_unique_ptr2 diff --git a/tests/env/test_variant.cpp b/tests/env/test_variant.cpp new file mode 100644 index 000000000..68aa17db0 --- /dev/null +++ b/tests/env/test_variant.cpp @@ -0,0 +1,31 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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}; + + write_and_read(r); +} +} // namespace test_variant diff --git a/tests/env/test_vector.cpp b/tests/env/test_vector.cpp new file mode 100644 index 000000000..099474dea --- /dev/null +++ b/tests/env/test_vector.cpp @@ -0,0 +1,43 @@ +#include + +#include +#include +#include + +#include "write_and_read.hpp" + +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"}}; + + 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 000000000..37c85738b --- /dev/null +++ b/tests/env/write_and_read.hpp @@ -0,0 +1,116 @@ +#ifndef WRITE_AND_READ_ +#define WRITE_AND_READ_ + +#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(); } + + ~ScopedEnvironment() { restore(snapshot_); } + + private: + using Environment = std::map; + + static Environment capture() { + auto env = Environment{}; + + for (char** current = get_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)) { + portable_unsetenv(name.c_str()); + } + } + + for (const auto& [name, value] : _snapshot) { + portable_setenv(name.c_str(), value.c_str()); + } + } + + static void clear() { + const auto current = capture(); + + for (const auto& [name, value] : current) { + portable_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