diff --git a/docs/default_val.md b/docs/default_val.md new file mode 100644 index 00000000..8c30ecf7 --- /dev/null +++ b/docs/default_val.md @@ -0,0 +1,64 @@ +# Default values (rfl::DefaultVal) + +The `rfl::DefaultVal` wrapper allows a struct field to have a predefined default value when serializing and deserializing. When a field is declared as `rfl::DefaultVal`, the library will accept input that omits that field and will populate it with the provided default (or with a default-constructed T when no explicit default is given). + +## Declaration and initialization + +You can declare a default-valued field like this: + +```cpp +struct Person { + std::string first_name; // required + rfl::DefaultVal last_name = "Simpson"; // has explicit default + rfl::DefaultVal town; // default-constructed (empty string) +}; +``` + +DefaultVal behaves like a thin wrapper around the underlying type. You can construct and assign it from the underlying type, from other DefaultVal instances (if convertible), or assign the special token `rfl::Default` to reset it to the default-constructed value (if the type is default-constructible): + +```cpp +Person p; +p.last_name = "Smith"; // assign underlying value +p.town = rfl::Default{}; // reset to default (empty string) +std::string s = p.last_name.value(); +``` + +API convenience: + +- .get(), .value(), operator()() — access the underlying value (const and non-const overloads). +- set(...) — assign underlying value. + +## JSON behaviour + +When writing JSON, fields that are DefaultVal are written like normal fields using their current underlying value. When reading JSON, omitted DefaultVal fields are filled with the default value (the value assigned in the declaration, or the type's default-constructed value). + +Example (object fields): + +```cpp +// Person from above +const auto homer = rfl::json::read(R"({"first_name":"Homer"})").value(); +// homer.last_name == "Simpson" (declared default) +// homer.town == "" (default-constructed) +``` + +Example (no field names / positional arrays): + +DefaultVal also works when using rfl::NoFieldNames (positional JSON arrays). Omitted positions that correspond to DefaultVal fields get their default values: + +```cpp +const auto homer = rfl::json::read(R"(["Homer"])" ).value(); +// homer.first_name == "Homer" +// homer.last_name == "Simpson" +// homer.town == "" +``` + +## When to use + +Use rfl::DefaultVal when you want a field to be optional at the input side but still available as a value on the resulting object (no std::optional or pointer indirection). It is particularly useful for fields with sensible defaults (for example, a common last name, a default configuration value, or empty containers/strings). + +## Notes + +- The underlying type must be default-constructible to allow resetting via `rfl::Default` or when no explicit default is supplied. +- DefaultVal preserves normal read/write semantics; other fields that are not DefaultVal remain required unless expressed as optionals or handled by processors (e.g., rfl::DefaultIfMissing). + +For more advanced control over when fields are considered missing and how defaults are applied, see the processors documentation (e.g., `rfl::DefaultIfMissing`). diff --git a/docs/index.md b/docs/index.md index 5cbda969..6d1f6068 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,6 +34,7 @@ reflect-cpp fills an important gap in C++ development. It minimizes boilerplate - Simple [installation](https://rfl.getml.com/install) - Simple extendability to [other serialization formats](https://rfl.getml.com/supported_formats/supporting_your_own_format) - Simple extendability to [custom classes](https://rfl.getml.com/concepts/custom_classes) +- Support for default-valued fields via `rfl::DefaultVal` (see [Default values](default_val.md)) - Being one of the fastest serialization libraries in existence, as demonstrated by our [benchmarks](https://rfl.getml.com/benchmarks) diff --git a/include/rfl.hpp b/include/rfl.hpp index 4753749c..8aaa0cbc 100644 --- a/include/rfl.hpp +++ b/include/rfl.hpp @@ -17,6 +17,7 @@ #include "rfl/Box.hpp" #include "rfl/Bytestring.hpp" #include "rfl/DefaultIfMissing.hpp" +#include "rfl/DefaultVal.hpp" #include "rfl/Description.hpp" #include "rfl/ExtraFields.hpp" #include "rfl/Field.hpp" diff --git a/include/rfl/DefaultVal.hpp b/include/rfl/DefaultVal.hpp new file mode 100644 index 00000000..be6a6812 --- /dev/null +++ b/include/rfl/DefaultVal.hpp @@ -0,0 +1,129 @@ +#ifndef RFL_DEFAULTVAL_HPP_ +#define RFL_DEFAULTVAL_HPP_ + +#include +#include + +#include "default.hpp" + +namespace rfl { + +template +struct DefaultVal { + public: + using Type = std::remove_cvref_t; + + DefaultVal() : value_(Type()) {} + + DefaultVal(const Type& _value) : value_(_value) {} + + DefaultVal(Type&& _value) noexcept : value_(std::move(_value)) {} + + DefaultVal(DefaultVal&& _field) noexcept = default; + + DefaultVal(const DefaultVal& _field) = default; + + template + DefaultVal(const DefaultVal& _field) : value_(_field.get()) {} + + template + DefaultVal(DefaultVal&& _field) noexcept( + noexcept(Type(std::move(_field.value())))) + : value_(std::move(_field.value())) {} + + template + requires(std::is_convertible_v) + DefaultVal(const U& _value) : value_(_value) {} + + template + requires(std::is_convertible_v) + DefaultVal(U&& _value) noexcept : value_(std::forward(_value)) {} + + template + requires(std::is_convertible_v) + DefaultVal(const DefaultVal& _field) : value_(_field.value()) {} + + /// Assigns the underlying object to its default value. + template + requires(std::is_default_constructible_v) + DefaultVal(const Default&) : value_(Type()) {} + + ~DefaultVal() = default; + + /// Returns the underlying object. + const Type& get() const { return value_; } + + /// Returns the underlying object. + Type& operator()() { return value_; } + + /// Returns the underlying object. + const Type& operator()() const { return value_; } + + /// Assigns the underlying object. + auto& operator=(const Type& _value) { + value_ = _value; + return *this; + } + + /// Assigns the underlying object. + auto& operator=(Type&& _value) noexcept { + value_ = std::move(_value); + return *this; + } + + /// Assigns the underlying object. + template , + bool>::type = true> + auto& operator=(const U& _value) { + value_ = _value; + return *this; + } + + /// Assigns the underlying object to its default value. + template , + bool>::type = true> + auto& operator=(const Default&) { + value_ = Type(); + return *this; + } + + /// Assigns the underlying object. + DefaultVal& operator=(const DefaultVal& _field) = default; + + /// Assigns the underlying object. + DefaultVal& operator=(DefaultVal&& _field) = default; + + /// Assigns the underlying object. + template + auto& operator=(const DefaultVal& _field) { + value_ = _field.get(); + return *this; + } + + /// Assigns the underlying object. + template + auto& operator=(DefaultVal&& _field) { + value_ = std::forward(_field.value_); + return *this; + } + + /// Assigns the underlying object. + void set(const Type& _value) { value_ = _value; } + + /// Assigns the underlying object. + void set(Type&& _value) { value_ = std::move(_value); } + + /// Returns the underlying object. + Type& value() { return value_; } + + /// Returns the underlying object. + const Type& value() const { return value_; } + + /// The underlying value. + Type value_; +}; + +} // namespace rfl + +#endif diff --git a/include/rfl/internal/has_default_val_v.hpp b/include/rfl/internal/has_default_val_v.hpp new file mode 100644 index 00000000..6920ae7e --- /dev/null +++ b/include/rfl/internal/has_default_val_v.hpp @@ -0,0 +1,27 @@ +#ifndef RFL_HASDEFAULTVALV_HPP_ +#define RFL_HASDEFAULTVALV_HPP_ +#include + +#include "../NamedTuple.hpp" +#include "../named_tuple_t.hpp" +#include "is_default_val_v.hpp" + +namespace rfl::internal { + +template +struct HasDefaultVal; + +template +struct HasDefaultVal> { + static constexpr bool value = + (false || ... || + is_default_val_v< + std::remove_cvref_t>>); +}; + +template +constexpr bool has_default_val_v = HasDefaultVal>::value; + +} // namespace rfl::internal + +#endif diff --git a/include/rfl/internal/has_tag_v.hpp b/include/rfl/internal/has_tag_v.hpp index 3affc143..299618ff 100644 --- a/include/rfl/internal/has_tag_v.hpp +++ b/include/rfl/internal/has_tag_v.hpp @@ -1,30 +1,21 @@ #ifndef RFL_HASTAGV_HPP_ #define RFL_HASTAGV_HPP_ -#include +#include -namespace rfl { -namespace internal { +namespace rfl::internal { -template -class HasTag { - private: - template - static std::int64_t foo(...); - - template - static std::int32_t foo(typename U::Tag*); - - public: - static constexpr bool value = - sizeof(foo(nullptr)) == sizeof(std::int32_t); -}; +template +struct TagWrapper {}; /// Used for tagged unions - determines whether a struct as a Tag. -template -constexpr bool has_tag_v = HasTag::value; +template +constexpr bool has_tag_v = requires() { + { + TagWrapper{} + } -> std::same_as>; +}; -} // namespace internal -} // namespace rfl +} // namespace rfl::internal #endif diff --git a/include/rfl/internal/is_default_val_v.hpp b/include/rfl/internal/is_default_val_v.hpp new file mode 100644 index 00000000..98977549 --- /dev/null +++ b/include/rfl/internal/is_default_val_v.hpp @@ -0,0 +1,25 @@ +#ifndef RFL_INTERNAL_ISDEFAULTVAL_HPP_ +#define RFL_INTERNAL_ISDEFAULTVAL_HPP_ + +#include + +#include "../DefaultVal.hpp" + +namespace rfl::internal { + +template +class is_default_val; + +template +class is_default_val : public std::false_type {}; + +template +class is_default_val> : public std::true_type {}; + +template +constexpr bool is_default_val_v = + is_default_val>>::value; + +} // namespace rfl::internal + +#endif diff --git a/include/rfl/parsing/NamedTupleParser.hpp b/include/rfl/parsing/NamedTupleParser.hpp index b081d88b..7f423e04 100644 --- a/include/rfl/parsing/NamedTupleParser.hpp +++ b/include/rfl/parsing/NamedTupleParser.hpp @@ -10,9 +10,11 @@ #include "../NamedTuple.hpp" #include "../Result.hpp" #include "../always_false.hpp" +#include "../internal/has_default_val_v.hpp" #include "../internal/is_array.hpp" #include "../internal/is_attribute.hpp" #include "../internal/is_basic_type.hpp" +#include "../internal/is_default_val_v.hpp" #include "../internal/is_extra_fields.hpp" #include "../internal/is_skip.hpp" #include "../internal/no_duplicate_field_names.hpp" @@ -108,7 +110,6 @@ struct NamedTupleParser { auto arr = _r.to_array(_var); if (!arr) [[unlikely]] { auto set = std::array{}; - // return std::make_pair(set, arr.error()); return std::make_pair(set, arr.error()); } return read_object_or_array(_r, *arr, _view); @@ -254,8 +255,10 @@ struct NamedTupleParser { if (!std::get<_i>(_found)) { constexpr bool is_required_field = + !internal::is_default_val_v && !internal::is_extra_fields_v && (_all_required || is_required()); + if constexpr (is_required_field) { constexpr auto current_name = internal::nth_element_t<_i, FieldTypes...>::name(); @@ -263,7 +266,8 @@ struct NamedTupleParser { stream << "Field named '" << std::string(current_name) << "' not found."; _errors->emplace_back(Error(stream.str())); - } else { + + } else if constexpr (!internal::has_default_val_v) { if constexpr (!std::is_const_v) { ::new (rfl::get<_i>(_view)) ValueType(); } else { @@ -338,9 +342,15 @@ struct NamedTupleParser { return err; } } + if constexpr (internal::has_default_val_v && + !ProcessorsType::default_if_missing_) { + handle_missing_fields(reader.found(), *_view, nullptr, &errors, + std::make_integer_sequence()); + } if (errors.size() != 0) { return to_single_error_message(errors); } + return std::nullopt; } }; diff --git a/include/rfl/parsing/Parser.hpp b/include/rfl/parsing/Parser.hpp index c7a1f9fd..0f7ef390 100644 --- a/include/rfl/parsing/Parser.hpp +++ b/include/rfl/parsing/Parser.hpp @@ -7,6 +7,7 @@ #include "Parser_bytestring.hpp" #include "Parser_c_array.hpp" #include "Parser_default.hpp" +#include "Parser_default_val.hpp" #include "Parser_duration.hpp" #include "Parser_filepath.hpp" #include "Parser_map_like.hpp" diff --git a/include/rfl/parsing/Parser_default.hpp b/include/rfl/parsing/Parser_default.hpp index eed67f9a..ced5e8db 100644 --- a/include/rfl/parsing/Parser_default.hpp +++ b/include/rfl/parsing/Parser_default.hpp @@ -8,6 +8,7 @@ #include "../always_false.hpp" #include "../enums.hpp" #include "../from_named_tuple.hpp" +#include "../internal/has_default_val_v.hpp" #include "../internal/has_reflection_method_v.hpp" #include "../internal/has_reflection_type_v.hpp" #include "../internal/has_reflector.hpp" @@ -79,7 +80,8 @@ struct Parser { .and_then(wrap_in_t); } else if constexpr (std::is_class_v && std::is_aggregate_v) { - if constexpr (ProcessorsType::default_if_missing_) { + if constexpr (ProcessorsType::default_if_missing_ || + internal::has_default_val_v) { return read_struct_with_default(_r, _var); } else { return read_struct(_r, _var); diff --git a/include/rfl/parsing/Parser_default_val.hpp b/include/rfl/parsing/Parser_default_val.hpp new file mode 100644 index 00000000..38f4b359 --- /dev/null +++ b/include/rfl/parsing/Parser_default_val.hpp @@ -0,0 +1,46 @@ +#ifndef RFL_PARSING_PARSER_DEFAULTVAL_HPP_ +#define RFL_PARSING_PARSER_DEFAULTVAL_HPP_ + +#include +#include + +#include "../DefaultVal.hpp" +#include "AreReaderAndWriter.hpp" +#include "Parent.hpp" +#include "Parser_base.hpp" +#include "schema/Type.hpp" + +namespace rfl::parsing { + +template + requires AreReaderAndWriter> +struct Parser, ProcessorsType> { + using InputVarType = typename R::InputVarType; + + using ParentType = Parent; + + static Result> read(const R& _r, + const InputVarType& _var) noexcept { + return Parser, ProcessorsType>::read(_r, _var) + .transform([](auto&& _t) { + return DefaultVal(std::forward(_t)); + }); + } + + template + static void write(const W& _w, const DefaultVal& _d, const P& _parent) { + Parser, ProcessorsType>::write(_w, _d.value(), + _parent); + } + + static schema::Type to_schema( + std::map* _definitions) { + using U = std::remove_cvref_t; + return schema::Type{ + Parser::to_schema(_definitions)}; + } +}; + +} // namespace rfl::parsing + +#endif diff --git a/include/rfl/parsing/ViewReaderWithDefault.hpp b/include/rfl/parsing/ViewReaderWithDefault.hpp index a5a56563..8ee2d79e 100644 --- a/include/rfl/parsing/ViewReaderWithDefault.hpp +++ b/include/rfl/parsing/ViewReaderWithDefault.hpp @@ -31,6 +31,8 @@ class ViewReaderWithDefault { ~ViewReaderWithDefault() = default; + const std::array& found() const { return *found_; } + /// Assigns the parsed version of _var to the field signified by _name, if /// such a field exists in the underlying view. void read(const std::string_view& _name, const InputVarType& _var) const { diff --git a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp index 697060ab..7b12a2d8 100644 --- a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp @@ -27,6 +27,13 @@ class ViewReaderWithDefaultAndStrippedFieldNames { ~ViewReaderWithDefaultAndStrippedFieldNames() = default; + std::array found() const { + std::array f; + std::fill(f.begin(), f.begin() + i_, true); + std::fill(f.begin() + i_, f.end(), false); + return f; + } + /// Assigns the parsed version of _var to the field signified by i_, to be /// used when the field names are stripped. std::optional read(const InputVarType& _var) const { diff --git a/tests/json/test_default_val.cpp b/tests/json/test_default_val.cpp new file mode 100644 index 00000000..1039746e --- /dev/null +++ b/tests/json/test_default_val.cpp @@ -0,0 +1,36 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_default_val { + +struct Person { + std::string first_name; + rfl::DefaultVal last_name = "Simpson"; + rfl::DefaultVal town; +}; + +TEST(json, test_default_val) { + static_assert(rfl::internal::has_default_val_v, + "Should have default val"); + + const auto should_fail = rfl::json::read(R"({})"); + + EXPECT_EQ(should_fail && true, false) + << "Should have failed due to missing required field"; + + const auto homer = + rfl::json::read(R"({"first_name":"Homer"})").value(); + + EXPECT_EQ(Person{}.last_name.value(), "Simpson"); + EXPECT_EQ(homer.first_name, "Homer"); + EXPECT_EQ(homer.last_name.value(), "Simpson"); + EXPECT_EQ(homer.town.value(), ""); + + write_and_read( + Person{"Waylon", "Smith", "Springfield"}, + R"({"first_name":"Waylon","last_name":"Smith","town":"Springfield"})"); +} +} // namespace test_default_val diff --git a/tests/json/test_default_val_no_field_names.cpp b/tests/json/test_default_val_no_field_names.cpp new file mode 100644 index 00000000..1aac232b --- /dev/null +++ b/tests/json/test_default_val_no_field_names.cpp @@ -0,0 +1,35 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_default_val_no_field_names { + +struct Person { + std::string first_name; + rfl::DefaultVal last_name = "Simpson"; + rfl::DefaultVal town; +}; + +TEST(json, test_default_val_no_field_names) { + static_assert(rfl::internal::has_default_val_v, + "Should have default val"); + + const auto should_fail = rfl::json::read(R"([])"); + + EXPECT_EQ(should_fail && true, false) + << "Should have failed due to missing required field"; + + const auto homer = + rfl::json::read(R"(["Homer"])").value(); + + EXPECT_EQ(Person{}.last_name.value(), "Simpson"); + EXPECT_EQ(homer.first_name, "Homer"); + EXPECT_EQ(homer.last_name.value(), "Simpson"); + EXPECT_EQ(homer.town.value(), ""); + + write_and_read(Person{"Waylon", "Smith", "Springfield"}, + R"(["Waylon","Smith","Springfield"])"); +} +} // namespace test_default_val_no_field_names