Skip to content

Commit 50cb14f

Browse files
haenoeEricson2314
andauthored
Improve checked json casting (#10087)
This introduces new utility functions to get elements from JSON — in an ergonomic way and with nice error messages if the expected type does not match. Co-authored-by: John Ericson <[email protected]>
1 parent bf86b93 commit 50cb14f

File tree

11 files changed

+315
-69
lines changed

11 files changed

+315
-69
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ perl/Makefile.config
4949
/src/libexpr/tests
5050
/tests/unit/libexpr/libnixexpr-tests
5151

52+
# /src/libfetchers
53+
/tests/unit/libfetchers/libnixfetchers-tests
54+
5255
# /src/libstore/
5356
*.gen.*
5457
/src/libstore/tests

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ makefiles += \
3434
tests/unit/libutil-support/local.mk \
3535
tests/unit/libstore/local.mk \
3636
tests/unit/libstore-support/local.mk \
37+
tests/unit/libfetchers/local.mk \
3738
tests/unit/libexpr/local.mk \
3839
tests/unit/libexpr-support/local.mk
3940
endif

src/libfetchers/unix/git.cc

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,12 @@ std::vector<PublicKey> getPublicKeys(const Attrs & attrs)
147147
{
148148
std::vector<PublicKey> publicKeys;
149149
if (attrs.contains("publicKeys")) {
150-
nlohmann::json publicKeysJson = nlohmann::json::parse(getStrAttr(attrs, "publicKeys"));
151-
ensureType(publicKeysJson, nlohmann::json::value_t::array);
152-
publicKeys = publicKeysJson.get<std::vector<PublicKey>>();
150+
auto pubKeysJson = nlohmann::json::parse(getStrAttr(attrs, "publicKeys"));
151+
auto & pubKeys = getArray(pubKeysJson);
152+
153+
for (auto & key : pubKeys) {
154+
publicKeys.push_back(key);
155+
}
153156
}
154157
if (attrs.contains("publicKey"))
155158
publicKeys.push_back(PublicKey{maybeGetStrAttr(attrs, "keytype").value_or("ssh-ed25519"),getStrAttr(attrs, "publicKey")});

src/libstore/derivations.cc

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,16 +1239,14 @@ DerivationOutput DerivationOutput::fromJSON(
12391239
const ExperimentalFeatureSettings & xpSettings)
12401240
{
12411241
std::set<std::string_view> keys;
1242-
ensureType(_json, nlohmann::detail::value_t::object);
1243-
auto json = (std::map<std::string, nlohmann::json>) _json;
1242+
auto & json = getObject(_json);
12441243

12451244
for (const auto & [key, _] : json)
12461245
keys.insert(key);
12471246

12481247
auto methodAlgo = [&]() -> std::pair<ContentAddressMethod, HashAlgorithm> {
1249-
std::string hashAlgoStr = json["hashAlgo"];
1250-
// remaining to parse, will be mutated by parsers
1251-
std::string_view s = hashAlgoStr;
1248+
auto & str = getString(valueAt(json, "hashAlgo"));
1249+
std::string_view s = str;
12521250
ContentAddressMethod method = ContentAddressMethod::parsePrefix(s);
12531251
if (method == TextIngestionMethod {})
12541252
xpSettings.require(Xp::DynamicDerivations);
@@ -1258,7 +1256,7 @@ DerivationOutput DerivationOutput::fromJSON(
12581256

12591257
if (keys == (std::set<std::string_view> { "path" })) {
12601258
return DerivationOutput::InputAddressed {
1261-
.path = store.parseStorePath((std::string) json["path"]),
1259+
.path = store.parseStorePath(getString(valueAt(json, "path"))),
12621260
};
12631261
}
12641262

@@ -1267,10 +1265,10 @@ DerivationOutput DerivationOutput::fromJSON(
12671265
auto dof = DerivationOutput::CAFixed {
12681266
.ca = ContentAddress {
12691267
.method = std::move(method),
1270-
.hash = Hash::parseNonSRIUnprefixed((std::string) json["hash"], hashAlgo),
1268+
.hash = Hash::parseNonSRIUnprefixed(getString(valueAt(json, "hash")), hashAlgo),
12711269
},
12721270
};
1273-
if (dof.path(store, drvName, outputName) != store.parseStorePath((std::string) json["path"]))
1271+
if (dof.path(store, drvName, outputName) != store.parseStorePath(getString(valueAt(json, "path"))))
12741272
throw Error("Path doesn't match derivation output");
12751273
return dof;
12761274
}
@@ -1357,20 +1355,19 @@ nlohmann::json Derivation::toJSON(const StoreDirConfig & store) const
13571355

13581356
Derivation Derivation::fromJSON(
13591357
const StoreDirConfig & store,
1360-
const nlohmann::json & json,
1358+
const nlohmann::json & _json,
13611359
const ExperimentalFeatureSettings & xpSettings)
13621360
{
13631361
using nlohmann::detail::value_t;
13641362

13651363
Derivation res;
13661364

1367-
ensureType(json, value_t::object);
1365+
auto & json = getObject(_json);
13681366

1369-
res.name = ensureType(valueAt(json, "name"), value_t::string);
1367+
res.name = getString(valueAt(json, "name"));
13701368

13711369
try {
1372-
auto & outputsObj = ensureType(valueAt(json, "outputs"), value_t::object);
1373-
for (auto & [outputName, output] : outputsObj.items()) {
1370+
for (auto & [outputName, output] : getObject(valueAt(json, "outputs"))) {
13741371
res.outputs.insert_or_assign(
13751372
outputName,
13761373
DerivationOutput::fromJSON(store, res.name, outputName, output));
@@ -1381,8 +1378,7 @@ Derivation Derivation::fromJSON(
13811378
}
13821379

13831380
try {
1384-
auto & inputsList = ensureType(valueAt(json, "inputSrcs"), value_t::array);
1385-
for (auto & input : inputsList)
1381+
for (auto & input : getArray(valueAt(json, "inputSrcs")))
13861382
res.inputSrcs.insert(store.parseStorePath(static_cast<const std::string &>(input)));
13871383
} catch (Error & e) {
13881384
e.addTrace({}, "while reading key 'inputSrcs'");
@@ -1391,29 +1387,28 @@ Derivation Derivation::fromJSON(
13911387

13921388
try {
13931389
std::function<DerivedPathMap<StringSet>::ChildNode(const nlohmann::json &)> doInput;
1394-
doInput = [&](const auto & json) {
1390+
doInput = [&](const auto & _json) {
1391+
auto & json = getObject(_json);
13951392
DerivedPathMap<StringSet>::ChildNode node;
1396-
node.value = static_cast<const StringSet &>(
1397-
ensureType(valueAt(json, "outputs"), value_t::array));
1398-
for (auto & [outputId, childNode] : ensureType(valueAt(json, "dynamicOutputs"), value_t::object).items()) {
1393+
node.value = getStringSet(valueAt(json, "outputs"));
1394+
for (auto & [outputId, childNode] : getObject(valueAt(json, "dynamicOutputs"))) {
13991395
xpSettings.require(Xp::DynamicDerivations);
14001396
node.childMap[outputId] = doInput(childNode);
14011397
}
14021398
return node;
14031399
};
1404-
auto & inputDrvsObj = ensureType(valueAt(json, "inputDrvs"), value_t::object);
1405-
for (auto & [inputDrvPath, inputOutputs] : inputDrvsObj.items())
1400+
for (auto & [inputDrvPath, inputOutputs] : getObject(valueAt(json, "inputDrvs")))
14061401
res.inputDrvs.map[store.parseStorePath(inputDrvPath)] =
14071402
doInput(inputOutputs);
14081403
} catch (Error & e) {
14091404
e.addTrace({}, "while reading key 'inputDrvs'");
14101405
throw;
14111406
}
14121407

1413-
res.platform = ensureType(valueAt(json, "system"), value_t::string);
1414-
res.builder = ensureType(valueAt(json, "builder"), value_t::string);
1415-
res.args = ensureType(valueAt(json, "args"), value_t::array);
1416-
res.env = ensureType(valueAt(json, "env"), value_t::object);
1408+
res.platform = getString(valueAt(json, "system"));
1409+
res.builder = getString(valueAt(json, "builder"));
1410+
res.args = getStringList(valueAt(json, "args"));
1411+
res.env = getStringMap(valueAt(json, "env"));
14171412

14181413
return res;
14191414
}

src/libstore/nar-info.cc

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,19 +172,18 @@ NarInfo NarInfo::fromJSON(
172172
};
173173

174174
if (json.contains("url"))
175-
res.url = ensureType(valueAt(json, "url"), value_t::string);
175+
res.url = getString(valueAt(json, "url"));
176176

177177
if (json.contains("compression"))
178-
res.compression = ensureType(valueAt(json, "compression"), value_t::string);
178+
res.compression = getString(valueAt(json, "compression"));
179179

180180
if (json.contains("downloadHash"))
181181
res.fileHash = Hash::parseAny(
182-
static_cast<const std::string &>(
183-
ensureType(valueAt(json, "downloadHash"), value_t::string)),
182+
getString(valueAt(json, "downloadHash")),
184183
std::nullopt);
185184

186185
if (json.contains("downloadSize"))
187-
res.fileSize = ensureType(valueAt(json, "downloadSize"), value_t::number_integer);
186+
res.fileSize = getInteger(valueAt(json, "downloadSize"));
188187

189188
return res;
190189
}

src/libstore/path-info.cc

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -190,23 +190,18 @@ nlohmann::json UnkeyedValidPathInfo::toJSON(
190190

191191
UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON(
192192
const Store & store,
193-
const nlohmann::json & json)
193+
const nlohmann::json & _json)
194194
{
195-
using nlohmann::detail::value_t;
196-
197195
UnkeyedValidPathInfo res {
198196
Hash(Hash::dummy),
199197
};
200198

201-
ensureType(json, value_t::object);
202-
res.narHash = Hash::parseAny(
203-
static_cast<const std::string &>(
204-
ensureType(valueAt(json, "narHash"), value_t::string)),
205-
std::nullopt);
206-
res.narSize = ensureType(valueAt(json, "narSize"), value_t::number_integer);
199+
auto & json = getObject(_json);
200+
res.narHash = Hash::parseAny(getString(valueAt(json, "narHash")), std::nullopt);
201+
res.narSize = getInteger(valueAt(json, "narSize"));
207202

208203
try {
209-
auto & references = ensureType(valueAt(json, "references"), value_t::array);
204+
auto references = getStringList(valueAt(json, "references"));
210205
for (auto & input : references)
211206
res.references.insert(store.parseStorePath(static_cast<const std::string &>
212207
(input)));
@@ -216,20 +211,16 @@ UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON(
216211
}
217212

218213
if (json.contains("ca"))
219-
res.ca = ContentAddress::parse(
220-
static_cast<const std::string &>(
221-
ensureType(valueAt(json, "ca"), value_t::string)));
214+
res.ca = ContentAddress::parse(getString(valueAt(json, "ca")));
222215

223216
if (json.contains("deriver"))
224-
res.deriver = store.parseStorePath(
225-
static_cast<const std::string &>(
226-
ensureType(valueAt(json, "deriver"), value_t::string)));
217+
res.deriver = store.parseStorePath(getString(valueAt(json, "deriver")));
227218

228219
if (json.contains("registrationTime"))
229-
res.registrationTime = ensureType(valueAt(json, "registrationTime"), value_t::number_integer);
220+
res.registrationTime = getInteger(valueAt(json, "registrationTime"));
230221

231222
if (json.contains("ultimate"))
232-
res.ultimate = ensureType(valueAt(json, "ultimate"), value_t::boolean);
223+
res.ultimate = getBoolean(valueAt(json, "ultimate"));
233224

234225
if (json.contains("signatures"))
235226
res.sigs = valueAt(json, "signatures");

src/libutil/json-utils.cc

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#include "json-utils.hh"
22
#include "error.hh"
3+
#include "types.hh"
4+
#include <nlohmann/json_fwd.hpp>
5+
#include <iostream>
36

47
namespace nix {
58

@@ -18,26 +21,115 @@ nlohmann::json * get(nlohmann::json & map, const std::string & key)
1821
}
1922

2023
const nlohmann::json & valueAt(
21-
const nlohmann::json & map,
24+
const nlohmann::json::object_t & map,
2225
const std::string & key)
2326
{
2427
if (!map.contains(key))
25-
throw Error("Expected JSON object to contain key '%s' but it doesn't", key);
28+
throw Error("Expected JSON object to contain key '%s' but it doesn't: %s", key, nlohmann::json(map).dump());
2629

27-
return map[key];
30+
return map.at(key);
2831
}
2932

30-
const nlohmann::json & ensureType(
33+
std::optional<nlohmann::json> optionalValueAt(const nlohmann::json & value, const std::string & key)
34+
{
35+
try {
36+
auto & v = valueAt(value, key);
37+
return v.get<nlohmann::json>();
38+
} catch (...) {
39+
return std::nullopt;
40+
}
41+
}
42+
43+
44+
std::optional<nlohmann::json> getNullable(const nlohmann::json & value)
45+
{
46+
if (value.is_null())
47+
return std::nullopt;
48+
49+
return value.get<nlohmann::json>();
50+
}
51+
52+
/**
53+
* Ensure the type of a JSON object is what you expect, failing with a
54+
* ensure type if it isn't.
55+
*
56+
* Use before type conversions and element access to avoid ugly
57+
* exceptions, but only part of this module to define the other `get*`
58+
* functions. It is too cumbersome and easy to forget to expect regular
59+
* JSON code to use it directly.
60+
*/
61+
static const nlohmann::json & ensureType(
3162
const nlohmann::json & value,
3263
nlohmann::json::value_type expectedType
3364
)
3465
{
3566
if (value.type() != expectedType)
3667
throw Error(
37-
"Expected JSON value to be of type '%s' but it is of type '%s'",
68+
"Expected JSON value to be of type '%s' but it is of type '%s': %s",
3869
nlohmann::json(expectedType).type_name(),
39-
value.type_name());
70+
value.type_name(), value.dump());
4071

4172
return value;
4273
}
74+
75+
const nlohmann::json::object_t & getObject(const nlohmann::json & value)
76+
{
77+
return ensureType(value, nlohmann::json::value_t::object).get_ref<const nlohmann::json::object_t &>();
78+
}
79+
80+
const nlohmann::json::array_t & getArray(const nlohmann::json & value)
81+
{
82+
return ensureType(value, nlohmann::json::value_t::array).get_ref<const nlohmann::json::array_t &>();
83+
}
84+
85+
const nlohmann::json::string_t & getString(const nlohmann::json & value)
86+
{
87+
return ensureType(value, nlohmann::json::value_t::string).get_ref<const nlohmann::json::string_t &>();
88+
}
89+
90+
const nlohmann::json::number_integer_t & getInteger(const nlohmann::json & value)
91+
{
92+
return ensureType(value, nlohmann::json::value_t::number_integer).get_ref<const nlohmann::json::number_integer_t &>();
93+
}
94+
95+
const nlohmann::json::boolean_t & getBoolean(const nlohmann::json & value)
96+
{
97+
return ensureType(value, nlohmann::json::value_t::boolean).get_ref<const nlohmann::json::boolean_t &>();
98+
}
99+
100+
Strings getStringList(const nlohmann::json & value)
101+
{
102+
auto & jsonArray = getArray(value);
103+
104+
Strings stringList;
105+
106+
for (const auto & elem : jsonArray)
107+
stringList.push_back(getString(elem));
108+
109+
return stringList;
110+
}
111+
112+
StringMap getStringMap(const nlohmann::json & value)
113+
{
114+
auto & jsonObject = getObject(value);
115+
116+
StringMap stringMap;
117+
118+
for (const auto & [key, value] : jsonObject)
119+
stringMap[getString(key)] = getString(value);
120+
121+
return stringMap;
122+
}
123+
124+
StringSet getStringSet(const nlohmann::json & value)
125+
{
126+
auto & jsonArray = getArray(value);
127+
128+
StringSet stringSet;
129+
130+
for (const auto & elem : jsonArray)
131+
stringSet.insert(getString(elem));
132+
133+
return stringSet;
134+
}
43135
}

0 commit comments

Comments
 (0)