diff --git a/docs/content/custom_classes.md b/docs/content/custom_classes.md index 99082c05..348c27da 100644 --- a/docs/content/custom_classes.md +++ b/docs/content/custom_classes.md @@ -129,3 +129,53 @@ class Person { int age; }; ``` + +## Example 4: Keeping your class an aggregate + +Declaring a constructor makes your class a non-aggregate, which costs you +designated-initializer and aggregate initialization: + +```cpp +struct Config { + Config(ReflectionType&&); // this makes Config a non-aggregate... + int port; + bool tls; +}; + +// ...so this no longer compiles +auto config = Config{.port = 443, .tls = true}; +``` + +If you want to keep those, provide a static `from_reflection()` factory instead +of a constructor: + +```cpp +struct Config { + struct ConfigImpl { + rfl::Rename<"portNumber", int> port; + bool tls; + }; + + // 1) Publicly define `ReflectionType` + using ReflectionType = ConfigImpl; + + // 2) A static factory, instead of a constructor + static Config from_reflection(const ReflectionType& _impl) { + return Config{.port = _impl.port(), .tls = _impl.tls}; + } + + // 3) Method called `reflection` that returns `ReflectionType` + ReflectionType reflection() const { + return ReflectionType{.port = port, .tls = tls}; + } + + int port; + bool tls; +}; + +// Config is still an aggregate, so this keeps working +auto config = Config{.port = 443, .tls = true}; +``` + +If a class provides both a `from_reflection()` factory and a converting +constructor, the factory is used. diff --git a/include/rfl/internal/HasFromReflectionMethod.hpp b/include/rfl/internal/HasFromReflectionMethod.hpp new file mode 100644 index 00000000..8c794cf0 --- /dev/null +++ b/include/rfl/internal/HasFromReflectionMethod.hpp @@ -0,0 +1,17 @@ +#ifndef RFL_INTERNAL_HASFROMREFLECTIONMETHOD_HPP_ +#define RFL_INTERNAL_HASFROMREFLECTIONMETHOD_HPP_ + +#include +#include + +namespace rfl::internal { + +/// Satisfied by a type `T` providing `static T from_reflection(ReflectionType&&)`. +template +concept HasFromReflectionMethod = requires(typename T::ReflectionType&& _r) { + { T::from_reflection(std::move(_r)) } -> std::convertible_to; +}; + +} // namespace rfl::internal + +#endif diff --git a/include/rfl/parsing/Parser_default.hpp b/include/rfl/parsing/Parser_default.hpp index ad50b3e2..97e9ee80 100644 --- a/include/rfl/parsing/Parser_default.hpp +++ b/include/rfl/parsing/Parser_default.hpp @@ -6,6 +6,7 @@ #include "../Result.hpp" #include "../always_false.hpp" +#include "../internal/HasFromReflectionMethod.hpp" #include "../internal/default_if_missing_v.hpp" #include "../internal/has_default_val_v.hpp" #include "../internal/has_reflection_method_v.hpp" @@ -242,7 +243,11 @@ struct Parser { const auto wrap_in_t = [](auto&& _named_tuple) -> Result { try { using NT = decltype(_named_tuple); - return T{std::forward(_named_tuple)}; + if constexpr (internal::HasFromReflectionMethod) { + return T::from_reflection(std::forward(_named_tuple)); + } else { + return T{std::forward(_named_tuple)}; + } } catch (std::exception& e) { return error(e.what()); } diff --git a/tests/json/test_custom_class5.cpp b/tests/json/test_custom_class5.cpp new file mode 100644 index 00000000..acd4d4bd --- /dev/null +++ b/tests/json/test_custom_class5.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_custom_class5 { + +/// Uses a static `from_reflection` factory instead of a converting +/// constructor. Because no constructor is declared, `Config` remains an +/// aggregate and can still be built with a designated initializer list. +struct Config { + struct ConfigImpl { + rfl::Rename<"portNumber", int> port; + std::string host = "localhost"; + std::vector tags; + }; + + using ReflectionType = ConfigImpl; + + static Config from_reflection(ReflectionType&& _impl) { + return Config{.port = _impl.port(), + .host = _impl.host, + .tags = _impl.tags}; + } + + ReflectionType reflection() const { + return ReflectionType{.port = port, .host = host, .tags = tags}; + } + + int port; + std::string host; + std::vector tags; +}; + +// The whole point of the factory: no constructor is declared, so designated +// initializers keep working. +static_assert(std::is_aggregate_v); + +TEST(json, test_custom_class5) { + const auto config = + Config{.port = 8080, .host = "example.com", .tags = {"a", "b"}}; + + write_and_read( + config, + R"({"portNumber":8080,"host":"example.com","tags":["a","b"]})"); +} + +} // namespace test_custom_class5