This commit is contained in:
Didier BRIZET 2022-12-03 11:55:41 -03:00 committed by GitHub
commit 9fa740d63e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 488 additions and 45 deletions

View File

@ -59,6 +59,12 @@ public:
virtual void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const = 0;
virtual void validate_inplace(const json::json_pointer &ptr, json &instance, error_handler &e) const
{
json_patch patch;
validate(ptr, instance, patch, e);
}
virtual const json &default_value(const json::json_pointer &, const json &, error_handler &) const
{
return default_value_;
@ -89,6 +95,16 @@ class schema_ref : public schema
e.error(ptr, instance, "unresolved or freed schema-reference " + id_);
}
void validate_inplace(const json::json_pointer &ptr, json &instance, error_handler &e) const final
{
auto target = target_.lock();
if (target)
target->validate_inplace(ptr, instance, e);
else
e.error(ptr, instance, "unresolved or freed schema-reference " + id_);
}
const json &default_value(const json::json_pointer &ptr, const json &instance, error_handler &e) const override final
{
if (!default_value_.is_null())
@ -362,6 +378,32 @@ public:
sch->second->validate(ptr, instance, patch, e);
}
void validate_inplace(const json::json_pointer &ptr,
json &instance,
error_handler &e,
const json_uri &initial) const
{
if (!root_) {
e.error(ptr, "", "no root schema has yet been set for validating an instance");
return;
}
auto file_entry = files_.find(initial.location());
if (file_entry == files_.end()) {
e.error(ptr, "", "no file found serving requested root-URI. " + initial.location());
return;
}
auto &file = file_entry->second;
auto sch = file.schemas.find(initial.fragment());
if (sch == file.schemas.end()) {
e.error(ptr, "", "no schema find for request initial URI: " + initial.to_string());
return;
}
sch->second->validate_inplace(ptr, instance, e);
}
};
} // namespace json_schema
@ -404,6 +446,15 @@ class logical_not : public schema
e.error(ptr, instance, "the subschema has succeeded, but it is required to not validate");
}
void validate_inplace(const json::json_pointer &ptr, json &instance, error_handler &e) const final
{
first_error_handler esub;
subschema_->validate_inplace(ptr, instance, esub);
if (!esub)
e.error(ptr, instance, "the subschema has succeeded, but it is required to not validate");
}
const json &default_value(const json::json_pointer &ptr, const json &instance, error_handler &e) const override
{
return subschema_->default_value(ptr, instance, e);
@ -450,6 +501,29 @@ class logical_combination : public schema
e.error(ptr, instance, "no subschema has succeeded, but one of them is required to validate");
}
void validate_inplace(const json::json_pointer &ptr, json &instance, error_handler &e) const final
{
size_t count = 0;
for (auto &s : subschemata_) {
first_error_handler esub;
json new_instance(instance);
s->validate_inplace(ptr, new_instance, esub);
if (!esub) {
count++;
instance = new_instance;
}
if (is_validate_complete(instance, ptr, e, esub, count))
return;
}
// could accumulate esub details for anyOf and oneOf, but not clear how to select which subschema failure to report
// or how to report multiple such failures
if (count == 0)
e.error(ptr, instance, "no subschema has succeeded, but one of them is required to validate");
}
// specialized for each of the logical_combination_types
static const std::string key;
static bool is_validate_complete(const json &, const json::json_pointer &, error_handler &, const first_error_handler &, size_t);
@ -555,6 +629,48 @@ class type_schema : public schema
}
}
void validate_inplace(const json::json_pointer &ptr, json &instance, error_handler &e) const override final
{
// depending on the type of instance run the type specific validator - if present
auto type = type_[static_cast<uint8_t>(instance.type())];
if (type)
type->validate_inplace(ptr, instance, e);
else
e.error(ptr, instance, "unexpected instance type");
if (enum_.first) {
bool seen_in_enum = false;
for (auto &v : enum_.second)
if (instance == v) {
seen_in_enum = true;
break;
}
if (!seen_in_enum)
e.error(ptr, instance, "instance not found in required enum");
}
if (const_.first &&
const_.second != instance)
e.error(ptr, instance, "instance not const");
for (auto l : logic_)
l->validate_inplace(ptr, instance, e);
if (if_) {
first_error_handler err;
if_->validate_inplace(ptr, instance, err);
if (!err) {
if (then_)
then_->validate_inplace(ptr, instance, e);
} else {
if (else_)
else_->validate_inplace(ptr, instance, e);
}
}
}
protected:
virtual std::shared_ptr<schema> make_for_default_(
std::shared_ptr<::schema> & /* sch */,
@ -949,14 +1065,6 @@ class boolean : public schema
void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override
{
if (!true_) { // false schema
// empty array
// switch (instance.type()) {
// case json::value_t::array:
// if (instance.size() != 0) // valid false-schema
// e.error(ptr, instance, "false-schema required empty array");
// return;
//}
e.error(ptr, instance, "instance invalid as per false-schema");
}
}
@ -982,6 +1090,20 @@ public:
: schema(root), required_(r) {}
};
json find_patch_add(const json::json_pointer &ptr, const json_patch &patch)
{
if (!patch.operator json().is_array()) {
return nullptr;
}
std::string path = ptr.to_string();
for (const auto &op : patch.operator json()) {
if (op.at("path") == path && op.at("op") == "add") {
return op.at("value");
}
}
return nullptr;
}
class object : public schema
{
std::pair<bool, size_t> maxProperties_{false, 0};
@ -1006,10 +1128,6 @@ class object : public schema
if (minProperties_.first && instance.size() < minProperties_.second)
e.error(ptr, instance, "too few properties");
for (auto &r : required_)
if (instance.find(r) == instance.end())
e.error(ptr, instance, "required property '" + r + "' not found in object");
// for each property in instance
for (auto &p : instance.items()) {
if (propertyNames_)
@ -1045,6 +1163,12 @@ class object : public schema
for (auto const &prop : properties_) {
const auto finding = instance.find(prop.first);
if (instance.end() == finding) { // if the prop is not in the instance
{
json default_value = find_patch_add((ptr / prop.first), patch);
if (!default_value.is_null()) {
continue;
}
}
const auto &default_value = prop.second->default_value(ptr, instance, e);
if (!default_value.is_null()) { // if default value is available
patch.add((ptr / prop.first), default_value);
@ -1052,6 +1176,14 @@ class object : public schema
}
}
for (const auto &r : required_) {
if (instance.find(r) != instance.end())
continue;
if (!find_patch_add((ptr / r), patch).is_null())
continue;
e.error(ptr, instance, "required property '" + r + "' not found in object");
}
for (auto &dep : dependencies_) {
auto prop = instance.find(dep.first);
if (prop != instance.end()) // if dependency-property is present in instance
@ -1059,6 +1191,70 @@ class object : public schema
}
}
void validate_inplace(const json::json_pointer &ptr, json &instance, error_handler &e) const override
{
if (maxProperties_.first && instance.size() > maxProperties_.second)
e.error(ptr, instance, "too many properties");
if (minProperties_.first && instance.size() < minProperties_.second)
e.error(ptr, instance, "too few properties");
// reverse search
for (auto const &prop : properties_) {
const auto finding = instance.find(prop.first);
if (instance.end() == finding) { // if the prop is not in the instance
const auto &default_value = prop.second->default_value(ptr, instance, e);
if (!default_value.is_null()) { // if default value is available
instance[prop.first] = default_value;
}
}
}
// for each property in instance
json_patch patch;
for (auto &p : instance.items()) {
if (propertyNames_)
propertyNames_->validate(ptr, p.key(), patch, e);
bool a_prop_or_pattern_matched = false;
auto schema_p = properties_.find(p.key());
// check if it is in "properties"
if (schema_p != properties_.end()) {
a_prop_or_pattern_matched = true;
schema_p->second->validate_inplace(ptr / p.key(), p.value(), e);
}
#ifndef NO_STD_REGEX
// check all matching patternProperties
for (auto &schema_pp : patternProperties_)
if (REGEX_NAMESPACE::regex_search(p.key(), schema_pp.first)) {
a_prop_or_pattern_matched = true;
schema_pp.second->validate_inplace(ptr / p.key(), p.value(), e);
}
#endif
// check additionalProperties as a last resort
if (!a_prop_or_pattern_matched && additionalProperties_) {
first_error_handler additional_prop_err;
additionalProperties_->validate_inplace(ptr / p.key(), p.value(), additional_prop_err);
if (additional_prop_err)
e.error(ptr, instance, "validation failed for additional property '" + p.key() + "': " + additional_prop_err.message_);
}
}
for (const auto &r : required_) {
if (instance.find(r) != instance.end())
continue;
e.error(ptr, instance, "required property '" + r + "' not found in object");
}
for (auto &dep : dependencies_) {
auto prop = instance.find(dep.first);
if (prop != instance.end()) // if dependency-property is present in instance
dep.second->validate_inplace(ptr / dep.first, instance, e); // validate
}
}
public:
object(json &sch,
root_schema *root,
@ -1205,6 +1401,61 @@ class array : public schema
}
}
void validate_inplace(const json::json_pointer &ptr, json &instance, error_handler &e) const override
{
if (maxItems_.first && instance.size() > maxItems_.second)
e.error(ptr, instance, "array has too many items");
if (minItems_.first && instance.size() < minItems_.second)
e.error(ptr, instance, "array has too few items");
if (uniqueItems_) {
for (auto it = instance.begin(); it != instance.end(); ++it) {
auto v = std::find(it + 1, instance.end(), *it);
if (v != instance.end())
e.error(ptr, instance, "items have to be unique for this array");
}
}
size_t index = 0;
if (items_schema_)
for (auto &i : instance) {
items_schema_->validate_inplace(ptr / index, i, e);
index++;
}
else {
auto item = items_.cbegin();
for (auto &i : instance) {
std::shared_ptr<schema> item_validator;
if (item == items_.cend())
item_validator = additionalItems_;
else {
item_validator = *item;
item++;
}
if (!item_validator)
break;
item_validator->validate_inplace(ptr / index, i, e);
}
}
if (contains_) {
bool contained = false;
for (auto &item : instance) {
first_error_handler local_e;
contains_->validate_inplace(ptr, item, local_e);
if (!local_e) {
contained = true;
break;
}
}
if (!contained)
e.error(ptr, instance, "array does not contain required element as per 'contains'");
}
}
public:
array(json &sch, root_schema *root, const std::vector<nlohmann::json_uri> &uris)
: schema(root)
@ -1450,5 +1701,17 @@ json json_validator::validate(const json &instance, error_handler &err, const js
return patch;
}
void json_validator::validate_inplace(json &instance) const
{
throwing_error_handler err;
validate_inplace(instance, err);
}
void json_validator::validate_inplace(json &instance, error_handler &err, const json_uri &initial_uri) const
{
json::json_pointer ptr;
root_->validate_inplace(ptr, instance, err, initial_uri);
}
} // namespace json_schema
} // namespace nlohmann

