From 45017f7508eb14266686c4fbc7cb945084066a6f Mon Sep 17 00:00:00 2001 From: eldritch horrors Date: Sun, 23 Mar 2025 15:36:11 +0100 Subject: [PATCH] libutil: specialize nix::JSON we want to own this specialization fully so we can change the default serializer behavior without also forcing downstream users of our code to use the same behavior. it'll also let us do things we cannot do in regular nlohmann::json, such as selectively enabling serialization of enums as integral types, or using `to_json`/`from_json` overloads for not-default-constructible types instead of serializer specializations Change-Id: I91a1db362e37d654090f1824b1cd3ce783d32134 --- lix/libstore/outputs-spec.cc | 5 +- lix/libutil/json-fwd.hh | 51 ++++++++++++++++- lix/libutil/json-impls.hh | 3 +- lix/libutil/json-utils.hh | 80 ++------------------------ lix/libutil/json.hh | 87 ++++++++++++++++++++++++++++- tests/unit/libstore/outputs-spec.cc | 6 +- tests/unit/libutil/config.cc | 10 ++-- 7 files changed, 150 insertions(+), 92 deletions(-) diff --git a/lix/libstore/outputs-spec.cc b/lix/libstore/outputs-spec.cc index da56851e5..41b92a1ba 100644 --- a/lix/libstore/outputs-spec.cc +++ b/lix/libstore/outputs-spec.cc @@ -1,5 +1,6 @@ #include +#include "lix/libutil/json.hh" #include "lix/libutil/regex-combinators.hh" #include "lix/libstore/outputs-spec.hh" #include "lix/libstore/path-regex.hh" @@ -149,9 +150,7 @@ bool OutputsSpec::isSubsetOf(const OutputsSpec & that) const } -namespace nlohmann { - -using namespace nix; +namespace nix::json { OutputsSpec adl_serializer::from_json(const JSON & json) { auto names = json.get(); diff --git a/lix/libutil/json-fwd.hh b/lix/libutil/json-fwd.hh index 487e2638e..170bf1f80 100644 --- a/lix/libutil/json-fwd.hh +++ b/lix/libutil/json-fwd.hh @@ -5,6 +5,55 @@ namespace nix { -using JSON = nlohmann::json; +namespace json { + +/** + * For `adl_serializer>` below, we need to track what + * types are not already using `null`. Only for them can we use `null` + * to represent `std::nullopt`. + */ +template +struct avoids_null; + +template +struct is_integral_enum; + +template +struct adl_serializer; + +} + +/** + * Specialization of `nlohmann::basic_json`. We do not use `nlohmann::json` + * because we want full control over the default serializer without needing + * to force users of lix as a library to use our customized serializer code + * as well, such as our specializations for `std::optional` with checks. + */ +using JSON = nlohmann::basic_json< + std::map, + std::vector, + std::string, + bool, + std::int64_t, + std::uint64_t, + double, + std::allocator, + json::adl_serializer>; + +const JSON * get(const JSON & map, const std::string & key); + +JSON * get(JSON & map, const std::string & key); + +/** + * Get the value of a json object at a key safely, failing + * with a Nix Error if the key does not exist. + * + * Use instead of JSON::at() to avoid ugly exceptions. + * + * _Does not check whether `map` is an object_, use `ensureType` for that. + */ +const JSON & valueAt( + const JSON & map, + const std::string & key); } diff --git a/lix/libutil/json-impls.hh b/lix/libutil/json-impls.hh index 31dbd9eff..d53b99087 100644 --- a/lix/libutil/json-impls.hh +++ b/lix/libutil/json-impls.hh @@ -5,8 +5,7 @@ // Following https://github.com/nlohmann/json#how-can-i-use-get-for-non-default-constructiblenon-copyable-types #define JSON_IMPL(TYPE) \ - namespace nlohmann { \ - using namespace nix; \ + namespace nix::json { \ template <> \ struct adl_serializer { \ static TYPE from_json(const JSON & json); \ diff --git a/lix/libutil/json-utils.hh b/lix/libutil/json-utils.hh index 7f75f1baa..9a955d9c6 100644 --- a/lix/libutil/json-utils.hh +++ b/lix/libutil/json-utils.hh @@ -2,74 +2,6 @@ ///@file #include "lix/libutil/json.hh" -#include -#include - -namespace nix { - -const JSON * get(const JSON & map, const std::string & key); - -JSON * get(JSON & map, const std::string & key); - -/** - * Get the value of a json object at a key safely, failing - * with a Nix Error if the key does not exist. - * - * Use instead of JSON::at() to avoid ugly exceptions. - * - * _Does not check whether `map` is an object_, use `ensureType` for that. - */ -const JSON & valueAt( - const JSON & map, - const std::string & key); - -/** - * Ensure the type of a json object is what you expect, failing - * with a Nix Error if it isn't. - * - * Use before type conversions and element access to avoid ugly exceptions. - */ -const JSON & ensureType( - const JSON & value, - JSON::value_type expectedType); - -/** - * For `adl_serializer>` below, we need to track what - * types are not already using `null`. Only for them can we use `null` - * to represent `std::nullopt`. - */ -template -struct json_avoids_null; - -/** - * Handle numbers and enums in default impl - */ -template -struct json_avoids_null - : std::bool_constant || std::is_floating_point_v || std::is_enum_v> -{}; - -template<> -struct json_avoids_null : std::false_type {}; - -template<> -struct json_avoids_null : std::true_type {}; - -template<> -struct json_avoids_null : std::true_type {}; - -template -struct json_avoids_null> : std::true_type {}; - -template -struct json_avoids_null> : std::true_type {}; - -template -struct json_avoids_null> : std::true_type {}; - -} - -namespace nlohmann { /** * This "instance" is widely requested, see @@ -80,14 +12,14 @@ namespace nlohmann { * round trip. We do that with a static assert. */ template -struct adl_serializer> { +struct nix::json::adl_serializer> { /** * @brief Convert a JSON type to an `optional` treating * `null` as `std::nullopt`. */ - static void from_json(const json & json, std::optional & t) { + static void from_json(const auto & json, std::optional & t) { static_assert( - nix::json_avoids_null::value, + nix::json::avoids_null::value, "null is already in use for underlying type's JSON"); t = json.is_null() ? std::nullopt @@ -98,9 +30,9 @@ struct adl_serializer> { * @brief Convert an optional type to a JSON type treating `std::nullopt` * as `null`. */ - static void to_json(json & json, const std::optional & t) { + static void to_json(auto & json, const std::optional & t) { static_assert( - nix::json_avoids_null::value, + nix::json::avoids_null::value, "null is already in use for underlying type's JSON"); if (t) json = *t; @@ -108,5 +40,3 @@ struct adl_serializer> { json = nullptr; } }; - -} diff --git a/lix/libutil/json.hh b/lix/libutil/json.hh index 040cf4e5c..e2cd81574 100644 --- a/lix/libutil/json.hh +++ b/lix/libutil/json.hh @@ -1,5 +1,90 @@ #pragma once -///@file Lix-specific JSON handling. +/// @file Lix-specific JSON handling. We do not use plain `nlohmann::json` +/// because we want to override serializer behavior without imposing these +/// overrides on out-of-tree users of libutil, as we'd be required to when +/// specializing templates in the nlohmann namespace. nlohmann::json can't +/// deal with `std::optional` types until 3.11.3 at, and we need those. #include "lix/libutil/json-fwd.hh" // IWYU pragma: keep #include // IWYU pragma: keep +#include +#include + +namespace nix { + +/** + * Ensure the type of a json object is what you expect, failing + * with a Nix Error if it isn't. + * + * Use before type conversions and element access to avoid ugly exceptions. + */ +const JSON & ensureType( + const JSON & value, + JSON::value_type expectedType); + +namespace json { + +/** + * Handle numbers and enums in default impl + */ +template +struct avoids_null + : std::bool_constant || std::is_floating_point_v || std::is_enum_v> +{}; + +template<> +struct avoids_null : std::false_type {}; + +template<> +struct avoids_null : std::true_type {}; + +template<> +struct avoids_null : std::true_type {}; + +template +struct avoids_null> : std::true_type {}; + +template +struct avoids_null> : std::true_type {}; + +template +struct avoids_null> : std::true_type {}; + +namespace detail { +template + requires requires(Json & j, T value) { to_json(j, value); } +void call_to_json(Json & j, const T & value) +{ + to_json(j, value); +} + +template + requires requires(Json && j, T & value) { from_json(std::forward(j), value); } +void call_from_json(Json && j, T & value) +{ + from_json(std::forward(j), value); +} +} + +template +struct adl_serializer +{ + template + requires requires(Json & j, T value) { detail::call_to_json(j, value); } + static void to_json(Json & j, const T & value) + { + detail::call_to_json(j, value); + } + + template + requires requires(Json && j, T & value) { + detail::call_from_json(std::forward(j), value); + } + static void from_json(Json && j, T & value) + { + detail::call_from_json(std::forward(j), value); + } +}; +} + +} diff --git a/tests/unit/libstore/outputs-spec.cc b/tests/unit/libstore/outputs-spec.cc index 6c8be230f..1139215a7 100644 --- a/tests/unit/libstore/outputs-spec.cc +++ b/tests/unit/libstore/outputs-spec.cc @@ -173,19 +173,17 @@ TEST(ExtendedOutputsSpec, many_carrot) { #define TEST_JSON(TYPE, NAME, STR, VAL) \ \ TEST(TYPE, NAME ## _to_json) { \ - using nlohmann::literals::operator "" _json; \ ASSERT_EQ( \ - STR ## _json, \ + JSON::parse(STR), \ /* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \ ((JSON) TYPE { VAL })); \ } \ \ TEST(TYPE, NAME ## _from_json) { \ - using nlohmann::literals::operator "" _json; \ ASSERT_EQ( \ /* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \ TYPE { VAL }, \ - (STR ## _json).get()); \ + JSON::parse(STR).get()); \ } TEST_JSON(OutputsSpec, all, R"(["*"])", OutputsSpec::All { }) diff --git a/tests/unit/libutil/config.cc b/tests/unit/libutil/config.cc index f9112e4a5..447491bde 100644 --- a/tests/unit/libutil/config.cc +++ b/tests/unit/libutil/config.cc @@ -162,7 +162,6 @@ namespace nix { } TEST(Config, toJSONOnNonEmptyConfig) { - using nlohmann::literals::operator "" _json; Config config; Setting setting{ &config, @@ -173,7 +172,7 @@ namespace nix { setting.override("value"); ASSERT_EQ(config.toJSON(), - R"#({ + JSON::parse(R"#({ "name-of-the-setting": { "aliases": [], "defaultValue": "", @@ -182,11 +181,10 @@ namespace nix { "value": "value", "experimentalFeature": null } - })#"_json); + })#")); } TEST(Config, toJSONOnNonEmptyConfigWithExperimentalSetting) { - using nlohmann::literals::operator "" _json; Config config; Setting setting{ &config, @@ -200,7 +198,7 @@ namespace nix { setting.override("value"); ASSERT_EQ(config.toJSON(), - R"#({ + JSON::parse(R"#({ "name-of-the-setting": { "aliases": [], "defaultValue": "", @@ -209,7 +207,7 @@ namespace nix { "value": "value", "experimentalFeature": "flakes" } - })#"_json); + })#")); } TEST(Config, setSettingAlias) {