1
1
Fork 0
mirror of https://github.com/NixOS/nix.git synced 2025-11-22 18:29:36 +01:00

Merge pull request #14319 from obsidiansystems/json-schema-fso

`nlohmann::json` instance and JSON Schema for `MemorySourceAccessor`
This commit is contained in:
John Ericson 2025-11-20 21:52:57 +00:00 committed by GitHub
commit a835d6ad2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 605 additions and 101 deletions

View file

@ -37,6 +37,7 @@ mkMesonDerivation (finalAttrs: {
(fileset.unions [ (fileset.unions [
../../.version ../../.version
# For example JSON # For example JSON
../../src/libutil-tests/data/memory-source-accessor
../../src/libutil-tests/data/hash ../../src/libutil-tests/data/hash
../../src/libstore-tests/data/content-address ../../src/libstore-tests/data/content-address
../../src/libstore-tests/data/store-path ../../src/libstore-tests/data/store-path

View file

@ -121,6 +121,7 @@
- [Architecture and Design](architecture/architecture.md) - [Architecture and Design](architecture/architecture.md)
- [Formats and Protocols](protocols/index.md) - [Formats and Protocols](protocols/index.md)
- [JSON Formats](protocols/json/index.md) - [JSON Formats](protocols/json/index.md)
- [File System Object](protocols/json/file-system-object.md)
- [Hash](protocols/json/hash.md) - [Hash](protocols/json/hash.md)
- [Content Address](protocols/json/content-address.md) - [Content Address](protocols/json/content-address.md)
- [Store Path](protocols/json/store-path.md) - [Store Path](protocols/json/store-path.md)

View file

@ -0,0 +1,21 @@
{{#include file-system-object-v1-fixed.md}}
## Examples
### Simple
```json
{{#include schema/file-system-object-v1/simple.json}}
```
### Complex
```json
{{#include schema/file-system-object-v1/complex.json}}
```
<!-- need to convert YAML to JSON first
## Raw Schema
[JSON Schema for File System Object v1](schema/file-system-object-v1.json)
-->

View file

@ -11,6 +11,7 @@ s/\\`/`/g
# #
# As we have more such relative links, more replacements of this nature # As we have more such relative links, more replacements of this nature
# should appear below. # should appear below.
s^#/\$defs/\(regular\|symlink\|directory\)^In this schema^g
s^\(./hash-v1.yaml\)\?#/$defs/algorithm^[JSON format for `Hash`](./hash.html#algorithm)^g s^\(./hash-v1.yaml\)\?#/$defs/algorithm^[JSON format for `Hash`](./hash.html#algorithm)^g
s^\(./hash-v1.yaml\)^[JSON format for `Hash`](./hash.html)^g s^\(./hash-v1.yaml\)^[JSON format for `Hash`](./hash.html)^g
s^\(./content-address-v1.yaml\)\?#/$defs/method^[JSON format for `ContentAddress`](./content-address.html#method)^g s^\(./content-address-v1.yaml\)\?#/$defs/method^[JSON format for `ContentAddress`](./content-address.html#method)^g

View file

@ -9,6 +9,7 @@ json_schema_for_humans = find_program('generate-schema-doc', required : false)
json_schema_config = files('json-schema-for-humans-config.yaml') json_schema_config = files('json-schema-for-humans-config.yaml')
schemas = [ schemas = [
'file-system-object-v1',
'hash-v1', 'hash-v1',
'content-address-v1', 'content-address-v1',
'store-path-v1', 'store-path-v1',

View file

@ -0,0 +1 @@
../../../../../../src/libutil-tests/data/memory-source-accessor

View file

@ -0,0 +1,71 @@
"$schema": http://json-schema.org/draft-04/schema#
"$id": https://nix.dev/manual/nix/latest/protocols/json/schema/file-system-object-v1.json
title: File System Object
description: |
This schema describes the JSON representation of Nix's [File System Object](@docroot@/store/file-system-object.md).
The schema is recursive because file system objects contain other file system objects.
type: object
required: ["type"]
properties:
type:
type: string
enum: ["regular", "symlink", "directory"]
# Enforce conditional structure based on `type`
anyOf:
- $ref: "#/$defs/regular"
required: ["type", "contents"]
- $ref: "#/$defs/directory"
required: ["type", "entries"]
- $ref: "#/$defs/symlink"
required: ["type", "target"]
"$defs":
regular:
title: Regular File
description: |
See [Regular File](@docroot@/store/file-system-object.md#regular) in the manual for details.
required: ["contents"]
properties:
type:
const: "regular"
contents:
type: string
description: File contents
executable:
type: boolean
description: Whether the file is executable.
default: false
additionalProperties: false
directory:
title: Directory
description: |
See [Directory](@docroot@/store/file-system-object.md#directory) in the manual for details.
required: ["entries"]
properties:
type:
const: "directory"
entries:
type: object
description: |
Map of names to nested file system objects (for type=directory)
additionalProperties:
$ref: "#"
additionalProperties: false
symlink:
title: Symbolic Link
description: |
See [Symbolic Link](@docroot@/store/file-system-object.md#symlink) in the manual for details.
required: ["target"]
properties:
type:
const: "symlink"
target:
type: string
description: Target path of the symlink.
additionalProperties: false

View file

@ -3,19 +3,23 @@
Nix uses a simplified model of the file system, which consists of file system objects. Nix uses a simplified model of the file system, which consists of file system objects.
Every file system object is one of the following: Every file system object is one of the following:
- File - [**Regular File**]{#regular}
- A possibly empty sequence of bytes for contents - A possibly empty sequence of bytes for contents
- A single boolean representing the [executable](https://en.m.wikipedia.org/wiki/File-system_permissions#Permissions) permission - A single boolean representing the [executable](https://en.m.wikipedia.org/wiki/File-system_permissions#Permissions) permission
- Directory - [**Directory**]{#directory}
Mapping of names to child file system objects Mapping of names to child file system objects
- [Symbolic link](https://en.m.wikipedia.org/wiki/Symbolic_link) - [**Symbolic link**]{#symlink}
An arbitrary string. An arbitrary string, known as the *target* of the symlink.
Nix does not assign any semantics to symbolic links.
In general, Nix does not assign any semantics to symbolic links.
Certain operations however, may make additional assumptions and attempt to use the target to find another file system object.
> See [the Wikpedia article on symbolic links](https://en.m.wikipedia.org/wiki/Symbolic_link) for background information if you are unfamiliar with this Unix concept.
File system objects and their children form a tree. File system objects and their children form a tree.
A bare file or symlink can be a root file system object. A bare file or symlink can be a root file system object.

View file

@ -0,0 +1 @@
../../src/libutil-tests/data/memory-source-accessor

View file

@ -20,6 +20,14 @@ schema_dir = meson.current_source_dir() / 'schema'
# Get all example files # Get all example files
schemas = [ schemas = [
{
'stem' : 'file-system-object',
'schema' : schema_dir / 'file-system-object-v1.yaml',
'files' : [
'simple.json',
'complex.json',
],
},
{ {
'stem' : 'hash', 'stem' : 'hash',
'schema' : schema_dir / 'hash-v1.yaml', 'schema' : schema_dir / 'hash-v1.yaml',

View file

@ -20,6 +20,7 @@ mkMesonDerivation (finalAttrs: {
fileset = lib.fileset.unions [ fileset = lib.fileset.unions [
../../.version ../../.version
../../doc/manual/source/protocols/json/schema ../../doc/manual/source/protocols/json/schema
../../src/libutil-tests/data/memory-source-accessor
../../src/libutil-tests/data/hash ../../src/libutil-tests/data/hash
../../src/libstore-tests/data/content-address ../../src/libstore-tests/data/content-address
../../src/libstore-tests/data/store-path ../../src/libstore-tests/data/store-path

View file

@ -0,0 +1,24 @@
{
"entries": {
"bar": {
"entries": {
"baz": {
"contents": "good day,\n\u0000\n\tworld!",
"executable": true,
"type": "regular"
},
"quux": {
"target": "/over/there",
"type": "symlink"
}
},
"type": "directory"
},
"foo": {
"contents": "hello\n\u0000\n\tworld!",
"executable": false,
"type": "regular"
}
},
"type": "directory"
}

View file

@ -0,0 +1,5 @@
{
"contents": "asdf",
"executable": false,
"type": "regular"
}

View file

@ -0,0 +1,23 @@
{
"entries": {
"bar": {
"entries": {
"baz": {
"executable": true,
"size": 19,
"type": "regular"
},
"quux": {
"target": "/over/there",
"type": "symlink"
}
},
"type": "directory"
},
"foo": {
"size": 15,
"type": "regular"
}
},
"type": "directory"
}

View file

@ -0,0 +1,7 @@
{
"entries": {
"bar": {},
"foo": {}
},
"type": "directory"
}

View file

@ -224,42 +224,15 @@ TEST_F(GitTest, tree_sha256_write)
}); });
} }
namespace memory_source_accessor {
extern ref<MemorySourceAccessor> exampleComplex();
}
TEST_F(GitTest, both_roundrip) TEST_F(GitTest, both_roundrip)
{ {
using File = MemorySourceAccessor::File; auto files = memory_source_accessor::exampleComplex();
auto files = make_ref<MemorySourceAccessor>();
files->root = File::Directory{
.entries{
{
"foo",
File::Regular{
.contents = "hello\n\0\n\tworld!",
},
},
{
"bar",
File::Directory{
.entries =
{
{
"baz",
File::Regular{
.executable = true,
.contents = "good day,\n\0\n\tworld!",
},
},
{
"quux",
File::Symlink{
.target = "/over/there",
},
},
},
},
},
},
};
for (const auto hashAlgo : {HashAlgorithm::SHA1, HashAlgorithm::SHA256}) { for (const auto hashAlgo : {HashAlgorithm::SHA1, HashAlgorithm::SHA256}) {
std::map<Hash, std::string> cas; std::map<Hash, std::string> cas;

View file

@ -0,0 +1,116 @@
#include <string_view>
#include "nix/util/memory-source-accessor.hh"
#include "nix/util/tests/json-characterization.hh"
namespace nix {
namespace memory_source_accessor {
using namespace std::literals;
using File = MemorySourceAccessor::File;
ref<MemorySourceAccessor> exampleSimple()
{
auto sc = make_ref<MemorySourceAccessor>();
sc->root = File{File::Regular{
.executable = false,
.contents = "asdf",
}};
return sc;
}
ref<MemorySourceAccessor> exampleComplex()
{
auto files = make_ref<MemorySourceAccessor>();
files->root = File::Directory{
.entries{
{
"foo",
File::Regular{
.contents = "hello\n\0\n\tworld!"s,
},
},
{
"bar",
File::Directory{
.entries =
{
{
"baz",
File::Regular{
.executable = true,
.contents = "good day,\n\0\n\tworld!"s,
},
},
{
"quux",
File::Symlink{
.target = "/over/there",
},
},
},
},
},
},
};
return files;
}
} // namespace memory_source_accessor
/* ----------------------------------------------------------------------------
* JSON
* --------------------------------------------------------------------------*/
class MemorySourceAccessorTest : public virtual CharacterizationTest
{
std::filesystem::path unitTestData = getUnitTestData() / "memory-source-accessor";
public:
std::filesystem::path goldenMaster(std::string_view testStem) const override
{
return unitTestData / testStem;
}
};
using nlohmann::json;
struct MemorySourceAccessorJsonTest : MemorySourceAccessorTest,
JsonCharacterizationTest<MemorySourceAccessor>,
::testing::WithParamInterface<std::pair<std::string_view, MemorySourceAccessor>>
{};
TEST_P(MemorySourceAccessorJsonTest, from_json)
{
auto & [name, expected] = GetParam();
/* Cannot use `readJsonTest` because need to compare `root` field of
the source accessors for equality. */
readTest(Path{name} + ".json", [&](const auto & encodedRaw) {
auto encoded = json::parse(encodedRaw);
auto decoded = static_cast<MemorySourceAccessor>(encoded);
ASSERT_EQ(decoded.root, expected.root);
});
}
TEST_P(MemorySourceAccessorJsonTest, to_json)
{
auto & [name, value] = GetParam();
writeJsonTest(name, value);
}
INSTANTIATE_TEST_SUITE_P(
MemorySourceAccessorJSON,
MemorySourceAccessorJsonTest,
::testing::Values(
std::pair{
"simple",
*memory_source_accessor::exampleSimple(),
},
std::pair{
"complex",
*memory_source_accessor::exampleComplex(),
}));
} // namespace nix

View file

@ -63,7 +63,9 @@ sources = files(
'json-utils.cc', 'json-utils.cc',
'logging.cc', 'logging.cc',
'lru-cache.cc', 'lru-cache.cc',
'memory-source-accessor.cc',
'monitorfdhup.cc', 'monitorfdhup.cc',
'nar-listing.cc',
'nix_api_util.cc', 'nix_api_util.cc',
'nix_api_util_internal.cc', 'nix_api_util_internal.cc',
'pool.cc', 'pool.cc',

View file

@ -0,0 +1,83 @@
#include <string_view>
#include "nix/util/nar-accessor.hh"
#include "nix/util/tests/json-characterization.hh"
namespace nix {
// Forward declaration from memory-source-accessor.cc
namespace memory_source_accessor {
ref<MemorySourceAccessor> exampleComplex();
}
/* ----------------------------------------------------------------------------
* JSON
* --------------------------------------------------------------------------*/
class NarListingTest : public virtual CharacterizationTest
{
std::filesystem::path unitTestData = getUnitTestData() / "nar-listing";
public:
std::filesystem::path goldenMaster(std::string_view testStem) const override
{
return unitTestData / testStem;
}
};
using nlohmann::json;
struct NarListingJsonTest : NarListingTest,
JsonCharacterizationTest<NarListing>,
::testing::WithParamInterface<std::pair<std::string_view, NarListing>>
{};
TEST_P(NarListingJsonTest, from_json)
{
auto & [name, expected] = GetParam();
readJsonTest(name, expected);
}
TEST_P(NarListingJsonTest, to_json)
{
auto & [name, value] = GetParam();
writeJsonTest(name, value);
}
INSTANTIATE_TEST_SUITE_P(
NarListingJSON,
NarListingJsonTest,
::testing::Values(
std::pair{
"deep",
listNarDeep(*memory_source_accessor::exampleComplex(), CanonPath::root),
}));
struct ShallowNarListingJsonTest : NarListingTest,
JsonCharacterizationTest<ShallowNarListing>,
::testing::WithParamInterface<std::pair<std::string_view, ShallowNarListing>>
{};
TEST_P(ShallowNarListingJsonTest, from_json)
{
auto & [name, expected] = GetParam();
readJsonTest(name, expected);
}
TEST_P(ShallowNarListingJsonTest, to_json)
{
auto & [name, value] = GetParam();
writeJsonTest(name, value);
}
INSTANTIATE_TEST_SUITE_P(
ShallowNarListingJSON,
ShallowNarListingJsonTest,
::testing::Values(
std::pair{
"shallow",
listNarShallow(*memory_source_accessor::exampleComplex(), CanonPath::root),
}));
} // namespace nix

View file

@ -6,15 +6,18 @@
#include "nix/util/experimental-features.hh" #include "nix/util/experimental-features.hh"
// Following https://github.com/nlohmann/json#how-can-i-use-get-for-non-default-constructiblenon-copyable-types // Following https://github.com/nlohmann/json#how-can-i-use-get-for-non-default-constructiblenon-copyable-types
#define JSON_IMPL(TYPE) \ #define JSON_IMPL_INNER(TYPE) \
namespace nlohmann { \
using namespace nix; \
template<> \
struct adl_serializer<TYPE> \ struct adl_serializer<TYPE> \
{ \ { \
static TYPE from_json(const json & json); \ static TYPE from_json(const json & json); \
static void to_json(json & json, const TYPE & t); \ static void to_json(json & json, const TYPE & t); \
}; \ };
#define JSON_IMPL(TYPE) \
namespace nlohmann { \
using namespace nix; \
template<> \
JSON_IMPL_INNER(TYPE) \
} }
#define JSON_IMPL_WITH_XP_FEATURES(TYPE) \ #define JSON_IMPL_WITH_XP_FEATURES(TYPE) \

View file

@ -160,4 +160,53 @@ struct MemorySink : FileSystemObjectSink
void createSymlink(const CanonPath & path, const std::string & target) override; void createSymlink(const CanonPath & path, const std::string & target) override;
}; };
template<>
struct json_avoids_null<MemorySourceAccessor::File::Regular> : std::true_type
{};
template<>
struct json_avoids_null<MemorySourceAccessor::File::Directory> : std::true_type
{};
template<>
struct json_avoids_null<MemorySourceAccessor::File::Symlink> : std::true_type
{};
template<>
struct json_avoids_null<MemorySourceAccessor::File> : std::true_type
{};
template<>
struct json_avoids_null<MemorySourceAccessor> : std::true_type
{};
} // namespace nix } // namespace nix
namespace nlohmann {
using namespace nix;
#define ARG fso::Regular<RegularContents>
template<typename RegularContents>
JSON_IMPL_INNER(ARG)
#undef ARG
#define ARG fso::DirectoryT<Child>
template<typename Child>
JSON_IMPL_INNER(ARG)
#undef ARG
template<>
JSON_IMPL_INNER(fso::Symlink)
template<>
JSON_IMPL_INNER(fso::Opaque)
#define ARG fso::VariantT<RegularContents, recur>
template<typename RegularContents, bool recur>
JSON_IMPL_INNER(ARG)
#undef ARG
} // namespace nlohmann
JSON_IMPL(MemorySourceAccessor)

View file

@ -75,14 +75,6 @@ NarListing listNarDeep(SourceAccessor & accessor, const CanonPath & path);
*/ */
ShallowNarListing listNarShallow(SourceAccessor & accessor, const CanonPath & path); ShallowNarListing listNarShallow(SourceAccessor & accessor, const CanonPath & path);
/** // All json_avoids_null and JSON_IMPL covered by generic templates in memory-source-accessor.hh
* Serialize a NarListing to JSON.
*/
void to_json(nlohmann::json & j, const NarListing & listing);
/**
* Serialize a ShallowNarListing to JSON.
*/
void to_json(nlohmann::json & j, const ShallowNarListing & listing);
} // namespace nix } // namespace nix

View file

@ -1,4 +1,5 @@
#include "nix/util/memory-source-accessor.hh" #include "nix/util/memory-source-accessor.hh"
#include "nix/util/json-utils.hh"
namespace nix { namespace nix {

View file

@ -0,0 +1,162 @@
#include "nix/util/memory-source-accessor.hh"
#include "nix/util/nar-accessor.hh"
#include "nix/util/json-utils.hh"
#include <nlohmann/json.hpp>
namespace nlohmann {
using namespace nix;
// fso::Regular<RegularContents>
template<>
MemorySourceAccessor::File::Regular adl_serializer<MemorySourceAccessor::File::Regular>::from_json(const json & json)
{
auto & obj = getObject(json);
return MemorySourceAccessor::File::Regular{
.executable = getBoolean(valueAt(obj, "executable")),
.contents = getString(valueAt(obj, "contents")),
};
}
template<>
void adl_serializer<MemorySourceAccessor::File::Regular>::to_json(
json & json, const MemorySourceAccessor::File::Regular & r)
{
json = {
{"executable", r.executable},
{"contents", r.contents},
};
}
template<>
NarListing::Regular adl_serializer<NarListing::Regular>::from_json(const json & json)
{
auto & obj = getObject(json);
auto * execPtr = optionalValueAt(obj, "executable");
auto * sizePtr = optionalValueAt(obj, "size");
auto * offsetPtr = optionalValueAt(obj, "narOffset");
return NarListing::Regular{
.executable = execPtr ? getBoolean(*execPtr) : false,
.contents{
.fileSize = ptrToOwned<uint64_t>(sizePtr),
.narOffset = ptrToOwned<uint64_t>(offsetPtr).and_then(
[](auto v) { return v != 0 ? std::optional{v} : std::nullopt; }),
},
};
}
template<>
void adl_serializer<NarListing::Regular>::to_json(json & j, const NarListing::Regular & r)
{
if (r.contents.fileSize)
j["size"] = *r.contents.fileSize;
if (r.executable)
j["executable"] = true;
if (r.contents.narOffset)
j["narOffset"] = *r.contents.narOffset;
}
template<typename Child>
void adl_serializer<fso::DirectoryT<Child>>::to_json(json & j, const fso::DirectoryT<Child> & d)
{
j["entries"] = d.entries;
}
template<typename Child>
fso::DirectoryT<Child> adl_serializer<fso::DirectoryT<Child>>::from_json(const json & json)
{
auto & obj = getObject(json);
return fso::DirectoryT<Child>{
.entries = valueAt(obj, "entries"),
};
}
// fso::Symlink
fso::Symlink adl_serializer<fso::Symlink>::from_json(const json & json)
{
auto & obj = getObject(json);
return fso::Symlink{
.target = getString(valueAt(obj, "target")),
};
}
void adl_serializer<fso::Symlink>::to_json(json & json, const fso::Symlink & s)
{
json = {
{"target", s.target},
};
}
// fso::Opaque
fso::Opaque adl_serializer<fso::Opaque>::from_json(const json &)
{
return fso::Opaque{};
}
void adl_serializer<fso::Opaque>::to_json(json & j, const fso::Opaque &)
{
j = nlohmann::json::object();
}
// fso::VariantT<RegularContents, recur> - generic implementation
template<typename RegularContents, bool recur>
void adl_serializer<fso::VariantT<RegularContents, recur>>::to_json(
json & j, const fso::VariantT<RegularContents, recur> & val)
{
using Variant = fso::VariantT<RegularContents, recur>;
j = nlohmann::json::object();
std::visit(
overloaded{
[&](const typename Variant::Regular & r) {
j = r;
j["type"] = "regular";
},
[&](const typename Variant::Directory & d) {
j = d;
j["type"] = "directory";
},
[&](const typename Variant::Symlink & s) {
j = s;
j["type"] = "symlink";
},
},
val.raw);
}
template<typename RegularContents, bool recur>
fso::VariantT<RegularContents, recur>
adl_serializer<fso::VariantT<RegularContents, recur>>::from_json(const json & json)
{
using Variant = fso::VariantT<RegularContents, recur>;
auto & obj = getObject(json);
auto type = getString(valueAt(obj, "type"));
if (type == "regular")
return static_cast<typename Variant::Regular>(json);
if (type == "directory")
return static_cast<typename Variant::Directory>(json);
if (type == "symlink")
return static_cast<typename Variant::Symlink>(json);
else
throw Error("unknown type of file '%s'", type);
}
// Explicit instantiations for VariantT types we use
template struct adl_serializer<MemorySourceAccessor::File>;
template struct adl_serializer<NarListing>;
template struct adl_serializer<ShallowNarListing>;
// MemorySourceAccessor
MemorySourceAccessor adl_serializer<MemorySourceAccessor>::from_json(const json & json)
{
MemorySourceAccessor res;
res.root = json;
return res;
}
void adl_serializer<MemorySourceAccessor>::to_json(json & json, const MemorySourceAccessor & val)
{
json = val.root;
}
} // namespace nlohmann

View file

@ -146,6 +146,7 @@ sources = [ config_priv_h ] + files(
'json-utils.cc', 'json-utils.cc',
'logging.cc', 'logging.cc',
'memory-source-accessor.cc', 'memory-source-accessor.cc',
'memory-source-accessor/json.cc',
'mounted-source-accessor.cc', 'mounted-source-accessor.cc',
'nar-accessor.cc', 'nar-accessor.cc',
'pos-table.cc', 'pos-table.cc',

View file

@ -324,52 +324,4 @@ ShallowNarListing listNarShallow(SourceAccessor & accessor, const CanonPath & pa
return listNarImpl<false>(accessor, path); return listNarImpl<false>(accessor, path);
} }
template<typename Listing>
static void to_json_impl(nlohmann::json & j, const Listing & listing)
{
std::visit(
overloaded{
[&](const typename Listing::Regular & r) {
j = nlohmann::json::object();
j["type"] = "regular";
if (r.contents.fileSize)
j["size"] = *r.contents.fileSize;
if (r.executable)
j["executable"] = true;
if (r.contents.narOffset)
j["narOffset"] = *r.contents.narOffset;
},
[&](const typename Listing::Directory & d) {
j = nlohmann::json::object();
j["type"] = "directory";
j["entries"] = nlohmann::json::object();
for (const auto & [name, child] : d.entries) {
if constexpr (std::is_same_v<Listing, NarListing>) {
to_json(j["entries"][name], child);
} else if constexpr (std::is_same_v<Listing, ShallowNarListing>) {
j["entries"][name] = nlohmann::json::object();
} else {
static_assert(false);
}
}
},
[&](const typename Listing::Symlink & s) {
j = nlohmann::json::object();
j["type"] = "symlink";
j["target"] = s.target;
},
},
listing.raw);
}
void to_json(nlohmann::json & j, const NarListing & listing)
{
to_json_impl(j, listing);
}
void to_json(nlohmann::json & j, const ShallowNarListing & listing)
{
to_json_impl(j, listing);
}
} // namespace nix } // namespace nix