View File

@ -190,6 +190,14 @@ public:
// validate a json-document based on the root-schema with a custom error-handler
json validate(const json &, error_handler &, const json_uri &initial_uri = json_uri("#")) const;
// validate a json-document in place based on the root-schema.
// Default values of schema are inserted in-place with the given json-document
void validate_inplace(json &) const;
// validate a json-document based on the root-schema with a custom error-handler.
// Default values of schema are inserted in-place with the given json-document
void validate_inplace(json &, error_handler &, const json_uri &initial_uri = json_uri("#")) const;
};
} // namespace json_schema

View File

@ -73,3 +73,7 @@ add_test(NAME issue-149-entry-selection COMMAND issue-149-entry-selection)
add_executable(issue-189-default-values issue-189-default-values.cpp)
target_link_libraries(issue-189-default-values nlohmann_json_schema_validator)
add_test(NAME issue-189-default-values COMMAND issue-189-default-values)
add_executable(issue-232-patch-in-place issue-232-patch-in-place.cpp)
target_link_libraries(issue-232-patch-in-place nlohmann_json_schema_validator)
add_test(NAME issue-232-patch-in-place COMMAND issue-232-patch-in-place)

View File

@ -111,34 +111,49 @@ int main(void)
validator.set_root_schema(schema);
for (auto &test_case : test_group["tests"]) {
std::cout << " Testing Case " << test_case["description"] << "\n";
for (bool inplace : {false, true}) {
for (auto &test_case : test_group["tests"]) {
std::cout << " Testing Case " << test_case["description"] << "\n";
bool valid = true;
bool valid = true;
try {
validator.validate(test_case["data"]);
} catch (const std::out_of_range &e) {
valid = false;
std::cout << " Test Case Exception (out of range): " << e.what() << "\n";
try {
json data(test_case["data"]);
if (inplace)
validator.validate_inplace(data);
else
validator.validate(data);
} catch (const std::invalid_argument &e) {
valid = false;
std::cout << " Test Case Exception (invalid argument): " << e.what() << "\n";
} catch (const std::out_of_range &e) {
valid = false;
std::cout << " Test Case Exception (out of range): " << e.what() << "\n";
} catch (const std::logic_error &e) {
valid = !test_case["valid"]; /* force test-case failure */
std::cout << " Not yet implemented: " << e.what() << "\n";
} catch (const std::invalid_argument &e) {
valid = false;
std::cout << " Test Case Exception (invalid argument): " << e.what() << "\n";
} catch (const std::logic_error &e) {
valid = !test_case["valid"]; /* force test-case failure */
std::cout << " Not yet implemented: " << e.what() << "\n";
}
bool expected = //
test_case.at("valid").is_boolean() //
? test_case.at("valid").get<bool>() //
: inplace //
? test_case.at("/valid/inplace"_json_pointer).get<bool>() //
: test_case.at("/valid/not_inplace"_json_pointer).get<bool>(); //
std::string inplace_prefix = inplace ? "valid_inplace" : "valid";
if (valid == expected)
std::cout
<< " --> [" << inplace_prefix << "] Test Case exited with " << valid << " as expected.\n";
else {
group_failed++;
std::cout << " --> [" << inplace_prefix << "] Test Case exited with " << valid << " NOT expected.\n";
}
group_total++;
std::cout << "\n";
}
if (valid == test_case["valid"])
std::cout << " --> Test Case exited with " << valid << " as expected.\n";
else {
group_failed++;
std::cout << " --> Test Case exited with " << valid << " NOT expected.\n";
}
group_total++;
std::cout << "\n";
}
total_failed += group_failed;
total += group_total;

View File

@ -18,7 +18,10 @@
{
"description": "still valid when the invalid default is used",
"data": {},
"valid": true
"valid": {
"inplace": false,
"not_inplace": true
}
}
]
},
@ -42,7 +45,10 @@
{
"description": "still valid when the invalid default is used",
"data": {},
"valid": true
"valid": {
"inplace": false,
"not_inplace": true
}
}
]
},
@ -72,7 +78,10 @@
{
"description": "missing properties are not filled in with the default",
"data": {},
"valid": true
"valid": {
"inplace": false,
"not_inplace": true
}
}
]
}

