Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 42 additions & 11 deletions src/jse/jse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,28 @@ namespace jse
return true;
}

// If there is only one candidate rule (no ambiguity) and it is an object rule
// that failed only because of extra/unknown fields, pinpoint those fields
// instead of dumping the whole subtree.
if (strict && matching_rules.size() == 1 && matching_rules[0].value("type", "") == "object")
{
std::vector<std::string> extra_keys = find_extra_keys(input, matching_rules[0]);
if (!extra_keys.empty())
{
for (const std::string &key : extra_keys)
{
const string extra_pointer = append_pointer(pointer, key);
std::stringstream s;
s << "No rule matched for \"" << extra_pointer << "\": " << input[key].dump(/*indent=*/4) << std::endl;
s << "No valid rules in this list:\n";
for (int i = 0; i < matching_rules.size(); i++)
s << i << ": " << matching_rules[i].dump(/*indent=*/4) << "\n";
log.push_back(log_item("error", s.str()));
}
return false;
}
}

std::stringstream s;
s << "No rule matched for \"" << pointer << "\": " << input.dump(/*indent=*/4) << std::endl;
s << "No valid rules in this list:\n";
Expand Down Expand Up @@ -414,6 +436,24 @@ namespace jse

return true;
}
std::vector<std::string> JSE::find_extra_keys(const json &input, const json &rule)
{
std::vector<std::string> keys;
keys.reserve((rule.contains("required") ? rule["required"].size() : 0)
+ (rule.contains("optional") ? rule["optional"].size() : 0));
if (rule.contains("required"))
keys.insert(keys.end(), rule["required"].begin(), rule["required"].end());
if (rule.contains("optional"))
keys.insert(keys.end(), rule["optional"].begin(), rule["optional"].end());

std::vector<std::string> extra;
if (input.is_object())
for (const auto &[key, value] : input.items())
if (std::find(keys.begin(), keys.end(), key) == keys.end())
extra.push_back(key);
return extra;
}

bool JSE::verify_rule_object(const json &input, const json &rule)
{
assert(rule.at("type") == "object");
Expand All @@ -429,17 +469,8 @@ namespace jse

if (strict) // strict mode: check that no extra fields are present
{
std::vector<std::string> keys;
keys.reserve((rule.contains("required") ? rule["required"].size() : 0)
+ (rule.contains("optional") ? rule["optional"].size() : 0));
if (rule.contains("required"))
keys.insert(keys.end(), rule["required"].begin(), rule["required"].end());
if (rule.contains("optional"))
keys.insert(keys.end(), rule["optional"].begin(), rule["optional"].end());

for (const auto &[key, value] : input.items())
if (std::find(keys.begin(), keys.end(), key) == keys.end())
return false;
if (!find_extra_keys(input, rule).empty())
return false;
}

if (rule.contains("type_name")
Expand Down
4 changes: 4 additions & 0 deletions src/jse/jse.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ namespace jse
// Find the first rule matching a pointer
json find_valid_rule(const string &pointer, const json &input, const json &rules);

// Returns the keys of input that are not listed in rule's "required" or "optional" lists.
// Only meaningful for rules of type "object".
std::vector<std::string> find_extra_keys(const json &input, const json &rule);

// Utils
bool contained_in_list(string item, const json &list);

Expand Down
59 changes: 59 additions & 0 deletions tests/test_validator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,65 @@ TEST_CASE("type_object", "[validator]")
REQUIRE(!jse.verify_json(input, rules));
}

TEST_CASE("strict_unknown_parameter", "[validator]")
{
json input = R"(
{
"string1": "teststring"
}
)"_json;

json rules = R"(
[
{
"pointer": "/",
"type": "object",
"required": ["string1"]
}
]
)"_json;

JSE jse;

jse.strict = true;
REQUIRE(!jse.verify_json(input, rules));
REQUIRE(jse.log2str().find("Unknown entry /string1") != std::string::npos);

jse.strict = false;
REQUIRE(jse.verify_json(input, rules));
}

TEST_CASE("strict_unlisted_extra_parameter", "[validator]")
{
// "skip_simplif" is a typo for "skip_simplify" and is not listed in
// required/optional, so it should be pinpointed rather than triggering
// a dump of the whole object.
json input = R"(
{
"string1": "teststring",
"foo2": false
}
)"_json;

json rules = R"(
[
{
"pointer": "/",
"type": "object",
"required": ["string1"],
"optional": ["foo1"]
}
]
)"_json;

JSE jse;

jse.strict = true;
REQUIRE(!jse.verify_json(input, rules));
REQUIRE(jse.log2str().find("No rule matched for \"/foo2\": false") != std::string::npos);
REQUIRE(jse.log2str().find("No valid rules in this list") != std::string::npos);
}

TEST_CASE("include_rule", "[validator]")
{
json rules = R"(
Expand Down
Loading