From 228b4910f3923162c03fd1e981edd197ffba46fa Mon Sep 17 00:00:00 2001 From: metabeyond Date: Mon, 18 Nov 2024 12:22:56 +0800 Subject: [PATCH] Fixed JSON serialization 'enum_to_string' issues. Added unit tests for CLI, Encoding, JSON Serialization Exceptions --- include/essence/meta/detail/json.hpp | 24 +++ .../essence/meta/runtime/json_serializer.hpp | 75 +++++----- test/cli_test.cpp | 138 ++++++++++++++++++ test/encoding_test.cpp | 74 ++++++++++ test/json_test.cpp | 67 +++++++++ test/meta_test.cpp | 10 +- 6 files changed, 350 insertions(+), 38 deletions(-) create mode 100644 test/cli_test.cpp create mode 100644 test/encoding_test.cpp diff --git a/include/essence/meta/detail/json.hpp b/include/essence/meta/detail/json.hpp index 1a7833d..2ae281b 100644 --- a/include/essence/meta/detail/json.hpp +++ b/include/essence/meta/detail/json.hpp @@ -22,13 +22,37 @@ #pragma once +#include "../../json.hpp" #include "../common_types.hpp" #include "../runtime/struct.hpp" +#include #include #include namespace essence::meta::detail { + template + concept basic_json_context = nlohmann::detail::is_basic_json_context::value; + + template + concept basic_json = nlohmann::detail::is_basic_json::value; + + template + concept json_compatible_type = nlohmann::detail::is_compatible_type::value; + + template + concept primitive_json_serializable = + (json_compatible_type || std::ranges::forward_range) &&!std::is_enum_v; + + template + concept non_iterable_object_json_serializable = + std::is_class_v && !json_compatible_type && !std::ranges::forward_range && !std_optional; + + template + concept iterable_json_serializable = std::ranges::forward_range + && (json_compatible_type> + || non_iterable_object_json_serializable>); + template concept has_json_serialization_config = std::is_class_v && requires { typename T::json_serialization; diff --git a/include/essence/meta/runtime/json_serializer.hpp b/include/essence/meta/runtime/json_serializer.hpp index e5d19bd..d0dddb9 100644 --- a/include/essence/meta/runtime/json_serializer.hpp +++ b/include/essence/meta/runtime/json_serializer.hpp @@ -38,14 +38,35 @@ #include namespace essence::meta::runtime { + /** + * @brief Stashes the current 'enum_to_string' and 'naming_convention' configurations during the serialization. + */ + struct json_serializer_base { + static bool& get_enum_to_string_ref() noexcept { + thread_local bool enabled{}; + + return enabled; + } + + static naming_convention& get_naming_convention_ref() noexcept { + thread_local auto convention = detail::get_json_naming_convention>(); + + return convention; + } + + static void reset() noexcept { + get_enum_to_string_ref() = {}; + get_naming_convention_ref() = detail::get_json_naming_convention>(); + } + }; + /** * @brief A JSON serializer by using the meta reflection implementation in this project. * @tparam T The type of the value. */ template - struct json_serializer { - template - requires nlohmann::detail::is_basic_json_context::value + struct json_serializer : json_serializer_base { + template [[noreturn]] static void throw_exception(const U& member, std::string_view json_key, std::string_view message, std::string_view internal, BasicJsonContext context) { @@ -63,11 +84,7 @@ namespace essence::meta::runtime { * @param json The JSON value. * @param value The primitive value. */ - template - requires( - nlohmann::detail::is_basic_json::value - && (nlohmann::detail::is_compatible_type::value || std::ranges::forward_range) - && !std::is_enum_v) + template static void to_json(BasicJson& json, const U& value) { nlohmann::to_json(json, value); } @@ -79,11 +96,7 @@ namespace essence::meta::runtime { * @param json The JSON value. * @param value The primitive value. */ - template - requires( - nlohmann::detail::is_basic_json::value - && (nlohmann::detail::is_compatible_type::value || std::ranges::forward_range) - && !std::is_enum_v) + template static void from_json(const BasicJson& json, U& value) { nlohmann::from_json(json, value); } @@ -95,8 +108,7 @@ namespace essence::meta::runtime { * @param json The JSON value. * @param value The std::optional<> value. */ - template - requires(nlohmann::detail::is_basic_json::value && std_optional) + template static void to_json(BasicJson& json, const U& value) { if (value) { to_json(json, *value); @@ -112,8 +124,7 @@ namespace essence::meta::runtime { * @param json The JSON value. * @param value The std::optional<> value. */ - template - requires(nlohmann::detail::is_basic_json::value && std_optional) + template static void from_json(const BasicJson& json, U& value) { if (json.is_null()) { value.reset(); @@ -132,10 +143,7 @@ namespace essence::meta::runtime { * @param json The JSON value. * @param value The class object. */ - template - requires(std::is_class_v && nlohmann::detail::is_basic_json::value - && !nlohmann::detail::is_compatible_type::value - && !std::ranges::forward_range && !std_optional) + template static void to_json(BasicJson& json, const U& value) { auto handler = [&](const auto& item) { try { @@ -151,8 +159,13 @@ namespace essence::meta::runtime { } }; + get_enum_to_string_ref() = detail::has_enum_to_string_config; + get_naming_convention_ref() = detail::get_json_naming_convention(); + enumerate_data_members()>( value, [&](const auto&... members) { (handler(members), ...); }); + + reset(); } /** @@ -162,10 +175,7 @@ namespace essence::meta::runtime { * @param json The JSON value. * @param value The class object. */ - template - requires(std::is_class_v && nlohmann::detail::is_basic_json::value - && !nlohmann::detail::is_compatible_type::value - && !std::ranges::forward_range && !std_optional) + template static void from_json(const BasicJson& json, U& value) { auto handler = [&](const auto& item) { try { @@ -189,13 +199,11 @@ namespace essence::meta::runtime { * @param json The JSON value. * @param value The enumeration value. */ - template - requires(nlohmann::detail::is_basic_json::value - && std::same_as && std::is_enum_v) + template + requires(std::same_as && std::is_enum_v) static void to_json(BasicJson& json, const U& value) { - if constexpr (detail::has_enum_to_string_config) { - nlohmann::to_json( - json, convert_naming_convention(to_string(value), detail::get_json_naming_convention())); + if (get_enum_to_string_ref()) { + nlohmann::to_json(json, convert_naming_convention(to_string(value), get_naming_convention_ref())); } else { nlohmann::to_json(json, value); } @@ -208,9 +216,8 @@ namespace essence::meta::runtime { * @param json The JSON value. * @param value The enumeration value. */ - template - requires(nlohmann::detail::is_basic_json::value - && std::same_as && std::is_enum_v) + template + requires(std::same_as && std::is_enum_v) static void from_json(const BasicJson& json, U& value) { if (json.is_string()) { const auto name = json.template get_ptr(); diff --git a/test/cli_test.cpp b/test/cli_test.cpp new file mode 100644 index 0000000..3a95997 --- /dev/null +++ b/test/cli_test.cpp @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2024 The RefValue Project + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include + +using namespace essence; + +#define MAKE_TEST(name) TEST(cli_test, name) + +MAKE_TEST(option) { + enum class animal_type { + cat, + dog, + mouse, + }; + + const std::array options{ + cli::option{} + .set_bound_name(U8("boolean")) + .set_description(U8("test")) + .add_aliases(U8("b")) + .as_abstract(), + + cli::option{} + .set_bound_name(U8("int32")) + .set_description(U8("test")) + .add_aliases(U8("i")) + .set_valid_values(1, 2, 3) + .as_abstract(), + + cli::option{} + .set_bound_name(U8("float32")) + .set_description(U8("test")) + .add_aliases(U8("f")) + .as_abstract(), + + cli::option{} + .set_bound_name(U8("string")) + .set_description(U8("test")) + .add_aliases(U8("s")) + .as_abstract(), + + cli::option{} + .set_bound_name(U8("animal")) + .set_description(U8("test")) + .add_aliases(U8("a")) + .as_abstract(), + + cli::option>{} + .set_bound_name(U8("lines")) + .set_description(U8("test")) + .add_aliases(U8("l")) + .set_valid_values(U8("abc"), U8("123")) + .as_abstract(), + + cli::option>{} + .set_bound_name(U8("numbers")) + .set_description(U8("test")) + .add_aliases(U8("n")) + .set_valid_values(1, 2, 3) + .as_abstract(), + + cli::option>{} + .set_bound_name(U8("animals")) + .set_description(U8("test")) + .add_aliases(U8("z")) + .as_abstract(), + }; + + const cli::arg_parser parser; + + for (auto&& item : options) { + parser.add_option(item); + } + + parser.on_error([](std::string_view message) {}); + parser.on_output([](std::string_view message) {}); + + if (parser.parse(std::vector{U8("-b"), U8("-i=2"), U8("--float32=3.14"), U8("--string"), U8("hello"), + U8("-a=dog"), U8("--lines"), U8("123,abc"), U8("--numbers=2,2,2,3,1,1"), U8("-z"), U8("cat,mouse,dog"), + U8("other"), U8("lol")}); + parser) { + struct foo { + bool boolean{}; + std::int32_t int32{}; + float float32{}; + std::string string; + animal_type animal{}; + std::vector lines; + std::vector numbers; + std::vector animals; + }; + + ASSERT_EQ(parser.unmatched_args().size(), 2); + ASSERT_EQ(parser.unmatched_args()[0], U8("other")); + ASSERT_EQ(parser.unmatched_args()[1], U8("lol")); + + const auto model = parser.to_model(); + + ASSERT_TRUE(model); + ASSERT_EQ(model->boolean, true); + ASSERT_EQ(model->int32, 2); + ASSERT_EQ(model->float32, 3.14f); + ASSERT_EQ(model->string, U8("hello")); + ASSERT_EQ(model->animal, animal_type::dog); + ASSERT_EQ(model->lines, (std::vector{U8("123"), U8("abc")})); + ASSERT_EQ(model->numbers, (std::vector{2, 2, 2, 3, 1, 1})); + ASSERT_EQ(model->animals, (std::vector{animal_type::cat, animal_type::mouse, animal_type::dog})); + } +} diff --git a/test/encoding_test.cpp b/test/encoding_test.cpp new file mode 100644 index 0000000..ea35cf3 --- /dev/null +++ b/test/encoding_test.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 The RefValue Project + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include + +#include +#include +#include + +#include + +using namespace essence; + +#define MAKE_TEST(name) TEST(encoding_test, name) + +MAKE_TEST(conversion) { + constexpr std::string_view str{U8("Hello world!")}; + constexpr std::string_view cjk_str{U8("中日韩汉字")}; + + constexpr std::wstring_view wide_str{L"Hello world!"}; + constexpr std::wstring_view wide_cjk_str{L"中日韩汉字"}; + + constexpr std::u16string_view u16_str{u"Hello world!"}; + constexpr std::u16string_view u16_cjk_str{u"中日韩汉字"}; + + const abi::vector u16_vec{u'H', u'e', u'l', u'l', u'o', u' ', u'w', u'o', u'r', u'l', u'd', u'!'}; + const abi::vector u16_cjk_vec{u'中', u'日', u'韩', u'汉', u'字'}; + +#ifdef _WIN32 + ASSERT_EQ(to_native_string(str), wide_str); + ASSERT_EQ(to_native_string(cjk_str), wide_cjk_str); +#else + ASSERT_EQ(to_native_string(str), str); + ASSERT_EQ(to_native_string(cjk_str), cjk_str); +#endif + + ASSERT_EQ(to_utf16_string(str), u16_str); + ASSERT_EQ(to_utf16_string(cjk_str), u16_cjk_str); + ASSERT_EQ(to_uint16_t_literal(str), u16_vec); + ASSERT_EQ(to_uint16_t_literal(cjk_str), u16_cjk_vec); + + ASSERT_EQ(to_utf8_string(u16_str), str); + ASSERT_EQ(to_utf8_string(u16_vec), str); + ASSERT_EQ(to_utf8_string(u16_cjk_str), cjk_str); + ASSERT_EQ(to_utf8_string(u16_cjk_vec), cjk_str); + +#ifdef _WIN32 + ASSERT_EQ(to_utf8_string(wide_str), str); + ASSERT_EQ(to_utf8_string(wide_cjk_str), cjk_str); +#else + ASSERT_EQ(to_utf8_string(str), str); + ASSERT_EQ(to_utf8_string(cjk_str), cjk_str); +#endif +} diff --git a/test/json_test.cpp b/test/json_test.cpp index 1034e4b..0ec2ca8 100644 --- a/test/json_test.cpp +++ b/test/json_test.cpp @@ -211,3 +211,70 @@ MAKE_TEST(naming_convention) { EXPECT_EQ(obj_qux.pacific_ocean, 3); EXPECT_EQ(obj_qux.indian_ocean, true); } + +MAKE_TEST(enum_to_string) { + struct foo { + enum class catalog { + first_item, + second_item, + third_item, + }; + + enum class json_serialization { + pascal_case, + enum_to_string, + }; + + catalog root{catalog::second_item}; + + std::vector catalogs{ + catalog::first_item, + catalog::second_item, + catalog::third_item, + }; + }; + + static_assert(json_serializable); + + const json json(foo{}); + + EXPECT_EQ(json[U8("Root")], U8("SecondItem")); + + EXPECT_EQ(json[U8("Catalogs")].size(), 3); + EXPECT_EQ(json[U8("Catalogs")][0], U8("FirstItem")); + EXPECT_EQ(json[U8("Catalogs")][1], U8("SecondItem")); + EXPECT_EQ(json[U8("Catalogs")][2], U8("ThirdItem")); + + const auto obj = json.get(); + + EXPECT_EQ(obj.root, foo::catalog::second_item); + + EXPECT_EQ(obj.catalogs.size(), 3); + EXPECT_EQ(obj.catalogs[0], foo::catalog::first_item); + EXPECT_EQ(obj.catalogs[1], foo::catalog::second_item); + EXPECT_EQ(obj.catalogs[2], foo::catalog::third_item); +} + +MAKE_TEST(exceptions) { + struct foo { + enum class json_serialization { camel_case }; + enum class catalog { here }; + + catalog value{}; + }; + + try { + [[maybe_unused]] const auto obj = json{{U8("value"), U8("non-existance")}}.get(); + } catch (const std::exception& ex) { + EXPECT_TRUE( + std::string_view{ex.what()}.find(meta::fingerprint{std::type_identity{}}.friendly_name()) + != std::string_view::npos); + } + + try { + [[maybe_unused]] const auto obj = json{{U8("non_existance"), U8("whatever")}}.get(); + } catch (const std::exception& ex) { + EXPECT_TRUE(std::string_view{ex.what()}.find(U8("Failed to deserialize the JSON value to the data member.")) + != std::string_view::npos); + } +} diff --git a/test/meta_test.cpp b/test/meta_test.cpp index 8731ce5..f0e33f6 100644 --- a/test/meta_test.cpp +++ b/test/meta_test.cpp @@ -38,6 +38,8 @@ #include +#define MAKE_TEST(name) TEST(meta_test, name) + namespace essence::testing { struct foo {}; @@ -65,7 +67,7 @@ namespace essence::testing { using namespace essence; using namespace essence::testing; -TEST(meta_test, fingerprint_for_nontemplate_types) { +MAKE_TEST(fingerprint_for_nontemplate_types) { static constexpr std::array fingerprints{ std::pair{meta::fingerprint{std::type_identity{}}, U8("int8")}, std::pair{meta::fingerprint{std::type_identity{}}, U8("int16")}, @@ -90,7 +92,7 @@ TEST(meta_test, fingerprint_for_nontemplate_types) { } } -TEST(meta_test, enumerations) { +MAKE_TEST(enumerations) { EXPECT_EQ(meta::runtime::to_string(problem::nuts), U8("nuts")); EXPECT_TRUE(meta::runtime::from_string(U8("playing"))); EXPECT_FALSE(meta::runtime::from_string(U8("none"))); @@ -137,7 +139,7 @@ TEST(meta_test, enumerations) { EXPECT_EQ(customized_range_enum_names[2], (std::pair{U8("crying"), face_action::crying})); } -TEST(meta_test, boolean) { +MAKE_TEST(boolean) { EXPECT_EQ(meta::true_string, U8("true")); EXPECT_EQ(meta::false_string, U8("false")); @@ -150,7 +152,7 @@ TEST(meta_test, boolean) { EXPECT_EQ(meta::runtime::to_string(false), U8("false")); } -TEST(meta_test, literal_string) { +MAKE_TEST(literal_string) { static constexpr meta::literal_string str1{U8("Hello")}; static constexpr meta::literal_string str2{U8("World")};