View File

@ -30,7 +30,11 @@ cmake \
${EXTRA_ARGS} \
${SRC_DIR}
CPU_COUNT=$(nproc)
CPU_COUNT=$( \
which nproc &>/dev/null && nproc \
|| which getconf &>/dev/null && getconf _NPROCESSORS_ONLN 2>/dev/null \
|| echo 1 \
)
# Build and install json-schema-validator
cmake --build . -- -j${CPU_COUNT}

View File

@ -0,0 +1,140 @@
#include <chrono>
#include <iostream>
#include <nlohmann/json-schema.hpp>
using nlohmann::json;
using nlohmann::json_uri;
using nlohmann::json_schema::json_validator;
using namespace std::chrono;
void add_sub_schema(json &schema, int depth)
{
schema["type"] = "object";
schema["default"] = json::object();
schema["properties"] = R"(
{
"array": {
"type": "array",
"default": [
{"name": "foo"}
],
"items": {
"required": ["name"],
"oneOf": [
{
"type": "object",
"properties": {
"name": {
"enum": "foo"
},
"code": {
"const": 1,
"default": 1
}
}
},
{
"type": "object",
"properties": {
"name": {
"enum": "bar"
},
"code": {
"const": 2,
"default": 2
}
}
}
]
}
}
}
)"_json;
if (--depth >= 0) {
json &properties = schema.at("/properties/array/items/oneOf/0/properties"_json_pointer);
properties["sub"] = json::object();
add_sub_schema(properties.at("sub"), depth);
}
}
void add_sub_data(json &data, int depth)
{
data["array"] = R"(
[
{
"name": "foo",
"code": 1
}
]
)"_json;
if (--depth >= 0) {
json &item = data.at("/array/0"_json_pointer);
item["sub"] = json::object();
add_sub_schema(item.at("sub"), depth);
}
}
static const int DEPTH = 100;
static const json get_schema()
{
static json schema;
if (schema.empty()) {
schema = R"(
{
"$schema": "http://json-schema.org/draft-07/schema#"
}
)"_json;
add_sub_schema(schema, DEPTH);
}
return schema;
}
static void loader(const json_uri &uri, json &schema)
{
schema = get_schema();
}
int main(void)
{
json_validator validator(loader);
validator.set_root_schema(get_schema());
json data = json::object();
json expected(data);
add_sub_data(expected, DEPTH);
auto start = high_resolution_clock::now();
#if 1
validator.validate_inplace(data);
#else
size_t count = 0;
while (true) { // https://github.com/pboettch/json-schema-validator/issues/206#issuecomment-1173404152
json patch = validator.validate(data);
if (patch.empty()) {
break;
}
++count;
data.patch_inplace(patch);
}
std::cout << "Number of iterations: " << count << std::endl;
#endif
auto stop = high_resolution_clock::now();
auto duration = duration_cast<microseconds>(stop - start);
if (duration.count() > 300000) {
std::cerr << "To long duration: " << duration.count() << " us" << std::endl;
return 1;
}
auto diff = json::diff( //
data, //
expected //
);
if (diff.empty()) {
std::cerr << "Unexpected data: '" << data.dump() << "' instead of expected '" << expected.dump() << "' differences are:" << diff.dump() << std::endl;
return 1;
}
return 0;
}

