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
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include <regex>
|
||||
|
||||
#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<OutputsSpec>::from_json(const JSON & json) {
|
||||
auto names = json.get<StringSet>();
|
||||
|
||||
+50
-1
@@ -5,6 +5,55 @@
|
||||
|
||||
namespace nix {
|
||||
|
||||
using JSON = nlohmann::json;
|
||||
namespace json {
|
||||
|
||||
/**
|
||||
* For `adl_serializer<std::optional<T>>` below, we need to track what
|
||||
* types are not already using `null`. Only for them can we use `null`
|
||||
* to represent `std::nullopt`.
|
||||
*/
|
||||
template<typename T>
|
||||
struct avoids_null;
|
||||
|
||||
template<typename T>
|
||||
struct is_integral_enum;
|
||||
|
||||
template<typename T = void, typename SFINAE = void>
|
||||
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<T>` 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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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<TYPE> { \
|
||||
static TYPE from_json(const JSON & json); \
|
||||
|
||||
@@ -2,74 +2,6 @@
|
||||
///@file
|
||||
|
||||
#include "lix/libutil/json.hh"
|
||||
#include <list>
|
||||
#include <type_traits>
|
||||
|
||||
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<std::optional<T>>` below, we need to track what
|
||||
* types are not already using `null`. Only for them can we use `null`
|
||||
* to represent `std::nullopt`.
|
||||
*/
|
||||
template<typename T>
|
||||
struct json_avoids_null;
|
||||
|
||||
/**
|
||||
* Handle numbers and enums in default impl
|
||||
*/
|
||||
template<typename T>
|
||||
struct json_avoids_null
|
||||
: std::bool_constant<std::is_integral_v<T> || std::is_floating_point_v<T> || std::is_enum_v<T>>
|
||||
{};
|
||||
|
||||
template<>
|
||||
struct json_avoids_null<std::nullptr_t> : std::false_type {};
|
||||
|
||||
template<>
|
||||
struct json_avoids_null<bool> : std::true_type {};
|
||||
|
||||
template<>
|
||||
struct json_avoids_null<std::string> : std::true_type {};
|
||||
|
||||
template<typename T>
|
||||
struct json_avoids_null<std::vector<T>> : std::true_type {};
|
||||
|
||||
template<typename T>
|
||||
struct json_avoids_null<std::list<T>> : std::true_type {};
|
||||
|
||||
template<typename K, typename V>
|
||||
struct json_avoids_null<std::map<K, V>> : 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<typename T>
|
||||
struct adl_serializer<std::optional<T>> {
|
||||
struct nix::json::adl_serializer<std::optional<T>> {
|
||||
/**
|
||||
* @brief Convert a JSON type to an `optional<T>` treating
|
||||
* `null` as `std::nullopt`.
|
||||
*/
|
||||
static void from_json(const json & json, std::optional<T> & t) {
|
||||
static void from_json(const auto & json, std::optional<T> & t) {
|
||||
static_assert(
|
||||
nix::json_avoids_null<T>::value,
|
||||
nix::json::avoids_null<T>::value,
|
||||
"null is already in use for underlying type's JSON");
|
||||
t = json.is_null()
|
||||
? std::nullopt
|
||||
@@ -98,9 +30,9 @@ struct adl_serializer<std::optional<T>> {
|
||||
* @brief Convert an optional type to a JSON type treating `std::nullopt`
|
||||
* as `null`.
|
||||
*/
|
||||
static void to_json(json & json, const std::optional<T> & t) {
|
||||
static void to_json(auto & json, const std::optional<T> & t) {
|
||||
static_assert(
|
||||
nix::json_avoids_null<T>::value,
|
||||
nix::json::avoids_null<T>::value,
|
||||
"null is already in use for underlying type's JSON");
|
||||
if (t)
|
||||
json = *t;
|
||||
@@ -108,5 +40,3 @@ struct adl_serializer<std::optional<T>> {
|
||||
json = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
+86
-1
@@ -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<T>` types until 3.11.3 at, and we need those.
|
||||
|
||||
#include "lix/libutil/json-fwd.hh" // IWYU pragma: keep
|
||||
#include <nlohmann/json.hpp> // IWYU pragma: keep
|
||||
#include <list>
|
||||
#include <type_traits>
|
||||
|
||||
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<typename T>
|
||||
struct avoids_null
|
||||
: std::bool_constant<std::is_integral_v<T> || std::is_floating_point_v<T> || std::is_enum_v<T>>
|
||||
{};
|
||||
|
||||
template<>
|
||||
struct avoids_null<std::nullptr_t> : std::false_type {};
|
||||
|
||||
template<>
|
||||
struct avoids_null<bool> : std::true_type {};
|
||||
|
||||
template<>
|
||||
struct avoids_null<std::string> : std::true_type {};
|
||||
|
||||
template<typename T>
|
||||
struct avoids_null<std::vector<T>> : std::true_type {};
|
||||
|
||||
template<typename T>
|
||||
struct avoids_null<std::list<T>> : std::true_type {};
|
||||
|
||||
template<typename K, typename V>
|
||||
struct avoids_null<std::map<K, V>> : std::true_type {};
|
||||
|
||||
namespace detail {
|
||||
template<typename Json, typename T>
|
||||
requires requires(Json & j, T value) { to_json(j, value); }
|
||||
void call_to_json(Json & j, const T & value)
|
||||
{
|
||||
to_json(j, value);
|
||||
}
|
||||
|
||||
template<typename Json, typename T>
|
||||
requires requires(Json && j, T & value) { from_json(std::forward<Json>(j), value); }
|
||||
void call_from_json(Json && j, T & value)
|
||||
{
|
||||
from_json(std::forward<Json>(j), value);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
struct adl_serializer<T, void>
|
||||
{
|
||||
template<typename Json>
|
||||
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<typename Json>
|
||||
requires requires(Json && j, T & value) {
|
||||
detail::call_from_json(std::forward<Json>(j), value);
|
||||
}
|
||||
static void from_json(Json && j, T & value)
|
||||
{
|
||||
detail::call_from_json(std::forward<Json>(j), value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<TYPE>()); \
|
||||
JSON::parse(STR).get<TYPE>()); \
|
||||
}
|
||||
|
||||
TEST_JSON(OutputsSpec, all, R"(["*"])", OutputsSpec::All { })
|
||||
|
||||
@@ -162,7 +162,6 @@ namespace nix {
|
||||
}
|
||||
|
||||
TEST(Config, toJSONOnNonEmptyConfig) {
|
||||
using nlohmann::literals::operator "" _json;
|
||||
Config config;
|
||||
Setting<std::string> 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<std::string> 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) {
|
||||
|
||||
Reference in New Issue
Block a user