From dec6c8fd0d4ffd77be350c927170b6b3d7b16a6f Mon Sep 17 00:00:00 2001 From: Michael Walcott Date: Fri, 20 Jan 2023 13:08:49 -0600 Subject: [PATCH 1/3] Basic generic JSON support. Limited auto deduction of variants when having at most one of each basic type. --- include/glaze/json/generic_json.hpp | 50 +++++++ include/glaze/json/read.hpp | 199 +++++++++++++++++++++------- tests/json_test/json_test.cpp | 52 ++++++-- 3 files changed, 247 insertions(+), 54 deletions(-) create mode 100644 include/glaze/json/generic_json.hpp diff --git a/include/glaze/json/generic_json.hpp b/include/glaze/json/generic_json.hpp new file mode 100644 index 0000000000..98c97b8d63 --- /dev/null +++ b/include/glaze/json/generic_json.hpp @@ -0,0 +1,50 @@ +// Glaze Library +// For the license information refer to glaze.hpp + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace glz +{ + struct generic_json + { + using null_t = std::monostate; + using array_t = std::vector; + using object_t = std::map>; + using val_t = std::variant; + val_t data{}; + + template + T& get() + { + return std::get(data); + } + + generic_json& operator[](std::integral auto&& index) { return std::get(data)[index]; } + + generic_json& operator[](std::convertible_to auto&& key) + { + //[] operator for maps does not support heterogeneous lookups yet + auto& object = std::get(data); + auto iter = object.find(key); + if (iter == object.end()) { + iter = object.insert(std::make_pair(std::string(key), generic_json{})).first; + } + return iter->second; + } + + operator bool() const { return !std::holds_alternative(data); } + + val_t* operator->() { return &data; } + + val_t& operator*() { return data; } + + const val_t& operator*() const { return data; } + }; +} diff --git a/include/glaze/json/read.hpp b/include/glaze/json/read.hpp index d66cf9570b..c3a63944c2 100644 --- a/include/glaze/json/read.hpp +++ b/include/glaze/json/read.hpp @@ -15,6 +15,7 @@ #include "glaze/util/for_each.hpp" #include "glaze/file/file_ops.hpp" #include "glaze/util/strod.hpp" +#include "glaze/json/generic_json.hpp" namespace glz { @@ -115,6 +116,38 @@ namespace glz return ret; } + + template + constexpr auto variant_is_auto_deducible() + { + //Contains at most one each of the basic json types bool, numeric, string, object, array + int bools{}, numbers{}, strings{}, objects{}, arrays{}; + constexpr auto N = std::variant_size_v; + for_each([&](auto I) { + using V = std::decay_t>; + if constexpr (bool_t) ++bools; + if constexpr (num_t) ++numbers; + if constexpr (str_t || glaze_enum_t) ++strings; + if constexpr (map_t || glaze_object_t) ++objects; + if constexpr (array_t || glaze_array_t)++ arrays; + }); + return bools < 2 && numbers < 2 && strings < 2 && objects < 2 && arrays < 2; + } + + template + struct variant_types; + + template + struct variant_types> + { + //TODO this way of filtering types is compile time intensive. + using bool_types = decltype(std::tuple_cat(std::conditional_t,std::tuple,std::tuple<>>{}...)); + using number_types = decltype(std::tuple_cat(std::conditional_t,std::tuple,std::tuple<>>{}...)); + using string_types = decltype(std::tuple_cat(std::conditional_t || glaze_enum_t, std::tuple, std::tuple<>>{}...)); + using object_types = decltype(std::tuple_cat(std::conditional_t || glaze_object_t, std::tuple, std::tuple<>>{}...)); + using array_types = decltype(std::tuple_cat(std::conditional_t || glaze_array_t, std::tuple, std::tuple<>>{}...)); + using nullable_types = decltype(std::tuple_cat(std::conditional_t, std::tuple, std::tuple<>>{}...)); + }; template struct from_json @@ -122,56 +155,130 @@ namespace glz template static void op(auto&& value, is_context auto&& ctx, auto&& it, auto&& end) { - const auto is_glaze_object = std::visit([](auto&& v) { - using V = std::decay_t; - if constexpr (glaze_object_t) { - return true; - } - else { - return false; - } - }, value); - - if (is_glaze_object) { - skip_ws(it, end); - match<'{'>(it); - skip_ws(it, end); - - match(it, end); - skip_ws(it, end); - match<':'>(it); - - using V = std::decay_t; - - static constexpr auto names = variant_type_names(); - static constexpr auto N = names.size(); - - // TODO: Change from linear search to map? - static thread_local std::string type{}; - read::op(type, ctx, it, end); + if constexpr (variant_is_auto_deducible()) { skip_ws(it, end); - match<','>(it); - - for_each([&](auto I) { - constexpr auto N = [] { - return names[decltype(I)::value].size(); - }(); // MSVC internal compiler error workaround - if (string_cmp_n(type, names[I])) { - using V = std::variant_alternative_t; - if (!std::holds_alternative(value)) { - // default construct the value if not matching + switch (*it) { + case '{': + using object_types = variant_types::object_types; + if constexpr (std::tuple_size_v < 1) { + throw std::runtime_error("Encounted object in variant with no object type"); + } + else { + ++it; + using V = std::tuple_element_t<0, object_types>; value = V{}; + read::op()>(std::get(value), ctx, it, end); } - read::op()>(std::get(value), ctx, it, end); - return; + break; + case '[': + using array_types = variant_types::array_types; + if constexpr (std::tuple_size_v < 1) { + throw std::runtime_error("Encountered array in variant with no array type"); + } + else { + using V = std::tuple_element_t<0, array_types>; + value = V{}; + read::op()>(std::get(value), ctx, it, end); + } + break; + case '"': { + using string_types = variant_types::string_types; + if constexpr (std::tuple_size_v < 1) { + throw std::runtime_error("Encountered string in variant with no string type"); + } + else { + using V = std::tuple_element_t<0, string_types>; + value = V{}; + read::op()>(std::get(value), ctx, it, end); + } + break; + } + case 't': + case 'f': { + using bool_types = variant_types::bool_types; + if constexpr (std::tuple_size_v < 1) { + throw std::runtime_error("No matching type in variant"); + } + else { + using V = std::tuple_element_t<0, bool_types>; + value = V{}; + read::op()>(std::get(value), ctx, it, end); + } + break; } - }); + //TODO handle nullable + //case 'n': + // break; + default: { + // Not bool, string, object, or array so must be number or null + using number_types = variant_types::number_types; + if constexpr (std::tuple_size_v < 1) { + throw std::runtime_error("No matching type in variant"); + } + else { + using V = std::tuple_element_t<0, number_types>; + value = V{}; + read::op()>(std::get(value), ctx, it, end); + } + } + } } else { - std::visit([&](auto&& v) { - using V = std::decay_t; - read::op(v, ctx, it, end); - }, value); + const auto is_glaze_object = std::visit( + [](auto&& v) { + using V = std::decay_t; + if constexpr (glaze_object_t) { + return true; + } + else { + return false; + } + }, + value); + + if (is_glaze_object) { + skip_ws(it, end); + match<'{'>(it); + skip_ws(it, end); + + match(it, end); + skip_ws(it, end); + match<':'>(it); + + using V = std::decay_t; + + static constexpr auto names = variant_type_names(); + static constexpr auto N = names.size(); + + // TODO: Change from linear search to map? + static thread_local std::string type{}; + read::op(type, ctx, it, end); + skip_ws(it, end); + match<','>(it); + + for_each([&](auto I) { + constexpr auto N = [] { + return names[decltype(I)::value].size(); + }(); // MSVC internal compiler error workaround + if (string_cmp_n(type, names[I])) { + using V = std::variant_alternative_t; + if (!std::holds_alternative(value)) { + // default construct the value if not matching + value = V{}; + } + read::op()>(std::get(value), ctx, it, end); + return; + } + }); + } + else { + std::visit( + [&](auto&& v) { + using V = std::decay_t; + read::op(v, ctx, it, end); + }, + value); + } } } }; @@ -849,7 +956,7 @@ namespace glz if (*it == 'n') { ++it; match<"ull">(it, end); - if constexpr (!std::is_pointer_v) { + if constexpr (!std::is_pointer_v && !std::same_as) { value.reset(); } } @@ -861,6 +968,8 @@ namespace glz value = std::make_unique(); else if constexpr (is_specialization_v) value = std::make_shared(); + else if constexpr (std::same_as) + value = {}; else throw std::runtime_error( "Cannot read into unset nullable that is not " diff --git a/tests/json_test/json_test.cpp b/tests/json_test/json_test.cpp index 8d7bae7209..61c05df4c8 100644 --- a/tests/json_test/json_test.cpp +++ b/tests/json_test/json_test.cpp @@ -2212,17 +2212,32 @@ suite variant_tests = [] { "variant_read_"_test = [] { std::variant x = 44; - glz::read_json(x, "33"); - expect(std::get(x) == 33); - - std::variant m{}; - glz::read_json(m, R"("std::monostate")"); - expect(std::holds_alternative(m) == true); - - m = 44; - expect(throws([&]{ glz::read_json(m, R"("std::monostate")"); })); + }; + + "variant_read_auto"_test = [] { + // Auto deduce variant with no conflicting basic types + std::variant, std::vector> m{}; + glz::read_json(m, R"("Hello World")"); + expect(std::holds_alternative(m) == true); + expect(std::get(m) == "Hello World"); + + glz::read_json(m, R"(872)"); + expect(std::holds_alternative(m) == true); + expect(std::get(m) == 872); + + glz::read_json(m, R"({"pi":3.14})"); + expect(std::holds_alternative>(m) == true); + expect(std::get>(m)["pi"] == 3.14); + + glz::read_json(m, R"(true)"); + expect(std::holds_alternative(m) == true); + expect(std::get(m) == true); + + glz::read_json(m, R"(["a", "b", "c"])"); + expect(std::holds_alternative>(m) == true); + expect(std::get>(m)[1] == "b"); }; "variant_read_obj"_test = [] { @@ -2247,6 +2262,25 @@ suite variant_tests = [] { }; }; +suite generic_json_tests = [] { + "generic_json_write"_test = [] { + glz::generic_json json = { + glz::generic_json::array_t{{5.0}, {"Hello World"}, {glz::generic_json::object_t{{"pi", {3.14}}}}}}; + std::string buffer{}; + glz::write_json(json, buffer); + expect(buffer == R"([5,"Hello World",{"pi":3.14}])"); + }; + + "generic_json_read"_test = [] { + glz::generic_json json{}; + std::string buffer = R"([5,"Hello World",{"pi":3.14}])"; + glz::read_json(json, buffer); + expect(json[0].get() == 5.0); + expect(json[1].get() == "Hello World"); + expect(json[2]["pi"].get() == 3.14); + }; +}; + struct holder0_t { int i{}; }; From 7a59c2e135865efc34daffcbdd624406ff444081 Mon Sep 17 00:00:00 2001 From: Michael Walcott Date: Fri, 20 Jan 2023 13:41:23 -0600 Subject: [PATCH 2/3] Rename generic_json to json_t and add more helpers --- .../json/{generic_json.hpp => json_t.hpp} | 36 +++++++++++++++---- include/glaze/json/read.hpp | 16 ++++----- tests/json_test/json_test.cpp | 5 ++- 3 files changed, 40 insertions(+), 17 deletions(-) rename include/glaze/json/{generic_json.hpp => json_t.hpp} (53%) diff --git a/include/glaze/json/generic_json.hpp b/include/glaze/json/json_t.hpp similarity index 53% rename from include/glaze/json/generic_json.hpp rename to include/glaze/json/json_t.hpp index 98c97b8d63..5418db24be 100644 --- a/include/glaze/json/generic_json.hpp +++ b/include/glaze/json/json_t.hpp @@ -12,11 +12,12 @@ namespace glz { - struct generic_json + // Generic json type. + struct json_t { using null_t = std::monostate; - using array_t = std::vector; - using object_t = std::map>; + using array_t = std::vector; + using object_t = std::map>; using val_t = std::variant; val_t data{}; @@ -26,19 +27,42 @@ namespace glz return std::get(data); } - generic_json& operator[](std::integral auto&& index) { return std::get(data)[index]; } + template + T* get_if() + { + return std::get_if(data); + } + + template + bool holds() + { + return std::holds_alternative(data); + } + + json_t& operator[](std::integral auto&& index) { return std::get(data)[index]; } - generic_json& operator[](std::convertible_to auto&& key) + json_t& operator[](std::convertible_to auto&& key) { //[] operator for maps does not support heterogeneous lookups yet auto& object = std::get(data); auto iter = object.find(key); if (iter == object.end()) { - iter = object.insert(std::make_pair(std::string(key), generic_json{})).first; + iter = object.insert(std::make_pair(std::string(key), json_t{})).first; } return iter->second; } + bool contains(std::convertible_to auto&& key) + { + if (!holds()) return false; + auto& object = std::get(data); + auto iter = object.find(key); + if (iter == object.end()) { + return true; + } + return false; + } + operator bool() const { return !std::holds_alternative(data); } val_t* operator->() { return &data; } diff --git a/include/glaze/json/read.hpp b/include/glaze/json/read.hpp index c3a63944c2..783f830d51 100644 --- a/include/glaze/json/read.hpp +++ b/include/glaze/json/read.hpp @@ -15,7 +15,7 @@ #include "glaze/util/for_each.hpp" #include "glaze/file/file_ops.hpp" #include "glaze/util/strod.hpp" -#include "glaze/json/generic_json.hpp" +#include "glaze/json/json_t.hpp" namespace glz { @@ -159,7 +159,7 @@ namespace glz skip_ws(it, end); switch (*it) { case '{': - using object_types = variant_types::object_types; + using object_types = typename variant_types::object_types; if constexpr (std::tuple_size_v < 1) { throw std::runtime_error("Encounted object in variant with no object type"); } @@ -171,7 +171,7 @@ namespace glz } break; case '[': - using array_types = variant_types::array_types; + using array_types = typename variant_types::array_types; if constexpr (std::tuple_size_v < 1) { throw std::runtime_error("Encountered array in variant with no array type"); } @@ -182,7 +182,7 @@ namespace glz } break; case '"': { - using string_types = variant_types::string_types; + using string_types = typename variant_types::string_types; if constexpr (std::tuple_size_v < 1) { throw std::runtime_error("Encountered string in variant with no string type"); } @@ -195,7 +195,7 @@ namespace glz } case 't': case 'f': { - using bool_types = variant_types::bool_types; + using bool_types = typename variant_types::bool_types; if constexpr (std::tuple_size_v < 1) { throw std::runtime_error("No matching type in variant"); } @@ -211,7 +211,7 @@ namespace glz // break; default: { // Not bool, string, object, or array so must be number or null - using number_types = variant_types::number_types; + using number_types = typename variant_types::number_types; if constexpr (std::tuple_size_v < 1) { throw std::runtime_error("No matching type in variant"); } @@ -956,7 +956,7 @@ namespace glz if (*it == 'n') { ++it; match<"ull">(it, end); - if constexpr (!std::is_pointer_v && !std::same_as) { + if constexpr (!std::is_pointer_v && !std::same_as) { value.reset(); } } @@ -968,7 +968,7 @@ namespace glz value = std::make_unique(); else if constexpr (is_specialization_v) value = std::make_shared(); - else if constexpr (std::same_as) + else if constexpr (std::same_as) value = {}; else throw std::runtime_error( diff --git a/tests/json_test/json_test.cpp b/tests/json_test/json_test.cpp index 61c05df4c8..16a48edab9 100644 --- a/tests/json_test/json_test.cpp +++ b/tests/json_test/json_test.cpp @@ -2264,15 +2264,14 @@ suite variant_tests = [] { suite generic_json_tests = [] { "generic_json_write"_test = [] { - glz::generic_json json = { - glz::generic_json::array_t{{5.0}, {"Hello World"}, {glz::generic_json::object_t{{"pi", {3.14}}}}}}; + glz::json_t json = {glz::json_t::array_t{{5.0}, {"Hello World"}, {glz::json_t::object_t{{"pi", {3.14}}}}}}; std::string buffer{}; glz::write_json(json, buffer); expect(buffer == R"([5,"Hello World",{"pi":3.14}])"); }; "generic_json_read"_test = [] { - glz::generic_json json{}; + glz::json_t json{}; std::string buffer = R"([5,"Hello World",{"pi":3.14}])"; glz::read_json(json, buffer); expect(json[0].get() == 5.0); From 56529225614de320b3dadf238a3405ec3d1f53e4 Mon Sep 17 00:00:00 2001 From: Michael Walcott Date: Fri, 20 Jan 2023 20:57:05 -0600 Subject: [PATCH 3/3] Easier initialization for generic json type glz::json_t --- include/glaze/core/common.hpp | 2 +- include/glaze/json/json_t.hpp | 21 ++++++++++++++++++++- tests/json_test/json_test.cpp | 15 +++++++++++++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/include/glaze/core/common.hpp b/include/glaze/core/common.hpp index 9c61490860..3f3dd2b75a 100644 --- a/include/glaze/core/common.hpp +++ b/include/glaze/core/common.hpp @@ -275,7 +275,7 @@ namespace glz concept complex_t = glaze_t>; template - concept str_t = !complex_t && std::convertible_to, std::string_view>; + concept str_t = !complex_t && !std::same_as && std::convertible_to, std::string_view>; template concept pair_t = requires(T pair) diff --git a/include/glaze/json/json_t.hpp b/include/glaze/json/json_t.hpp index 5418db24be..192bc10702 100644 --- a/include/glaze/json/json_t.hpp +++ b/include/glaze/json/json_t.hpp @@ -15,7 +15,7 @@ namespace glz // Generic json type. struct json_t { - using null_t = std::monostate; + using null_t = double*; using array_t = std::vector; using object_t = std::map>; using val_t = std::variant; @@ -44,6 +44,7 @@ namespace glz json_t& operator[](std::convertible_to auto&& key) { //[] operator for maps does not support heterogeneous lookups yet + if (holds()) data = object_t{}; auto& object = std::get(data); auto iter = object.find(key); if (iter == object.end()) { @@ -70,5 +71,23 @@ namespace glz val_t& operator*() { return data; } const val_t& operator*() const { return data; } + + json_t() = default; + + template + requires std::convertible_to && (!std::same_as>) + json_t(T&& val) + { + data = val; + } + + json_t(std::initializer_list>&& obj) { data = object_t(obj); } + + // Prevent conflict with object initializer list + template + json_t(std::initializer_list&& arr) + { + data = array_t(arr); + } }; } diff --git a/tests/json_test/json_test.cpp b/tests/json_test/json_test.cpp index 16a48edab9..76b2dd2337 100644 --- a/tests/json_test/json_test.cpp +++ b/tests/json_test/json_test.cpp @@ -2264,10 +2264,21 @@ suite variant_tests = [] { suite generic_json_tests = [] { "generic_json_write"_test = [] { - glz::json_t json = {glz::json_t::array_t{{5.0}, {"Hello World"}, {glz::json_t::object_t{{"pi", {3.14}}}}}}; + glz::json_t json = { + {"pi", 3.141}, + {"happy", true}, + {"name", "Niels"}, + {"nothing", nullptr}, + {"answer", {{"everything", 42.0}}}, + {"list", {1.0, 0.0, 2.0}}, + {"object", { + {"currency", "USD"}, + {"value", 42.99} + }} + }; std::string buffer{}; glz::write_json(json, buffer); - expect(buffer == R"([5,"Hello World",{"pi":3.14}])"); + expect(buffer == R"({"answer":{"everything":42},"happy":true,"list":[1,0,2],"name":"Niels","object":{"currency":"USD","value":42.99},"pi":3.141})") << buffer; }; "generic_json_read"_test = [] {