diff --git a/src/json-validator.cpp b/src/json-validator.cpp index 7f34553..cda1144 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -59,6 +59,12 @@ class schema 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 @@ class root_schema 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(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 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 @@ class required : public schema : 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 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 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 &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 diff --git a/src/nlohmann/json-schema.hpp b/src/nlohmann/json-schema.hpp index 07befd3..1398aa5 100644 --- a/src/nlohmann/json-schema.hpp +++ b/src/nlohmann/json-schema.hpp @@ -190,6 +190,14 @@ class JSON_SCHEMA_VALIDATOR_API json_validator // 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 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fd1c309..ce32094 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/test/JSON-Schema-Test-Suite/json-schema-test.cpp b/test/JSON-Schema-Test-Suite/json-schema-test.cpp index deee4c8..f4e95bc 100644 --- a/test/JSON-Schema-Test-Suite/json-schema-test.cpp +++ b/test/JSON-Schema-Test-Suite/json-schema-test.cpp @@ -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"; - - 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"; - - } 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"; - } - - 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"; + for (bool inplace : {false, true}) { + for (auto &test_case : test_group["tests"]) { + std::cout << " Testing Case " << test_case["description"] << "\n"; + + bool valid = true; + + try { + json data(test_case["data"]); + if (inplace) + validator.validate_inplace(data); + else + validator.validate(data); + + } catch (const std::out_of_range &e) { + valid = false; + std::cout << " Test Case Exception (out of range): " << 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() // + : inplace // + ? test_case.at("/valid/inplace"_json_pointer).get() // + : test_case.at("/valid/not_inplace"_json_pointer).get(); // + 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"; } - group_total++; - std::cout << "\n"; } total_failed += group_failed; total += group_total; diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/default.json b/test/JSON-Schema-Test-Suite/tests/draft7/default.json index 289a9b6..3b64b6a 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft7/default.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/default.json @@ -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 + } } ] } diff --git a/test/cmake-install/test.sh.in b/test/cmake-install/test.sh.in index 466bcbf..ac17899 100755 --- a/test/cmake-install/test.sh.in +++ b/test/cmake-install/test.sh.in @@ -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} diff --git a/test/issue-232-patch-in-place.cpp b/test/issue-232-patch-in-place.cpp new file mode 100644 index 0000000..28a1a72 --- /dev/null +++ b/test/issue-232-patch-in-place.cpp @@ -0,0 +1,140 @@ +#include +#include +#include + +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(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; +} diff --git a/test/issue-25-default-values.cpp b/test/issue-25-default-values.cpp index 354acf3..6b3e0fa 100644 --- a/test/issue-25-default-values.cpp +++ b/test/issue-25-default-values.cpp @@ -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() != "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() != "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; }