Skip to content

Commit

Permalink
Fixed JSON serialization 'enum_to_string' issues.
Browse files Browse the repository at this point in the history
Added unit tests for CLI, Encoding, JSON Serialization Exceptions
  • Loading branch information
refvalue committed Nov 18, 2024
1 parent 6c43394 commit 228b491
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 38 deletions.
24 changes: 24 additions & 0 deletions include/essence/meta/detail/json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,37 @@

#pragma once

#include "../../json.hpp"
#include "../common_types.hpp"
#include "../runtime/struct.hpp"

#include <ranges>
#include <type_traits>
#include <unordered_set>

namespace essence::meta::detail {
template <typename T>
concept basic_json_context = nlohmann::detail::is_basic_json_context<T>::value;

template <typename T>
concept basic_json = nlohmann::detail::is_basic_json<T>::value;

template <typename T>
concept json_compatible_type = nlohmann::detail::is_compatible_type<nlohmann::json, T>::value;

template <typename T>
concept primitive_json_serializable =
(json_compatible_type<T> || std::ranges::forward_range<T>) &&!std::is_enum_v<T>;

template <typename T>
concept non_iterable_object_json_serializable =
std::is_class_v<T> && !json_compatible_type<T> && !std::ranges::forward_range<T> && !std_optional<T>;

template <typename T>
concept iterable_json_serializable = std::ranges::forward_range<T>
&& (json_compatible_type<std::ranges::range_value_t<T>>
|| non_iterable_object_json_serializable<std::ranges::range_value_t<T>>);

template <typename T>
concept has_json_serialization_config = std::is_class_v<T> && requires {
typename T::json_serialization;
Expand Down
75 changes: 41 additions & 34 deletions include/essence/meta/runtime/json_serializer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,35 @@
#include <utility>

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<std::type_identity<void>>();

return convention;
}

static void reset() noexcept {
get_enum_to_string_ref() = {};
get_naming_convention_ref() = detail::get_json_naming_convention<std::type_identity<void>>();
}
};

