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) {