diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8242b2f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "modules/nlohmann_json"] + path = modules/nlohmann_json + url = https://github.com/nlohmann/json.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 510f932..4846caf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -235,6 +235,7 @@ file( ${PROJECT_SOURCE_DIR}/include/iris/*.hpp ${PROJECT_SOURCE_DIR}/include/iris/*.ipp ) +list(FILTER IRIS_HEADERS EXCLUDE REGEX /include/iris/marshal[./]) if(MSVC) add_custom_command( @@ -262,6 +263,37 @@ source_group(TREE ${PROJECT_SOURCE_DIR}/include/iris PREFIX iris FILES ${IRIS_HE target_include_directories(iris INTERFACE ${PROJECT_SOURCE_DIR}/include) +# ----------------------------------------------------------------- +# Additional sublibraries + +file( + GLOB_RECURSE IRIS_MARSHAL_HEADERS + ${PROJECT_SOURCE_DIR}/include/iris/marshal/*.hpp + ${PROJECT_SOURCE_DIR}/include/iris/marshal*.hpp +) + +add_library(iris_marshal INTERFACE) +add_library(Iris::Marshal ALIAS iris_marshal) + +target_sources(iris_marshal PRIVATE FILE_SET HEADERS TYPE HEADERS FILES ${IRIS_MARSHAL_HEADERS}) +source_group(TREE ${PROJECT_SOURCE_DIR}/include/iris PREFIX iris FILES ${IRIS_MARSHAL_HEADERS}) + +set_target_properties( + iris_marshal PROPERTIES + CXX_EXTENSIONS OFF + EXCLUDE_FROM_ALL true + # EXCLUDE_FROM_DEFAULT_BUILD true +) + +if(NOT NLOHMANN_JSON_FOUND) + set(JSON_BuildTests OFF CACHE INTERNAL "") + set(JSON_Install OFF CACHE INTERNAL "") + set(JSON_ImplicitConversions OFF) + add_subdirectory(modules/nlohmann_json EXCLUDE_FROM_ALL) +endif() + +target_link_libraries(iris_marshal INTERFACE nlohmann_json::nlohmann_json Iris::Iris) + # ----------------------------------------------------------------- # Test diff --git a/include/iris/marshal/adapt.hpp b/include/iris/marshal/adapt.hpp new file mode 100644 index 0000000..2515852 --- /dev/null +++ b/include/iris/marshal/adapt.hpp @@ -0,0 +1,282 @@ +#ifndef IRIS_ZZ_MARSHAL_ADAPT_HPP +#define IRIS_ZZ_MARSHAL_ADAPT_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include // IWYU pragma: keep +#include // IWYU pragma: keep + +#include // IWYU pragma: export + +#include +#include +#include +#include +#include + +#include + +namespace iris::marshal { + +namespace detail { + +template +struct is_value_semantics_preferred : std::false_type +{}; + +template + requires + std::is_arithmetic_v || + std::is_reference_v || + std::is_pointer_v || + std::is_enum_v +struct is_value_semantics_preferred : std::true_type +{}; + +template +using getter_return_t = std::conditional_t< + is_value_semantics_preferred::value, + T, + T const& +>; + +template +using setter_param_t = std::conditional_t< + is_value_semantics_preferred::value, + T, + T const& +>; + +} // detail + + +#define IRIS_ZZ_MARSHAL_FIELD_EXPAND_CLASS_QUALIFIED_SCOPE(class_name) class_name:: +#define IRIS_ZZ_MARSHAL_FIELD_EXPAND_DECLARE_ONLY(...) ; +#define IRIS_ZZ_MARSHAL_FIELD_EXPAND_BODY(...) __VA_ARGS__ + +#define IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(field_name) field_name ## _ + +#define IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, ...) \ + private: \ + IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type) IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(field_name){__VA_ARGS__}; + +#define IRIS_ZZ_MARSHAL_FIELD_GETTER(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) \ + access_mod_macro(public:) \ + [[nodiscard]] iris::marshal::detail::getter_return_t \ + qualified_scope_macro(class_name) IRIS_PP_CAT(get_, field_name) () const noexcept \ + body_macro({ \ + return variable_name_macro(field_name); \ + }) + +#define IRIS_ZZ_MARSHAL_FIELD_GETTER_BOOL(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) \ + access_mod_macro(public:) \ + [[nodiscard]] bool qualified_scope_macro(class_name) IRIS_PP_CAT(is_, field_name) () const noexcept \ + body_macro({ \ + return variable_name_macro(field_name); \ + }) \ + \ + access_mod_macro(private:) \ + [[nodiscard]] bool qualified_scope_macro(class_name) IRIS_PP_CAT(get_, field_name) () const noexcept \ + body_macro({ \ + return variable_name_macro(field_name); \ + }) + +#define IRIS_ZZ_MARSHAL_FIELD_SETTER(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) \ + access_mod_macro(public:) \ + void qualified_scope_macro(class_name) IRIS_PP_CAT(set_, field_name) (iris::marshal::detail::setter_param_t new_value) \ + noexcept(std::is_nothrow_copy_assignable_v) \ + body_macro({ \ + variable_name_macro(field_name) = new_value; \ + }) + + +// Default value for bool field is mandatory because it is error prone if omitted +#define IRIS_ZZ_MARSHAL_FIELD_BOOL_GET(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) \ + IRIS_ZZ_MARSHAL_FIELD_GETTER_BOOL(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) + +// Default value for bool field is mandatory because it is error prone if omitted +#define IRIS_ZZ_MARSHAL_FIELD_BOOL_GET_SET(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) \ + IRIS_ZZ_MARSHAL_FIELD_GETTER_BOOL(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) \ + IRIS_ZZ_MARSHAL_FIELD_SETTER(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) + + +#define IRIS_ZZ_MARSHAL_FIELD_TYPE_IS_bool (bool) + +#define IRIS_ZZ_MARSHAL_FIELD_GET_I(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) \ + IRIS_ZZ_MARSHAL_FIELD_GETTER(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) + +#define IRIS_MARSHAL_FIELD_GET(maybe_paren_type, field_name, ...) \ + IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ + IRIS_PP_IF( \ + IRIS_PP_IS_PAREN( IRIS_PP_CAT_ONLY_TWO(IRIS_ZZ_MARSHAL_FIELD_TYPE_IS_, IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type)) ), \ + IRIS_ZZ_MARSHAL_FIELD_BOOL_GET, \ + IRIS_ZZ_MARSHAL_FIELD_GET_I \ + ) (~, maybe_paren_type, field_name, IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME, IRIS_ZZ_MARSHAL_FIELD_EXPAND_BODY, IRIS_PP_IDENTITY, IRIS_PP_EMPTY) + +#define IRIS_ZZ_MARSHAL_FIELD_GET_SET_I(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) \ + IRIS_ZZ_MARSHAL_FIELD_GETTER(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) \ + IRIS_ZZ_MARSHAL_FIELD_SETTER(class_name, maybe_paren_type, field_name, variable_name_macro, body_macro, access_mod_macro, qualified_scope_macro) + +#define IRIS_MARSHAL_FIELD_GET_SET(maybe_paren_type, field_name, ...) \ + IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ + IRIS_PP_IF( \ + IRIS_PP_IS_PAREN( IRIS_PP_CAT_ONLY_TWO(IRIS_ZZ_MARSHAL_FIELD_TYPE_IS_, IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type)) ), \ + IRIS_ZZ_MARSHAL_FIELD_BOOL_GET_SET, \ + IRIS_ZZ_MARSHAL_FIELD_GET_SET_I \ + ) (~, maybe_paren_type, field_name, IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME, IRIS_ZZ_MARSHAL_FIELD_EXPAND_BODY, IRIS_PP_IDENTITY, IRIS_PP_EMPTY) + +#define IRIS_MARSHAL_FIELD(maybe_paren_type, field_name, ...) \ + IRIS_MARSHAL_FIELD_GET_SET(maybe_paren_type, field_name, __VA_ARGS__) + + +#define IRIS_ZZ_MARSHAL_CLASS_COMMON(class_name) \ + public: \ + template \ + friend struct ::iris::marshal::adapted_class_traits; + +#define IRIS_MARSHAL_CLASS(class_name) \ + IRIS_ZZ_MARSHAL_CLASS_COMMON(class_name) \ + public: \ + [[nodiscard]] constexpr bool operator==(class_name const&) const noexcept = default; + +#define IRIS_MARSHAL_PIMPL_CLASS(class_name) \ + IRIS_ZZ_MARSHAL_CLASS_COMMON(class_name) \ + public: \ + class_name(); \ + class_name(class_name const&) = delete; \ + class_name(class_name&&) noexcept; \ + ~class_name(); \ + \ + class_name& operator=(class_name const&) = delete; \ + class_name& operator=(class_name&&) noexcept; \ + \ + private: \ + struct Impl; \ + std::unique_ptr impl_; \ + \ + public: + +// ------------------------------------------------------------ +// pimpl declare + +#define IRIS_ZZ_MARSHAL_PIMPL_FIELD_DECLARE_FIELD_I(maybe_paren_type, field_name, ...) \ + IRIS_PP_IF( \ + IRIS_PP_IS_PAREN( IRIS_PP_CAT_ONLY_TWO(IRIS_ZZ_MARSHAL_FIELD_TYPE_IS_, IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type)) ), \ + IRIS_ZZ_MARSHAL_FIELD_BOOL_GET_SET, \ + IRIS_ZZ_MARSHAL_FIELD_GET_SET_I \ + ) (~, maybe_paren_type, field_name, IRIS_PP_EMPTY, IRIS_ZZ_MARSHAL_FIELD_EXPAND_DECLARE_ONLY, IRIS_PP_IDENTITY, IRIS_PP_EMPTY) + +#define IRIS_ZZ_MARSHAL_PIMPL_FIELD_DECLARE_I(field_def, macro_data) \ + IRIS_ZZ_MARSHAL_PIMPL_FIELD_DECLARE_FIELD_I field_def + +#define IRIS_MARSHAL_PIMPL_FIELD_DECLARE(field_defs) \ + IRIS_PP_SEQ_FOR_EACH(field_defs, IRIS_ZZ_MARSHAL_PIMPL_FIELD_DECLARE_I, ~) + +// ------------------------------------------------------------ +// pimpl define + +#define IRIS_ZZ_MARSHAL_PIMPL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, ...) \ + public: \ + IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type) field_name{__VA_ARGS__}; + +#define IRIS_ZZ_MARSHAL_PIMPL_FIELD_DEFS_I(field_def, macro_data) \ + IRIS_ZZ_MARSHAL_PIMPL_FIELD_DATA_MEMBER field_def + +#define IRIS_MARSHAL_PIMPL_FIELD_DEFS(field_defs) \ + IRIS_PP_SEQ_FOR_EACH(field_defs, IRIS_ZZ_MARSHAL_PIMPL_FIELD_DEFS_I, ~) + + +#define IRIS_ZZ_MARSHAL_PIMPL_FIELD_VARIABLE(field_name) impl_->field_name + +#define IRIS_ZZ_MARSHAL_PIMPL_FIELD_ACCESS_DEFINE_FIELD_I(class_name, maybe_paren_type, field_name, ...) \ + IRIS_PP_IF( \ + IRIS_PP_IS_PAREN( IRIS_PP_CAT_ONLY_TWO(IRIS_ZZ_MARSHAL_FIELD_TYPE_IS_, IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type)) ), \ + IRIS_ZZ_MARSHAL_FIELD_BOOL_GET_SET, \ + IRIS_ZZ_MARSHAL_FIELD_GET_SET_I \ + ) (class_name, maybe_paren_type, field_name, IRIS_ZZ_MARSHAL_PIMPL_FIELD_VARIABLE, IRIS_ZZ_MARSHAL_FIELD_EXPAND_BODY, IRIS_PP_EMPTY, IRIS_ZZ_MARSHAL_FIELD_EXPAND_CLASS_QUALIFIED_SCOPE) + +#define IRIS_ZZ_CONCAT_TUPLE_1_3_II(class_name, ...) (class_name, __VA_ARGS__) + +#define IRIS_ZZ_CONCAT_TUPLE_1_3_I(class_name, ...) \ + IRIS_ZZ_CONCAT_TUPLE_1_3_II(class_name, __VA_ARGS__) + +#define IRIS_ZZ_CONCAT_TUPLE_1_3(class_name, field_def) \ + IRIS_ZZ_CONCAT_TUPLE_1_3_I(class_name, IRIS_PP_EXPAND field_def) + +#define IRIS_ZZ_MARSHAL_PIMPL_INVOKE(...) __VA_ARGS__ + +#define IRIS_ZZ_MARSHAL_PIMPL_FIELD_ACCESS_DEFINE_I(field_def, class_name) \ + IRIS_ZZ_MARSHAL_PIMPL_INVOKE( \ + IRIS_ZZ_MARSHAL_PIMPL_FIELD_ACCESS_DEFINE_FIELD_I \ + IRIS_ZZ_CONCAT_TUPLE_1_3(class_name, field_def) \ + ) + +#define IRIS_MARSHAL_PIMPL_FIELD_ACCESS_DEFINE(class_name, field_defs) \ + IRIS_PP_SEQ_FOR_EACH(field_defs, IRIS_ZZ_MARSHAL_PIMPL_FIELD_ACCESS_DEFINE_I, class_name) + + +#define IRIS_MARSHAL_PIMPL_CLASS_DEFINE(class_name) \ + class_name::class_name() : impl_(std::make_unique()) {} \ + class_name::class_name(class_name&&) noexcept = default; \ + class_name::~class_name() = default; \ + class_name& class_name::operator=(class_name&&) noexcept = default; + +// ------------------------------------------------------------ + +#define IRIS_ZZ_MARSHAL_SELECT_FIELD_TYPE(class_name, field_name) typename decltype([]() consteval { \ + if constexpr (requires { &ClassT::field_name; }) { \ + return std::type_identity{}; \ + } else { \ + return std::type_identity()))>>{}; \ + } \ + }.template operator()())::type + +// Select `&class_name::field_name` if it exists; otherwise `&class_name::(get|set)_field_name` +#define IRIS_ZZ_MARSHAL_SELECT_MEMBER_ACCESS(class_name, field_name, access) []() consteval { \ + if constexpr (requires { &ClassT::field_name; }) { \ + return &ClassT::field_name; \ + } else { \ + return &ClassT::IRIS_PP_CAT(access ## _, field_name); \ + } \ + }.template operator()() + +#define IRIS_ZZ_MARSHAL_ADAPT_FIELD(field_name, class_name) \ + ::iris::marshal::detail::field_definition< \ + IRIS_ZZ_MARSHAL_SELECT_FIELD_TYPE(class_name, field_name), \ + IRIS_ZZ_MARSHAL_SELECT_MEMBER_ACCESS(class_name, field_name, get), \ + IRIS_ZZ_MARSHAL_SELECT_MEMBER_ACCESS(class_name, field_name, set) \ + >{IRIS_PP_STRINGIZE(field_name)}, + +#define IRIS_ZZ_MARSHAL_ADAPT_I(class_name, field_defs) \ + template<> \ + struct iris::marshal::adapted_class_traits \ + { \ + inline static constexpr auto fields = ::iris::alloy::tuple{ \ + IRIS_PP_SEQ_FOR_EACH( \ + field_defs, \ + IRIS_ZZ_MARSHAL_ADAPT_FIELD, \ + class_name \ + ) \ + }; \ + }; + +#define IRIS_MARSHAL_ADAPT(class_name, ...) \ + IRIS_ZZ_MARSHAL_ADAPT_I( \ + class_name, \ + IRIS_PP_TUPLE_TO_SEQ((__VA_ARGS__)) \ + ) + +#define IRIS_ZZ_MARSHAL_ADAPT_DEFS_EXTRACT_FIELD_NAME(field_def, macro_data) \ + (IRIS_PP_TUPLE_ELEM(1, field_def)) + +#define IRIS_MARSHAL_ADAPT_DEFS(class_name, field_defs) \ + IRIS_ZZ_MARSHAL_ADAPT_I( \ + class_name, \ + IRIS_PP_SEQ_FOR_EACH(field_defs, IRIS_ZZ_MARSHAL_ADAPT_DEFS_EXTRACT_FIELD_NAME, ~) \ + ) + +} // iris::marshal + +#endif diff --git a/include/iris/marshal/detail/field.hpp b/include/iris/marshal/detail/field.hpp new file mode 100644 index 0000000..35b9234 --- /dev/null +++ b/include/iris/marshal/detail/field.hpp @@ -0,0 +1,40 @@ +#ifndef IRIS_ZZ_MARSHAL_DETAIL_FIELD_HPP +#define IRIS_ZZ_MARSHAL_DETAIL_FIELD_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include +#include +#include +#include + +namespace iris::marshal::detail { + +template +struct field_definition +{ + using value_type = FieldT; + std::string_view name; + + template + [[nodiscard]] static constexpr decltype(auto) get(ClassT const& klass) noexcept + { + return std::invoke(GetMem, klass); + } + + template + static constexpr void set(ClassT& klass, T&& value) + { + if constexpr (std::is_member_object_pointer_v) { + (klass.*SetMem) = std::forward(value); + } else { + std::invoke(SetMem, klass, std::forward(value)); + } + } +}; + +} // iris::marshal::detail + +#endif diff --git a/include/iris/marshal/serialize.hpp b/include/iris/marshal/serialize.hpp new file mode 100644 index 0000000..ef026bc --- /dev/null +++ b/include/iris/marshal/serialize.hpp @@ -0,0 +1,369 @@ +#ifndef IRIS_ZZ_MARSHAL_SERIALIZE_HPP +#define IRIS_ZZ_MARSHAL_SERIALIZE_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include +#include + +#include + +#include +#include + +namespace iris::marshal { + +struct load_error : std::runtime_error +{ + using std::runtime_error::runtime_error; +}; + + +template +concept writer = + requires { + typename WriterT::format; + } && + requires(WriterT& wr, std::string_view map_key) { + wr.null(); + wr.begin_array(); + wr.end_array(); + wr.begin_object(); + wr.map_key(map_key); + wr.end_object(); + }; + +template +struct basic_save_fn +{ + using format = WriterT::format; + + template T> + requires requires { + typename WriterT::document_type; + } && std::constructible_from + static constexpr void operator()(WriterT::document_type& res, T const& value) + { + WriterT wr{res}; + basic_save_fn{}(wr, value); + } + + template T> + requires requires(WriterT& wr, T const& value) { wr.scalar(value); } + static constexpr void operator()(WriterT& wr, T const& value) + { + wr.scalar(value); + } + + template OptionalT> + static constexpr void operator()(WriterT& wr, OptionalT const& opt) + { + if (opt) { + basic_save_fn{}(wr, *opt); + } else { + wr.null(); + } + } + + template ProxyT> + static constexpr void operator()(WriterT& wr, ProxyT const& proxy) + { + basic_save_fn{}(wr, adapted_proxy_traits::to_native_type(proxy)); + } + + template R> + static constexpr void operator()(WriterT& wr, R const& arr) + { + wr.begin_array(); + for (auto const& elem : arr) { + basic_save_fn{}(wr, elem); + } + wr.end_array(); + } + + template MapT> + static constexpr void operator()(WriterT& wr, MapT const& map) + { + wr.begin_object(); + for (auto const& [k, v] : map) { + if constexpr (adapted_proxy, format>) { + wr.map_key(adapted_proxy_traits, format>::to_native_type(k)); + } else { + wr.map_key(k); + } + basic_save_fn{}(wr, v); + } + wr.end_object(); + } + + template TupleT> + static constexpr void operator()(WriterT& wr, TupleT const& tup) + { + wr.begin_array(); + alloy::for_each(tup, [&](auto const& elem) { + basic_save_fn{}(wr, elem); + }); + wr.end_array(); + } + + template ClassT> + static constexpr void operator()(WriterT& wr, ClassT const& klass) + { + wr.begin_object(); + constexpr auto const& fields = adapted_class_traits::fields; + alloy::for_each(fields, [&](detail::field_definition const& def) { + if constexpr (adapted_proxy) { + wr.map_key(adapted_proxy_traits::to_native_type(def.name)); + } else { + wr.map_key(def.name); + } + basic_save_fn{}(wr, def.get(klass)); + }); + wr.end_object(); + } +}; + + +namespace detail { + +struct noop_element_visitor +{ + template + void operator()(R&) const + {} +}; +struct noop_member_visitor +{ + template + void operator()(std::string_view, R&) const + {} +}; + +} // detail + +template +concept reader = + requires { + typename ReaderT::format; + } && + requires(ReaderT& rd) { + { rd.is_null() } -> std::convertible_to; + { rd.array(detail::noop_element_visitor{}) } -> std::convertible_to; + { rd.object(detail::noop_member_visitor{}) } -> std::convertible_to; + }; + +template +struct basic_load_fn +{ + using format = ReaderT::format; + + template + requires + requires { typename ReaderT::document_type; } && + std::constructible_from && + deserializable + static constexpr void operator()(ReaderT::document_type const& doc, T& value) + { + ReaderT rd{doc}; + basic_load_fn{}(rd, value); + } + + template T> + requires requires(ReaderT& rd, T& value) { + { rd.scalar(value) } -> std::convertible_to; + } + static constexpr void operator()(ReaderT& rd, T& value) + { + if (!rd.scalar(value)) { + throw load_error{"scalar: kind mismatch"}; + } + } + + template OptionalT> + static constexpr void operator()(ReaderT& rd, OptionalT& opt) + { + using value_type = adapted_optional_traits::value_type; + + if (rd.is_null()) { + opt = OptionalT{}; + return; + } + + value_type tmp{}; + basic_load_fn{}(rd, tmp); + opt = std::move(tmp); + } + + template ProxyT> + static constexpr void operator()(ReaderT& rd, ProxyT& proxy) + { + using native_type = adapted_proxy_traits::native_type; + + native_type tmp{}; + basic_load_fn{}(rd, tmp); + proxy = adapted_proxy_traits::from_native_type(std::move(tmp)); + } + + template R> + requires ranges::growable_array_writable + static constexpr void operator()(ReaderT& rd, R& arr) + { + R tmp{}; + + bool const ok = rd.array([&](ReaderT& elem_rd) { + std::ranges::range_value_t elem{}; + basic_load_fn{}(elem_rd, elem); + + if constexpr (ranges::back_emplaceable) { + tmp.emplace_back(std::move(elem)); + } else { + tmp.emplace(std::move(elem)); + } + }); + + if (!ok) { + throw load_error{"array: kind mismatch"}; + } + + arr = std::move(tmp); + } + + template R> + requires ranges::fixed_array_writable + static constexpr void operator()(ReaderT& rd, R& arr) + { + auto it = std::ranges::begin(arr); + auto const last = std::ranges::end(arr); + + bool const ok = rd.array([&](ReaderT& elem_rd) { + if (it == last) { + throw load_error{"array: too many elements"}; + } + basic_load_fn{}(elem_rd, *it); + ++it; + }); + + if (!ok) { + throw load_error{"array: kind mismatch"}; + } + if (it != last) { + throw load_error{"array: too few elements"}; + } + } + + template MapT> + static constexpr void operator()(ReaderT& rd, MapT& map) + { + using key_type = MapT::key_type; + using mapped_type = MapT::mapped_type; + + MapT tmp{}; + + bool const ok = rd.object([&](std::string_view map_key, ReaderT& member_rd) { + key_type k = [&] { + if constexpr (adapted_proxy) { + using native_type = adapted_proxy_traits::native_type; + return adapted_proxy_traits::from_native_type(native_type{map_key}); + } else { + return key_type{map_key}; + } + }(); + + mapped_type v{}; + basic_load_fn{}(member_rd, v); + + if constexpr (ranges::unique_mapping_container) { + if (!tmp.try_emplace(std::move(k), std::move(v)).second) { + throw load_error{"object: duplicate key"}; + } + } else { + tmp.emplace(std::move(k), std::move(v)); + } + }); + + if (!ok) { + throw load_error{"object: kind mismatch"}; + } + map = std::move(tmp); + } + + template TupleT> + static constexpr void operator()(ReaderT& rd, TupleT& tup) + { + constexpr std::size_t size = alloy::tuple_size_v; + std::size_t i = 0; + + bool const ok = rd.array([&](ReaderT& elem_rd) { + if (i >= size) { + throw load_error{"tuple: too many elements"}; + } + alloy::visit_at(i, [&](auto& elem) { + basic_load_fn{}(elem_rd, elem); + }, tup); + ++i; + }); + + if (!ok) { + throw load_error{"tuple: kind mismatch"}; + } + if (i != size) { + throw load_error{"tuple: too few elements"}; + } + } + + template ClassT> + static constexpr void operator()(ReaderT& rd, ClassT& klass) + { + constexpr auto const& fields = adapted_class_traits::fields; + + std::bitset>> + seen; + + bool const ok = rd.object([&](std::string_view map_key, ReaderT& member_rd) { + std::size_t index = 0; + bool matched = false; + + alloy::for_each(fields, [&](detail::field_definition const& def) { + if (!matched && basic_load_fn::field_name_matches(def.name, map_key)) { + FieldT tmp{}; + basic_load_fn{}(member_rd, tmp); + def.set(klass, std::move(tmp)); + + seen.set(index); + matched = true; + } + ++index; + }); + // unmatched `map_key` is ignored ... + }); + + if (!ok) { + throw load_error{"object: kind mismatch"}; + } + + std::size_t index = 0; + alloy::for_each(fields, [&](auto&& /* def */) { + if (!seen.test(index)) { + throw load_error{"object: missing required field"}; + } + ++index; + }); + } + +private: + template + [[nodiscard]] static constexpr bool field_name_matches(NameT const& name, std::string_view map_key) + { + if constexpr (adapted_proxy) { + return adapted_proxy_traits::to_native_type(name) == map_key; + } else { + return name == map_key; + } + } +}; + +} // iris::marshal + +#endif diff --git a/include/iris/marshal/serialize_json.hpp b/include/iris/marshal/serialize_json.hpp new file mode 100644 index 0000000..0d32a43 --- /dev/null +++ b/include/iris/marshal/serialize_json.hpp @@ -0,0 +1,242 @@ +#ifndef IRIS_ZZ_MARSHAL_SERIALIZE_JSON_HPP +#define IRIS_ZZ_MARSHAL_SERIALIZE_JSON_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include + +#include + +#include + +#include + +#include +#include +#include +#include +#include + +namespace iris::marshal::json { + +struct format : generic_format +{ + template + static constexpr bool map_key = StringLike; + + template + static constexpr bool loadable_key = StringLike; +}; + +} // iris::marshal::json + +namespace iris::marshal { + +template + requires (!std::same_as, char>) +struct adapted_proxy_traits +{ + using native_type = std::string; + + [[nodiscard]] static native_type const& to_native_type(native_type const& t) noexcept + { + return t; + } + + [[nodiscard]] static native_type to_native_type(native_type const&& t) + { + return t; + } + + [[nodiscard]] static native_type to_native_type(StringLike auto const& u) + { + return unicode::transcode(u); + } + + [[nodiscard]] static T from_native_type(native_type const& t) + { + return unicode::transcode()})::value_type>(t); + } +}; + +} // iris::marshal + +namespace iris::marshal::json { + +template concept serializable_proxy = marshal::serializable_proxy; +template concept serializable_class = marshal::serializable_class; +template concept serializable_optional = marshal::serializable_optional; +template concept serializable_scalar = marshal::serializable_scalar; +template concept serializable_map = marshal::serializable_map; +template concept serializable_array = marshal::serializable_array; +template concept serializable_tuple = marshal::serializable_tuple; +template concept serializable = marshal::serializable; + +template concept deserializable_proxy = marshal::deserializable_proxy; +template concept deserializable_class = marshal::deserializable_class; +template concept deserializable_optional = marshal::deserializable_optional; +template concept deserializable_scalar = marshal::deserializable_scalar; +template concept deserializable_map = marshal::deserializable_map; +template concept deserializable_array = marshal::deserializable_array; +template concept deserializable_tuple = marshal::deserializable_tuple; +template concept deserializable = marshal::deserializable; + +class dom_writer +{ +public: + using format = json::format; + using document_type = nlohmann::json; + + explicit dom_writer(document_type& node) + : node_(&node) + {} + + template + requires format::template loadable_scalar + void scalar(T const& value) + { + if constexpr (std::is_enum_v) { + slot() = std::to_underlying(value); + } else { + slot() = value; + } + } + + void null() + { + slot() = nullptr; + } + + void begin_array() + { + stack_.push_back(&(slot() = document_type::array())); + } + void end_array() + { + stack_.pop_back(); + } + + void begin_object() + { + stack_.push_back(&(slot() = document_type::object())); + } + void map_key(std::string_view k) + { + pending_key_.assign(k); + } + void end_object() + { + stack_.pop_back(); + } + +private: + [[nodiscard]] document_type& slot() + { + if (stack_.empty()) return *node_; + auto& parent = *stack_.back(); + return parent.is_object() ? parent[pending_key_] : parent.emplace_back(); + } + + nlohmann::json* node_; + std::vector stack_; + std::string pending_key_; +}; + +[[maybe_unused]] inline constexpr basic_save_fn save{}; + + +class dom_reader +{ +public: + using format = json::format; + using document_type = nlohmann::json; + + explicit dom_reader(document_type const& node) + : node_{&node} + {} + + [[nodiscard]] bool is_null() const + { + return node_->is_null(); + } + + template + requires format::template loadable_scalar + [[nodiscard]] bool scalar(T& value) const + { + if constexpr (std::same_as) { + if (!node_->is_boolean()) return false; + value = node_->get(); + + } else if constexpr (std::is_integral_v) { + if (!node_->is_number_integer() && !node_->is_number_unsigned()) return false; + auto const wide = node_->get(); + if (!std::in_range(wide)) return false; + value = static_cast(wide); + + } else if constexpr (std::is_floating_point_v) { + if (!node_->is_number()) return false; + value = node_->get(); + + } else if constexpr (std::is_enum_v) { + std::underlying_type_t underlying; + if (!scalar(underlying)) return false; + value = static_cast(underlying); + + } else { // string-like + if (!node_->is_string()) return false; + value = std::string_view{node_->get_ref()}; + } + return true; + } + + template + [[nodiscard]] bool array(VisitorT&& vis) const + { + if (!node_->is_array()) return false; + + for (auto const& elem : *node_) { + dom_reader elem_rd{elem}; + vis(elem_rd); + } + return true; + } + + template + [[nodiscard]] bool object(VisitorT&& vis) const + { + if (!node_->is_object()) return false; + + for (auto const& [map_key, member] : node_->items()) { + dom_reader member_rd{member}; + vis(std::string_view{map_key}, member_rd); + } + return true; + } + +private: + document_type const* node_; +}; + +template + requires requires(nlohmann::json const& j) { + basic_load_fn{}(j, std::declval()); + } +[[nodiscard]] T load(nlohmann::json const& j) +{ + T value; // default-initialize + basic_load_fn{}(j, value); + return value; +} + +template +void load(nlohmann::json const& j, T& value) +{ + basic_load_fn{}(j, value); +} + +} // iris::marshal::json + +#endif diff --git a/include/iris/marshal/serialize_traits.hpp b/include/iris/marshal/serialize_traits.hpp new file mode 100644 index 0000000..ee098f5 --- /dev/null +++ b/include/iris/marshal/serialize_traits.hpp @@ -0,0 +1,392 @@ +#ifndef IRIS_ZZ_MARSHAL_SERIALIZE_TRAITS_HPP +#define IRIS_ZZ_MARSHAL_SERIALIZE_TRAITS_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#include + +namespace iris::marshal { + +struct generic_format +{ + // Checks whether the *exact* type `K` is eligible for the key of this format's "map" type + template + static constexpr bool map_key = true; + + // Checks whether `K` can be converted to the key of this format's "map" type + template + static constexpr bool loadable_key = true; + + template + static constexpr bool loadable_scalar = + std::is_arithmetic_v || + std::is_enum_v || + StringLike; +}; + +namespace detail { + +template +concept marshal_format = requires { + { Format::template map_key } -> std::convertible_to; + { Format::template loadable_key } -> std::convertible_to; + { Format::template loadable_scalar } -> std::convertible_to; +}; + +} // detail + +// Customization point: serialize/deserialize `T` by converting it to/from `native_type`. +template +struct adapted_proxy_traits; + +template +using adapted_proxy_native_type_t = adapted_proxy_traits, Format>::native_type; + +template +concept adapted_proxy = + detail::marshal_format && + requires(std::remove_cvref_t const& v) { + typename adapted_proxy_native_type_t; + { adapted_proxy_traits, Format>::to_native_type(v) } + -> std::convertible_to>; + }; + +namespace detail { + +template +concept proxy_writable = + detail::marshal_format && + adapted_proxy && + requires(adapted_proxy_native_type_t p) { + { adapted_proxy_traits, Format>::from_native_type(std::move(p)) } + -> std::convertible_to>; + } && + std::is_assignable_v&, std::remove_cvref_t>; + +} // detail + +// -------------------------------------------------------------- + +// Customization point +template +struct adapted_class_traits; + +template +concept adapted_class = requires { + adapted_class_traits>::fields; +}; + +// Customization point +template +struct adapted_optional_traits; + +template +using adapted_optional_value_t = adapted_optional_traits>::value_type; + +template + requires is_ttp_specialization_of_v +struct adapted_optional_traits +{ + using value_type = T::value_type; +}; + +template +concept adapted_optional = requires(T const& opt) { + typename adapted_optional_traits>::value_type; + static_cast(opt); + { *opt } -> std::convertible_to const&>; +}; + +namespace detail { + +template +concept optional_writable = + adapted_optional && + std::default_initializable> && + std::is_assignable_v&, std::remove_cvref_t> && + std::is_assignable_v&, adapted_optional_value_t>; + + +template +[[nodiscard]] consteval bool is_serializable_impl(); + +template +struct is_serializable_scalar : std::false_type +{}; + +template + requires + std::is_arithmetic_v || + std::is_enum_v || + StringLike +struct is_serializable_scalar : std::true_type +{}; + +// --------------------------------------------------- + +template +[[nodiscard]] consteval bool is_tuple_elements_serializable_impl(); + +template +consteval bool is_serializable_impl() +{ + using V = std::remove_cvref_t; + + static_assert( + !(adapted_class && adapted_proxy), + "a type cannot be adapted both as a class and as a proxy" + ); + + if constexpr (adapted_proxy) { + return is_serializable_impl, Format>(); + + } else if constexpr (adapted_class) { + return true; + + } else if constexpr (adapted_optional) { + return is_serializable_impl, Format>(); + + } else if constexpr (is_serializable_scalar::value) { + return true; + + } else if constexpr (ranges::mapping_range) { + return + Format::template map_key> && + is_serializable_impl, Format>() && + is_serializable_impl, Format>(); + + } else if constexpr (std::ranges::input_range) { + return is_serializable_impl, Format>(); + + } else if constexpr (alloy::TupleLike) { + return is_tuple_elements_serializable_impl(); + + } else { + return false; + } +} + +template +consteval bool is_tuple_elements_serializable_impl() +{ + if constexpr (I == alloy::tuple_size_v) { + return true; + + } else if constexpr (!is_serializable_impl::type, Format>()) { + return false; + + } else { + return is_tuple_elements_serializable_impl(); + } +} + +} // detail + + +template +concept serializable_proxy = + detail::marshal_format && + adapted_proxy && + detail::is_serializable_impl, Format>(); + +template +concept serializable_class = + detail::marshal_format && + !serializable_proxy && + adapted_class; + +template +concept serializable_optional = + detail::marshal_format && + !serializable_proxy && + !serializable_class && + adapted_optional && + detail::is_serializable_impl, Format>(); + +template +concept serializable_scalar = + detail::marshal_format && + !serializable_proxy && + !serializable_class && + !adapted_optional && + detail::is_serializable_scalar>::value; + +template +concept serializable_map = + detail::marshal_format && + !serializable_proxy && + !serializable_class && + !adapted_optional && + !serializable_scalar && + ranges::mapping_range && + Format::template map_key> && + detail::is_serializable_impl, Format>() && + detail::is_serializable_impl, Format>(); + +template +concept serializable_array = + detail::marshal_format && + !serializable_proxy && + !serializable_class && + !adapted_optional && + !serializable_scalar && + !ranges::mapping_range && + std::ranges::input_range && + detail::is_serializable_impl, Format>(); + +template +concept serializable_tuple = + detail::marshal_format && + !serializable_proxy && + !serializable_class && + !adapted_optional && + !serializable_scalar && + !std::ranges::range && + alloy::TupleLike && + detail::is_tuple_elements_serializable_impl, Format>(); + +template +concept serializable = + detail::marshal_format && + ( + serializable_proxy || + serializable_class || + serializable_optional || + serializable_scalar || + serializable_map || + serializable_array || + serializable_tuple + ); + +// --------------------------------------------------- + +namespace detail { + +template +[[nodiscard]] consteval bool is_deserializable_impl(); + +template +concept loadable = + marshal_format && + std::default_initializable> && + std::movable> && + is_deserializable_impl, Format>(); + +template +consteval bool is_deserializable_impl() +{ + using V = std::remove_cvref_t; + + if constexpr (std::is_const_v>) { + return false; + + } else if constexpr (!is_serializable_impl()) { + return false; + + } else if constexpr (adapted_proxy) { + return loadable, Format> && proxy_writable; + + } else if constexpr (adapted_class) { + return true; + + } else if constexpr (adapted_optional) { + return loadable, Format> && optional_writable; + + } else if constexpr (is_serializable_scalar::value) { + return Format::template loadable_scalar; + + } else if constexpr (ranges::mapping_range) { + return + ranges::mapping_container && + Format::template loadable_key> && + loadable, Format> && + loadable, Format>; + + } else if constexpr (std::ranges::input_range) { + if constexpr (ranges::growable_array_writable) { + return loadable, Format>; + + } else if constexpr (ranges::fixed_array_writable) { + return is_deserializable_impl, Format>(); + + } else { + return false; + } + + } else if constexpr (alloy::TupleLike) { + return is_tuple_elements_serializable_impl(); // non-const get + recurse + + } else { + return false; + } +} + +} // detail + +template +concept deserializable_proxy = + serializable_proxy && + detail::is_deserializable_impl(); + +template +concept deserializable_class = + serializable_class && + detail::is_deserializable_impl(); + +template +concept deserializable_optional = + serializable_optional && + detail::is_deserializable_impl(); + +template +concept deserializable_scalar = + serializable_scalar && + detail::is_deserializable_impl(); + +template +concept deserializable_map = + serializable_map && + detail::loadable; + +template +concept deserializable_array = + serializable_array && + detail::loadable; + +template +concept deserializable_tuple = + serializable_tuple && + detail::loadable; + +template +concept deserializable = + detail::marshal_format && + ( + deserializable_proxy || + deserializable_class || + deserializable_optional || + deserializable_scalar || + deserializable_map || + deserializable_array || + deserializable_tuple + ); + +} // iris::marshal + +#endif diff --git a/include/iris/pp/arg.hpp b/include/iris/pp/arg.hpp new file mode 100644 index 0000000..f2ef6d9 --- /dev/null +++ b/include/iris/pp/arg.hpp @@ -0,0 +1,27 @@ +#ifndef IRIS_ZZ_PP_ARG_HPP +#define IRIS_ZZ_PP_ARG_HPP + +// SPDX-License-Identifier: MIT + +#include + +#define IRIS_PP_IDENTITY(...) __VA_ARGS__ + +#define IRIS_PP_EMPTY(...) + +#define IRIS_ZZ_PP_EXPAND_I(...) __VA_ARGS__ +#define IRIS_PP_EXPAND(...) IRIS_ZZ_PP_EXPAND_I(__VA_ARGS__) + +#define IRIS_ZZ_PP_PROBE() ~, 1 + +#define IRIS_ZZ_PP_SECOND(a, b, ...) b +#define IRIS_ZZ_PP_IS_PROBE(...) IRIS_ZZ_PP_SECOND(__VA_ARGS__, 0) +#define IRIS_ZZ_PP_IS_PAREN_PROBE(...) IRIS_ZZ_PP_PROBE() + +#define IRIS_PP_IS_PAREN(x) IRIS_ZZ_PP_IS_PROBE(IRIS_ZZ_PP_IS_PAREN_PROBE x) + +#define IRIS_PP_UNPAREN(x) IRIS_PP_EXPAND x +#define IRIS_PP_UNPAREN_IF_PAREN(x) \ + IRIS_PP_IF(IRIS_PP_IS_PAREN(x), IRIS_PP_UNPAREN, IRIS_PP_IDENTITY) (x) + +#endif diff --git a/include/iris/pp/cat.hpp b/include/iris/pp/cat.hpp index 8ccbbc7..66e7280 100644 --- a/include/iris/pp/cat.hpp +++ b/include/iris/pp/cat.hpp @@ -6,4 +6,7 @@ #define IRIS_ZZ_PP_CAT_I(a, b) a##b #define IRIS_PP_CAT(a, b) IRIS_ZZ_PP_CAT_I(a, b) +#define IRIS_ZZ_PP_CAT_ONLY_TWO_I(a, b, ...) a##b +#define IRIS_PP_CAT_ONLY_TWO(a, b, ...) IRIS_ZZ_PP_CAT_ONLY_TWO_I(a, b) + #endif diff --git a/modules/nlohmann_json b/modules/nlohmann_json new file mode 160000 index 0000000..734fd30 --- /dev/null +++ b/modules/nlohmann_json @@ -0,0 +1 @@ +Subproject commit 734fd305a19995673c5b83b4a30c06a96ff5c9b0 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 96f1630..6d72e40 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -195,11 +195,14 @@ if(PROJECT_IS_TOP_LEVEL) snippet alloy alloy_visit + marshal ) foreach(test_name IN LISTS IRIS_TEST_IRIS_TESTS) iris_define_internal_test(${test_name} ${test_name}.cpp) endforeach() + target_link_libraries(iris_marshal_test PRIVATE Iris::Marshal) + set( IRIS_TEST_NGRAM_TESTS ngram diff --git a/test/marshal.cpp b/test/marshal.cpp new file mode 100644 index 0000000..efca8a7 --- /dev/null +++ b/test/marshal.cpp @@ -0,0 +1,429 @@ +#include "iris_test.hpp" + +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::string_view_literals; + +namespace marshal = iris::marshal; +namespace json = marshal::json; + +using marshal::generic_format; + +enum non_scoped_enum : int {}; +enum struct scoped_enum : int {}; + +struct NonSerializable {}; + + +TEST_CASE("marshal: type traits (generic_format)") +{ + STATIC_CHECK(marshal::detail::marshal_format); + STATIC_CHECK(!marshal::serializable_array); +} + +TEST_CASE("marshal: type traits (json::format)") +{ + STATIC_CHECK(marshal::detail::marshal_format); + STATIC_CHECK(marshal::writer); + + // CharT = char + { + STATIC_CHECK(!marshal::adapted_proxy); + STATIC_CHECK(!marshal::adapted_proxy); + STATIC_CHECK(!marshal::adapted_proxy); + } + // CharT = char32_t + { + STATIC_CHECK(marshal::adapted_proxy); + STATIC_CHECK(std::same_as, std::string>); + STATIC_CHECK(marshal::detail::proxy_writable); + + STATIC_CHECK(marshal::adapted_proxy); + STATIC_CHECK(std::same_as, std::string>); + STATIC_CHECK(marshal::detail::proxy_writable); + + STATIC_CHECK(marshal::adapted_proxy); + STATIC_CHECK(std::same_as, std::string>); + STATIC_CHECK(marshal::detail::proxy_writable); + } +} + +TEST_CASE("marshal: serialize (builtin types)") +{ + STATIC_CHECK(!json::serializable); + + STATIC_CHECK(json::serializable_scalar); + STATIC_CHECK(!json::deserializable_scalar); + +#define IRIS_CHECK_SERIALIZABLE(kind, ...) \ + STATIC_CHECK(json::serializable_ ## kind<__VA_ARGS__>); \ + STATIC_CHECK(json::deserializable_ ## kind<__VA_ARGS__>); + + IRIS_CHECK_SERIALIZABLE(scalar, int); + IRIS_CHECK_SERIALIZABLE(scalar, non_scoped_enum); + IRIS_CHECK_SERIALIZABLE(scalar, scoped_enum); + + IRIS_CHECK_SERIALIZABLE(scalar, std::string); + IRIS_CHECK_SERIALIZABLE(scalar, std::string_view); + IRIS_CHECK_SERIALIZABLE(proxy, std::u32string); + IRIS_CHECK_SERIALIZABLE(proxy, std::u32string_view); + + IRIS_CHECK_SERIALIZABLE(optional, std::optional); + IRIS_CHECK_SERIALIZABLE(optional, std::optional); + IRIS_CHECK_SERIALIZABLE(optional, std::optional); + STATIC_CHECK(!json::serializable>); + + IRIS_CHECK_SERIALIZABLE(tuple, std::pair); + IRIS_CHECK_SERIALIZABLE(tuple, std::tuple); + IRIS_CHECK_SERIALIZABLE(tuple, std::pair); + IRIS_CHECK_SERIALIZABLE(tuple, std::tuple); + IRIS_CHECK_SERIALIZABLE(tuple, std::pair); + IRIS_CHECK_SERIALIZABLE(tuple, std::tuple); + STATIC_CHECK(!json::serializable>); + STATIC_CHECK(!json::serializable>); + + IRIS_CHECK_SERIALIZABLE(array, std::vector); + IRIS_CHECK_SERIALIZABLE(array, std::vector); + IRIS_CHECK_SERIALIZABLE(array, std::vector); + STATIC_CHECK(!json::serializable>); + + IRIS_CHECK_SERIALIZABLE(map, std::map); + IRIS_CHECK_SERIALIZABLE(map, std::map); + IRIS_CHECK_SERIALIZABLE(map, std::map); + + IRIS_CHECK_SERIALIZABLE(map, std::map); + IRIS_CHECK_SERIALIZABLE(map, std::map); + IRIS_CHECK_SERIALIZABLE(map, std::map); + + STATIC_CHECK(!json::serializable_map>); + STATIC_CHECK(!json::serializable_array>); + STATIC_CHECK(!json::serializable>); + + // NOT map + IRIS_CHECK_SERIALIZABLE(array, std::vector>); + STATIC_CHECK(!json::serializable_map>>); + STATIC_CHECK(!json::deserializable_map>>); + + // Adapted map + { + using PipedPairVec = decltype(std::declval>>() | iris::ranges::as_map); + STATIC_CHECK(json::serializable_map); + STATIC_CHECK(!json::deserializable_map); + } + +#undef IRIS_CHECK_SERIALIZABLE + + { + nlohmann::json j; + int const value = 42; + json::save(j, value); + CHECK(j.get() == value); + CHECK(json::load(j) == value); + } + + { + nlohmann::json j; + auto const value = non_scoped_enum{42}; + json::save(j, value); + CHECK(j.get() == value); + CHECK(json::load(j) == value); + } + { + nlohmann::json j; + auto const value = scoped_enum{42}; + json::save(j, value); + CHECK(j.get() == value); + CHECK(json::load(j) == value); + } + + { + nlohmann::json j; + std::string const str = "foo"; + json::save(j, str); + CHECK(j.get() == str); + CHECK(json::load(j) == str); + } + { + nlohmann::json j; + std::u32string const str = U"あいう"; + json::save(j, str); + CHECK(iris::to_u32string_ref(j.get()) == str); + CHECK(json::load(j) == str); + } + + { + nlohmann::json j; + std::vector const arr{0, 1, 2}; + json::save(j, arr); + CHECK(j.get>() == arr); + CHECK(json::load>(j) == arr); + } + { + nlohmann::json j; + std::vector const arr{"foo", "bar"}; + json::save(j, arr); + CHECK(j.get>() == arr); + CHECK(json::load>(j) == arr); + } + { + nlohmann::json j; + std::vector const arr{U"foo", U"あいう"}; + json::save(j, arr); + CHECK(json::load>(j) == arr); + } + + { + nlohmann::json j; + std::vector> const arr{{"foo", 0}}; + json::save(j, arr); + CHECK(j.get>>() == arr); + CHECK(json::load>>(j) == arr); + } + { + nlohmann::json j; + std::vector> const arr{{U"あいう", 0}}; + json::save(j, arr); + CHECK(json::load>>(j) == arr); + } + + { + nlohmann::json j; + std::map const map{{"foo", 0}, {"bar", 1}}; + json::save(j, map); + CHECK(j.get>() == map); + CHECK(json::load>(j) == map); + } + { + nlohmann::json j; + std::map const map{{"foo", "0"}, {"bar", "1"}}; + json::save(j, map); + CHECK(j.get>() == map); + CHECK(json::load>(j) == map); + } + { + nlohmann::json j; + std::map const map{{U"foo", 0}, {U"あいう", 1}}; + json::save(j, map); + CHECK(json::load>(j) == map); + } + { + nlohmann::json j; + std::map const map{{U"foo", U"0"}, {U"あいう", U"1"}}; + json::save(j, map); + CHECK(json::load>(j) == map); + } + + { + nlohmann::json j; + std::unordered_map const map{{"foo", 0}, {"bar", 1}}; + json::save(j, map); + CHECK(j.get>() == std::map{std::from_range, map}); + CHECK(json::load>(j) == map); + } + + { + nlohmann::json j; + std::flat_map const map{{"foo", 0}, {"bar", 1}}; + json::save(j, map); + CHECK(j.get>() == std::map{std::from_range, map}); + CHECK(json::load>(j) == map); + } + + { + nlohmann::json j; + std::multimap const map{{"foo", 0}, {"bar", 1}}; + json::save(j, map); + CHECK(j.get>() == map); + CHECK(json::load>(j) == map); + } + + { + nlohmann::json j; + std::pair const pair{0, 1}; + json::save(j, pair); + CHECK(j.get>() == pair); + CHECK(json::load>(j) == pair); + } + + { + nlohmann::json j; + std::pair const pair{"foo", 0}; + json::save(j, pair); + CHECK(j.get>() == pair); + CHECK(json::load>(j) == pair); + } + { + nlohmann::json j; + std::pair const pair{U"あいう", 0}; + json::save(j, pair); + CHECK(json::load>(j) == pair); + } + + { + nlohmann::json j; + std::tuple const tup{0, 1, 2}; + json::save(j, tup); + CHECK(j.get>() == tup); + CHECK(json::load>(j) == tup); + } + + { + nlohmann::json j; + std::optional const opt = 42; + json::save(j, opt); + CHECK(j.get>() == opt); + CHECK(json::load>(j) == opt); + } + { + nlohmann::json j; + std::optional const opt = "foo"; + json::save(j, opt); + CHECK(j.get>() == opt); + CHECK(json::load>(j) == opt); + } + { + nlohmann::json j; + std::optional const opt = U"あいう"; + json::save(j, opt); + CHECK(json::load>(j) == opt); + } + + { + nlohmann::json j; + std::vector> const vec_opt = {42, std::nullopt, 44}; + json::save(j, vec_opt); + CHECK(j.get>>() == vec_opt); + CHECK(json::load>>(j) == vec_opt); + } +} + + +struct MyData +{ + IRIS_MARSHAL_CLASS(MyData) + + IRIS_MARSHAL_FIELD(std::string, name) + IRIS_MARSHAL_FIELD(std::string, text, "empty text") + IRIS_MARSHAL_FIELD((std::pair), pair) + IRIS_MARSHAL_FIELD(int, age) + IRIS_MARSHAL_FIELD(bool, enabled, true) + + std::string public_mem_str; +}; + +IRIS_MARSHAL_ADAPT( + MyData, name, text, age, enabled, public_mem_str +); + +template<> +struct std::formatter : iris::no_spec_formatter +{ + template + static Ctx::iterator format(MyData const& my_data, Ctx& ctx) + { + return std::format_to( + ctx.out(), + "{{name: \"{}\", text: \"{}\", pair: ({},{}), age: {}, enabled: {}}}", + my_data.get_name(), + my_data.get_text(), + my_data.get_pair().first, my_data.get_pair().second, + my_data.get_age(), + my_data.is_enabled() + ); + } +}; + +TEST_CASE("marshal: serialize (class type)") +{ + STATIC_CHECK(marshal::serializable_class); + STATIC_CHECK(!marshal::serializable_tuple); + + { + MyData my_data; + + STATIC_CHECK(std::same_as); + CHECK(my_data.get_name().empty()); + my_data.set_name("foo"); + CHECK(my_data.get_name() == "foo"sv); + + CHECK(my_data.get_text() == "empty text"sv); + + STATIC_CHECK(std::same_as); + CHECK(my_data.get_age() == 0); + + STATIC_CHECK(std::same_as); + CHECK(my_data.is_enabled() == true); + } + + { + nlohmann::json j; + MyData my_data; + my_data.set_name("foo"); + my_data.set_age(42); + my_data.set_enabled(true); + json::save(j, my_data); + + CHECK(j.at("name").get() == "foo"sv); + CHECK(j.at("text").get() == "empty text"sv); + CHECK(j.at("age").get() == 42); + CHECK(j.at("enabled").get() == true); + + CHECK(json::load(j) == my_data); + } +} + +// ---------------------------------------------------- +// pimpl related + +#define IRIS_TEST_MY_PIMPL_DATA_FIELD_DEFS \ + (((std::map), int_map)) \ + ((int, age)) + +struct MyPimplData +{ + IRIS_MARSHAL_PIMPL_CLASS(MyPimplData) + IRIS_MARSHAL_PIMPL_FIELD_DECLARE(IRIS_TEST_MY_PIMPL_DATA_FIELD_DEFS) +}; + +IRIS_MARSHAL_ADAPT_DEFS(MyPimplData, IRIS_TEST_MY_PIMPL_DATA_FIELD_DEFS); + +// ---------------------------------- + +struct MyPimplData::Impl +{ + IRIS_MARSHAL_PIMPL_FIELD_DEFS(IRIS_TEST_MY_PIMPL_DATA_FIELD_DEFS) +}; + +IRIS_MARSHAL_PIMPL_CLASS_DEFINE(MyPimplData); +IRIS_MARSHAL_PIMPL_FIELD_ACCESS_DEFINE(MyPimplData, IRIS_TEST_MY_PIMPL_DATA_FIELD_DEFS); + +TEST_CASE("marshal: pimpl") +{ + nlohmann::json j; + MyPimplData p_data; + p_data.set_age(42); + p_data.set_int_map({{"foo", 5}}); + + json::save(j, p_data); + CHECK(j.at("age").get() == 42); + CHECK(j.at("int_map").get>() == std::map{{"foo", 5}}); + + auto const loaded_p_data = json::load(j); + CHECK(loaded_p_data.get_age() == p_data.get_age()); + CHECK(loaded_p_data.get_int_map() == p_data.get_int_map()); +}