/**
* @brief A JSON serializer by using the meta reflection implementation in this project.
* @tparam T The type of the value.
*/
template <typename T, typename = void>
struct json_serializer {
template <typename U, typename BasicJsonContext>
requires nlohmann::detail::is_basic_json_context<BasicJsonContext>::value
struct json_serializer : json_serializer_base {
template <typename U, detail::basic_json_context BasicJsonContext>
[[noreturn]] static void throw_exception(const U& member, std::string_view json_key, std::string_view message,
std::string_view internal, BasicJsonContext context) {

Expand All @@ -63,11 +84,7 @@ namespace essence::meta::runtime {
* @param json The JSON value.
* @param value The primitive value.
*/
template <typename BasicJson, typename U = T>
requires(
nlohmann::detail::is_basic_json<BasicJson>::value
&& (nlohmann::detail::is_compatible_type<nlohmann::json, U>::value || std::ranges::forward_range<U>)
&& !std::is_enum_v<U>)
template <detail::basic_json BasicJson, detail::primitive_json_serializable U = T>
static void to_json(BasicJson& json, const U& value) {
nlohmann::to_json(json, value);
}
Expand All @@ -79,11 +96,7 @@ namespace essence::meta::runtime {
* @param json The JSON value.
* @param value The primitive value.
*/
template <typename BasicJson, typename U = T>
requires(
nlohmann::detail::is_basic_json<BasicJson>::value
&& (nlohmann::detail::is_compatible_type<nlohmann::json, U>::value || std::ranges::forward_range<U>)
&& !std::is_enum_v<U>)
template <detail::basic_json BasicJson, detail::primitive_json_serializable U = T>
static void from_json(const BasicJson& json, U& value) {
nlohmann::from_json(json, value);
}
Expand All @@ -95,8 +108,7 @@ namespace essence::meta::runtime {
* @param json The JSON value.
* @param value The std::optional<> value.
*/
template <typename BasicJson, typename U = T>
requires(nlohmann::detail::is_basic_json<BasicJson>::value && std_optional<U>)
template <detail::basic_json BasicJson, std_optional U = T>
static void to_json(BasicJson& json, const U& value) {
if (value) {
to_json(json, *value);
Expand All @@ -112,8 +124,7 @@ namespace essence::meta::runtime {
* @param json The JSON value.
* @param value The std::optional<> value.
*/
template <typename BasicJson, typename U = T>
requires(nlohmann::detail::is_basic_json<BasicJson>::value && std_optional<U>)
template <detail::basic_json BasicJson, std_optional U = T>
static void from_json(const BasicJson& json, U& value) {
if (json.is_null()) {
value.reset();
Expand All @@ -132,10 +143,7 @@ namespace essence::meta::runtime {
* @param json The JSON value.
* @param value The class object.
*/
template <typename BasicJson, typename U = T>
requires(std::is_class_v<U> && nlohmann::detail::is_basic_json<BasicJson>::value
&& !nlohmann::detail::is_compatible_type<nlohmann::json, U>::value
&& !std::ranges::forward_range<U> && !std_optional<U>)
template <detail::basic_json BasicJson, detail::non_iterable_object_json_serializable U = T>
static void to_json(BasicJson& json, const U& value) {
auto handler = [&](const auto& item) {
try {
Expand All @@ -151,8 +159,13 @@ namespace essence::meta::runtime {
}
};

get_enum_to_string_ref() = detail::has_enum_to_string_config<U>;
get_naming_convention_ref() = detail::get_json_naming_convention<U>();

enumerate_data_members<detail::get_json_naming_convention<U>()>(
value, [&](const auto&... members) { (handler(members), ...); });

reset();
}

/**
Expand All @@ -162,10 +175,7 @@ namespace essence::meta::runtime {
* @param json The JSON value.
* @param value The class object.
*/
template <typename BasicJson, typename U = T>
requires(std::is_class_v<U> && nlohmann::detail::is_basic_json<BasicJson>::value
&& !nlohmann::detail::is_compatible_type<nlohmann::json, U>::value
&& !std::ranges::forward_range<U> && !std_optional<U>)
template <detail::basic_json BasicJson, detail::non_iterable_object_json_serializable U = T>
static void from_json(const BasicJson& json, U& value) {
auto handler = [&](const auto& item) {
try {
Expand All @@ -189,13 +199,11 @@ namespace essence::meta::runtime {
* @param json The JSON value.
* @param value The enumeration value.
*/
template <typename BasicJson, typename U = T>
requires(nlohmann::detail::is_basic_json<BasicJson>::value
&& std::same_as<typename BasicJson::string_t::value_type, char> && std::is_enum_v<U>)
template <detail::basic_json BasicJson, typename U = T>
requires(std::same_as<typename BasicJson::string_t::value_type, char> && std::is_enum_v<U>)
static void to_json(BasicJson& json, const U& value) {
if constexpr (detail::has_enum_to_string_config<T>) {
nlohmann::to_json(
json, convert_naming_convention(to_string(value), detail::get_json_naming_convention<T>()));
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);
}
Expand All @@ -208,9 +216,8 @@ namespace essence::meta::runtime {
* @param json The JSON value.
* @param value The enumeration value.
*/
template <typename BasicJson, typename U = T>
requires(nlohmann::detail::is_basic_json<BasicJson>::value
&& std::same_as<typename BasicJson::string_t::value_type, char> && std::is_enum_v<U>)
template <detail::basic_json BasicJson, typename U = T>
requires(std::same_as<typename BasicJson::string_t::value_type, char> && std::is_enum_v<U>)
static void from_json(const BasicJson& json, U& value) {
if (json.is_string()) {
const auto name = json.template get_ptr<const typename BasicJson::string_t*>();
Expand Down
138 changes: 138 additions & 0 deletions test/cli_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <array>
#include <span>
#include <string>
#include <string_view>

#include <essence/abi/string.hpp>
#include <essence/cli/arg_parser.hpp>
#include <essence/cli/option.hpp>

#include <gtest/gtest.h>

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<bool>{}
.set_bound_name(U8("boolean"))
.set_description(U8("test"))
.add_aliases(U8("b"))
.as_abstract(),

cli::option<std::int32_t>{}
.set_bound_name(U8("int32"))
.set_description(U8("test"))
.add_aliases(U8("i"))
.set_valid_values(1, 2, 3)
.as_abstract(),

cli::option<float>{}
.set_bound_name(U8("float32"))
.set_description(U8("test"))
.add_aliases(U8("f"))
.as_abstract(),

cli::option<std::string>{}
.set_bound_name(U8("string"))
.set_description(U8("test"))
.add_aliases(U8("s"))
.as_abstract(),

cli::option<animal_type>{}
.set_bound_name(U8("animal"))
.set_description(U8("test"))
.add_aliases(U8("a"))
.as_abstract(),

cli::option<std::vector<std::string>>{}
.set_bound_name(U8("lines"))
.set_description(U8("test"))
.add_aliases(U8("l"))
.set_valid_values(U8("abc"), U8("123"))
.as_abstract(),

cli::option<std::vector<std::int32_t>>{}
.set_bound_name(U8("numbers"))
.set_description(U8("test"))
.add_aliases(U8("n"))
.set_valid_values(1, 2, 3)
.as_abstract(),

cli::option<std::vector<animal_type>>{}
.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<abi::string>{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<std::string> lines;
std::vector<std::int32_t> numbers;
std::vector<animal_type> 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<foo>();

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<std::string>{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}));
}
}
Loading

0 comments on commit 228b491

Please sign in to comment.