From c608d6a118cfa8bb6af7be30bf4440b756f1bf7d Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:00:04 +0900 Subject: [PATCH 01/22] Implement sfield WIP --- include/iris/sfield.hpp | 115 ++++++++++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 1 + test/sfield.cpp | 34 ++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 include/iris/sfield.hpp create mode 100644 test/sfield.cpp diff --git a/include/iris/sfield.hpp b/include/iris/sfield.hpp new file mode 100644 index 0000000..766ba30 --- /dev/null +++ b/include/iris/sfield.hpp @@ -0,0 +1,115 @@ +#ifndef IRIS_ZZ_SFIELD_HPP +#define IRIS_ZZ_SFIELD_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include // IWYU pragma: export + +#include + +#include +#include +#include +#include +#include + +#include + + +namespace iris::sfield { + +namespace detail { + +template +struct is_value_semantics_preferred : std::false_type +{}; + +template + requires + std::is_fundamental_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_SFIELD_EXPAND_TYPE(...) __VA_ARGS__ + +#define IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name) field_name ## _ + +#define IRIS_ZZ_SFIELD_DATA_MEMBER(paren_type, field_name, ...) \ + private: \ + IRIS_ZZ_SFIELD_EXPAND_TYPE paren_type IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name){__VA_ARGS__}; + +#define IRIS_ZZ_SFIELD_GETTER(paren_type, field_name) \ + public: \ + [[nodiscard]] constexpr iris::sfield::detail::getter_return_t get_ ## field_name () const noexcept \ + { \ + return IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name); \ + } \ + +#define IRIS_ZZ_SFIELD_GETTER_BOOL(paren_type, field_name) \ + public: \ + [[nodiscard]] constexpr bool is_ ## field_name () const noexcept \ + { \ + return IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name); \ + } + +#define IRIS_ZZ_SFIELD_SETTER(paren_type, field_name) \ + public: \ + constexpr void set_ ## field_name (iris::sfield::detail::setter_param_t new_value) \ + noexcept(std::is_nothrow_copy_assignable_v) \ + { \ + IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name) = new_value; \ + } + +#define IRIS_SFIELD_GET(paren_type, field_name, ...) \ + IRIS_ZZ_SFIELD_DATA_MEMBER(paren_type, field_name, __VA_ARGS__) \ + IRIS_ZZ_SFIELD_GETTER(paren_type, field_name) + +#define IRIS_SFIELD_GET_SET(paren_type, field_name, ...) \ + IRIS_ZZ_SFIELD_DATA_MEMBER(paren_type, field_name, __VA_ARGS__) \ + IRIS_ZZ_SFIELD_GETTER(paren_type, field_name) \ + IRIS_ZZ_SFIELD_SETTER(paren_type, field_name) + +#define IRIS_SFIELD(paren_type, field_name, ...) \ + IRIS_SFIELD_GET_SET(paren_type, field_name, __VA_ARGS__) + + +// Default value for bool field is mandatory because it is error prone if omitted +#define IRIS_SFIELD_BOOL_GET(paren_type, field_name, default_value) \ + IRIS_ZZ_SFIELD_DATA_MEMBER(paren_type, field_name, default_value) \ + IRIS_ZZ_SFIELD_GETTER_BOOL(paren_type, field_name) + +// Default value for bool field is mandatory because it is error prone if omitted +#define IRIS_SFIELD_BOOL_GET_SET(paren_type, field_name, default_value) \ + IRIS_ZZ_SFIELD_DATA_MEMBER(paren_type, field_name, default_value) \ + IRIS_ZZ_SFIELD_GETTER_BOOL(paren_type, field_name) \ + IRIS_ZZ_SFIELD_SETTER(paren_type, field_name) + +// Default value for bool field is mandatory because it is error prone if omitted +#define IRIS_SFIELD_BOOL(paren_type, field_name, default_value) \ + IRIS_SFIELD_BOOL_GET_SET(paren_type, field_name, default_value) + +} // iris::sfield + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 96f1630..49f78b3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -195,6 +195,7 @@ if(PROJECT_IS_TOP_LEVEL) snippet alloy alloy_visit + sfield ) foreach(test_name IN LISTS IRIS_TEST_IRIS_TESTS) iris_define_internal_test(${test_name} ${test_name}.cpp) diff --git a/test/sfield.cpp b/test/sfield.cpp new file mode 100644 index 0000000..f529935 --- /dev/null +++ b/test/sfield.cpp @@ -0,0 +1,34 @@ +#include "iris_test.hpp" + +#include + +#include +#include + +using namespace std::string_view_literals; + +struct MyData +{ + IRIS_SFIELD((std::string), name) + IRIS_SFIELD((std::string), text, "empty text") + IRIS_SFIELD((int), age) + IRIS_SFIELD_BOOL((bool), enabled, true) +}; + +TEST_CASE("sfield") +{ + 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); +} From 71d3af72dd8e18025aa81b28bd0d5ac8709396af Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:55:16 +0900 Subject: [PATCH 02/22] Implement basic serializer --- .gitmodules | 3 + CMakeLists.txt | 32 ++++ include/iris/{sfield.hpp => sfield/adapt.hpp} | 47 ++++- include/iris/sfield/serialize.hpp | 138 ++++++++++++++ include/iris/sfield/serialize_json.hpp | 83 +++++++++ modules/nlohmann_json | 1 + test/CMakeLists.txt | 2 + test/sfield.cpp | 170 ++++++++++++++++-- 8 files changed, 462 insertions(+), 14 deletions(-) create mode 100644 .gitmodules rename include/iris/{sfield.hpp => sfield/adapt.hpp} (74%) create mode 100644 include/iris/sfield/serialize.hpp create mode 100644 include/iris/sfield/serialize_json.hpp create mode 160000 modules/nlohmann_json 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..d9f0445 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/sfield[./]) 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_SFIELD_HEADERS + ${PROJECT_SOURCE_DIR}/include/iris/sfield/*.hpp + ${PROJECT_SOURCE_DIR}/include/iris/sfield*.hpp +) + +add_library(iris_sfield INTERFACE) +add_library(Iris::SField ALIAS iris_sfield) + +target_sources(iris_sfield PRIVATE FILE_SET HEADERS TYPE HEADERS FILES ${IRIS_SFIELD_HEADERS}) +source_group(TREE ${PROJECT_SOURCE_DIR}/include/iris PREFIX iris FILES ${IRIS_SFIELD_HEADERS}) + +set_target_properties( + iris_sfield 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_sfield INTERFACE nlohmann_json::nlohmann_json Iris::Iris) + # ----------------------------------------------------------------- # Test diff --git a/include/iris/sfield.hpp b/include/iris/sfield/adapt.hpp similarity index 74% rename from include/iris/sfield.hpp rename to include/iris/sfield/adapt.hpp index 766ba30..ba0dc47 100644 --- a/include/iris/sfield.hpp +++ b/include/iris/sfield/adapt.hpp @@ -1,19 +1,24 @@ -#ifndef IRIS_ZZ_SFIELD_HPP -#define IRIS_ZZ_SFIELD_HPP +#ifndef IRIS_ZZ_SFIELD_ADAPT_HPP +#define IRIS_ZZ_SFIELD_ADAPT_HPP // SPDX-License-Identifier: MIT #include // IWYU pragma: keep +#include + #include // IWYU pragma: export #include +#include +#include #include #include #include #include #include +#include #include @@ -49,6 +54,13 @@ using setter_param_t = std::conditional_t< T const& >; + +template +struct field_definition +{ + std::string_view name; +}; + } // detail @@ -70,6 +82,12 @@ using setter_param_t = std::conditional_t< #define IRIS_ZZ_SFIELD_GETTER_BOOL(paren_type, field_name) \ public: \ [[nodiscard]] constexpr bool is_ ## field_name () const noexcept \ + { \ + return IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name); \ + } \ + \ + private: \ + [[nodiscard]] constexpr bool get_ ## field_name () const noexcept \ { \ return IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name); \ } @@ -110,6 +128,31 @@ using setter_param_t = std::conditional_t< #define IRIS_SFIELD_BOOL(paren_type, field_name, default_value) \ IRIS_SFIELD_BOOL_GET_SET(paren_type, field_name, default_value) + +#define IRIS_SFIELD_CLASS(class_name) \ + template \ + friend struct ::iris::sfield::detail::adapted_class; + +// ------------------------------------------------------------ + +#define IRIS_ZZ_SFIELD_ADAPT_FIELD(field_name, class_name) \ + ::iris::sfield::detail::field_definition< \ + &class_name::IRIS_PP_CAT(get_, field_name) \ + >{IRIS_PP_STRINGIZE(field_name)}, + +#define IRIS_SFIELD_ADAPT(class_name, ...) \ + template<> \ + struct iris::sfield::detail::adapted_class \ + { \ + inline static constexpr auto fields = ::iris::alloy::tuple{ \ + IRIS_PP_SEQ_FOR_EACH( \ + IRIS_PP_TUPLE_TO_SEQ((__VA_ARGS__)), \ + IRIS_ZZ_SFIELD_ADAPT_FIELD, \ + class_name \ + ) \ + }; \ + }; + } // iris::sfield #endif diff --git a/include/iris/sfield/serialize.hpp b/include/iris/sfield/serialize.hpp new file mode 100644 index 0000000..9f139fe --- /dev/null +++ b/include/iris/sfield/serialize.hpp @@ -0,0 +1,138 @@ +#ifndef IRIS_ZZ_SFIELD_SERIALIZE_HPP +#define IRIS_ZZ_SFIELD_SERIALIZE_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + +namespace iris::sfield { + +namespace detail { + +template +struct adapted_class; + +template +struct is_adapted_class : std::false_type +{}; + +template + requires requires { + typename adapted_class; + adapted_class::fields; + } +struct is_adapted_class : std::true_type +{}; + + +template +struct is_serializable_primitive : std::false_type +{}; + +template +struct is_serializable : is_serializable_primitive +{}; + +template +struct is_serializable_primitive : is_serializable_primitive +{}; + +template + requires + std::is_arithmetic_v || + std::is_enum_v || + StringLike +struct is_serializable_primitive : std::true_type +{}; + +template + requires + is_ttp_specialization_of_v +struct is_serializable_primitive : is_serializable +{}; + +// --------------------------------------------------- + +template + requires (!StringLike) +struct is_serializable : is_serializable> +{}; + +} // detail + +// T is a type that is directly representable with JSON's native type +template +concept serializable_primitive = detail::is_serializable_primitive::value; + + +template +concept serializable_array = + std::ranges::forward_range && + !is_assoc_container_v && + !StringLike && + detail::is_serializable>::value; + +template +concept serializable_map = + std::ranges::forward_range && + is_assoc_container_v && + detail::is_serializable>::value; + + +namespace detail { + +template +struct serializable_tuple_impl : std::false_type +{}; + +template +struct serializable_tuple_impl + : serializable_tuple_impl>> +{}; + +template +struct serializable_tuple_impl> + : std::conjunction< + is_serializable::type>... + > +{}; + +template +struct is_serializable : serializable_tuple_impl::value>> +{}; + +} // detail + +template +concept serializable_tuple = + alloy::TupleLike && + detail::serializable_tuple_impl::value && + !detail::is_adapted_class::value; + +template +concept serializable_class = + detail::is_adapted_class::value; + +template +concept serializable = + serializable_primitive || + serializable_array || + serializable_tuple || + serializable_class; + +} // iris::sfield + +#endif diff --git a/include/iris/sfield/serialize_json.hpp b/include/iris/sfield/serialize_json.hpp new file mode 100644 index 0000000..b385bff --- /dev/null +++ b/include/iris/sfield/serialize_json.hpp @@ -0,0 +1,83 @@ +#ifndef IRIS_ZZ_SFIELD_SERIALIZE_JSON_HPP +#define IRIS_ZZ_SFIELD_SERIALIZE_JSON_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include + +#include + +#include + +namespace iris::sfield { + +template +void save(nlohmann::json& json, T const& value) +{ + json = value; +} + +template +void save(nlohmann::json& json, R const& arr) +{ + auto json_arr = nlohmann::json::array(); + + for (auto const& elem : arr) { + nlohmann::json elem_json; + save(elem_json, elem); + json_arr.emplace_back(std::move(elem_json)); + } + + json = std::move(json_arr); +} + +template +void save(nlohmann::json& json, MapT const& map) +{ + auto json_map = nlohmann::json::object(); + + for (auto const& [key, value] : map) { + save(json_map[key], value); + } + + json = std::move(json_map); +} + +template +void save(nlohmann::json& json, TupleT const& tup) +{ + auto json_arr = nlohmann::json::array(); + + alloy::for_each(tup, [&](auto const& elem) { + nlohmann::json elem_json; + save(elem_json, elem); + json_arr.emplace_back(std::move(elem_json)); + }); + + json = std::move(json_arr); +} + +template +void save(nlohmann::json& json, ClassT const& klass) +{ + auto json_map = nlohmann::json::object(); + + constexpr auto const& fields = detail::adapted_class::fields; + alloy::for_each(fields, [&](detail::field_definition const& def) { + save(json_map[def.name], (klass.*Mem)()); + }); + + json = std::move(json_map); +} + + + +// TODO: load + + + +} // iris::sfield + +#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 49f78b3..292451d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -201,6 +201,8 @@ if(PROJECT_IS_TOP_LEVEL) iris_define_internal_test(${test_name} ${test_name}.cpp) endforeach() + target_link_libraries(iris_sfield_test PRIVATE Iris::SField) + set( IRIS_TEST_NGRAM_TESTS ngram diff --git a/test/sfield.cpp b/test/sfield.cpp index f529935..e9df2cc 100644 --- a/test/sfield.cpp +++ b/test/sfield.cpp @@ -1,34 +1,180 @@ #include "iris_test.hpp" -#include +#include +#include + +#include +#include +#include #include +#include #include +#include +#include +#include using namespace std::string_view_literals; +enum non_scoped_enum : int {}; +enum struct scoped_enum : int {}; + +struct NonSerializable {}; + +TEST_CASE("sfield: serialize (builtin types)") +{ + STATIC_CHECK(!iris::sfield::serializable); + + STATIC_CHECK(iris::sfield::serializable_primitive); + STATIC_CHECK(iris::sfield::serializable_primitive); + STATIC_CHECK(iris::sfield::serializable_primitive); + STATIC_CHECK(iris::sfield::serializable_primitive); + + STATIC_CHECK(iris::sfield::serializable_primitive); + STATIC_CHECK(iris::sfield::serializable_primitive); + STATIC_CHECK(iris::sfield::serializable_primitive); + STATIC_CHECK(iris::sfield::serializable_primitive); + + STATIC_CHECK(iris::sfield::serializable_primitive>); + STATIC_CHECK(iris::sfield::serializable_primitive const>); + STATIC_CHECK(!iris::sfield::serializable>); + + STATIC_CHECK(iris::sfield::serializable_tuple>); + STATIC_CHECK(!iris::sfield::serializable>); + STATIC_CHECK(iris::sfield::serializable_tuple>); + STATIC_CHECK(!iris::sfield::serializable>); + + STATIC_CHECK(iris::sfield::serializable_array>); + STATIC_CHECK(!iris::sfield::serializable>); + + STATIC_CHECK(iris::sfield::serializable_map>); + STATIC_CHECK(!iris::sfield::serializable_array>); + STATIC_CHECK(!iris::sfield::serializable>); + + { + nlohmann::json json; + int const value = 42; + iris::sfield::save(json, value); + CHECK(json.get() == 42); + } + + { + nlohmann::json json; + std::optional const value = 42; + iris::sfield::save(json, value); + CHECK(json.get() == 42); + } + + { + nlohmann::json json; + auto const value = non_scoped_enum{42}; + iris::sfield::save(json, value); + CHECK(json.get() == non_scoped_enum{42}); + } + { + nlohmann::json json; + auto const value = scoped_enum{42}; + iris::sfield::save(json, value); + CHECK(json.get() == scoped_enum{42}); + } + + { + nlohmann::json json; + std::string const value = "foo"; + iris::sfield::save(json, value); + CHECK(json.get() == "foo"sv); + } + { + nlohmann::json json; + std::u32string const value = U"あいう"; + iris::sfield::save(json, value); + CHECK(json.get() == U"あいう"sv); + } + + { + nlohmann::json json; + std::vector const arr{0, 1, 2}; + iris::sfield::save(json, arr); + CHECK(json.get>() == arr); + } + + { + nlohmann::json json; + std::map const map{{"foo", 0}, {"bar", 1}}; + iris::sfield::save(json, map); + CHECK(json.get>() == map); + } + + { + nlohmann::json json; + std::unordered_map const map{{"foo", 0}, {"bar", 1}}; + iris::sfield::save(json, map); + CHECK(json.get>() == std::map{std::from_range, map}); + } + + { + nlohmann::json json; + std::pair const pair{0, 1}; + iris::sfield::save(json, pair); + CHECK(json.get>() == std::pair{0, 1}); + } + + { + nlohmann::json json; + std::tuple const tuple{0, 1, 2}; + iris::sfield::save(json, tuple); + CHECK(json.get>() == std::tuple{0, 1, 2}); + } +} + + struct MyData { + IRIS_SFIELD_CLASS(MyData) + IRIS_SFIELD((std::string), name) IRIS_SFIELD((std::string), text, "empty text") IRIS_SFIELD((int), age) IRIS_SFIELD_BOOL((bool), enabled, true) }; -TEST_CASE("sfield") +IRIS_SFIELD_ADAPT( + MyData, name, text, age, enabled +); + +TEST_CASE("sfield: serialize (class type)") { - MyData my_data; + STATIC_CHECK(iris::sfield::serializable_class); + STATIC_CHECK(!iris::sfield::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_name().empty()); - my_data.set_name("foo"); - CHECK(my_data.get_name() == "foo"sv); + STATIC_CHECK(std::same_as); + CHECK(my_data.get_age() == 0); - CHECK(my_data.get_text() == "empty text"sv); + STATIC_CHECK(std::same_as); + CHECK(my_data.is_enabled() == true); + } - STATIC_CHECK(std::same_as); - CHECK(my_data.get_age() == 0); + { + nlohmann::json json; + MyData my_data; + my_data.set_name("foo"); + my_data.set_age(42); + my_data.set_enabled(true); + iris::sfield::save(json, my_data); - STATIC_CHECK(std::same_as); - CHECK(my_data.is_enabled() == true); + CHECK(json.at("name").get() == "foo"sv); + CHECK(json.at("text").get() == "empty text"sv); + CHECK(json.at("age").get() == 42); + CHECK(json.at("enabled").get() == true); + } } From 6c21fbe0033f391422964b45fba68b9b07921477 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:56:12 +0900 Subject: [PATCH 03/22] Refactor --- include/iris/sfield/adapt.hpp | 10 ++-------- include/iris/sfield/serialize_json.hpp | 2 +- .../sfield/{serialize.hpp => serialize_traits.hpp} | 6 +++--- test/sfield.cpp | 2 +- 4 files changed, 7 insertions(+), 13 deletions(-) rename include/iris/sfield/{serialize.hpp => serialize_traits.hpp} (96%) diff --git a/include/iris/sfield/adapt.hpp b/include/iris/sfield/adapt.hpp index ba0dc47..f630ddd 100644 --- a/include/iris/sfield/adapt.hpp +++ b/include/iris/sfield/adapt.hpp @@ -5,7 +5,7 @@ #include // IWYU pragma: keep -#include +#include #include // IWYU pragma: export @@ -13,15 +13,9 @@ #include #include -#include #include #include #include -#include -#include - -#include - namespace iris::sfield { @@ -33,7 +27,7 @@ struct is_value_semantics_preferred : std::false_type template requires - std::is_fundamental_v || + std::is_arithmetic_v || std::is_reference_v || std::is_pointer_v || std::is_enum_v diff --git a/include/iris/sfield/serialize_json.hpp b/include/iris/sfield/serialize_json.hpp index b385bff..3ac2568 100644 --- a/include/iris/sfield/serialize_json.hpp +++ b/include/iris/sfield/serialize_json.hpp @@ -5,7 +5,7 @@ #include // IWYU pragma: keep -#include +#include #include diff --git a/include/iris/sfield/serialize.hpp b/include/iris/sfield/serialize_traits.hpp similarity index 96% rename from include/iris/sfield/serialize.hpp rename to include/iris/sfield/serialize_traits.hpp index 9f139fe..3aa4e1d 100644 --- a/include/iris/sfield/serialize.hpp +++ b/include/iris/sfield/serialize_traits.hpp @@ -1,5 +1,5 @@ -#ifndef IRIS_ZZ_SFIELD_SERIALIZE_HPP -#define IRIS_ZZ_SFIELD_SERIALIZE_HPP +#ifndef IRIS_ZZ_SFIELD_SERIALIZE_TRAITS_HPP +#define IRIS_ZZ_SFIELD_SERIALIZE_TRAITS_HPP // SPDX-License-Identifier: MIT @@ -14,9 +14,9 @@ #include -#include #include #include +#include namespace iris::sfield { diff --git a/test/sfield.cpp b/test/sfield.cpp index e9df2cc..86e5836 100644 --- a/test/sfield.cpp +++ b/test/sfield.cpp @@ -3,8 +3,8 @@ #include #include +#include #include -#include #include #include From 8ca914e2d3ff3cfee2ffe739612e4ece1555835e Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:39:47 +0900 Subject: [PATCH 04/22] Simplify macro, absorb existence of parenthesis on type name --- include/iris/pp/arg.hpp | 25 +++++++++++++ include/iris/pp/cat.hpp | 3 ++ include/iris/sfield/adapt.hpp | 69 +++++++++++++++++++---------------- test/sfield.cpp | 9 +++-- 4 files changed, 70 insertions(+), 36 deletions(-) create mode 100644 include/iris/pp/arg.hpp diff --git a/include/iris/pp/arg.hpp b/include/iris/pp/arg.hpp new file mode 100644 index 0000000..079ee3c --- /dev/null +++ b/include/iris/pp/arg.hpp @@ -0,0 +1,25 @@ +#ifndef IRIS_ZZ_PP_ARG_HPP +#define IRIS_ZZ_PP_ARG_HPP + +// SPDX-License-Identifier: MIT + +#include + +#define IRIS_PP_IDENTITY(x) x + +#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/include/iris/sfield/adapt.hpp b/include/iris/sfield/adapt.hpp index f630ddd..10ec823 100644 --- a/include/iris/sfield/adapt.hpp +++ b/include/iris/sfield/adapt.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -58,22 +59,20 @@ struct field_definition } // detail -#define IRIS_ZZ_SFIELD_EXPAND_TYPE(...) __VA_ARGS__ - #define IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name) field_name ## _ -#define IRIS_ZZ_SFIELD_DATA_MEMBER(paren_type, field_name, ...) \ +#define IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, ...) \ private: \ - IRIS_ZZ_SFIELD_EXPAND_TYPE paren_type IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name){__VA_ARGS__}; + IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type) IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name){__VA_ARGS__}; -#define IRIS_ZZ_SFIELD_GETTER(paren_type, field_name) \ +#define IRIS_ZZ_SFIELD_GETTER(maybe_paren_type, field_name) \ public: \ - [[nodiscard]] constexpr iris::sfield::detail::getter_return_t get_ ## field_name () const noexcept \ + [[nodiscard]] constexpr iris::sfield::detail::getter_return_t get_ ## field_name () const noexcept \ { \ return IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name); \ } \ -#define IRIS_ZZ_SFIELD_GETTER_BOOL(paren_type, field_name) \ +#define IRIS_ZZ_SFIELD_GETTER_BOOL(maybe_paren_type, field_name) \ public: \ [[nodiscard]] constexpr bool is_ ## field_name () const noexcept \ { \ @@ -86,45 +85,51 @@ struct field_definition return IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name); \ } -#define IRIS_ZZ_SFIELD_SETTER(paren_type, field_name) \ +#define IRIS_ZZ_SFIELD_SETTER(maybe_paren_type, field_name) \ public: \ - constexpr void set_ ## field_name (iris::sfield::detail::setter_param_t new_value) \ - noexcept(std::is_nothrow_copy_assignable_v) \ + constexpr void set_ ## field_name (iris::sfield::detail::setter_param_t new_value) \ + noexcept(std::is_nothrow_copy_assignable_v) \ { \ IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name) = new_value; \ } -#define IRIS_SFIELD_GET(paren_type, field_name, ...) \ - IRIS_ZZ_SFIELD_DATA_MEMBER(paren_type, field_name, __VA_ARGS__) \ - IRIS_ZZ_SFIELD_GETTER(paren_type, field_name) - -#define IRIS_SFIELD_GET_SET(paren_type, field_name, ...) \ - IRIS_ZZ_SFIELD_DATA_MEMBER(paren_type, field_name, __VA_ARGS__) \ - IRIS_ZZ_SFIELD_GETTER(paren_type, field_name) \ - IRIS_ZZ_SFIELD_SETTER(paren_type, field_name) - -#define IRIS_SFIELD(paren_type, field_name, ...) \ - IRIS_SFIELD_GET_SET(paren_type, field_name, __VA_ARGS__) - // Default value for bool field is mandatory because it is error prone if omitted -#define IRIS_SFIELD_BOOL_GET(paren_type, field_name, default_value) \ - IRIS_ZZ_SFIELD_DATA_MEMBER(paren_type, field_name, default_value) \ - IRIS_ZZ_SFIELD_GETTER_BOOL(paren_type, field_name) +#define IRIS_ZZ_SFIELD_BOOL_GET(maybe_paren_type, field_name, default_value) \ + IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, default_value) \ + IRIS_ZZ_SFIELD_GETTER_BOOL(maybe_paren_type, field_name) // Default value for bool field is mandatory because it is error prone if omitted -#define IRIS_SFIELD_BOOL_GET_SET(paren_type, field_name, default_value) \ - IRIS_ZZ_SFIELD_DATA_MEMBER(paren_type, field_name, default_value) \ - IRIS_ZZ_SFIELD_GETTER_BOOL(paren_type, field_name) \ - IRIS_ZZ_SFIELD_SETTER(paren_type, field_name) +#define IRIS_ZZ_SFIELD_BOOL_GET_SET(maybe_paren_type, field_name, default_value) \ + IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, default_value) \ + IRIS_ZZ_SFIELD_GETTER_BOOL(maybe_paren_type, field_name) \ + IRIS_ZZ_SFIELD_SETTER(maybe_paren_type, field_name) // Default value for bool field is mandatory because it is error prone if omitted -#define IRIS_SFIELD_BOOL(paren_type, field_name, default_value) \ - IRIS_SFIELD_BOOL_GET_SET(paren_type, field_name, default_value) +#define IRIS_ZZ_SFIELD_BOOL(maybe_paren_type, field_name, default_value) \ + IRIS_ZZ_SFIELD_BOOL_GET_SET(maybe_paren_type, field_name, default_value) + + +#define IRIS_SFIELD_GET(maybe_paren_type, field_name, ...) \ + IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ + IRIS_ZZ_SFIELD_GETTER(maybe_paren_type, field_name) + +#define IRIS_SFIELD_GET_SET(maybe_paren_type, field_name, ...) \ + IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ + IRIS_ZZ_SFIELD_GETTER(maybe_paren_type, field_name) \ + IRIS_ZZ_SFIELD_SETTER(maybe_paren_type, field_name) + +#define IRIS_ZZ_SFIELD_TYPE_IS_bool (bool) +#define IRIS_SFIELD(maybe_paren_type, field_name, ...) \ + IRIS_PP_IF( \ + IRIS_PP_IS_PAREN( IRIS_PP_CAT_ONLY_TWO(IRIS_ZZ_SFIELD_TYPE_IS_, IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type)) ), \ + IRIS_ZZ_SFIELD_BOOL_GET_SET, \ + IRIS_SFIELD_GET_SET \ + ) (maybe_paren_type, field_name, __VA_ARGS__) #define IRIS_SFIELD_CLASS(class_name) \ - template \ + template \ friend struct ::iris::sfield::detail::adapted_class; // ------------------------------------------------------------ diff --git a/test/sfield.cpp b/test/sfield.cpp index 86e5836..753691c 100644 --- a/test/sfield.cpp +++ b/test/sfield.cpp @@ -132,10 +132,11 @@ struct MyData { IRIS_SFIELD_CLASS(MyData) - IRIS_SFIELD((std::string), name) - IRIS_SFIELD((std::string), text, "empty text") - IRIS_SFIELD((int), age) - IRIS_SFIELD_BOOL((bool), enabled, true) + IRIS_SFIELD(std::string, name) + IRIS_SFIELD(std::string, text, "empty text") + IRIS_SFIELD((std::pair), pair) + IRIS_SFIELD(int, age) + IRIS_SFIELD(bool, enabled, true) }; IRIS_SFIELD_ADAPT( From dfd356012aff9c6812769c7c15df942c23225a70 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 21 Aug 2026 08:57:17 +0900 Subject: [PATCH 05/22] Refactor --- include/iris/sfield/adapt.hpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/include/iris/sfield/adapt.hpp b/include/iris/sfield/adapt.hpp index 10ec823..afe59a9 100644 --- a/include/iris/sfield/adapt.hpp +++ b/include/iris/sfield/adapt.hpp @@ -110,24 +110,34 @@ struct field_definition IRIS_ZZ_SFIELD_BOOL_GET_SET(maybe_paren_type, field_name, default_value) -#define IRIS_SFIELD_GET(maybe_paren_type, field_name, ...) \ +#define IRIS_ZZ_SFIELD_TYPE_IS_bool (bool) + +#define IRIS_ZZ_SFIELD_GET_I(maybe_paren_type, field_name, ...) \ IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ IRIS_ZZ_SFIELD_GETTER(maybe_paren_type, field_name) -#define IRIS_SFIELD_GET_SET(maybe_paren_type, field_name, ...) \ +#define IRIS_SFIELD_GET(maybe_paren_type, field_name, ...) \ + IRIS_PP_IF( \ + IRIS_PP_IS_PAREN( IRIS_PP_CAT_ONLY_TWO(IRIS_ZZ_SFIELD_TYPE_IS_, IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type)) ), \ + IRIS_ZZ_SFIELD_BOOL_GET, \ + IRIS_ZZ_SFIELD_GET_I \ + ) (maybe_paren_type, field_name, __VA_ARGS__) + +#define IRIS_ZZ_SFIELD_GET_SET_I(maybe_paren_type, field_name, ...) \ IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ IRIS_ZZ_SFIELD_GETTER(maybe_paren_type, field_name) \ IRIS_ZZ_SFIELD_SETTER(maybe_paren_type, field_name) -#define IRIS_ZZ_SFIELD_TYPE_IS_bool (bool) - -#define IRIS_SFIELD(maybe_paren_type, field_name, ...) \ +#define IRIS_SFIELD_GET_SET(maybe_paren_type, field_name, ...) \ IRIS_PP_IF( \ IRIS_PP_IS_PAREN( IRIS_PP_CAT_ONLY_TWO(IRIS_ZZ_SFIELD_TYPE_IS_, IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type)) ), \ IRIS_ZZ_SFIELD_BOOL_GET_SET, \ - IRIS_SFIELD_GET_SET \ + IRIS_ZZ_SFIELD_GET_SET_I \ ) (maybe_paren_type, field_name, __VA_ARGS__) +#define IRIS_SFIELD(maybe_paren_type, field_name, ...) \ + IRIS_SFIELD_GET_SET(maybe_paren_type, field_name, __VA_ARGS__) + #define IRIS_SFIELD_CLASS(class_name) \ template \ friend struct ::iris::sfield::detail::adapted_class; From 3d10c8f9e7ac5c2eeeed6e8c4501840bbb80a794 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 21 Aug 2026 09:11:38 +0900 Subject: [PATCH 06/22] Add `operator==` --- include/iris/sfield/adapt.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/iris/sfield/adapt.hpp b/include/iris/sfield/adapt.hpp index afe59a9..af7c067 100644 --- a/include/iris/sfield/adapt.hpp +++ b/include/iris/sfield/adapt.hpp @@ -140,7 +140,9 @@ struct field_definition #define IRIS_SFIELD_CLASS(class_name) \ template \ - friend struct ::iris::sfield::detail::adapted_class; + friend struct ::iris::sfield::detail::adapted_class; \ + \ + [[nodiscard]] constexpr bool operator==(class_name const&) const noexcept = default; // ------------------------------------------------------------ From e664004f79e4841a0bea12cb00ef4476f3821163 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:55:38 +0900 Subject: [PATCH 07/22] Make range and map related traits more robust --- include/iris/sfield/serialize_json.hpp | 59 +++++++++++++++++++++++- include/iris/sfield/serialize_traits.hpp | 23 +++++---- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/include/iris/sfield/serialize_json.hpp b/include/iris/sfield/serialize_json.hpp index 3ac2568..e1741ed 100644 --- a/include/iris/sfield/serialize_json.hpp +++ b/include/iris/sfield/serialize_json.hpp @@ -73,9 +73,66 @@ void save(nlohmann::json& json, ClassT const& klass) } +// ---------------------------------------------------- -// TODO: load +template +void load(nlohmann::json const& json, T& value) +{ + value = json.get_ref(); +} + +template +void load(nlohmann::json const& json, R& arr) +{ + for (auto const& elem_json : json) { + std::ranges::range_value_t elem; + load(elem_json, elem); + arr.emplace_back(std::move(elem)); + } +} + +template +void load(nlohmann::json const& json, MapT& map) +{ + if constexpr (requires (std::size_t count) { + map.reserve(count); + }) { + map.reserve(json.size()); + } + for (auto const& [json_key, json_value] : json.items()) { + typename MapT::mapped_type value; + load(json_value, value); + map.emplace(json_key, std::move(value)); + } +} + +template +void load(nlohmann::json const& json, TupleT& tup) +{ + auto json_arr = nlohmann::json::array(); + + alloy::for_each(tup, [&](auto const& elem) { + nlohmann::json elem_json; + save(elem_json, elem); + json_arr.emplace_back(std::move(elem_json)); + }); + + json = std::move(json_arr); +} + +template +void load(nlohmann::json const& json, ClassT& klass) +{ + auto json_map = nlohmann::json::object(); + + constexpr auto const& fields = detail::adapted_class::fields; + alloy::for_each(fields, [&](detail::field_definition const& def) { + save(json_map[def.name], (klass.*Mem)()); + }); + + json = std::move(json_map); +} } // iris::sfield diff --git a/include/iris/sfield/serialize_traits.hpp b/include/iris/sfield/serialize_traits.hpp index 3aa4e1d..cdf44bd 100644 --- a/include/iris/sfield/serialize_traits.hpp +++ b/include/iris/sfield/serialize_traits.hpp @@ -31,7 +31,6 @@ struct is_adapted_class : std::false_type template requires requires { - typename adapted_class; adapted_class::fields; } struct is_adapted_class : std::true_type @@ -73,22 +72,30 @@ struct is_serializable : is_serializable> } // detail -// T is a type that is directly representable with JSON's native type + template -concept serializable_primitive = detail::is_serializable_primitive::value; +concept serializable_class = + detail::is_adapted_class::value; +// T is a type that is directly representable with JSON's native type +template +concept serializable_primitive = + !serializable_class && + detail::is_serializable_primitive::value; template concept serializable_array = + !serializable_class && std::ranges::forward_range && - !is_assoc_container_v && + !ranges::key_value_range && !StringLike && detail::is_serializable>::value; template concept serializable_map = + !serializable_class && std::ranges::forward_range && - is_assoc_container_v && + ranges::key_value_range && detail::is_serializable>::value; @@ -118,19 +125,17 @@ struct is_serializable : serializable_tuple_impl concept serializable_tuple = + !serializable_class && alloy::TupleLike && detail::serializable_tuple_impl::value && !detail::is_adapted_class::value; -template -concept serializable_class = - detail::is_adapted_class::value; - template concept serializable = serializable_primitive || serializable_array || serializable_tuple || + serializable_map || serializable_class; } // iris::sfield From ecf1a0467c946434dfe1fbd0e4feefd4a9b99eb8 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:01:25 +0900 Subject: [PATCH 08/22] Rename sfield to marshal --- CMakeLists.txt | 20 +-- include/iris/marshal/adapt.hpp | 169 ++++++++++++++++++ .../{sfield => marshal}/serialize_json.hpp | 10 +- .../{sfield => marshal}/serialize_traits.hpp | 8 +- include/iris/sfield/adapt.hpp | 169 ------------------ test/CMakeLists.txt | 4 +- test/{sfield.cpp => marshal.cpp} | 94 +++++----- 7 files changed, 237 insertions(+), 237 deletions(-) create mode 100644 include/iris/marshal/adapt.hpp rename include/iris/{sfield => marshal}/serialize_json.hpp (94%) rename include/iris/{sfield => marshal}/serialize_traits.hpp (95%) delete mode 100644 include/iris/sfield/adapt.hpp rename test/{sfield.cpp => marshal.cpp} (54%) diff --git a/CMakeLists.txt b/CMakeLists.txt index d9f0445..4846caf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -235,7 +235,7 @@ file( ${PROJECT_SOURCE_DIR}/include/iris/*.hpp ${PROJECT_SOURCE_DIR}/include/iris/*.ipp ) -list(FILTER IRIS_HEADERS EXCLUDE REGEX /include/iris/sfield[./]) +list(FILTER IRIS_HEADERS EXCLUDE REGEX /include/iris/marshal[./]) if(MSVC) add_custom_command( @@ -267,19 +267,19 @@ target_include_directories(iris INTERFACE ${PROJECT_SOURCE_DIR}/include) # Additional sublibraries file( - GLOB_RECURSE IRIS_SFIELD_HEADERS - ${PROJECT_SOURCE_DIR}/include/iris/sfield/*.hpp - ${PROJECT_SOURCE_DIR}/include/iris/sfield*.hpp + GLOB_RECURSE IRIS_MARSHAL_HEADERS + ${PROJECT_SOURCE_DIR}/include/iris/marshal/*.hpp + ${PROJECT_SOURCE_DIR}/include/iris/marshal*.hpp ) -add_library(iris_sfield INTERFACE) -add_library(Iris::SField ALIAS iris_sfield) +add_library(iris_marshal INTERFACE) +add_library(Iris::Marshal ALIAS iris_marshal) -target_sources(iris_sfield PRIVATE FILE_SET HEADERS TYPE HEADERS FILES ${IRIS_SFIELD_HEADERS}) -source_group(TREE ${PROJECT_SOURCE_DIR}/include/iris PREFIX iris FILES ${IRIS_SFIELD_HEADERS}) +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_sfield PROPERTIES + iris_marshal PROPERTIES CXX_EXTENSIONS OFF EXCLUDE_FROM_ALL true # EXCLUDE_FROM_DEFAULT_BUILD true @@ -292,7 +292,7 @@ if(NOT NLOHMANN_JSON_FOUND) add_subdirectory(modules/nlohmann_json EXCLUDE_FROM_ALL) endif() -target_link_libraries(iris_sfield INTERFACE nlohmann_json::nlohmann_json Iris::Iris) +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..181c83a --- /dev/null +++ b/include/iris/marshal/adapt.hpp @@ -0,0 +1,169 @@ +#ifndef IRIS_ZZ_MARSHAL_ADAPT_HPP +#define IRIS_ZZ_MARSHAL_ADAPT_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include + +#include // IWYU pragma: export + +#include +#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& +>; + + +template +struct field_definition +{ + std::string_view name; +}; + +} // detail + + +#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(maybe_paren_type, field_name) \ + public: \ + [[nodiscard]] constexpr iris::marshal::detail::getter_return_t get_ ## field_name () const noexcept \ + { \ + return IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(field_name); \ + } \ + +#define IRIS_ZZ_MARSHAL_FIELD_GETTER_BOOL(maybe_paren_type, field_name) \ + public: \ + [[nodiscard]] constexpr bool is_ ## field_name () const noexcept \ + { \ + return IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(field_name); \ + } \ + \ + private: \ + [[nodiscard]] constexpr bool get_ ## field_name () const noexcept \ + { \ + return IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(field_name); \ + } + +#define IRIS_ZZ_MARSHAL_FIELD_SETTER(maybe_paren_type, field_name) \ + public: \ + constexpr void set_ ## field_name (iris::marshal::detail::setter_param_t new_value) \ + noexcept(std::is_nothrow_copy_assignable_v) \ + { \ + IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(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(maybe_paren_type, field_name, default_value) \ + IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, default_value) \ + IRIS_ZZ_MARSHAL_FIELD_GETTER_BOOL(maybe_paren_type, field_name) + +// Default value for bool field is mandatory because it is error prone if omitted +#define IRIS_ZZ_MARSHAL_FIELD_BOOL_GET_SET(maybe_paren_type, field_name, default_value) \ + IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, default_value) \ + IRIS_ZZ_MARSHAL_FIELD_GETTER_BOOL(maybe_paren_type, field_name) \ + IRIS_ZZ_MARSHAL_FIELD_SETTER(maybe_paren_type, field_name) + +// Default value for bool field is mandatory because it is error prone if omitted +#define IRIS_ZZ_MARSHAL_FIELD_BOOL(maybe_paren_type, field_name, default_value) \ + IRIS_ZZ_MARSHAL_FIELD_BOOL_GET_SET(maybe_paren_type, field_name, default_value) + + +#define IRIS_ZZ_MARSHAL_FIELD_TYPE_IS_bool (bool) + +#define IRIS_ZZ_MARSHAL_FIELD_GET_I(maybe_paren_type, field_name, ...) \ + IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ + IRIS_ZZ_MARSHAL_FIELD_GETTER(maybe_paren_type, field_name) + +#define IRIS_MARSHAL_FIELD_GET(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, \ + IRIS_ZZ_MARSHAL_FIELD_GET_I \ + ) (maybe_paren_type, field_name, __VA_ARGS__) + +#define IRIS_ZZ_MARSHAL_FIELD_GET_SET_I(maybe_paren_type, field_name, ...) \ + IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ + IRIS_ZZ_MARSHAL_FIELD_GETTER(maybe_paren_type, field_name) \ + IRIS_ZZ_MARSHAL_FIELD_SETTER(maybe_paren_type, field_name) + +#define IRIS_MARSHAL_FIELD_GET_SET(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, __VA_ARGS__) + +#define IRIS_MARSHAL_FIELD(maybe_paren_type, field_name, ...) \ + IRIS_MARSHAL_FIELD_GET_SET(maybe_paren_type, field_name, __VA_ARGS__) + +#define IRIS_MARSHAL_CLASS(class_name) \ + template \ + friend struct ::iris::marshal::detail::adapted_class; \ + \ + [[nodiscard]] constexpr bool operator==(class_name const&) const noexcept = default; + +// ------------------------------------------------------------ + +#define IRIS_ZZ_MARSHAL_ADAPT_FIELD(field_name, class_name) \ + ::iris::marshal::detail::field_definition< \ + &class_name::IRIS_PP_CAT(get_, field_name) \ + >{IRIS_PP_STRINGIZE(field_name)}, + +#define IRIS_MARSHAL_ADAPT(class_name, ...) \ + template<> \ + struct iris::marshal::detail::adapted_class \ + { \ + inline static constexpr auto fields = ::iris::alloy::tuple{ \ + IRIS_PP_SEQ_FOR_EACH( \ + IRIS_PP_TUPLE_TO_SEQ((__VA_ARGS__)), \ + IRIS_ZZ_MARSHAL_ADAPT_FIELD, \ + class_name \ + ) \ + }; \ + }; + +} // iris::marshal + +#endif diff --git a/include/iris/sfield/serialize_json.hpp b/include/iris/marshal/serialize_json.hpp similarity index 94% rename from include/iris/sfield/serialize_json.hpp rename to include/iris/marshal/serialize_json.hpp index e1741ed..8113027 100644 --- a/include/iris/sfield/serialize_json.hpp +++ b/include/iris/marshal/serialize_json.hpp @@ -1,17 +1,17 @@ -#ifndef IRIS_ZZ_SFIELD_SERIALIZE_JSON_HPP -#define IRIS_ZZ_SFIELD_SERIALIZE_JSON_HPP +#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 -namespace iris::sfield { +namespace iris::marshal { template void save(nlohmann::json& json, T const& value) @@ -135,6 +135,6 @@ void load(nlohmann::json const& json, ClassT& klass) } -} // iris::sfield +} // iris::marshal #endif diff --git a/include/iris/sfield/serialize_traits.hpp b/include/iris/marshal/serialize_traits.hpp similarity index 95% rename from include/iris/sfield/serialize_traits.hpp rename to include/iris/marshal/serialize_traits.hpp index cdf44bd..c470c25 100644 --- a/include/iris/sfield/serialize_traits.hpp +++ b/include/iris/marshal/serialize_traits.hpp @@ -1,5 +1,5 @@ -#ifndef IRIS_ZZ_SFIELD_SERIALIZE_TRAITS_HPP -#define IRIS_ZZ_SFIELD_SERIALIZE_TRAITS_HPP +#ifndef IRIS_ZZ_MARSHAL_SERIALIZE_TRAITS_HPP +#define IRIS_ZZ_MARSHAL_SERIALIZE_TRAITS_HPP // SPDX-License-Identifier: MIT @@ -18,7 +18,7 @@ #include #include -namespace iris::sfield { +namespace iris::marshal { namespace detail { @@ -138,6 +138,6 @@ concept serializable = serializable_map || serializable_class; -} // iris::sfield +} // iris::marshal #endif diff --git a/include/iris/sfield/adapt.hpp b/include/iris/sfield/adapt.hpp deleted file mode 100644 index af7c067..0000000 --- a/include/iris/sfield/adapt.hpp +++ /dev/null @@ -1,169 +0,0 @@ -#ifndef IRIS_ZZ_SFIELD_ADAPT_HPP -#define IRIS_ZZ_SFIELD_ADAPT_HPP - -// SPDX-License-Identifier: MIT - -#include // IWYU pragma: keep - -#include - -#include // IWYU pragma: export - -#include -#include -#include -#include - -#include -#include -#include - -namespace iris::sfield { - -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& ->; - - -template -struct field_definition -{ - std::string_view name; -}; - -} // detail - - -#define IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name) field_name ## _ - -#define IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, ...) \ - private: \ - IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type) IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name){__VA_ARGS__}; - -#define IRIS_ZZ_SFIELD_GETTER(maybe_paren_type, field_name) \ - public: \ - [[nodiscard]] constexpr iris::sfield::detail::getter_return_t get_ ## field_name () const noexcept \ - { \ - return IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name); \ - } \ - -#define IRIS_ZZ_SFIELD_GETTER_BOOL(maybe_paren_type, field_name) \ - public: \ - [[nodiscard]] constexpr bool is_ ## field_name () const noexcept \ - { \ - return IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name); \ - } \ - \ - private: \ - [[nodiscard]] constexpr bool get_ ## field_name () const noexcept \ - { \ - return IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name); \ - } - -#define IRIS_ZZ_SFIELD_SETTER(maybe_paren_type, field_name) \ - public: \ - constexpr void set_ ## field_name (iris::sfield::detail::setter_param_t new_value) \ - noexcept(std::is_nothrow_copy_assignable_v) \ - { \ - IRIS_ZZ_SFIELD_DATA_MEMBER_NAME(field_name) = new_value; \ - } - - -// Default value for bool field is mandatory because it is error prone if omitted -#define IRIS_ZZ_SFIELD_BOOL_GET(maybe_paren_type, field_name, default_value) \ - IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, default_value) \ - IRIS_ZZ_SFIELD_GETTER_BOOL(maybe_paren_type, field_name) - -// Default value for bool field is mandatory because it is error prone if omitted -#define IRIS_ZZ_SFIELD_BOOL_GET_SET(maybe_paren_type, field_name, default_value) \ - IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, default_value) \ - IRIS_ZZ_SFIELD_GETTER_BOOL(maybe_paren_type, field_name) \ - IRIS_ZZ_SFIELD_SETTER(maybe_paren_type, field_name) - -// Default value for bool field is mandatory because it is error prone if omitted -#define IRIS_ZZ_SFIELD_BOOL(maybe_paren_type, field_name, default_value) \ - IRIS_ZZ_SFIELD_BOOL_GET_SET(maybe_paren_type, field_name, default_value) - - -#define IRIS_ZZ_SFIELD_TYPE_IS_bool (bool) - -#define IRIS_ZZ_SFIELD_GET_I(maybe_paren_type, field_name, ...) \ - IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ - IRIS_ZZ_SFIELD_GETTER(maybe_paren_type, field_name) - -#define IRIS_SFIELD_GET(maybe_paren_type, field_name, ...) \ - IRIS_PP_IF( \ - IRIS_PP_IS_PAREN( IRIS_PP_CAT_ONLY_TWO(IRIS_ZZ_SFIELD_TYPE_IS_, IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type)) ), \ - IRIS_ZZ_SFIELD_BOOL_GET, \ - IRIS_ZZ_SFIELD_GET_I \ - ) (maybe_paren_type, field_name, __VA_ARGS__) - -#define IRIS_ZZ_SFIELD_GET_SET_I(maybe_paren_type, field_name, ...) \ - IRIS_ZZ_SFIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ - IRIS_ZZ_SFIELD_GETTER(maybe_paren_type, field_name) \ - IRIS_ZZ_SFIELD_SETTER(maybe_paren_type, field_name) - -#define IRIS_SFIELD_GET_SET(maybe_paren_type, field_name, ...) \ - IRIS_PP_IF( \ - IRIS_PP_IS_PAREN( IRIS_PP_CAT_ONLY_TWO(IRIS_ZZ_SFIELD_TYPE_IS_, IRIS_PP_UNPAREN_IF_PAREN(maybe_paren_type)) ), \ - IRIS_ZZ_SFIELD_BOOL_GET_SET, \ - IRIS_ZZ_SFIELD_GET_SET_I \ - ) (maybe_paren_type, field_name, __VA_ARGS__) - -#define IRIS_SFIELD(maybe_paren_type, field_name, ...) \ - IRIS_SFIELD_GET_SET(maybe_paren_type, field_name, __VA_ARGS__) - -#define IRIS_SFIELD_CLASS(class_name) \ - template \ - friend struct ::iris::sfield::detail::adapted_class; \ - \ - [[nodiscard]] constexpr bool operator==(class_name const&) const noexcept = default; - -// ------------------------------------------------------------ - -#define IRIS_ZZ_SFIELD_ADAPT_FIELD(field_name, class_name) \ - ::iris::sfield::detail::field_definition< \ - &class_name::IRIS_PP_CAT(get_, field_name) \ - >{IRIS_PP_STRINGIZE(field_name)}, - -#define IRIS_SFIELD_ADAPT(class_name, ...) \ - template<> \ - struct iris::sfield::detail::adapted_class \ - { \ - inline static constexpr auto fields = ::iris::alloy::tuple{ \ - IRIS_PP_SEQ_FOR_EACH( \ - IRIS_PP_TUPLE_TO_SEQ((__VA_ARGS__)), \ - IRIS_ZZ_SFIELD_ADAPT_FIELD, \ - class_name \ - ) \ - }; \ - }; - -} // iris::sfield - -#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 292451d..6d72e40 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -195,13 +195,13 @@ if(PROJECT_IS_TOP_LEVEL) snippet alloy alloy_visit - sfield + marshal ) foreach(test_name IN LISTS IRIS_TEST_IRIS_TESTS) iris_define_internal_test(${test_name} ${test_name}.cpp) endforeach() - target_link_libraries(iris_sfield_test PRIVATE Iris::SField) + target_link_libraries(iris_marshal_test PRIVATE Iris::Marshal) set( IRIS_TEST_NGRAM_TESTS diff --git a/test/sfield.cpp b/test/marshal.cpp similarity index 54% rename from test/sfield.cpp rename to test/marshal.cpp index 753691c..a8df2b7 100644 --- a/test/sfield.cpp +++ b/test/marshal.cpp @@ -3,9 +3,9 @@ #include #include -#include -#include -#include +#include +#include +#include #include #include @@ -21,108 +21,108 @@ enum struct scoped_enum : int {}; struct NonSerializable {}; -TEST_CASE("sfield: serialize (builtin types)") +TEST_CASE("marshal: serialize (builtin types)") { - STATIC_CHECK(!iris::sfield::serializable); + STATIC_CHECK(!iris::marshal::serializable); - STATIC_CHECK(iris::sfield::serializable_primitive); - STATIC_CHECK(iris::sfield::serializable_primitive); - STATIC_CHECK(iris::sfield::serializable_primitive); - STATIC_CHECK(iris::sfield::serializable_primitive); + STATIC_CHECK(iris::marshal::serializable_primitive); + STATIC_CHECK(iris::marshal::serializable_primitive); + STATIC_CHECK(iris::marshal::serializable_primitive); + STATIC_CHECK(iris::marshal::serializable_primitive); - STATIC_CHECK(iris::sfield::serializable_primitive); - STATIC_CHECK(iris::sfield::serializable_primitive); - STATIC_CHECK(iris::sfield::serializable_primitive); - STATIC_CHECK(iris::sfield::serializable_primitive); + STATIC_CHECK(iris::marshal::serializable_primitive); + STATIC_CHECK(iris::marshal::serializable_primitive); + STATIC_CHECK(iris::marshal::serializable_primitive); + STATIC_CHECK(iris::marshal::serializable_primitive); - STATIC_CHECK(iris::sfield::serializable_primitive>); - STATIC_CHECK(iris::sfield::serializable_primitive const>); - STATIC_CHECK(!iris::sfield::serializable>); + STATIC_CHECK(iris::marshal::serializable_primitive>); + STATIC_CHECK(iris::marshal::serializable_primitive const>); + STATIC_CHECK(!iris::marshal::serializable>); - STATIC_CHECK(iris::sfield::serializable_tuple>); - STATIC_CHECK(!iris::sfield::serializable>); - STATIC_CHECK(iris::sfield::serializable_tuple>); - STATIC_CHECK(!iris::sfield::serializable>); + STATIC_CHECK(iris::marshal::serializable_tuple>); + STATIC_CHECK(!iris::marshal::serializable>); + STATIC_CHECK(iris::marshal::serializable_tuple>); + STATIC_CHECK(!iris::marshal::serializable>); - STATIC_CHECK(iris::sfield::serializable_array>); - STATIC_CHECK(!iris::sfield::serializable>); + STATIC_CHECK(iris::marshal::serializable_array>); + STATIC_CHECK(!iris::marshal::serializable>); - STATIC_CHECK(iris::sfield::serializable_map>); - STATIC_CHECK(!iris::sfield::serializable_array>); - STATIC_CHECK(!iris::sfield::serializable>); + STATIC_CHECK(iris::marshal::serializable_map>); + STATIC_CHECK(!iris::marshal::serializable_array>); + STATIC_CHECK(!iris::marshal::serializable>); { nlohmann::json json; int const value = 42; - iris::sfield::save(json, value); + iris::marshal::save(json, value); CHECK(json.get() == 42); } { nlohmann::json json; std::optional const value = 42; - iris::sfield::save(json, value); + iris::marshal::save(json, value); CHECK(json.get() == 42); } { nlohmann::json json; auto const value = non_scoped_enum{42}; - iris::sfield::save(json, value); + iris::marshal::save(json, value); CHECK(json.get() == non_scoped_enum{42}); } { nlohmann::json json; auto const value = scoped_enum{42}; - iris::sfield::save(json, value); + iris::marshal::save(json, value); CHECK(json.get() == scoped_enum{42}); } { nlohmann::json json; std::string const value = "foo"; - iris::sfield::save(json, value); + iris::marshal::save(json, value); CHECK(json.get() == "foo"sv); } { nlohmann::json json; std::u32string const value = U"あいう"; - iris::sfield::save(json, value); + iris::marshal::save(json, value); CHECK(json.get() == U"あいう"sv); } { nlohmann::json json; std::vector const arr{0, 1, 2}; - iris::sfield::save(json, arr); + iris::marshal::save(json, arr); CHECK(json.get>() == arr); } { nlohmann::json json; std::map const map{{"foo", 0}, {"bar", 1}}; - iris::sfield::save(json, map); + iris::marshal::save(json, map); CHECK(json.get>() == map); } { nlohmann::json json; std::unordered_map const map{{"foo", 0}, {"bar", 1}}; - iris::sfield::save(json, map); + iris::marshal::save(json, map); CHECK(json.get>() == std::map{std::from_range, map}); } { nlohmann::json json; std::pair const pair{0, 1}; - iris::sfield::save(json, pair); + iris::marshal::save(json, pair); CHECK(json.get>() == std::pair{0, 1}); } { nlohmann::json json; std::tuple const tuple{0, 1, 2}; - iris::sfield::save(json, tuple); + iris::marshal::save(json, tuple); CHECK(json.get>() == std::tuple{0, 1, 2}); } } @@ -130,23 +130,23 @@ TEST_CASE("sfield: serialize (builtin types)") struct MyData { - IRIS_SFIELD_CLASS(MyData) + IRIS_MARSHAL_CLASS(MyData) - IRIS_SFIELD(std::string, name) - IRIS_SFIELD(std::string, text, "empty text") - IRIS_SFIELD((std::pair), pair) - IRIS_SFIELD(int, age) - IRIS_SFIELD(bool, enabled, true) + 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) }; -IRIS_SFIELD_ADAPT( +IRIS_MARSHAL_ADAPT( MyData, name, text, age, enabled ); -TEST_CASE("sfield: serialize (class type)") +TEST_CASE("marshal: serialize (class type)") { - STATIC_CHECK(iris::sfield::serializable_class); - STATIC_CHECK(!iris::sfield::serializable_tuple); + STATIC_CHECK(iris::marshal::serializable_class); + STATIC_CHECK(!iris::marshal::serializable_tuple); { MyData my_data; @@ -171,7 +171,7 @@ TEST_CASE("sfield: serialize (class type)") my_data.set_name("foo"); my_data.set_age(42); my_data.set_enabled(true); - iris::sfield::save(json, my_data); + iris::marshal::save(json, my_data); CHECK(json.at("name").get() == "foo"sv); CHECK(json.at("text").get() == "empty text"sv); From b3b6af4e86d3f3fa1e3e3f414ffbaf81b8705025 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Sat, 22 Aug 2026 07:43:03 +0900 Subject: [PATCH 09/22] Refine basic traits --- include/iris/marshal/adapt.hpp | 4 +- include/iris/marshal/serialize_json.hpp | 95 +++++------- include/iris/marshal/serialize_traits.hpp | 168 +++++++++++++--------- test/marshal.cpp | 55 +++---- 4 files changed, 166 insertions(+), 156 deletions(-) diff --git a/include/iris/marshal/adapt.hpp b/include/iris/marshal/adapt.hpp index 181c83a..763a4c6 100644 --- a/include/iris/marshal/adapt.hpp +++ b/include/iris/marshal/adapt.hpp @@ -140,7 +140,7 @@ struct field_definition #define IRIS_MARSHAL_CLASS(class_name) \ template \ - friend struct ::iris::marshal::detail::adapted_class; \ + friend struct ::iris::marshal::adapted_class_traits; \ \ [[nodiscard]] constexpr bool operator==(class_name const&) const noexcept = default; @@ -153,7 +153,7 @@ struct field_definition #define IRIS_MARSHAL_ADAPT(class_name, ...) \ template<> \ - struct iris::marshal::detail::adapted_class \ + struct iris::marshal::adapted_class_traits \ { \ inline static constexpr auto fields = ::iris::alloy::tuple{ \ IRIS_PP_SEQ_FOR_EACH( \ diff --git a/include/iris/marshal/serialize_json.hpp b/include/iris/marshal/serialize_json.hpp index 8113027..6bde08b 100644 --- a/include/iris/marshal/serialize_json.hpp +++ b/include/iris/marshal/serialize_json.hpp @@ -13,7 +13,26 @@ namespace iris::marshal { -template +template +void save(nlohmann::json& json, T const& value); + +template +void save(nlohmann::json& json, R const& arr); + +template +void save(nlohmann::json& json, MapT const& map); + +template +void save(nlohmann::json& json, TupleT const& tup); + +template +void save(nlohmann::json& json, ClassT const& klass); + +template +void save(nlohmann::json& json, OptionalT const& opt); + + +template void save(nlohmann::json& json, T const& value) { json = value; @@ -26,7 +45,7 @@ void save(nlohmann::json& json, R const& arr) for (auto const& elem : arr) { nlohmann::json elem_json; - save(elem_json, elem); + marshal::save(elem_json, elem); json_arr.emplace_back(std::move(elem_json)); } @@ -39,7 +58,7 @@ void save(nlohmann::json& json, MapT const& map) auto json_map = nlohmann::json::object(); for (auto const& [key, value] : map) { - save(json_map[key], value); + marshal::save(json_map[key], value); } json = std::move(json_map); @@ -52,7 +71,7 @@ void save(nlohmann::json& json, TupleT const& tup) alloy::for_each(tup, [&](auto const& elem) { nlohmann::json elem_json; - save(elem_json, elem); + marshal::save(elem_json, elem); json_arr.emplace_back(std::move(elem_json)); }); @@ -64,75 +83,27 @@ void save(nlohmann::json& json, ClassT const& klass) { auto json_map = nlohmann::json::object(); - constexpr auto const& fields = detail::adapted_class::fields; + constexpr auto const& fields = adapted_class_traits::fields; alloy::for_each(fields, [&](detail::field_definition const& def) { - save(json_map[def.name], (klass.*Mem)()); + marshal::save(json_map[def.name], (klass.*Mem)()); }); json = std::move(json_map); } - -// ---------------------------------------------------- - - -template -void load(nlohmann::json const& json, T& value) -{ - value = json.get_ref(); -} - -template -void load(nlohmann::json const& json, R& arr) -{ - for (auto const& elem_json : json) { - std::ranges::range_value_t elem; - load(elem_json, elem); - arr.emplace_back(std::move(elem)); - } -} - -template -void load(nlohmann::json const& json, MapT& map) +template +void save(nlohmann::json& json, OptionalT const& opt) { - if constexpr (requires (std::size_t count) { - map.reserve(count); - }) { - map.reserve(json.size()); - } - for (auto const& [json_key, json_value] : json.items()) { - typename MapT::mapped_type value; - load(json_value, value); - map.emplace(json_key, std::move(value)); + if (opt) { + marshal::save(json, *opt); + } else { + json = nullptr; } } -template -void load(nlohmann::json const& json, TupleT& tup) -{ - auto json_arr = nlohmann::json::array(); - - alloy::for_each(tup, [&](auto const& elem) { - nlohmann::json elem_json; - save(elem_json, elem); - json_arr.emplace_back(std::move(elem_json)); - }); - - json = std::move(json_arr); -} - -template -void load(nlohmann::json const& json, ClassT& klass) -{ - auto json_map = nlohmann::json::object(); - - constexpr auto const& fields = detail::adapted_class::fields; - alloy::for_each(fields, [&](detail::field_definition const& def) { - save(json_map[def.name], (klass.*Mem)()); - }); +// ---------------------------------------------------- - json = std::move(json_map); -} +// TODO: load } // iris::marshal diff --git a/include/iris/marshal/serialize_traits.hpp b/include/iris/marshal/serialize_traits.hpp index c470c25..d3c94b7 100644 --- a/include/iris/marshal/serialize_traits.hpp +++ b/include/iris/marshal/serialize_traits.hpp @@ -14,39 +14,49 @@ #include +#include +#include #include #include -#include -namespace iris::marshal { +#include -namespace detail { +namespace iris::marshal { -template -struct adapted_class; +// Customization point +template +struct adapted_optional_traits; template -struct is_adapted_class : std::false_type -{}; + requires is_ttp_specialization_of_v +struct adapted_optional_traits +{ + using value_type = T::value_type; +}; template - requires requires { - adapted_class::fields; - } -struct is_adapted_class : std::true_type -{}; +concept adapted_optional = requires(T const& opt) { + typename adapted_optional_traits>::value_type; + static_cast(opt); + { *opt } -> std::convertible_to>::value_type const&>; +}; +// Customization point +template +struct adapted_class_traits; template -struct is_serializable_primitive : std::false_type -{}; +concept adapted_class = requires { + adapted_class_traits>::fields; +}; + +namespace detail { template -struct is_serializable : is_serializable_primitive -{}; +[[nodiscard]] consteval bool is_serializable_impl(); template -struct is_serializable_primitive : is_serializable_primitive +struct is_serializable_scalar : std::false_type {}; template @@ -54,89 +64,111 @@ template std::is_arithmetic_v || std::is_enum_v || StringLike -struct is_serializable_primitive : std::true_type +struct is_serializable_scalar : std::true_type {}; +// --------------------------------------------------- + +template +[[nodiscard]] consteval bool is_tuple_elements_serializable_impl(); + template - requires - is_ttp_specialization_of_v -struct is_serializable_primitive : is_serializable -{}; +consteval bool is_serializable_impl() +{ + using V = std::remove_cvref_t; -// --------------------------------------------------- + if constexpr (adapted_class) { + return true; -template - requires (!StringLike) -struct is_serializable : is_serializable> -{}; + } else if constexpr (adapted_optional) { + return is_serializable_impl::value_type>(); + + } else if constexpr (is_serializable_scalar::value) { + return true; + + } else if constexpr (ranges::key_value_range) { + return is_serializable_impl>() && + is_serializable_impl>(); + + } else if constexpr (std::ranges::input_range) { + return is_serializable_impl>(); + + } 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>()) { + return false; + + } else { + return is_tuple_elements_serializable_impl(); + } +} } // detail template concept serializable_class = - detail::is_adapted_class::value; + adapted_class; -// T is a type that is directly representable with JSON's native type template -concept serializable_primitive = +concept serializable_optional = !serializable_class && - detail::is_serializable_primitive::value; + adapted_optional && + detail::is_serializable_impl>::value_type>(); template -concept serializable_array = +concept serializable_scalar = !serializable_class && - std::ranges::forward_range && - !ranges::key_value_range && - !StringLike && - detail::is_serializable>::value; + !adapted_optional && + detail::is_serializable_scalar>::value; template concept serializable_map = !serializable_class && - std::ranges::forward_range && + !adapted_optional && + !serializable_scalar && ranges::key_value_range && - detail::is_serializable>::value; + detail::is_serializable_impl>() && + detail::is_serializable_impl>(); - -namespace detail { - -template -struct serializable_tuple_impl : std::false_type -{}; - -template -struct serializable_tuple_impl - : serializable_tuple_impl>> -{}; - -template -struct serializable_tuple_impl> - : std::conjunction< - is_serializable::type>... - > -{}; - -template -struct is_serializable : serializable_tuple_impl::value>> -{}; - -} // detail +template +concept serializable_array = + !serializable_class && + !adapted_optional && + !serializable_scalar && + !ranges::key_value_range && + std::ranges::input_range && + detail::is_serializable_impl>(); template concept serializable_tuple = !serializable_class && + !adapted_optional && + !serializable_scalar && + !std::ranges::range && alloy::TupleLike && - detail::serializable_tuple_impl::value && - !detail::is_adapted_class::value; + detail::is_tuple_elements_serializable_impl>(); template concept serializable = - serializable_primitive || - serializable_array || - serializable_tuple || + serializable_class || + serializable_optional || + serializable_scalar || serializable_map || - serializable_class; + serializable_array || + serializable_tuple; } // iris::marshal diff --git a/test/marshal.cpp b/test/marshal.cpp index a8df2b7..b232579 100644 --- a/test/marshal.cpp +++ b/test/marshal.cpp @@ -25,18 +25,18 @@ TEST_CASE("marshal: serialize (builtin types)") { STATIC_CHECK(!iris::marshal::serializable); - STATIC_CHECK(iris::marshal::serializable_primitive); - STATIC_CHECK(iris::marshal::serializable_primitive); - STATIC_CHECK(iris::marshal::serializable_primitive); - STATIC_CHECK(iris::marshal::serializable_primitive); - - STATIC_CHECK(iris::marshal::serializable_primitive); - STATIC_CHECK(iris::marshal::serializable_primitive); - STATIC_CHECK(iris::marshal::serializable_primitive); - STATIC_CHECK(iris::marshal::serializable_primitive); - - STATIC_CHECK(iris::marshal::serializable_primitive>); - STATIC_CHECK(iris::marshal::serializable_primitive const>); + STATIC_CHECK(iris::marshal::serializable_scalar); + STATIC_CHECK(iris::marshal::serializable_scalar); + STATIC_CHECK(iris::marshal::serializable_scalar); + STATIC_CHECK(iris::marshal::serializable_scalar); + + STATIC_CHECK(iris::marshal::serializable_scalar); + STATIC_CHECK(iris::marshal::serializable_scalar); + STATIC_CHECK(iris::marshal::serializable_scalar); + STATIC_CHECK(iris::marshal::serializable_scalar); + + STATIC_CHECK(iris::marshal::serializable_optional>); + STATIC_CHECK(iris::marshal::serializable_optional const>); STATIC_CHECK(!iris::marshal::serializable>); STATIC_CHECK(iris::marshal::serializable_tuple>); @@ -55,40 +55,47 @@ TEST_CASE("marshal: serialize (builtin types)") nlohmann::json json; int const value = 42; iris::marshal::save(json, value); - CHECK(json.get() == 42); + CHECK(json.get() == value); } { nlohmann::json json; std::optional const value = 42; iris::marshal::save(json, value); - CHECK(json.get() == 42); + CHECK(json.get() == value); + } + + { + nlohmann::json json; + std::vector> const value = {42, std::nullopt, 44}; + iris::marshal::save(json, value); + CHECK(json.get>>() == value); } { nlohmann::json json; auto const value = non_scoped_enum{42}; iris::marshal::save(json, value); - CHECK(json.get() == non_scoped_enum{42}); + CHECK(json.get() == value); } { nlohmann::json json; auto const value = scoped_enum{42}; iris::marshal::save(json, value); - CHECK(json.get() == scoped_enum{42}); + CHECK(json.get() == value); } { nlohmann::json json; std::string const value = "foo"; iris::marshal::save(json, value); - CHECK(json.get() == "foo"sv); + CHECK(json.get() == value); } { nlohmann::json json; std::u32string const value = U"あいう"; iris::marshal::save(json, value); - CHECK(json.get() == U"あいう"sv); + CHECK(json.get() == value); } { @@ -114,16 +121,16 @@ TEST_CASE("marshal: serialize (builtin types)") { nlohmann::json json; - std::pair const pair{0, 1}; - iris::marshal::save(json, pair); - CHECK(json.get>() == std::pair{0, 1}); + std::pair const value{0, 1}; + iris::marshal::save(json, value); + CHECK(json.get>() == value); } { nlohmann::json json; - std::tuple const tuple{0, 1, 2}; - iris::marshal::save(json, tuple); - CHECK(json.get>() == std::tuple{0, 1, 2}); + std::tuple const value{0, 1, 2}; + iris::marshal::save(json, value); + CHECK(json.get>() == value); } } From d135941305bd864e6c531c31bece6a5ab5d7283b Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:10:38 +0900 Subject: [PATCH 10/22] Refine JSON-specific concepts --- include/iris/marshal/adapt.hpp | 12 +- include/iris/marshal/detail/field.hpp | 20 ++++ include/iris/marshal/serialize_json.hpp | 74 ++++++++----- include/iris/marshal/serialize_traits.hpp | 82 +++++++------- test/marshal.cpp | 129 ++++++++++++---------- 5 files changed, 185 insertions(+), 132 deletions(-) create mode 100644 include/iris/marshal/detail/field.hpp diff --git a/include/iris/marshal/adapt.hpp b/include/iris/marshal/adapt.hpp index 763a4c6..84a762a 100644 --- a/include/iris/marshal/adapt.hpp +++ b/include/iris/marshal/adapt.hpp @@ -5,7 +5,8 @@ #include // IWYU pragma: keep -#include +#include // IWYU pragma: keep +#include // IWYU pragma: keep #include // IWYU pragma: export @@ -14,8 +15,6 @@ #include #include -#include -#include #include namespace iris::marshal { @@ -49,13 +48,6 @@ using setter_param_t = std::conditional_t< T const& >; - -template -struct field_definition -{ - std::string_view name; -}; - } // detail diff --git a/include/iris/marshal/detail/field.hpp b/include/iris/marshal/detail/field.hpp new file mode 100644 index 0000000..7df9f82 --- /dev/null +++ b/include/iris/marshal/detail/field.hpp @@ -0,0 +1,20 @@ +#ifndef IRIS_ZZ_MARSHAL_DETAIL_FIELD_HPP +#define IRIS_ZZ_MARSHAL_DETAIL_FIELD_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include + +namespace iris::marshal::detail { + +template +struct field_definition +{ + std::string_view name; +}; + +} // iris::marshal::detail + +#endif diff --git a/include/iris/marshal/serialize_json.hpp b/include/iris/marshal/serialize_json.hpp index 6bde08b..3ed3dde 100644 --- a/include/iris/marshal/serialize_json.hpp +++ b/include/iris/marshal/serialize_json.hpp @@ -6,98 +6,122 @@ #include // IWYU pragma: keep #include +#include #include +#include + #include -namespace iris::marshal { +#include +#include + +namespace iris::marshal::json { + +struct format +{ + template + static constexpr bool key = StringLike>; +}; + +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 -void save(nlohmann::json& json, T const& value); +void save(nlohmann::json& out, T const& value); template -void save(nlohmann::json& json, R const& arr); +void save(nlohmann::json& out, R const& arr); template -void save(nlohmann::json& json, MapT const& map); +void save(nlohmann::json& out, MapT const& map); template -void save(nlohmann::json& json, TupleT const& tup); +void save(nlohmann::json& out, TupleT const& tup); template -void save(nlohmann::json& json, ClassT const& klass); +void save(nlohmann::json& out, ClassT const& klass); template -void save(nlohmann::json& json, OptionalT const& opt); +void save(nlohmann::json& out, OptionalT const& opt); template -void save(nlohmann::json& json, T const& value) +void save(nlohmann::json& out, T const& value) { - json = value; + if constexpr (std::is_enum_v) { + out = std::to_underlying(value); + } else { + out = value; + } } template -void save(nlohmann::json& json, R const& arr) +void save(nlohmann::json& out, R const& arr) { auto json_arr = nlohmann::json::array(); for (auto const& elem : arr) { nlohmann::json elem_json; - marshal::save(elem_json, elem); + json::save(elem_json, elem); json_arr.emplace_back(std::move(elem_json)); } - json = std::move(json_arr); + out = std::move(json_arr); } template -void save(nlohmann::json& json, MapT const& map) +void save(nlohmann::json& out, MapT const& map) { auto json_map = nlohmann::json::object(); for (auto const& [key, value] : map) { - marshal::save(json_map[key], value); + json::save(json_map[std::basic_string_view{key}], value); } - json = std::move(json_map); + out = std::move(json_map); } template -void save(nlohmann::json& json, TupleT const& tup) +void save(nlohmann::json& out, TupleT const& tup) { auto json_arr = nlohmann::json::array(); alloy::for_each(tup, [&](auto const& elem) { nlohmann::json elem_json; - marshal::save(elem_json, elem); + json::save(elem_json, elem); json_arr.emplace_back(std::move(elem_json)); }); - json = std::move(json_arr); + out = std::move(json_arr); } template -void save(nlohmann::json& json, ClassT const& klass) +void save(nlohmann::json& out, ClassT const& klass) { auto json_map = nlohmann::json::object(); constexpr auto const& fields = adapted_class_traits::fields; alloy::for_each(fields, [&](detail::field_definition const& def) { - marshal::save(json_map[def.name], (klass.*Mem)()); + json::save(json_map[def.name], (klass.*Mem)()); }); - json = std::move(json_map); + out = std::move(json_map); } template -void save(nlohmann::json& json, OptionalT const& opt) +void save(nlohmann::json& out, OptionalT const& opt) { if (opt) { - marshal::save(json, *opt); + json::save(out, *opt); } else { - json = nullptr; + out = nullptr; } } @@ -106,6 +130,6 @@ void save(nlohmann::json& json, OptionalT const& opt) // TODO: load -} // iris::marshal +} // iris::marshal::json #endif diff --git a/include/iris/marshal/serialize_traits.hpp b/include/iris/marshal/serialize_traits.hpp index d3c94b7..94790ce 100644 --- a/include/iris/marshal/serialize_traits.hpp +++ b/include/iris/marshal/serialize_traits.hpp @@ -23,6 +23,12 @@ namespace iris::marshal { +struct generic_format +{ + template + static constexpr bool key = true; +}; + // Customization point template struct adapted_optional_traits; @@ -52,7 +58,7 @@ concept adapted_class = requires { namespace detail { -template +template [[nodiscard]] consteval bool is_serializable_impl(); template @@ -69,10 +75,10 @@ struct is_serializable_scalar : std::true_type // --------------------------------------------------- -template +template [[nodiscard]] consteval bool is_tuple_elements_serializable_impl(); -template +template consteval bool is_serializable_impl() { using V = std::remove_cvref_t; @@ -81,94 +87,96 @@ consteval bool is_serializable_impl() return true; } else if constexpr (adapted_optional) { - return is_serializable_impl::value_type>(); + return is_serializable_impl::value_type, Format>(); } else if constexpr (is_serializable_scalar::value) { return true; } else if constexpr (ranges::key_value_range) { - return is_serializable_impl>() && - is_serializable_impl>(); + return Format::template key> && + is_serializable_impl, Format>() && + is_serializable_impl, Format>(); } else if constexpr (std::ranges::input_range) { - return is_serializable_impl>(); + return is_serializable_impl, Format>(); } else if constexpr (alloy::TupleLike) { - return is_tuple_elements_serializable_impl(); + return is_tuple_elements_serializable_impl(); } else { return false; } } -template +template consteval bool is_tuple_elements_serializable_impl() { if constexpr (I == alloy::tuple_size_v) { return true; - } else if constexpr (!is_serializable_impl::type>()) { + } else if constexpr (!is_serializable_impl::type, Format>()) { return false; } else { - return is_tuple_elements_serializable_impl(); + return is_tuple_elements_serializable_impl(); } } } // detail -template +template concept serializable_class = adapted_class; -template +template concept serializable_optional = - !serializable_class && + !serializable_class && adapted_optional && - detail::is_serializable_impl>::value_type>(); + detail::is_serializable_impl>::value_type, Format>(); -template +template concept serializable_scalar = - !serializable_class && + !serializable_class && !adapted_optional && detail::is_serializable_scalar>::value; -template +template concept serializable_map = - !serializable_class && + !serializable_class && !adapted_optional && - !serializable_scalar && + !serializable_scalar && ranges::key_value_range && - detail::is_serializable_impl>() && - detail::is_serializable_impl>(); + Format::template key> && + detail::is_serializable_impl, Format>() && + detail::is_serializable_impl, Format>(); -template +template concept serializable_array = - !serializable_class && + !serializable_class && !adapted_optional && - !serializable_scalar && + !serializable_scalar && !ranges::key_value_range && std::ranges::input_range && - detail::is_serializable_impl>(); + detail::is_serializable_impl, Format>(); -template +template concept serializable_tuple = - !serializable_class && + !serializable_class && !adapted_optional && - !serializable_scalar && + !serializable_scalar && !std::ranges::range && alloy::TupleLike && - detail::is_tuple_elements_serializable_impl>(); + detail::is_tuple_elements_serializable_impl, Format>(); -template +template concept serializable = - serializable_class || - serializable_optional || - serializable_scalar || - serializable_map || - serializable_array || - serializable_tuple; + serializable_class || + serializable_optional || + serializable_scalar || + serializable_map || + serializable_array || + serializable_tuple; } // iris::marshal diff --git a/test/marshal.cpp b/test/marshal.cpp index b232579..aabe71a 100644 --- a/test/marshal.cpp +++ b/test/marshal.cpp @@ -16,6 +16,9 @@ using namespace std::string_view_literals; +namespace marshal = iris::marshal; +namespace json = marshal::json; + enum non_scoped_enum : int {}; enum struct scoped_enum : int {}; @@ -23,114 +26,120 @@ struct NonSerializable {}; TEST_CASE("marshal: serialize (builtin types)") { - STATIC_CHECK(!iris::marshal::serializable); + STATIC_CHECK(!marshal::serializable); + + STATIC_CHECK(marshal::serializable_scalar); + STATIC_CHECK(marshal::serializable_scalar); + STATIC_CHECK(marshal::serializable_scalar); + STATIC_CHECK(marshal::serializable_scalar); - STATIC_CHECK(iris::marshal::serializable_scalar); - STATIC_CHECK(iris::marshal::serializable_scalar); - STATIC_CHECK(iris::marshal::serializable_scalar); - STATIC_CHECK(iris::marshal::serializable_scalar); + STATIC_CHECK(marshal::serializable_scalar); + STATIC_CHECK(marshal::serializable_scalar); + STATIC_CHECK(marshal::serializable_scalar); + STATIC_CHECK(marshal::serializable_scalar); - STATIC_CHECK(iris::marshal::serializable_scalar); - STATIC_CHECK(iris::marshal::serializable_scalar); - STATIC_CHECK(iris::marshal::serializable_scalar); - STATIC_CHECK(iris::marshal::serializable_scalar); + STATIC_CHECK(marshal::serializable_optional>); + STATIC_CHECK(marshal::serializable_optional const>); + STATIC_CHECK(!marshal::serializable>); - STATIC_CHECK(iris::marshal::serializable_optional>); - STATIC_CHECK(iris::marshal::serializable_optional const>); - STATIC_CHECK(!iris::marshal::serializable>); + STATIC_CHECK(marshal::serializable_tuple>); + STATIC_CHECK(!marshal::serializable>); + STATIC_CHECK(marshal::serializable_tuple>); + STATIC_CHECK(!marshal::serializable>); - STATIC_CHECK(iris::marshal::serializable_tuple>); - STATIC_CHECK(!iris::marshal::serializable>); - STATIC_CHECK(iris::marshal::serializable_tuple>); - STATIC_CHECK(!iris::marshal::serializable>); + STATIC_CHECK(marshal::serializable_array>); + STATIC_CHECK(!marshal::serializable>); - STATIC_CHECK(iris::marshal::serializable_array>); - STATIC_CHECK(!iris::marshal::serializable>); + STATIC_CHECK(marshal::serializable_map>); + STATIC_CHECK(!marshal::serializable_array>); + STATIC_CHECK(!marshal::serializable>); - STATIC_CHECK(iris::marshal::serializable_map>); - STATIC_CHECK(!iris::marshal::serializable_array>); - STATIC_CHECK(!iris::marshal::serializable>); + STATIC_CHECK(json::serializable_map>); + STATIC_CHECK(json::serializable_map>); + STATIC_CHECK(!json::serializable_map>); + STATIC_CHECK(json::serializable_map>>); + STATIC_CHECK(!json::serializable_map>>); { - nlohmann::json json; + nlohmann::json j; int const value = 42; - iris::marshal::save(json, value); - CHECK(json.get() == value); + json::save(j, value); + CHECK(j.get() == value); } { - nlohmann::json json; + nlohmann::json j; std::optional const value = 42; - iris::marshal::save(json, value); - CHECK(json.get() == value); + json::save(j, value); + CHECK(j.get() == value); } { - nlohmann::json json; + nlohmann::json j; std::vector> const value = {42, std::nullopt, 44}; - iris::marshal::save(json, value); - CHECK(json.get>>() == value); + json::save(j, value); + CHECK(j.get>>() == value); } { - nlohmann::json json; + nlohmann::json j; auto const value = non_scoped_enum{42}; - iris::marshal::save(json, value); - CHECK(json.get() == value); + json::save(j, value); + CHECK(j.get() == value); } { - nlohmann::json json; + nlohmann::json j; auto const value = scoped_enum{42}; - iris::marshal::save(json, value); - CHECK(json.get() == value); + json::save(j, value); + CHECK(j.get() == value); } { - nlohmann::json json; + nlohmann::json j; std::string const value = "foo"; - iris::marshal::save(json, value); - CHECK(json.get() == value); + json::save(j, value); + CHECK(j.get() == value); } { - nlohmann::json json; + nlohmann::json j; std::u32string const value = U"あいう"; - iris::marshal::save(json, value); - CHECK(json.get() == value); + json::save(j, value); + CHECK(j.get() == value); } { - nlohmann::json json; + nlohmann::json j; std::vector const arr{0, 1, 2}; - iris::marshal::save(json, arr); - CHECK(json.get>() == arr); + json::save(j, arr); + CHECK(j.get>() == arr); } { - nlohmann::json json; + nlohmann::json j; std::map const map{{"foo", 0}, {"bar", 1}}; - iris::marshal::save(json, map); - CHECK(json.get>() == map); + json::save(j, map); + CHECK(j.get>() == map); } { - nlohmann::json json; + nlohmann::json j; std::unordered_map const map{{"foo", 0}, {"bar", 1}}; - iris::marshal::save(json, map); - CHECK(json.get>() == std::map{std::from_range, map}); + json::save(j, map); + CHECK(j.get>() == std::map{std::from_range, map}); } { - nlohmann::json json; + nlohmann::json j; std::pair const value{0, 1}; - iris::marshal::save(json, value); - CHECK(json.get>() == value); + json::save(j, value); + CHECK(j.get>() == value); } { - nlohmann::json json; + nlohmann::json j; std::tuple const value{0, 1, 2}; - iris::marshal::save(json, value); - CHECK(json.get>() == value); + json::save(j, value); + CHECK(j.get>() == value); } } @@ -152,8 +161,8 @@ IRIS_MARSHAL_ADAPT( TEST_CASE("marshal: serialize (class type)") { - STATIC_CHECK(iris::marshal::serializable_class); - STATIC_CHECK(!iris::marshal::serializable_tuple); + STATIC_CHECK(marshal::serializable_class); + STATIC_CHECK(!marshal::serializable_tuple); { MyData my_data; @@ -178,7 +187,7 @@ TEST_CASE("marshal: serialize (class type)") my_data.set_name("foo"); my_data.set_age(42); my_data.set_enabled(true); - iris::marshal::save(json, my_data); + json::save(json, my_data); CHECK(json.at("name").get() == "foo"sv); CHECK(json.at("text").get() == "empty text"sv); From 27bb1a082b0c8ccbe9300d1dc2485ad9211bd7f1 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:25:51 +0900 Subject: [PATCH 11/22] Add proxy, make function CPO --- include/iris/marshal/serialize_json.hpp | 153 ++++++++++++---------- include/iris/marshal/serialize_traits.hpp | 54 ++++++-- 2 files changed, 125 insertions(+), 82 deletions(-) diff --git a/include/iris/marshal/serialize_json.hpp b/include/iris/marshal/serialize_json.hpp index 3ed3dde..ecae26a 100644 --- a/include/iris/marshal/serialize_json.hpp +++ b/include/iris/marshal/serialize_json.hpp @@ -25,6 +25,7 @@ struct format static constexpr bool key = StringLike>; }; +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; @@ -33,102 +34,110 @@ template concept serializable_array = marshal::serializable_array concept serializable_tuple = marshal::serializable_tuple; template concept serializable = marshal::serializable; -template -void save(nlohmann::json& out, T const& value); +namespace detail { -template -void save(nlohmann::json& out, R const& arr); - -template -void save(nlohmann::json& out, MapT const& map); - -template -void save(nlohmann::json& out, TupleT const& tup); - -template -void save(nlohmann::json& out, ClassT const& klass); +struct save_fn +{ + template + static void operator()(nlohmann::json& out, T const& value) + { + if constexpr (std::is_enum_v) { + out = std::to_underlying(value); + } else { + out = value; + } + } -template -void save(nlohmann::json& out, OptionalT const& opt); + template + static void operator()(nlohmann::json& out, R const& arr) + { + auto json_arr = nlohmann::json::array(); + for (auto const& elem : arr) { + nlohmann::json elem_json; + save_fn{}(elem_json, elem); + json_arr.emplace_back(std::move(elem_json)); + } -template -void save(nlohmann::json& out, T const& value) -{ - if constexpr (std::is_enum_v) { - out = std::to_underlying(value); - } else { - out = value; + out = std::move(json_arr); } -} -template -void save(nlohmann::json& out, R const& arr) -{ - auto json_arr = nlohmann::json::array(); + template + static void operator()(nlohmann::json& out, MapT const& map) + { + auto json_map = nlohmann::json::object(); + + for (auto const& [key, value] : map) { + save_fn{}(json_map[std::basic_string_view{key}], value); + } - for (auto const& elem : arr) { - nlohmann::json elem_json; - json::save(elem_json, elem); - json_arr.emplace_back(std::move(elem_json)); + out = std::move(json_map); } - out = std::move(json_arr); -} + template + static void operator()(nlohmann::json& out, TupleT const& tup) + { + auto json_arr = nlohmann::json::array(); -template -void save(nlohmann::json& out, MapT const& map) -{ - auto json_map = nlohmann::json::object(); + alloy::for_each(tup, [&](auto const& elem) { + nlohmann::json elem_json; + save_fn{}(elem_json, elem); + json_arr.emplace_back(std::move(elem_json)); + }); - for (auto const& [key, value] : map) { - json::save(json_map[std::basic_string_view{key}], value); + out = std::move(json_arr); } - out = std::move(json_map); -} + template + static void operator()(nlohmann::json& out, ClassT const& klass) + { + auto json_map = nlohmann::json::object(); -template -void save(nlohmann::json& out, TupleT const& tup) -{ - auto json_arr = nlohmann::json::array(); + constexpr auto const& fields = adapted_class_traits::fields; + alloy::for_each(fields, [&](marshal::detail::field_definition const& def) { + save_fn{}(json_map[def.name], (klass.*Mem)()); + }); - alloy::for_each(tup, [&](auto const& elem) { - nlohmann::json elem_json; - json::save(elem_json, elem); - json_arr.emplace_back(std::move(elem_json)); - }); + out = std::move(json_map); + } - out = std::move(json_arr); -} + template + static void operator()(nlohmann::json& out, OptionalT const& opt) + { + if (opt) { + save_fn{}(out, *opt); + } else { + out = nullptr; + } + } -template -void save(nlohmann::json& out, ClassT const& klass) -{ - auto json_map = nlohmann::json::object(); + template + static void operator()(nlohmann::json& out, ProxyT const& proxy) + { + save_fn{}(out, adapted_proxy_traits::to_proxy(proxy)); + } - constexpr auto const& fields = adapted_class_traits::fields; - alloy::for_each(fields, [&](detail::field_definition const& def) { - json::save(json_map[def.name], (klass.*Mem)()); - }); + template + [[nodiscard]] static nlohmann::json operator()(T const& value) + { + nlohmann::json out; + save_fn{}(out, value); + return out; + } +}; - out = std::move(json_map); -} +// ---------------------------------------------------- -template -void save(nlohmann::json& out, OptionalT const& opt) +// TODO +struct load_fn { - if (opt) { - json::save(out, *opt); - } else { - out = nullptr; - } -} -// ---------------------------------------------------- +}; -// TODO: load +} // detail +[[maybe_unused]] inline constexpr detail::save_fn save{}; +[[maybe_unused]] inline constexpr detail::load_fn load{}; } // iris::marshal::json diff --git a/include/iris/marshal/serialize_traits.hpp b/include/iris/marshal/serialize_traits.hpp index 94790ce..75e7264 100644 --- a/include/iris/marshal/serialize_traits.hpp +++ b/include/iris/marshal/serialize_traits.hpp @@ -29,6 +29,29 @@ struct generic_format static constexpr bool key = true; }; +// Customization point: serialize T by converting it to/from proxy_type. +template +struct adapted_proxy_traits; + +template +concept adapted_proxy = requires(std::remove_cvref_t const& v) { + typename adapted_proxy_traits>::proxy_type; + { adapted_proxy_traits>::to_proxy(v) } + -> std::convertible_to>::proxy_type>; +}; + +template +using adapted_proxy_t = adapted_proxy_traits>::proxy_type; + +// Customization point +template +struct adapted_class_traits; + +template +concept adapted_class = requires { + adapted_class_traits>::fields; +}; + // Customization point template struct adapted_optional_traits; @@ -47,15 +70,6 @@ concept adapted_optional = requires(T const& opt) { { *opt } -> std::convertible_to>::value_type const&>; }; -// Customization point -template -struct adapted_class_traits; - -template -concept adapted_class = requires { - adapted_class_traits>::fields; -}; - namespace detail { template @@ -83,7 +97,15 @@ consteval bool is_serializable_impl() { using V = std::remove_cvref_t; - if constexpr (adapted_class) { + 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) { @@ -125,24 +147,33 @@ consteval bool is_tuple_elements_serializable_impl() } // detail +template +concept serializable_proxy = + adapted_proxy && + detail::is_serializable_impl, Format>(); + template concept serializable_class = + !serializable_proxy && adapted_class; template concept serializable_optional = + !serializable_proxy && !serializable_class && adapted_optional && detail::is_serializable_impl>::value_type, Format>(); template concept serializable_scalar = + !serializable_proxy && !serializable_class && !adapted_optional && detail::is_serializable_scalar>::value; template concept serializable_map = + !serializable_proxy && !serializable_class && !adapted_optional && !serializable_scalar && @@ -153,6 +184,7 @@ concept serializable_map = template concept serializable_array = + !serializable_proxy && !serializable_class && !adapted_optional && !serializable_scalar && @@ -162,6 +194,7 @@ concept serializable_array = template concept serializable_tuple = + !serializable_proxy && !serializable_class && !adapted_optional && !serializable_scalar && @@ -171,6 +204,7 @@ concept serializable_tuple = template concept serializable = + serializable_proxy || serializable_class || serializable_optional || serializable_scalar || From 2be0bae14d4f856697dc8a0f0a574dd72fec856b Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:55:58 +0900 Subject: [PATCH 12/22] Implement load functions --- include/iris/marshal/adapt.hpp | 4 +- include/iris/marshal/detail/field.hpp | 3 +- include/iris/marshal/serialize_json.hpp | 194 ++++++++++++++- include/iris/marshal/serialize_traits.hpp | 224 ++++++++++++++--- test/marshal.cpp | 280 +++++++++++++++++----- 5 files changed, 610 insertions(+), 95 deletions(-) diff --git a/include/iris/marshal/adapt.hpp b/include/iris/marshal/adapt.hpp index 84a762a..99657d1 100644 --- a/include/iris/marshal/adapt.hpp +++ b/include/iris/marshal/adapt.hpp @@ -140,7 +140,9 @@ using setter_param_t = std::conditional_t< #define IRIS_ZZ_MARSHAL_ADAPT_FIELD(field_name, class_name) \ ::iris::marshal::detail::field_definition< \ - &class_name::IRIS_PP_CAT(get_, field_name) \ + decltype(class_name::IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(field_name)), \ + &class_name::IRIS_PP_CAT(get_, field_name), \ + &class_name::IRIS_PP_CAT(set_, field_name) \ >{IRIS_PP_STRINGIZE(field_name)}, #define IRIS_MARSHAL_ADAPT(class_name, ...) \ diff --git a/include/iris/marshal/detail/field.hpp b/include/iris/marshal/detail/field.hpp index 7df9f82..2099175 100644 --- a/include/iris/marshal/detail/field.hpp +++ b/include/iris/marshal/detail/field.hpp @@ -9,9 +9,10 @@ namespace iris::marshal::detail { -template +template struct field_definition { + using value_type = T; std::string_view name; }; diff --git a/include/iris/marshal/serialize_json.hpp b/include/iris/marshal/serialize_json.hpp index ecae26a..91acc2d 100644 --- a/include/iris/marshal/serialize_json.hpp +++ b/include/iris/marshal/serialize_json.hpp @@ -12,19 +12,76 @@ #include +#include + #include +#include #include #include namespace iris::marshal::json { -struct format +namespace detail { + +template +struct native_t_impl +{ + using type = T; +}; + +template +struct native_t_impl +{ + using type = std::string; +}; + +} // detail + +struct format : generic_format { template - static constexpr bool key = StringLike>; + 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; @@ -34,6 +91,15 @@ template concept serializable_array = marshal::serializable_array 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; + namespace detail { struct save_fn @@ -68,7 +134,12 @@ struct save_fn auto json_map = nlohmann::json::object(); for (auto const& [key, value] : map) { - save_fn{}(json_map[std::basic_string_view{key}], value); + if constexpr (adapted_proxy, format>) { + auto&& json_key = adapted_proxy_traits, format>::to_native_type(key); + save_fn{}(json_map[json_key], value); + } else { + save_fn{}(json_map[std::basic_string_view{key}], value); + } } out = std::move(json_map); @@ -94,8 +165,8 @@ struct save_fn auto json_map = nlohmann::json::object(); constexpr auto const& fields = adapted_class_traits::fields; - alloy::for_each(fields, [&](marshal::detail::field_definition const& def) { - save_fn{}(json_map[def.name], (klass.*Mem)()); + alloy::for_each(fields, [&](marshal::detail::field_definition const& def) { + save_fn{}(json_map[def.name], (klass.*GetMem)()); }); out = std::move(json_map); @@ -114,7 +185,7 @@ struct save_fn template static void operator()(nlohmann::json& out, ProxyT const& proxy) { - save_fn{}(out, adapted_proxy_traits::to_proxy(proxy)); + save_fn{}(out, adapted_proxy_traits::to_native_type(proxy)); } template @@ -128,16 +199,123 @@ struct save_fn // ---------------------------------------------------- -// TODO struct load_fn { + template + static void operator()(nlohmann::json const& j, T& value) + { + if constexpr (std::is_arithmetic_v) { + value = j.get(); + } else if constexpr (std::is_enum_v) { + value = static_cast(j.get>()); + + } else if constexpr (StringLike) { + using CharT = decltype(std::basic_string_view{value})::value_type; + value = unicode::transcode_ref(j.get_ref()); + + } else { + value = j.get_ref(); + } + } + + template + static void operator()(nlohmann::json const& j, R& arr) + { + R tmp; + for (auto const& elem_json : j) { + load_fn{}(elem_json, tmp.emplace_back()); + } + arr = std::move(tmp); + } + + template + static void operator()(nlohmann::json const& j, MapT& map) + { + MapT tmp; + for (auto const& [json_key, json_value] : j.items()) { + ranges::range_key_t key; + load_fn{}(json_key, key); + + ranges::range_mapped_t value; + load_fn{}(json_value, value); + + if constexpr (ranges::unique_key_value_container) { + tmp.insert_or_assign(std::move(key), std::move(value)); + } else { + tmp.emplace(std::move(key), std::move(value)); + } + } + map = std::move(tmp); + } + + template + static void operator()(nlohmann::json const& j, TupleT& tup) + { + TupleT tmp; + alloy::for_each(tmp, [&](std::in_place_index_t, auto& elem) { + load_fn{}(j.at(I), elem); + }); + tup = std::move(tmp); + } + + template + static void operator()(nlohmann::json const& j, ClassT& klass) + { + ClassT tmp; + constexpr auto const& fields = adapted_class_traits::fields; + alloy::for_each(fields, [&](marshal::detail::field_definition const& def) { + T value; + load_fn{}(j[def.name], value); + (tmp.*SetMem)(std::move(value)); + }); + klass = std::move(tmp); + } + + template + static void operator()(nlohmann::json const& j, OptionalT& opt) + { + if (j.is_null()) { + opt = {}; + } else { + adapted_optional_value_t value; + load_fn{}(j, value); + opt = std::move(value); + } + } + + template + static void operator()(nlohmann::json const& j, ProxyT& proxy) + { + using traits = adapted_proxy_traits; + proxy = traits::from_native_type( + j.get_ref() + ); + } }; } // detail [[maybe_unused]] inline constexpr detail::save_fn save{}; -[[maybe_unused]] inline constexpr detail::load_fn load{}; + + +template + requires requires(nlohmann::json const& j) { + detail::load_fn{}(j, std::declval()); + } +[[nodiscard]] T load(nlohmann::json const& j) +{ + T value; // default-initialize + detail::load_fn{}(j, value); + return value; +} + +template +void load(nlohmann::json const& j, T& value) +{ + static_assert(!std::is_const_v, "cannot deserialize into const variable"); + detail::load_fn{}(j, value); +} } // iris::marshal::json diff --git a/include/iris/marshal/serialize_traits.hpp b/include/iris/marshal/serialize_traits.hpp index 75e7264..4a2a563 100644 --- a/include/iris/marshal/serialize_traits.hpp +++ b/include/iris/marshal/serialize_traits.hpp @@ -25,23 +25,63 @@ 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 key = true; + static constexpr bool loadable_scalar = + std::is_arithmetic_v || + std::is_enum_v || + StringLike; }; -// Customization point: serialize T by converting it to/from proxy_type. -template -struct adapted_proxy_traits; +namespace detail { -template -concept adapted_proxy = requires(std::remove_cvref_t const& v) { - typename adapted_proxy_traits>::proxy_type; - { adapted_proxy_traits>::to_proxy(v) } - -> std::convertible_to>::proxy_type>; +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; }; -template -using adapted_proxy_t = adapted_proxy_traits>::proxy_type; +} // detail + +// Customization point: serialize/deserialize `T` by converting it to/from `native_type`. +template +struct adapted_proxy_traits; + +template +using 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 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(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 @@ -56,6 +96,9 @@ concept adapted_class = requires { 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 @@ -67,12 +110,20 @@ template concept adapted_optional = requires(T const& opt) { typename adapted_optional_traits>::value_type; static_cast(opt); - { *opt } -> std::convertible_to>::value_type const&>; + { *opt } -> std::convertible_to const&>; }; namespace detail { -template +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 @@ -89,35 +140,36 @@ struct is_serializable_scalar : std::true_type // --------------------------------------------------- -template +template [[nodiscard]] consteval bool is_tuple_elements_serializable_impl(); -template +template consteval bool is_serializable_impl() { using V = std::remove_cvref_t; static_assert( - !(adapted_class && adapted_proxy), + !(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>(); + 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::value_type, Format>(); + return is_serializable_impl, Format>(); } else if constexpr (is_serializable_scalar::value) { return true; } else if constexpr (ranges::key_value_range) { - return Format::template key> && - is_serializable_impl, Format>() && - is_serializable_impl, Format>(); + 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>(); @@ -130,7 +182,7 @@ consteval bool is_serializable_impl() } } -template +template consteval bool is_tuple_elements_serializable_impl() { if constexpr (I == alloy::tuple_size_v) { @@ -149,23 +201,27 @@ consteval bool is_tuple_elements_serializable_impl() template concept serializable_proxy = - adapted_proxy && - detail::is_serializable_impl, Format>(); + 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>::value_type, Format>(); + detail::is_serializable_impl, Format>(); template concept serializable_scalar = + detail::marshal_format && !serializable_proxy && !serializable_class && !adapted_optional && @@ -173,17 +229,19 @@ concept serializable_scalar = template concept serializable_map = + detail::marshal_format && !serializable_proxy && !serializable_class && !adapted_optional && !serializable_scalar && ranges::key_value_range && - Format::template key> && + 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 && @@ -194,6 +252,7 @@ concept serializable_array = template concept serializable_tuple = + detail::marshal_format && !serializable_proxy && !serializable_class && !adapted_optional && @@ -204,6 +263,7 @@ concept serializable_tuple = template concept serializable = + detail::marshal_format && serializable_proxy || serializable_class || serializable_optional || @@ -212,6 +272,116 @@ concept serializable = 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::key_value_range) { + return + ranges::key_value_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 = + deserializable_proxy || + deserializable_class || + deserializable_optional || + deserializable_scalar || + deserializable_map || + deserializable_array || + deserializable_tuple; + } // iris::marshal #endif diff --git a/test/marshal.cpp b/test/marshal.cpp index aabe71a..529277e 100644 --- a/test/marshal.cpp +++ b/test/marshal.cpp @@ -7,78 +7,122 @@ #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: serialize (builtin types)") -{ - STATIC_CHECK(!marshal::serializable); - STATIC_CHECK(marshal::serializable_scalar); - STATIC_CHECK(marshal::serializable_scalar); - STATIC_CHECK(marshal::serializable_scalar); - STATIC_CHECK(marshal::serializable_scalar); +TEST_CASE("marshal: type traits (generic_format)") +{ + STATIC_CHECK(marshal::detail::marshal_format); +} - STATIC_CHECK(marshal::serializable_scalar); - STATIC_CHECK(marshal::serializable_scalar); - STATIC_CHECK(marshal::serializable_scalar); - STATIC_CHECK(marshal::serializable_scalar); +TEST_CASE("marshal: type traits (json::format)") +{ + STATIC_CHECK(marshal::detail::marshal_format); - STATIC_CHECK(marshal::serializable_optional>); - STATIC_CHECK(marshal::serializable_optional const>); - STATIC_CHECK(!marshal::serializable>); + // 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::serializable_tuple>); - STATIC_CHECK(!marshal::serializable>); - STATIC_CHECK(marshal::serializable_tuple>); - STATIC_CHECK(!marshal::serializable>); + STATIC_CHECK(marshal::adapted_proxy); + STATIC_CHECK(std::same_as, std::string>); + STATIC_CHECK(marshal::detail::proxy_writable); - STATIC_CHECK(marshal::serializable_array>); - STATIC_CHECK(!marshal::serializable>); + STATIC_CHECK(marshal::adapted_proxy); + STATIC_CHECK(std::same_as, std::string>); + STATIC_CHECK(marshal::detail::proxy_writable); + } +} - STATIC_CHECK(marshal::serializable_map>); - STATIC_CHECK(!marshal::serializable_array>); - STATIC_CHECK(!marshal::serializable>); +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_map>); STATIC_CHECK(!json::serializable_map>); - STATIC_CHECK(json::serializable_map>>); + STATIC_CHECK(!json::serializable_array>); + STATIC_CHECK(!json::serializable>); + + //IRIS_CHECK_SERIALIZABLE(map, std::vector>); // TODO STATIC_CHECK(!json::serializable_map>>); - { - nlohmann::json j; - int const value = 42; - json::save(j, value); - CHECK(j.get() == value); - } +#undef IRIS_CHECK_SERIALIZABLE { nlohmann::json j; - std::optional const value = 42; + int const value = 42; json::save(j, value); CHECK(j.get() == value); - } - - { - nlohmann::json j; - std::vector> const value = {42, std::nullopt, 44}; - json::save(j, value); - CHECK(j.get>>() == value); + CHECK(json::load(j) == value); } { @@ -86,25 +130,29 @@ TEST_CASE("marshal: serialize (builtin types)") 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 value = "foo"; - json::save(j, value); - CHECK(j.get() == value); + std::string const str = "foo"; + json::save(j, str); + CHECK(j.get() == str); + CHECK(json::load(j) == str); } { nlohmann::json j; - std::u32string const value = U"あいう"; - json::save(j, value); - CHECK(j.get() == value); + std::u32string const str = U"あいう"; + json::save(j, str); + CHECK(iris::to_u32string_ref(j.get()) == str); + CHECK(json::load(j) == str); } { @@ -112,6 +160,20 @@ TEST_CASE("marshal: serialize (builtin types)") 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); } { @@ -119,6 +181,26 @@ TEST_CASE("marshal: serialize (builtin types)") 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); } { @@ -126,20 +208,82 @@ TEST_CASE("marshal: serialize (builtin types)") 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::pair const value{0, 1}; - json::save(j, value); - CHECK(j.get>() == value); + 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::tuple const value{0, 1, 2}; - json::save(j, value); - CHECK(j.get>() == value); + 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); } } @@ -159,6 +303,24 @@ IRIS_MARSHAL_ADAPT( MyData, name, text, age, enabled ); +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); @@ -182,16 +344,18 @@ TEST_CASE("marshal: serialize (class type)") } { - nlohmann::json json; + nlohmann::json j; MyData my_data; my_data.set_name("foo"); my_data.set_age(42); my_data.set_enabled(true); - json::save(json, my_data); + 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.at("name").get() == "foo"sv); - CHECK(json.at("text").get() == "empty text"sv); - CHECK(json.at("age").get() == 42); - CHECK(json.at("enabled").get() == true); + CHECK(json::load(j) == my_data); } } From 754ec8270a1546d6c92be7913083830d6fc919c2 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:56:12 +0900 Subject: [PATCH 13/22] Fix handling of `vector>` --- include/iris/marshal/serialize_json.hpp | 2 +- include/iris/marshal/serialize_traits.hpp | 10 ++++----- test/marshal.cpp | 27 +++++++++++++++++++++-- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/include/iris/marshal/serialize_json.hpp b/include/iris/marshal/serialize_json.hpp index 91acc2d..6919fe4 100644 --- a/include/iris/marshal/serialize_json.hpp +++ b/include/iris/marshal/serialize_json.hpp @@ -240,7 +240,7 @@ struct load_fn ranges::range_mapped_t value; load_fn{}(json_value, value); - if constexpr (ranges::unique_key_value_container) { + if constexpr (ranges::unique_mapping_container) { tmp.insert_or_assign(std::move(key), std::move(value)); } else { tmp.emplace(std::move(key), std::move(value)); diff --git a/include/iris/marshal/serialize_traits.hpp b/include/iris/marshal/serialize_traits.hpp index 4a2a563..ed88362 100644 --- a/include/iris/marshal/serialize_traits.hpp +++ b/include/iris/marshal/serialize_traits.hpp @@ -165,7 +165,7 @@ consteval bool is_serializable_impl() } else if constexpr (is_serializable_scalar::value) { return true; - } else if constexpr (ranges::key_value_range) { + } else if constexpr (ranges::mapping_range) { return Format::template map_key> && is_serializable_impl, Format>() && @@ -234,7 +234,7 @@ concept serializable_map = !serializable_class && !adapted_optional && !serializable_scalar && - ranges::key_value_range && + ranges::mapping_range && Format::template map_key> && detail::is_serializable_impl, Format>() && detail::is_serializable_impl, Format>(); @@ -246,7 +246,7 @@ concept serializable_array = !serializable_class && !adapted_optional && !serializable_scalar && - !ranges::key_value_range && + !ranges::mapping_range && std::ranges::input_range && detail::is_serializable_impl, Format>(); @@ -309,9 +309,9 @@ consteval bool is_deserializable_impl() } else if constexpr (is_serializable_scalar::value) { return Format::template loadable_scalar; - } else if constexpr (ranges::key_value_range) { + } else if constexpr (ranges::mapping_range) { return - ranges::key_value_container && + ranges::mapping_container && Format::template loadable_key> && loadable, Format> && loadable, Format>; diff --git a/test/marshal.cpp b/test/marshal.cpp index 529277e..0d49274 100644 --- a/test/marshal.cpp +++ b/test/marshal.cpp @@ -112,8 +112,17 @@ TEST_CASE("marshal: serialize (builtin types)") STATIC_CHECK(!json::serializable_array>); STATIC_CHECK(!json::serializable>); - //IRIS_CHECK_SERIALIZABLE(map, std::vector>); // TODO - STATIC_CHECK(!json::serializable_map>>); + // 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 @@ -176,6 +185,20 @@ TEST_CASE("marshal: serialize (builtin types)") 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}}; From 0fb82cfe5f732428c7cf980cf7c8ae6c0b7fb337 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:49:06 +0900 Subject: [PATCH 14/22] Make serializer generic --- include/iris/marshal/serialize.hpp | 123 +++++++++++++++++++ include/iris/marshal/serialize_json.hpp | 136 +++++++--------------- include/iris/marshal/serialize_traits.hpp | 14 +-- test/marshal.cpp | 8 +- 4 files changed, 177 insertions(+), 104 deletions(-) create mode 100644 include/iris/marshal/serialize.hpp diff --git a/include/iris/marshal/serialize.hpp b/include/iris/marshal/serialize.hpp new file mode 100644 index 0000000..0cdb178 --- /dev/null +++ b/include/iris/marshal/serialize.hpp @@ -0,0 +1,123 @@ +#ifndef IRIS_ZZ_MARSHAL_SERIALIZE_HPP +#define IRIS_ZZ_MARSHAL_SERIALIZE_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include +#include + +#include + +#include + +namespace iris::marshal { + +template +concept writer = + requires { + typename W::format; + } && + requires(W& w, std::string_view map_key) { + w.null(); + w.begin_array(); + w.end_array(); + w.begin_object(); + w.map_key(map_key); + w.end_object(); + }; + +template +struct basic_save_fn +{ + using format = W::format; + + template T> + requires requires { + typename W::result_type; + } && std::constructible_from + static constexpr void operator()(W::result_type& res, T const& value) + { + W w{res}; + basic_save_fn{}(w, value); + } + + template T> + requires requires(W& w, T const& value) { w.scalar(value); } + static constexpr void operator()(W& w, T const& value) + { + w.scalar(value); + } + + template OptionalT> + static constexpr void operator()(W& w, OptionalT const& opt) + { + if (opt) { + basic_save_fn{}(w, *opt); + } else { + w.null(); + } + } + + template ProxyT> + static constexpr void operator()(W& w, ProxyT const& proxy) + { + basic_save_fn{}(w, adapted_proxy_traits::to_native_type(proxy)); + } + + template R> + static constexpr void operator()(W& w, R const& arr) + { + w.begin_array(); + for (auto const& elem : arr) { + basic_save_fn{}(w, elem); + } + w.end_array(); + } + + template MapT> + static constexpr void operator()(W& w, MapT const& map) + { + w.begin_object(); + for (auto const& [k, v] : map) { + if constexpr (adapted_proxy, format>) { + w.map_key(adapted_proxy_traits, format>::to_native_type(k)); + } else { + w.map_key(k); + } + basic_save_fn{}(w, v); + } + w.end_object(); + } + + template TupleT> + static constexpr void operator()(W& w, TupleT const& tup) + { + w.begin_array(); + alloy::for_each(tup, [&](auto const& elem) { + basic_save_fn{}(w, elem); + }); + w.end_array(); + } + + template ClassT> + static constexpr void operator()(W& w, ClassT const& klass) + { + w.begin_object(); + constexpr auto const& fields = adapted_class_traits::fields; + alloy::for_each(fields, [&](detail::field_definition const& def) { + if constexpr (adapted_proxy) { + w.map_key(adapted_proxy_traits::to_native_type(def.name)); + } else { + w.map_key(def.name); + } + basic_save_fn{}(w, (klass.*GetMem)()); + }); + w.end_object(); + } +}; + +} // iris::marshal + +#endif diff --git a/include/iris/marshal/serialize_json.hpp b/include/iris/marshal/serialize_json.hpp index 6919fe4..ac12f93 100644 --- a/include/iris/marshal/serialize_json.hpp +++ b/include/iris/marshal/serialize_json.hpp @@ -5,7 +5,7 @@ #include // IWYU pragma: keep -#include +#include #include #include @@ -16,28 +16,13 @@ #include +#include #include #include #include namespace iris::marshal::json { -namespace detail { - -template -struct native_t_impl -{ - using type = T; -}; - -template -struct native_t_impl -{ - using type = std::string; -}; - -} // detail - struct format : generic_format { template @@ -100,104 +85,70 @@ template concept deserializable_array = marshal::deserializable_arra template concept deserializable_tuple = marshal::deserializable_tuple; template concept deserializable = marshal::deserializable; -namespace detail { - -struct save_fn +class dom_writer { - template - static void operator()(nlohmann::json& out, T const& value) - { - if constexpr (std::is_enum_v) { - out = std::to_underlying(value); - } else { - out = value; - } - } + nlohmann::json& root_; + std::vector stack_; + std::string pending_key_; - template - static void operator()(nlohmann::json& out, R const& arr) + [[nodiscard]] nlohmann::json& slot() { - auto json_arr = nlohmann::json::array(); + if (stack_.empty()) return root_; + auto& parent = *stack_.back(); + return parent.is_object() ? parent[pending_key_] : parent.emplace_back(); + } - for (auto const& elem : arr) { - nlohmann::json elem_json; - save_fn{}(elem_json, elem); - json_arr.emplace_back(std::move(elem_json)); - } +public: + using format = json::format; + using result_type = nlohmann::json; - out = std::move(json_arr); - } + explicit dom_writer(nlohmann::json& root) + : root_(root) + {} - template - static void operator()(nlohmann::json& out, MapT const& map) + template + requires format::loadable_scalar + void scalar(T const& value) { - auto json_map = nlohmann::json::object(); - - for (auto const& [key, value] : map) { - if constexpr (adapted_proxy, format>) { - auto&& json_key = adapted_proxy_traits, format>::to_native_type(key); - save_fn{}(json_map[json_key], value); - } else { - save_fn{}(json_map[std::basic_string_view{key}], value); - } + if constexpr (std::is_enum_v) { + slot() = std::to_underlying(value); + } else { + slot() = value; } - - out = std::move(json_map); } - template - static void operator()(nlohmann::json& out, TupleT const& tup) + void null() { - auto json_arr = nlohmann::json::array(); - - alloy::for_each(tup, [&](auto const& elem) { - nlohmann::json elem_json; - save_fn{}(elem_json, elem); - json_arr.emplace_back(std::move(elem_json)); - }); - - out = std::move(json_arr); + slot() = nullptr; } - template - static void operator()(nlohmann::json& out, ClassT const& klass) + void begin_array() { - auto json_map = nlohmann::json::object(); - - constexpr auto const& fields = adapted_class_traits::fields; - alloy::for_each(fields, [&](marshal::detail::field_definition const& def) { - save_fn{}(json_map[def.name], (klass.*GetMem)()); - }); - - out = std::move(json_map); + stack_.push_back(&(slot() = nlohmann::json::array())); } - - template - static void operator()(nlohmann::json& out, OptionalT const& opt) + void end_array() { - if (opt) { - save_fn{}(out, *opt); - } else { - out = nullptr; - } + stack_.pop_back(); } - template - static void operator()(nlohmann::json& out, ProxyT const& proxy) + void begin_object() { - save_fn{}(out, adapted_proxy_traits::to_native_type(proxy)); + stack_.push_back(&(slot() = nlohmann::json::object())); } - - template - [[nodiscard]] static nlohmann::json operator()(T const& value) + void map_key(std::string_view k) + { + pending_key_.assign(k); + } + void end_object() { - nlohmann::json out; - save_fn{}(out, value); - return out; + stack_.pop_back(); } }; -// ---------------------------------------------------- +[[maybe_unused]] inline constexpr basic_save_fn save{}; + + +namespace detail { struct load_fn { @@ -296,9 +247,6 @@ struct load_fn } // detail -[[maybe_unused]] inline constexpr detail::save_fn save{}; - - template requires requires(nlohmann::json const& j) { detail::load_fn{}(j, std::declval()); diff --git a/include/iris/marshal/serialize_traits.hpp b/include/iris/marshal/serialize_traits.hpp index ed88362..d31d64c 100644 --- a/include/iris/marshal/serialize_traits.hpp +++ b/include/iris/marshal/serialize_traits.hpp @@ -56,15 +56,15 @@ template struct adapted_proxy_traits; template -using proxy_native_type_t = adapted_proxy_traits, Format>::native_type; +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 proxy_native_type_t; + typename adapted_proxy_native_type_t; { adapted_proxy_traits, Format>::to_native_type(v) } - -> std::convertible_to>; + -> std::convertible_to>; }; namespace detail { @@ -73,7 +73,7 @@ template concept proxy_writable = detail::marshal_format && adapted_proxy && - requires(proxy_native_type_t p) { + requires(adapted_proxy_native_type_t p) { { adapted_proxy_traits, Format>::from_native_type(std::move(p)) } -> std::convertible_to>; } && @@ -154,7 +154,7 @@ consteval bool is_serializable_impl() ); if constexpr (adapted_proxy) { - return is_serializable_impl, Format>(); + return is_serializable_impl, Format>(); } else if constexpr (adapted_class) { return true; @@ -203,7 +203,7 @@ template concept serializable_proxy = detail::marshal_format && adapted_proxy && - detail::is_serializable_impl, Format>(); + detail::is_serializable_impl, Format>(); template concept serializable_class = @@ -298,7 +298,7 @@ consteval bool is_deserializable_impl() return false; } else if constexpr (adapted_proxy) { - return loadable, Format> && proxy_writable; + return loadable, Format> && proxy_writable; } else if constexpr (adapted_class) { return true; diff --git a/test/marshal.cpp b/test/marshal.cpp index 0d49274..5623782 100644 --- a/test/marshal.cpp +++ b/test/marshal.cpp @@ -33,11 +33,13 @@ 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 { @@ -48,15 +50,15 @@ TEST_CASE("marshal: type traits (json::format)") // CharT = char32_t { STATIC_CHECK(marshal::adapted_proxy); - STATIC_CHECK(std::same_as, std::string>); + 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(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(std::same_as, std::string>); STATIC_CHECK(marshal::detail::proxy_writable); } } From fde4389d69e9a847ed8a3bfd6427cc4992aa1890 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:31:19 +0900 Subject: [PATCH 15/22] Fix parenthesis --- include/iris/marshal/serialize_traits.hpp | 33 +++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/include/iris/marshal/serialize_traits.hpp b/include/iris/marshal/serialize_traits.hpp index d31d64c..ee098f5 100644 --- a/include/iris/marshal/serialize_traits.hpp +++ b/include/iris/marshal/serialize_traits.hpp @@ -264,13 +264,15 @@ concept serializable_tuple = template concept serializable = detail::marshal_format && - serializable_proxy || - serializable_class || - serializable_optional || - serializable_scalar || - serializable_map || - serializable_array || - serializable_tuple; + ( + serializable_proxy || + serializable_class || + serializable_optional || + serializable_scalar || + serializable_map || + serializable_array || + serializable_tuple + ); // --------------------------------------------------- @@ -374,13 +376,16 @@ concept deserializable_tuple = template concept deserializable = - deserializable_proxy || - deserializable_class || - deserializable_optional || - deserializable_scalar || - deserializable_map || - deserializable_array || - deserializable_tuple; + detail::marshal_format && + ( + deserializable_proxy || + deserializable_class || + deserializable_optional || + deserializable_scalar || + deserializable_map || + deserializable_array || + deserializable_tuple + ); } // iris::marshal From cbd9c544aaf10e2509089f2e5fa4a799ecee8bff Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:35:38 +0900 Subject: [PATCH 16/22] Make abbreviation longer --- include/iris/marshal/serialize.hpp | 88 +++++++++++++++--------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/include/iris/marshal/serialize.hpp b/include/iris/marshal/serialize.hpp index 0cdb178..ee4e3ae 100644 --- a/include/iris/marshal/serialize.hpp +++ b/include/iris/marshal/serialize.hpp @@ -14,107 +14,107 @@ namespace iris::marshal { -template +template concept writer = requires { - typename W::format; + typename WriterT::format; } && - requires(W& w, std::string_view map_key) { - w.null(); - w.begin_array(); - w.end_array(); - w.begin_object(); - w.map_key(map_key); - w.end_object(); + 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 +template struct basic_save_fn { - using format = W::format; + using format = WriterT::format; template T> requires requires { - typename W::result_type; - } && std::constructible_from - static constexpr void operator()(W::result_type& res, T const& value) + typename WriterT::result_type; + } && std::constructible_from + static constexpr void operator()(WriterT::result_type& res, T const& value) { - W w{res}; - basic_save_fn{}(w, value); + WriterT wr{res}; + basic_save_fn{}(wr, value); } template T> - requires requires(W& w, T const& value) { w.scalar(value); } - static constexpr void operator()(W& w, T const& value) + requires requires(WriterT& wr, T const& value) { wr.scalar(value); } + static constexpr void operator()(WriterT& wr, T const& value) { - w.scalar(value); + wr.scalar(value); } template OptionalT> - static constexpr void operator()(W& w, OptionalT const& opt) + static constexpr void operator()(WriterT& wr, OptionalT const& opt) { if (opt) { - basic_save_fn{}(w, *opt); + basic_save_fn{}(wr, *opt); } else { - w.null(); + wr.null(); } } template ProxyT> - static constexpr void operator()(W& w, ProxyT const& proxy) + static constexpr void operator()(WriterT& wr, ProxyT const& proxy) { - basic_save_fn{}(w, adapted_proxy_traits::to_native_type(proxy)); + basic_save_fn{}(wr, adapted_proxy_traits::to_native_type(proxy)); } template R> - static constexpr void operator()(W& w, R const& arr) + static constexpr void operator()(WriterT& wr, R const& arr) { - w.begin_array(); + wr.begin_array(); for (auto const& elem : arr) { - basic_save_fn{}(w, elem); + basic_save_fn{}(wr, elem); } - w.end_array(); + wr.end_array(); } template MapT> - static constexpr void operator()(W& w, MapT const& map) + static constexpr void operator()(WriterT& wr, MapT const& map) { - w.begin_object(); + wr.begin_object(); for (auto const& [k, v] : map) { if constexpr (adapted_proxy, format>) { - w.map_key(adapted_proxy_traits, format>::to_native_type(k)); + wr.map_key(adapted_proxy_traits, format>::to_native_type(k)); } else { - w.map_key(k); + wr.map_key(k); } - basic_save_fn{}(w, v); + basic_save_fn{}(wr, v); } - w.end_object(); + wr.end_object(); } template TupleT> - static constexpr void operator()(W& w, TupleT const& tup) + static constexpr void operator()(WriterT& wr, TupleT const& tup) { - w.begin_array(); + wr.begin_array(); alloy::for_each(tup, [&](auto const& elem) { - basic_save_fn{}(w, elem); + basic_save_fn{}(wr, elem); }); - w.end_array(); + wr.end_array(); } template ClassT> - static constexpr void operator()(W& w, ClassT const& klass) + static constexpr void operator()(WriterT& wr, ClassT const& klass) { - w.begin_object(); + wr.begin_object(); constexpr auto const& fields = adapted_class_traits::fields; alloy::for_each(fields, [&](detail::field_definition const& def) { if constexpr (adapted_proxy) { - w.map_key(adapted_proxy_traits::to_native_type(def.name)); + wr.map_key(adapted_proxy_traits::to_native_type(def.name)); } else { - w.map_key(def.name); + wr.map_key(def.name); } - basic_save_fn{}(w, (klass.*GetMem)()); + basic_save_fn{}(wr, (klass.*GetMem)()); }); - w.end_object(); + wr.end_object(); } }; From 61ed33148c8bbc2e7bee57eb0b7d7198bb5636a5 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:02:26 +0900 Subject: [PATCH 17/22] Re-implement generic version of loader WIP --- include/iris/marshal/serialize.hpp | 254 +++++++++++++++++++++++- include/iris/marshal/serialize_json.hpp | 4 +- 2 files changed, 253 insertions(+), 5 deletions(-) diff --git a/include/iris/marshal/serialize.hpp b/include/iris/marshal/serialize.hpp index ee4e3ae..953a180 100644 --- a/include/iris/marshal/serialize.hpp +++ b/include/iris/marshal/serialize.hpp @@ -11,9 +11,16 @@ #include #include +#include namespace iris::marshal { +struct load_error : std::runtime_error +{ + using std::runtime_error::runtime_error; +}; + + template concept writer = requires { @@ -35,9 +42,9 @@ struct basic_save_fn template T> requires requires { - typename WriterT::result_type; - } && std::constructible_from - static constexpr void operator()(WriterT::result_type& res, T const& value) + 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); @@ -118,6 +125,247 @@ struct basic_save_fn } }; + +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)) { + T tmp{}; + basic_load_fn{}(member_rd, tmp); + (klass.*SetMem)(std::move(tmp)); + + seen.set(index); + matched = true; + } + ++index; + }); + // unmatched map_key: ignored (policy) + }); + + if (!ok) { + throw load_error{"object: kind mismatch"}; + } + + std::size_t index = 0; + alloy::for_each(fields, [&](detail::field_definition const& def) { + if (!seen.test(index)) { + if constexpr (requires { def.default_value; }) { + (klass.*SetMem)(def.default_value); + } else { + 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 index ac12f93..3738935 100644 --- a/include/iris/marshal/serialize_json.hpp +++ b/include/iris/marshal/serialize_json.hpp @@ -100,7 +100,7 @@ class dom_writer public: using format = json::format; - using result_type = nlohmann::json; + using document_type = nlohmann::json; explicit dom_writer(nlohmann::json& root) : root_(root) @@ -204,7 +204,7 @@ struct load_fn static void operator()(nlohmann::json const& j, TupleT& tup) { TupleT tmp; - alloy::for_each(tmp, [&](std::in_place_index_t, auto& elem) { + alloy::for_each(tmp, [&](std::integral_constant, auto& elem) { load_fn{}(j.at(I), elem); }); tup = std::move(tmp); From 51546f83f7fcb57199d6f11a515ac9da55a35cdb Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:25:35 +0900 Subject: [PATCH 18/22] Implement generic version of json reader --- include/iris/marshal/serialize.hpp | 4 +- include/iris/marshal/serialize_json.hpp | 178 ++++++++++-------------- 2 files changed, 78 insertions(+), 104 deletions(-) diff --git a/include/iris/marshal/serialize.hpp b/include/iris/marshal/serialize.hpp index 953a180..7984baa 100644 --- a/include/iris/marshal/serialize.hpp +++ b/include/iris/marshal/serialize.hpp @@ -317,7 +317,9 @@ struct basic_load_fn static constexpr void operator()(ReaderT& rd, ClassT& klass) { constexpr auto const& fields = adapted_class_traits::fields; - std::bitset seen; + + std::bitset>> + seen; bool const ok = rd.object([&](std::string_view map_key, ReaderT& member_rd) { std::size_t index = 0; diff --git a/include/iris/marshal/serialize_json.hpp b/include/iris/marshal/serialize_json.hpp index 3738935..0d32a43 100644 --- a/include/iris/marshal/serialize_json.hpp +++ b/include/iris/marshal/serialize_json.hpp @@ -6,18 +6,16 @@ #include // IWYU pragma: keep #include -#include -#include +#include #include -#include - #include #include #include +#include #include #include @@ -87,27 +85,16 @@ template concept deserializable = marshal::deserializable stack_; - std::string pending_key_; - - [[nodiscard]] nlohmann::json& slot() - { - if (stack_.empty()) return root_; - auto& parent = *stack_.back(); - return parent.is_object() ? parent[pending_key_] : parent.emplace_back(); - } - public: using format = json::format; using document_type = nlohmann::json; - explicit dom_writer(nlohmann::json& root) - : root_(root) + explicit dom_writer(document_type& node) + : node_(&node) {} template - requires format::loadable_scalar + requires format::template loadable_scalar void scalar(T const& value) { if constexpr (std::is_enum_v) { @@ -124,7 +111,7 @@ class dom_writer void begin_array() { - stack_.push_back(&(slot() = nlohmann::json::array())); + stack_.push_back(&(slot() = document_type::array())); } void end_array() { @@ -133,7 +120,7 @@ class dom_writer void begin_object() { - stack_.push_back(&(slot() = nlohmann::json::object())); + stack_.push_back(&(slot() = document_type::object())); } void map_key(std::string_view k) { @@ -143,126 +130,111 @@ class dom_writer { 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{}; -namespace detail { - -struct load_fn +class dom_reader { - template - static void operator()(nlohmann::json const& j, T& value) - { - if constexpr (std::is_arithmetic_v) { - value = j.get(); - - } else if constexpr (std::is_enum_v) { - value = static_cast(j.get>()); - - } else if constexpr (StringLike) { - using CharT = decltype(std::basic_string_view{value})::value_type; - value = unicode::transcode_ref(j.get_ref()); +public: + using format = json::format; + using document_type = nlohmann::json; - } else { - value = j.get_ref(); - } - } + explicit dom_reader(document_type const& node) + : node_{&node} + {} - template - static void operator()(nlohmann::json const& j, R& arr) + [[nodiscard]] bool is_null() const { - R tmp; - for (auto const& elem_json : j) { - load_fn{}(elem_json, tmp.emplace_back()); - } - arr = std::move(tmp); + return node_->is_null(); } - template - static void operator()(nlohmann::json const& j, MapT& map) + template + requires format::template loadable_scalar + [[nodiscard]] bool scalar(T& value) const { - MapT tmp; - for (auto const& [json_key, json_value] : j.items()) { - ranges::range_key_t key; - load_fn{}(json_key, key); - - ranges::range_mapped_t value; - load_fn{}(json_value, value); - - if constexpr (ranges::unique_mapping_container) { - tmp.insert_or_assign(std::move(key), std::move(value)); - } else { - tmp.emplace(std::move(key), std::move(value)); - } + 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()}; } - map = std::move(tmp); + return true; } - template - static void operator()(nlohmann::json const& j, TupleT& tup) + template + [[nodiscard]] bool array(VisitorT&& vis) const { - TupleT tmp; - alloy::for_each(tmp, [&](std::integral_constant, auto& elem) { - load_fn{}(j.at(I), elem); - }); - tup = std::move(tmp); - } + if (!node_->is_array()) return false; - template - static void operator()(nlohmann::json const& j, ClassT& klass) - { - ClassT tmp; - constexpr auto const& fields = adapted_class_traits::fields; - alloy::for_each(fields, [&](marshal::detail::field_definition const& def) { - T value; - load_fn{}(j[def.name], value); - (tmp.*SetMem)(std::move(value)); - }); - klass = std::move(tmp); + for (auto const& elem : *node_) { + dom_reader elem_rd{elem}; + vis(elem_rd); + } + return true; } - template - static void operator()(nlohmann::json const& j, OptionalT& opt) + template + [[nodiscard]] bool object(VisitorT&& vis) const { - if (j.is_null()) { - opt = {}; - } else { - adapted_optional_value_t value; - load_fn{}(j, value); - opt = std::move(value); + 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; } - template - static void operator()(nlohmann::json const& j, ProxyT& proxy) - { - using traits = adapted_proxy_traits; - proxy = traits::from_native_type( - j.get_ref() - ); - } +private: + document_type const* node_; }; -} // detail - template requires requires(nlohmann::json const& j) { - detail::load_fn{}(j, std::declval()); + basic_load_fn{}(j, std::declval()); } [[nodiscard]] T load(nlohmann::json const& j) { T value; // default-initialize - detail::load_fn{}(j, value); + basic_load_fn{}(j, value); return value; } template void load(nlohmann::json const& j, T& value) { - static_assert(!std::is_const_v, "cannot deserialize into const variable"); - detail::load_fn{}(j, value); + basic_load_fn{}(j, value); } } // iris::marshal::json From b21b212514bb91b5911a2d1cf40aef5bbbed9732 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:30:28 +0900 Subject: [PATCH 19/22] Adapt plain member variable (without function getter/setter) --- include/iris/marshal/adapt.hpp | 24 +++++++++++++++++++++--- include/iris/marshal/detail/field.hpp | 23 +++++++++++++++++++++-- include/iris/marshal/serialize.hpp | 8 ++++---- test/marshal.cpp | 4 +++- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/include/iris/marshal/adapt.hpp b/include/iris/marshal/adapt.hpp index 99657d1..2f32c99 100644 --- a/include/iris/marshal/adapt.hpp +++ b/include/iris/marshal/adapt.hpp @@ -138,11 +138,29 @@ using setter_param_t = std::conditional_t< // ------------------------------------------------------------ +// Select `decltype(class_name::field_name)` if it exists; otherwise `decltype(class_name::field_name##_)` +#define IRIS_ZZ_MARSHAL_SELECT_MEMBER(class_name, field_name) []() consteval { \ + if constexpr (requires { &ClassT::field_name; }) { \ + return std::type_identity{}; \ + } else { \ + return std::type_identity{}; \ + } \ + }.template operator()() + +// 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< \ - decltype(class_name::IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(field_name)), \ - &class_name::IRIS_PP_CAT(get_, field_name), \ - &class_name::IRIS_PP_CAT(set_, field_name) \ + typename decltype(IRIS_ZZ_MARSHAL_SELECT_MEMBER(class_name, field_name))::type, \ + 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_MARSHAL_ADAPT(class_name, ...) \ diff --git a/include/iris/marshal/detail/field.hpp b/include/iris/marshal/detail/field.hpp index 2099175..35b9234 100644 --- a/include/iris/marshal/detail/field.hpp +++ b/include/iris/marshal/detail/field.hpp @@ -6,14 +6,33 @@ #include // IWYU pragma: keep #include +#include +#include +#include namespace iris::marshal::detail { -template +template struct field_definition { - using value_type = T; + 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 diff --git a/include/iris/marshal/serialize.hpp b/include/iris/marshal/serialize.hpp index 7984baa..f47a6b0 100644 --- a/include/iris/marshal/serialize.hpp +++ b/include/iris/marshal/serialize.hpp @@ -119,7 +119,7 @@ struct basic_save_fn } else { wr.map_key(def.name); } - basic_save_fn{}(wr, (klass.*GetMem)()); + basic_save_fn{}(wr, def.get(klass)); }); wr.end_object(); } @@ -325,11 +325,11 @@ struct basic_load_fn std::size_t index = 0; bool matched = false; - alloy::for_each(fields, [&](detail::field_definition const& def) { + alloy::for_each(fields, [&](detail::field_definition const& def) { if (!matched && basic_load_fn::field_name_matches(def.name, map_key)) { - T tmp{}; + FieldT tmp{}; basic_load_fn{}(member_rd, tmp); - (klass.*SetMem)(std::move(tmp)); + def.set(klass, std::move(tmp)); seen.set(index); matched = true; diff --git a/test/marshal.cpp b/test/marshal.cpp index 5623782..489e89f 100644 --- a/test/marshal.cpp +++ b/test/marshal.cpp @@ -322,10 +322,12 @@ struct MyData 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 + MyData, name, text, age, enabled, public_mem_str ); template<> From f19d31682eb754d8298dc34dd5131fff1ede3d8d Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:36:48 +0900 Subject: [PATCH 20/22] Refactor --- include/iris/marshal/serialize.hpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/include/iris/marshal/serialize.hpp b/include/iris/marshal/serialize.hpp index f47a6b0..ef026bc 100644 --- a/include/iris/marshal/serialize.hpp +++ b/include/iris/marshal/serialize.hpp @@ -113,7 +113,7 @@ struct basic_save_fn { wr.begin_object(); constexpr auto const& fields = adapted_class_traits::fields; - alloy::for_each(fields, [&](detail::field_definition const& def) { + 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 { @@ -336,7 +336,7 @@ struct basic_load_fn } ++index; }); - // unmatched map_key: ignored (policy) + // unmatched `map_key` is ignored ... }); if (!ok) { @@ -344,13 +344,9 @@ struct basic_load_fn } std::size_t index = 0; - alloy::for_each(fields, [&](detail::field_definition const& def) { + alloy::for_each(fields, [&](auto&& /* def */) { if (!seen.test(index)) { - if constexpr (requires { def.default_value; }) { - (klass.*SetMem)(def.default_value); - } else { - throw load_error{"object: missing required field"}; - } + throw load_error{"object: missing required field"}; } ++index; }); From baa8c93328993dc897259f830a121d9a1a2dae57 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:46:09 +0900 Subject: [PATCH 21/22] Add pimpl related facilities --- include/iris/marshal/adapt.hpp | 211 ++++++++++++++++++++++++--------- include/iris/pp/arg.hpp | 4 +- test/marshal.cpp | 41 +++++++ 3 files changed, 200 insertions(+), 56 deletions(-) diff --git a/include/iris/marshal/adapt.hpp b/include/iris/marshal/adapt.hpp index 2f32c99..2515852 100644 --- a/include/iris/marshal/adapt.hpp +++ b/include/iris/marshal/adapt.hpp @@ -11,6 +11,7 @@ #include // IWYU pragma: export #include +#include #include #include #include @@ -51,101 +52,186 @@ using setter_param_t = std::conditional_t< } // 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(maybe_paren_type, field_name) \ - public: \ - [[nodiscard]] constexpr iris::marshal::detail::getter_return_t get_ ## field_name () const noexcept \ - { \ - return IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(field_name); \ - } \ - -#define IRIS_ZZ_MARSHAL_FIELD_GETTER_BOOL(maybe_paren_type, field_name) \ - public: \ - [[nodiscard]] constexpr bool is_ ## field_name () const noexcept \ - { \ - return IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(field_name); \ - } \ +#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); \ + }) \ \ - private: \ - [[nodiscard]] constexpr bool get_ ## field_name () const noexcept \ - { \ - return IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(field_name); \ - } - -#define IRIS_ZZ_MARSHAL_FIELD_SETTER(maybe_paren_type, field_name) \ - public: \ - constexpr void set_ ## field_name (iris::marshal::detail::setter_param_t new_value) \ + 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) \ - { \ - IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER_NAME(field_name) = new_value; \ - } - + 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(maybe_paren_type, field_name, default_value) \ - IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, default_value) \ - IRIS_ZZ_MARSHAL_FIELD_GETTER_BOOL(maybe_paren_type, field_name) // Default value for bool field is mandatory because it is error prone if omitted -#define IRIS_ZZ_MARSHAL_FIELD_BOOL_GET_SET(maybe_paren_type, field_name, default_value) \ - IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, default_value) \ - IRIS_ZZ_MARSHAL_FIELD_GETTER_BOOL(maybe_paren_type, field_name) \ - IRIS_ZZ_MARSHAL_FIELD_SETTER(maybe_paren_type, field_name) +#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(maybe_paren_type, field_name, default_value) \ - IRIS_ZZ_MARSHAL_FIELD_BOOL_GET_SET(maybe_paren_type, field_name, default_value) +#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(maybe_paren_type, field_name, ...) \ - IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ - IRIS_ZZ_MARSHAL_FIELD_GETTER(maybe_paren_type, field_name) +#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, __VA_ARGS__) + ) (~, 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(maybe_paren_type, field_name, ...) \ - IRIS_ZZ_MARSHAL_FIELD_DATA_MEMBER(maybe_paren_type, field_name, __VA_ARGS__) \ - IRIS_ZZ_MARSHAL_FIELD_GETTER(maybe_paren_type, field_name) \ - IRIS_ZZ_MARSHAL_FIELD_SETTER(maybe_paren_type, field_name) +#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, __VA_ARGS__) + ) (~, 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) \ - template \ - friend struct ::iris::marshal::adapted_class_traits; \ + 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; \ \ - [[nodiscard]] constexpr bool operator==(class_name const&) const noexcept = default; + 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) -// Select `decltype(class_name::field_name)` if it exists; otherwise `decltype(class_name::field_name##_)` -#define IRIS_ZZ_MARSHAL_SELECT_MEMBER(class_name, field_name) []() consteval { \ +#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{}; \ + return std::type_identity()))>>{}; \ } \ - }.template operator()() + }.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 { \ @@ -158,24 +244,39 @@ using setter_param_t = std::conditional_t< #define IRIS_ZZ_MARSHAL_ADAPT_FIELD(field_name, class_name) \ ::iris::marshal::detail::field_definition< \ - typename decltype(IRIS_ZZ_MARSHAL_SELECT_MEMBER(class_name, field_name))::type, \ + 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_MARSHAL_ADAPT(class_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( \ - IRIS_PP_TUPLE_TO_SEQ((__VA_ARGS__)), \ + 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/pp/arg.hpp b/include/iris/pp/arg.hpp index 079ee3c..f2ef6d9 100644 --- a/include/iris/pp/arg.hpp +++ b/include/iris/pp/arg.hpp @@ -5,7 +5,9 @@ #include -#define IRIS_PP_IDENTITY(x) x +#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__) diff --git a/test/marshal.cpp b/test/marshal.cpp index 489e89f..42ba653 100644 --- a/test/marshal.cpp +++ b/test/marshal.cpp @@ -386,3 +386,44 @@ TEST_CASE("marshal: serialize (class type)") 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()); +} From 0e8fd8219f696c3b07293673bc20a71fcdf9749e Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:49:37 +0900 Subject: [PATCH 22/22] Remove extra semicolon --- test/marshal.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/marshal.cpp b/test/marshal.cpp index 42ba653..efca8a7 100644 --- a/test/marshal.cpp +++ b/test/marshal.cpp @@ -396,8 +396,8 @@ TEST_CASE("marshal: serialize (class type)") struct MyPimplData { - IRIS_MARSHAL_PIMPL_CLASS(MyPimplData); - IRIS_MARSHAL_PIMPL_FIELD_DECLARE(IRIS_TEST_MY_PIMPL_DATA_FIELD_DEFS); + 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);