diff --git a/docs/content/variants_and_tagged_unions.md b/docs/content/variants_and_tagged_unions.md index 3b39522e7..d86100e1c 100644 --- a/docs/content/variants_and_tagged_unions.md +++ b/docs/content/variants_and_tagged_unions.md @@ -252,6 +252,26 @@ The resulting JSON looks like this: Because the tag is external, this is called *externally tagged*. It is the standard in Rust's [serde-json](https://serde.rs/enum-representations.html). +## `std::monostate` + +`std::monostate` is serialized as `null`. A `std::variant` or `rfl::Variant` whose +first alternative is `std::monostate` behaves like `std::optional`: as a field, it is +omitted when it holds `std::monostate` and a missing field is read as `std::monostate`. + +```cpp +struct Order { + std::variant price; +}; + +rfl::json::write(Order{.price = std::monostate{}}); // {} +rfl::json::write(Order{.price = 2}); // {"price":2} +``` + +Both `{}` and `{"price":null}` are read as `std::monostate`. + +As with `std::optional`, passing `rfl::NoOptionals` writes the field as `null` +(`{"price":null}`) and makes it required on read. + ## The visitor pattern diff --git a/include/rfl/parsing/ParserMonostate.hpp b/include/rfl/parsing/ParserMonostate.hpp new file mode 100644 index 000000000..1c07de129 --- /dev/null +++ b/include/rfl/parsing/ParserMonostate.hpp @@ -0,0 +1,58 @@ +#ifndef RFL_PARSING_PARSER_MONOSTATE_HPP_ +#define RFL_PARSING_PARSER_MONOSTATE_HPP_ + +#include +#include +#include + +#include "../Result.hpp" +#include "Parent.hpp" +#include "Parser_base.hpp" +#include "is_monostate.hpp" +#include "schema/Type.hpp" + +namespace rfl::parsing { + +template +struct ParserMonostate { + using InputVarType = typename R::InputVarType; + + using ParentType = Parent; + + /** + * @brief Reads a std::monostate from the input, which must be null. + * @param _r The reader to use. + * @param _var The input variable to read. + * @return A Result containing the std::monostate or an error. + */ + static Result read(const R& _r, + const InputVarType& _var) noexcept { + if (!_r.is_empty(_var)) { + return error("Expected null for std::monostate."); + } + return std::monostate{}; + } + + /** + * @brief Writes a std::monostate to the output as null. + * @tparam P The type of the parent. + * @param _w The writer to use. + * @param _parent The parent object. + */ + template + static void write(const W& _w, const std::monostate&, const P& _parent) { + ParentType::add_null(_w, _parent); + } + + /** + * @brief Generates the schema for std::monostate. + * @return The null schema type. + */ + static schema::Type to_schema(std::map*) { + return schema::Type{schema::Type::Null{}}; + } +}; + +} // namespace rfl::parsing + +#endif diff --git a/include/rfl/parsing/Parser_default.hpp b/include/rfl/parsing/Parser_default.hpp index 3a92a86cf..7d1986a8d 100644 --- a/include/rfl/parsing/Parser_default.hpp +++ b/include/rfl/parsing/Parser_default.hpp @@ -38,6 +38,7 @@ #include "ParserEnum.hpp" #include "ParserExpected.hpp" #include "ParserFilepath.hpp" +#include "ParserMonostate.hpp" #include "ParserOptional.hpp" #include "ParserPair.hpp" #include "ParserPositional.hpp" @@ -152,6 +153,9 @@ struct Parser { } else if constexpr (is_filepath_v) { return ParserFilepath::read(_r, _var); + } else if constexpr (is_monostate_v) { + return ParserMonostate::read(_r, _var); + } else if constexpr (is_skip_v) { using U = std::remove_cvref_t; return ParserSkip) { ParserFilepath::write(_w, _var, _parent); + } else if constexpr (is_monostate_v) { + ParserMonostate::write(_w, _var, _parent); + } else if constexpr (is_skip_v) { using U = std::remove_cvref_t; ParserSkip) { return ParserFilepath::to_schema(_definitions); + } else if constexpr (is_monostate_v) { + return ParserMonostate::to_schema(_definitions); + } else if constexpr (is_skip_v) { return ParserSkip) { return is_empty(_var.value()); + } else if constexpr (is_monostate_variant_v) { + return _var.index() == 0; } else if constexpr (is_map_like_v || is_vector_like_v) { return _var.begin() == _var.end(); } else { diff --git a/include/rfl/parsing/is_monostate.hpp b/include/rfl/parsing/is_monostate.hpp new file mode 100644 index 000000000..cb6ce7b72 --- /dev/null +++ b/include/rfl/parsing/is_monostate.hpp @@ -0,0 +1,20 @@ +#ifndef RFL_PARSING_IS_MONOSTATE_HPP_ +#define RFL_PARSING_IS_MONOSTATE_HPP_ + +#include +#include + +namespace rfl::parsing { + +template +struct is_monostate : std::false_type {}; + +template <> +struct is_monostate : std::true_type {}; + +template +constexpr bool is_monostate_v = is_monostate>::value; + +} // namespace rfl::parsing + +#endif diff --git a/include/rfl/parsing/is_monostate_variant.hpp b/include/rfl/parsing/is_monostate_variant.hpp new file mode 100644 index 000000000..ffe397beb --- /dev/null +++ b/include/rfl/parsing/is_monostate_variant.hpp @@ -0,0 +1,29 @@ +#ifndef RFL_PARSING_IS_MONOSTATE_VARIANT_HPP_ +#define RFL_PARSING_IS_MONOSTATE_VARIANT_HPP_ + +#include +#include + +#include "../Variant.hpp" +#include "is_monostate.hpp" + +namespace rfl::parsing { + +template +struct is_monostate_variant : std::false_type {}; + +template +struct is_monostate_variant> + : std::bool_constant> {}; + +template +struct is_monostate_variant> + : std::bool_constant> {}; + +template +constexpr bool is_monostate_variant_v = + is_monostate_variant>::value; + +} // namespace rfl::parsing + +#endif diff --git a/include/rfl/parsing/is_required.hpp b/include/rfl/parsing/is_required.hpp index 30fe10f6a..e59c7f932 100644 --- a/include/rfl/parsing/is_required.hpp +++ b/include/rfl/parsing/is_required.hpp @@ -15,6 +15,7 @@ #include "../internal/is_rename.hpp" #include "../internal/is_short.hpp" #include "is_map_like.hpp" +#include "is_monostate_variant.hpp" #include "is_vector_like.hpp" namespace rfl { @@ -64,7 +65,7 @@ consteval bool is_required() { } else if constexpr (internal::is_short_v) { return is_required(); } else { - return !(is_never_required_v || + return !(is_never_required_v || is_monostate_variant_v || (_ignore_empty_containers && (is_vector_like_v || is_map_like_v))); } diff --git a/include/rfl/parsing/schema/Type.hpp b/include/rfl/parsing/schema/Type.hpp index c81679ce6..6159cc176 100644 --- a/include/rfl/parsing/schema/Type.hpp +++ b/include/rfl/parsing/schema/Type.hpp @@ -19,6 +19,8 @@ namespace rfl::parsing::schema { struct RFL_API Type { struct Boolean {}; + struct Null {}; + struct Bytestring {}; struct Vectorstring {}; @@ -115,9 +117,9 @@ struct RFL_API Type { }; using VariantType = - rfl::Variant; diff --git a/src/rfl/avro/to_schema.cpp b/src/rfl/avro/to_schema.cpp index 5b8e25a3b..d1dedc867 100644 --- a/src/rfl/avro/to_schema.cpp +++ b/src/rfl/avro/to_schema.cpp @@ -50,6 +50,9 @@ schema::Type type_to_avro_schema_type( if constexpr (std::is_same()) { return schema::Type{.value = schema::Type::Boolean{}}; + } else if constexpr (std::is_same()) { + return schema::Type{.value = schema::Type::Null{}}; + } else if constexpr (std::is_same() || std::is_same()) { return schema::Type{.value = schema::Type::Bytes{}}; diff --git a/src/rfl/capnproto/to_schema.cpp b/src/rfl/capnproto/to_schema.cpp index 6d166e286..cf2fd84ce 100644 --- a/src/rfl/capnproto/to_schema.cpp +++ b/src/rfl/capnproto/to_schema.cpp @@ -188,6 +188,9 @@ schema::Type type_to_capnproto_schema_type( if constexpr (std::is_same()) { return schema::Type{.value = schema::Type::Bool{}}; + } else if constexpr (std::is_same()) { + return schema::Type{.value = schema::Type::Void{}}; + } else if constexpr (std::is_same() || std::is_same()) { return schema::Type{.value = schema::Type::Data{}}; diff --git a/src/rfl/json/to_schema.cpp b/src/rfl/json/to_schema.cpp index 3c5426e8d..06bb96442 100644 --- a/src/rfl/json/to_schema.cpp +++ b/src/rfl/json/to_schema.cpp @@ -47,6 +47,11 @@ bool is_optional(const parsing::schema::Type& _t) { } else if constexpr (std::is_same_v) { return is_optional(*_v.type_); + } else if constexpr (std::is_same_v) { + return !_v.types_.empty() && + rfl::holds_alternative( + _v.types_.front().variant_); + } else { return std::is_same_v || std::is_same_v; @@ -199,6 +204,9 @@ schema::Type type_to_json_schema_type(const parsing::schema::Type& _type, if constexpr (std::is_same()) { return schema::Type{.value = schema::Type::Boolean{}}; + } else if constexpr (std::is_same()) { + return schema::Type{.value = schema::Type::Null{}}; + } else if constexpr (std::is_same() || std::is_same() || std::is_same() || diff --git a/tests/avro/test_monostate.cpp b/tests/avro/test_monostate.cpp new file mode 100644 index 000000000..d50af663d --- /dev/null +++ b/tests/avro/test_monostate.cpp @@ -0,0 +1,29 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_monostate { + +struct Order { + std::variant price; +}; + +struct RflOrder { + rfl::Variant price; +}; + +TEST(avro, test_monostate) { + write_and_read(Order{.price = std::monostate{}}); + write_and_read(Order{.price = "1.25"}); + write_and_read(Order{.price = 2}); +} + +TEST(avro, test_monostate_rfl_variant) { + write_and_read(RflOrder{.price = std::monostate{}}); + write_and_read(RflOrder{.price = std::string("1.25")}); + write_and_read(RflOrder{.price = 2}); +} + +} // namespace test_monostate diff --git a/tests/cbor/test_monostate.cpp b/tests/cbor/test_monostate.cpp new file mode 100644 index 000000000..a6f11daa6 --- /dev/null +++ b/tests/cbor/test_monostate.cpp @@ -0,0 +1,39 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_monostate { + +struct Order { + std::variant price; +}; + +struct RflOrder { + rfl::Variant price; +}; + +struct Empty {}; + +TEST(cbor, test_monostate) { + write_and_read(Order{.price = std::monostate{}}); + write_and_read(Order{.price = "1.25"}); + write_and_read(Order{.price = 2}); + + const auto res = rfl::cbor::read(rfl::cbor::write(Empty{})); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_TRUE(std::holds_alternative(res->price)); +} + +TEST(cbor, test_monostate_rfl_variant) { + write_and_read(RflOrder{.price = std::monostate{}}); + write_and_read(RflOrder{.price = std::string("1.25")}); + write_and_read(RflOrder{.price = 2}); + + const auto res = rfl::cbor::read(rfl::cbor::write(Empty{})); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_TRUE(rfl::holds_alternative(res->price)); +} + +} // namespace test_monostate diff --git a/tests/json/test_monostate.cpp b/tests/json/test_monostate.cpp new file mode 100644 index 000000000..28e6aff9c --- /dev/null +++ b/tests/json/test_monostate.cpp @@ -0,0 +1,79 @@ +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_monostate { + +struct Order { + std::variant price; +}; + +struct RflOrder { + rfl::Variant price; +}; + +TEST(json, test_monostate) { + write_and_read(Order{.price = std::monostate{}}, R"({})"); + write_and_read(Order{.price = "1.25"}, R"({"price":"1.25"})"); + write_and_read(Order{.price = 2}, R"({"price":2})"); + + for (const auto json : {R"({})", R"({"price":null})"}) { + const auto res = rfl::json::read(json); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_TRUE(std::holds_alternative(res->price)); + } +} + +TEST(json, test_monostate_rfl_variant) { + write_and_read(RflOrder{.price = std::monostate{}}, R"({})"); + write_and_read(RflOrder{.price = std::string("1.25")}, R"({"price":"1.25"})"); + write_and_read(RflOrder{.price = 2}, R"({"price":2})"); + + for (const auto json : {R"({})", R"({"price":null})"}) { + const auto res = rfl::json::read(json); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_TRUE(rfl::holds_alternative(res->price)); + } +} + +TEST(json, test_monostate_no_optionals) { + write_and_read(Order{.price = std::monostate{}}, + R"({"price":null})"); + write_and_read(Order{.price = 2}, R"({"price":2})"); + + EXPECT_FALSE((rfl::json::read(R"({})"))); +} + +TEST(json, test_monostate_no_optionals_rfl_variant) { + write_and_read(RflOrder{.price = std::monostate{}}, + R"({"price":null})"); + write_and_read(RflOrder{.price = 2}, R"({"price":2})"); + + EXPECT_FALSE((rfl::json::read(R"({})"))); +} + +TEST(json, test_monostate_top_level) { + write_and_read(std::variant{}, "null"); + write_and_read(rfl::Variant{}, "null"); +} + +TEST(json, test_monostate_rejects_non_null) { + EXPECT_FALSE(rfl::json::read("1")); +} + +TEST(json, test_monostate_schema) { + EXPECT_EQ( + rfl::json::to_schema(), + R"({"$schema":"https://json-schema.org/draft/2020-12/schema","type":"null","$defs":{}})"); +} + +TEST(json, test_monostate_field_not_required_in_schema) { + EXPECT_EQ( + rfl::json::to_schema(), + R"({"$schema":"https://json-schema.org/draft/2020-12/schema","$ref":"#/$defs/test_monostate__Order","$defs":{"test_monostate__Order":{"type":"object","properties":{"price":{"anyOf":[{"type":"null"},{"type":"string"},{"type":"integer"}]}},"required":[]}}})"); +} + +} // namespace test_monostate