View File

@ -78,19 +78,19 @@ int main(void)
}
if (default_patch.size() != 1) {
std::cerr << "Patch with defaults is expected to contain one opperation" << std::endl;
std::cerr << "Patch with defaults is expected to contain one operation" << std::endl;
return 1;
}
const auto &single_op = default_patch[0];
if (!single_op.contains("op")) {
std::cerr << "Patch with defaults is expected to contain opperation entry" << std::endl;
std::cerr << "Patch with defaults is expected to contain operation entry" << std::endl;
return 1;
}
if (single_op["op"].get<std::string>() != "add") {
std::cerr << "Patch with defaults is expected to contain add opperation" << std::endl;
std::cerr << "Patch with defaults is expected to contain add operation" << std::endl;
return 1;
}
@ -132,19 +132,19 @@ int main(void)
}
if (default_patch.size() != 1) {
std::cerr << "Patch with defaults is expected to contain one opperation" << std::endl;
std::cerr << "Patch with defaults is expected to contain one operation" << std::endl;
return 1;
}
const auto &single_op = default_patch[0];
if (!single_op.contains("op")) {
std::cerr << "Patch with defaults is expected to contain opperation entry" << std::endl;
std::cerr << "Patch with defaults is expected to contain operation entry" << std::endl;
return 1;
}
if (single_op["op"].get<std::string>() != "add") {
std::cerr << "Patch with defaults is expected to contain add opperation" << std::endl;
std::cerr << "Patch with defaults is expected to contain add operation" << std::endl;
return 1;
}