diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a4dc9d..87a7171 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ add_library(nlohmann_json_schema_validator src/json-schema-draft7.json.cpp src/json-uri.cpp src/json-validator.cpp + src/json-patch.cpp src/string-format-check.cpp) target_include_directories(nlohmann_json_schema_validator @@ -29,6 +30,10 @@ set_target_properties(nlohmann_json_schema_validator VERSION ${PROJECT_VERSION} SOVERSION 1) +# if used as a sub-directory, do not create install-rules - +# because of the dependency to nlohmann_json. +set(JSON_VALIDATOR_INSTALL ON) + # here we decice how nlohmann::json is found and used to build this project # first, check whether a nlohmann_json::nlohmann_json target exists already @@ -39,11 +44,15 @@ if(TARGET nlohmann_json::nlohmann_json) nlohmann_json_schema_validator PUBLIC nlohmann_json::nlohmann_json) + set(JSON_VALIDATOR_INSTALL OFF) + elseif(TARGET nlohmann_json) # or nlohmann_json, we are used a sub-project next to nlohmann-json's git repo message(STATUS "Found nlohmann_json-target - linking with it") target_link_libraries( nlohmann_json_schema_validator PUBLIC nlohmann_json) + set(JSON_VALIDATOR_INSTALL OFF) + else() if (NOT IS_ABSOLUTE ${nlohmann_json_DIR}) # make nlohmann_json_DIR absolute get_filename_component(nlohmann_json_DIR @@ -122,6 +131,7 @@ install(TARGETS nlohmann_json_schema_validator install(FILES src/nlohmann/json-schema.hpp DESTINATION ${CMAKE_INSTALL_PREFIX}/include/nlohmann) + if (BUILD_EXAMPLES) # simple nlohmann_json_schema_validator-executable add_executable(json-schema-validate app/json-schema-validate.cpp) @@ -129,6 +139,9 @@ if (BUILD_EXAMPLES) add_executable(readme-json-schema app/readme.cpp) target_link_libraries(readme-json-schema nlohmann_json_schema_validator) + + install(TARGETS json-schema-validate readme-json-schema + DESTINATION bin) endif() if (BUILD_TESTS) @@ -136,35 +149,36 @@ if (BUILD_TESTS) enable_testing() add_subdirectory(test) endif() -#---------------------------------------------------------------------------## -# Set Up the Project Targets and Config Files for CMake -#---------------------------------------------------------------------------## -# Set the install path to the cmake config files -set(INSTALL_CMAKE_DIR ${CMAKE_INSTALL_PREFIX}/lib/cmake/${PROJECT_NAME}) +if(JSON_VALIDATOR_INSTALL) + # Set Up the Project Targets and Config Files for CMake -# Create the ConfigVersion file -include(CMakePackageConfigHelpers) # write_basic_package_version_file -write_basic_package_version_file( ${PROJECT_NAME}ConfigVersion.cmake - VERSION ${PACKAGE_VERSION} - COMPATIBILITY SameMajorVersion) + # Set the install path to the cmake config files + set(INSTALL_CMAKE_DIR ${CMAKE_INSTALL_PREFIX}/lib/cmake/${PROJECT_NAME}) -# Get the relative path from the INSTALL_CMAKE_DIR to the include directory -file(RELATIVE_PATH REL_INCLUDE_DIR "${INSTALL_CMAKE_DIR}" "${CMAKE_INSTALL_PREFIX}/include") + # Create the ConfigVersion file + include(CMakePackageConfigHelpers) # write_basic_package_version_file + write_basic_package_version_file( ${PROJECT_NAME}ConfigVersion.cmake + VERSION ${PACKAGE_VERSION} + COMPATIBILITY SameMajorVersion) + + # Get the relative path from the INSTALL_CMAKE_DIR to the include directory + file(RELATIVE_PATH REL_INCLUDE_DIR "${INSTALL_CMAKE_DIR}" "${CMAKE_INSTALL_PREFIX}/include") -# Configure the Config.cmake file with the proper include directory -set(CONF_INCLUDE_DIRS "\${JSON_SCHEMA_VALIDATOR_CMAKE_DIR}/${REL_INCLUDE_DIR}") -configure_file(${PROJECT_NAME}Config.cmake.in - "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" @ONLY) + # Configure the Config.cmake file with the proper include directory + set(CONF_INCLUDE_DIRS "\${JSON_SCHEMA_VALIDATOR_CMAKE_DIR}/${REL_INCLUDE_DIR}") + configure_file(${PROJECT_NAME}Config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" @ONLY) -# Install the Config.cmake and ConfigVersion.cmake files -install(FILES - "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" - "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" - DESTINATION "${INSTALL_CMAKE_DIR}") + # Install the Config.cmake and ConfigVersion.cmake files + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" + DESTINATION "${INSTALL_CMAKE_DIR}") -# Install Targets -install(EXPORT ${PROJECT_NAME}Targets - FILE ${PROJECT_NAME}Targets.cmake - DESTINATION "${INSTALL_CMAKE_DIR}") + # Install Targets + install(EXPORT ${PROJECT_NAME}Targets + FILE ${PROJECT_NAME}Targets.cmake + DESTINATION "${INSTALL_CMAKE_DIR}") +endif() diff --git a/app/readme.cpp b/app/readme.cpp index da95fed..45f4d35 100644 --- a/app/readme.cpp +++ b/app/readme.cpp @@ -1,5 +1,5 @@ -#include #include +#include #include @@ -12,16 +12,25 @@ static json person_schema = R"( "$schema": "http://json-schema.org/draft-07/schema#", "title": "A person", "properties": { - "name": { - "description": "Name", - "type": "string" - }, - "age": { - "description": "Age of the person", - "type": "number", - "minimum": 2, - "maximum": 200 + "name": { + "description": "Name", + "type": "string" + }, + "age": { + "description": "Age of the person", + "type": "number", + "minimum": 2, + "maximum": 200 + }, + "address":{ + "type": "object", + "properties":{ + "street":{ + "type": "string", + "default": "Abbey Road" + } } + } }, "required": [ "name", @@ -34,7 +43,8 @@ static json person_schema = R"( // The people are defined with brace initialization static json bad_person = {{"age", 42}}; -static json good_person = {{"name", "Albert"}, {"age", 42}}; +static json good_person = {{"name", "Albert"}, {"age", 42}, {"address", {{"street", "Main Street"}}}}; +static json good_defaulted_person = {{"name", "Knut"}, {"age", 69}, {"address", {}}}; int main() { @@ -51,12 +61,13 @@ int main() /* json-parse the people - API of 1.0.0, default throwing error handler */ - for (auto &person : {bad_person, good_person}) { + for (auto &person : {bad_person, good_person, good_defaulted_person}) { std::cout << "About to validate this person:\n" << std::setw(2) << person << std::endl; try { - validator.validate(person); // validate the document - uses the default throwing error-handler + auto defaultPatch = validator.validate(person); // validate the document - uses the default throwing error-handler std::cout << "Validation succeeded\n"; + std::cout << "Patch with defaults: " << defaultPatch.dump(2) << std::endl; } catch (const std::exception &e) { std::cerr << "Validation failed, here is why: " << e.what() << "\n"; } @@ -72,7 +83,6 @@ int main() } }; - for (auto &person : {bad_person, good_person}) { std::cout << "About to validate this person:\n" << std::setw(2) << person << std::endl; diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 0000000..707d494 --- /dev/null +++ b/conanfile.py @@ -0,0 +1,69 @@ +import os +import re +from conans import load, tools, ConanFile, CMake + + +def get_version(): + try: + version = os.getenv('PROJECT_VERSION', None) + if version: + return version + + content = load('CMakeLists.txt') + version = re.search('set\(PROJECT_VERSION (.*)\)', content).group(1) + return version.strip() + except: + return None + + +class JsonSchemaValidatorConan(ConanFile): + name = 'JsonSchemaValidator' + version = get_version() + url = 'https://github.com/pboettch/json-schema-validator' + license = 'MIT' + settings = 'os', 'compiler', 'build_type', 'arch' + options = { + 'shared': [True, False], + 'fPIC': [True, False] + } + default_options = { + 'shared': False, + 'fPIC': True + } + generators = "cmake" + exports_sources = [ + 'CMakeLists.txt', + 'nlohmann_json_schema_validatorConfig.cmake.in', + 'src/*', + 'app/*', + ] + + requires = ( + 'nlohmann_json/3.7.3' + ) + + def build(self): + cmake = CMake(self) + cmake.definitions['nlohmann_json_DIR'] = os.path.join(self.deps_cpp_info['nlohmann_json'].rootpath, 'include') + cmake.definitions['BUILD_EXAMPLES'] = True + cmake.definitions['BUILD_TESTS'] = False + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + includedir = os.path.join(self.package_folder, "include") + self.cpp_info.includedirs = [includedir] + + libdir = os.path.join(self.package_folder, "lib") + self.cpp_info.libdirs = [libdir] + self.cpp_info.libs += tools.collect_libs(self, libdir) + + bindir = os.path.join(self.package_folder, "bin") + self.output.info("Appending PATH environment variable: {}".format(bindir)) + self.env_info.PATH.append(bindir) + + self.user_info.VERSION = self.version diff --git a/src/json-patch.cpp b/src/json-patch.cpp new file mode 100644 index 0000000..3f24d80 --- /dev/null +++ b/src/json-patch.cpp @@ -0,0 +1,75 @@ +#include "json-patch.hpp" + +namespace nlohmann +{ + +json_patch::json_patch(json &&patch) + : j_{std::move(patch)} +{ + validateJsonPatch(j_); +} + +json_patch::json_patch(const json &patch) + : j_{std::move(patch)} +{ + validateJsonPatch(j_); +} + +json_patch &json_patch::add(std::string path, json value) +{ + j_.push_back(json{{"op", "add"}, {"path", std::move(path)}, {"value", std::move(value)}}); + return *this; +} + +json_patch &json_patch::replace(std::string path, json value) +{ + j_.push_back(json{{"op", "replace"}, {"path", std::move(path)}, {"value", std::move(value)}}); + return *this; +} + +json_patch &json_patch::remove(std::string path) +{ + j_.push_back(json{{"op", "remove"}, {"path", std::move(path)}}); + return *this; +} + +void json_patch::validateJsonPatch(json const &patch) +{ + if (!patch.is_array()) { + throw JsonPatchFormatException{"Json is not an array"}; + } + + for (auto const &op : patch) { + if (!op.is_object()) { + throw JsonPatchFormatException{"Each json patch entry needs to be an op object"}; + } + + if (!op.contains("op")) { + throw JsonPatchFormatException{"Each json patch entry needs op element"}; + } + + const auto opType = op["op"].get(); + if ((opType != "add") && (opType != "remove") && (opType != "replace")) { + throw JsonPatchFormatException{std::string{"Operation "} + opType + std::string{"is invalid"}}; + } + + if (!op.contains("path")) { + throw JsonPatchFormatException{"Each json patch entry needs path element"}; + } + + try { + // try parse to path + [[maybe_unused]] const auto p = json::json_pointer{op["path"].get()}; + } catch (json::exception &e) { + throw JsonPatchFormatException{e.what()}; + } + + if (opType != "remove") { + if (!op.contains("value")) { + throw JsonPatchFormatException{"Remove and replace needs value element"}; + } + } + } +} + +} // namespace nlohmann diff --git a/src/json-patch.hpp b/src/json-patch.hpp new file mode 100644 index 0000000..4f204be --- /dev/null +++ b/src/json-patch.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace nlohmann +{ +class JsonPatchFormatException : public std::exception +{ +public: + explicit JsonPatchFormatException(std::string msg) + : ex_{std::move(msg)} {} + + inline const char *what() const noexcept override final { return ex_.c_str(); } + +private: + std::string ex_; +}; + +class json_patch +{ +public: + json_patch() = default; + json_patch(json &&patch); + json_patch(const json &patch); + + json_patch &add(std::string path, json value); + json_patch &replace(std::string path, json value); + json_patch &remove(std::string path); + + operator json() const { return j_; } + +private: + json j_; + + static void validateJsonPatch(json const &patch); +}; +} // namespace nlohmann diff --git a/src/json-uri.cpp b/src/json-uri.cpp index 2809fa1..6ff1c16 100644 --- a/src/json-uri.cpp +++ b/src/json-uri.cpp @@ -44,7 +44,7 @@ void json_uri::update(const std::string &uri) auto location = uri.substr(0, pointer_separator); - if (location.size()) { // a location part has been found + if (location.size()) { // a location part has been found // if it is an URN take it as it is if (location.find("urn:") == 0) { @@ -99,7 +99,7 @@ void json_uri::update(const std::string &uri) identifier_ = pointer; } -const std::string json_uri::location() const +std::string json_uri::location() const { if (urn_.size()) return urn_; diff --git a/src/json-validator.cpp b/src/json-validator.cpp index e10234f..c684578 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -8,11 +8,14 @@ */ #include +#include "json-patch.hpp" + #include #include #include using nlohmann::json; +using nlohmann::json_patch; using nlohmann::json_uri; using nlohmann::json_schema::root_schema; using namespace nlohmann::json_schema; @@ -30,16 +33,25 @@ using namespace nlohmann::json_schema; namespace { +static const json EmptyDefault{}; + class schema { protected: root_schema *root_; public: + virtual ~schema() = default; + schema(root_schema *root) : root_(root) {} - virtual void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const = 0; + virtual void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const = 0; + + virtual const json &defaultValue(const json::json_pointer &, const json &, error_handler &) const + { + return EmptyDefault; + } static std::shared_ptr make(json &schema, root_schema *root, @@ -52,14 +64,24 @@ class schema_ref : public schema const std::string id_; std::shared_ptr target_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final { if (target_) - target_->validate(ptr, instance, e); + target_->validate(ptr, instance, patch, e); else e.error(ptr, instance, "unresolved schema-reference " + id_); } + const json &defaultValue(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + { + if (target_) + target_->defaultValue(ptr, instance, e); + else + e.error(ptr, instance, "unresolved schema-reference " + id_); + + return EmptyDefault; + } + public: schema_ref(const std::string &id, root_schema *root) : schema(root), id_(id) {} @@ -101,9 +123,9 @@ class root_schema : public schema } public: - root_schema(schema_loader loader, - format_checker format) - : schema(this), loader_(loader), format_check_(format) {} + root_schema(schema_loader &&loader, + format_checker &&format) + : schema(this), loader_(std::move(loader)), format_check_(std::move(format)) {} format_checker &format_check() { return format_check_; } @@ -208,13 +230,23 @@ public: } while (1); } - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final { if (root_) - root_->validate(ptr, instance, e); + root_->validate(ptr, instance, patch, e); else e.error(ptr, "", "no root schema has yet been set for validating an instance"); } + + const json &defaultValue(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + { + if (root_) + root_->defaultValue(ptr, instance, e); + else + e.error(ptr, "", "no root schema has yet been set for validating an instance"); + + return EmptyDefault; + } }; } // namespace json_schema @@ -248,15 +280,20 @@ class logical_not : public schema { std::shared_ptr subschema_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final { first_error_handler esub; - subschema_->validate(ptr, instance, esub); + subschema_->validate(ptr, instance, patch, esub); if (!esub) e.error(ptr, instance, "the subschema has succeeded, but it is required to not validate"); } + const json &defaultValue(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + { + return subschema_->defaultValue(ptr, instance, e); + } + public: logical_not(json &sch, root_schema *root, @@ -278,13 +315,13 @@ class logical_combination : public schema { std::vector> subschemata_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final { size_t count = 0; for (auto &s : subschemata_) { first_error_handler esub; - s->validate(ptr, instance, esub); + s->validate(ptr, instance, patch, esub); if (!esub) count++; @@ -348,6 +385,7 @@ bool logical_combination::is_validate_complete(const json &instance, cons class type_schema : public schema { + json defaultValue_{}; std::vector> type_; std::pair enum_, const_; std::vector> logic_; @@ -360,13 +398,18 @@ class type_schema : public schema std::shared_ptr if_, then_, else_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override final + const json &defaultValue(const json::json_pointer &, const json &, error_handler &) const override + { + return defaultValue_; + } + + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override final { // depending on the type of instance run the type specific validator - if present auto type = type_[(uint8_t) instance.type()]; if (type) - type->validate(ptr, instance, e); + type->validate(ptr, instance, patch, e); else e.error(ptr, instance, "unexpected instance type"); @@ -387,18 +430,18 @@ class type_schema : public schema e.error(ptr, instance, "instance not const"); for (auto l : logic_) - l->validate(ptr, instance, e); + l->validate(ptr, instance, patch, e); if (if_) { first_error_handler err; - if_->validate(ptr, instance, err); + if_->validate(ptr, instance, patch, err); if (!err) { if (then_) - then_->validate(ptr, instance, e); + then_->validate(ptr, instance, patch, e); } else { if (else_) - else_->validate(ptr, instance, e); + else_->validate(ptr, instance, patch, e); } } } @@ -450,6 +493,11 @@ public: sch.erase(attr); } + const auto defaultAttr = sch.find("default"); + if (defaultAttr != sch.end()) { + defaultValue_ = defaultAttr.value(); + } + for (auto &key : known_keywords) sch.erase(key); @@ -542,7 +590,7 @@ class string : public schema return len; } - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { if (minLength_.first) { if (utf8_length(instance) < minLength_.second) { @@ -632,7 +680,7 @@ class numeric : public schema return std::fabs(res) > std::fabs(eps); } - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { T value = instance; // conversion of json to value_type @@ -691,7 +739,7 @@ public: class null : public schema { - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { if (!instance.is_null()) e.error(ptr, instance, "expected to be null"); @@ -704,7 +752,7 @@ public: class boolean_type : public schema { - void validate(const json::json_pointer &, const json &, error_handler &) const override {} + void validate(const json::json_pointer &, const json &, json_patch &, error_handler &) const override {} public: boolean_type(json &, root_schema *root) @@ -714,7 +762,7 @@ public: class boolean : public schema { bool true_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { if (!true_) { // false schema // empty array @@ -738,7 +786,7 @@ class required : public schema { const std::vector required_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override final { for (auto &r : required_) if (instance.find(r) == instance.end()) @@ -766,7 +814,7 @@ class object : public schema std::shared_ptr propertyNames_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override { if (maxProperties_.first && instance.size() > maxProperties_.second) e.error(ptr, instance, "too many properties"); @@ -781,14 +829,14 @@ class object : public schema // for each property in instance for (auto &p : instance.items()) { if (propertyNames_) - propertyNames_->validate(ptr, p.key(), e); + 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(ptr / p.key(), p.value(), e); + schema_p->second->validate(ptr / p.key(), p.value(), patch, e); } #ifndef NO_STD_REGEX @@ -796,23 +844,34 @@ class object : public schema 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(ptr / p.key(), p.value(), e); + schema_pp.second->validate(ptr / p.key(), p.value(), patch, e); } #endif // check additionalProperties as a last resort if (!a_prop_or_pattern_matched && additionalProperties_) { first_error_handler additional_prop_err; - additionalProperties_->validate(ptr / p.key(), p.value(), additional_prop_err); + additionalProperties_->validate(ptr / p.key(), p.value(), patch, additional_prop_err); if (additional_prop_err) e.error(ptr, instance, "validation failed for additional property '" + p.key() + "': " + additional_prop_err.message_); } } + // 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 &defaultValue = prop.second->defaultValue(ptr, instance, e); + if (!defaultValue.empty()) { // if default value is available + patch.add((ptr / prop.first), defaultValue); + } + } + } + for (auto &dep : dependencies_) { auto prop = instance.find(dep.first); - if (prop != instance.end()) // if dependency-property is present in instance - dep.second->validate(ptr / dep.first, instance, e); // validate + if (prop != instance.end()) // if dependency-property is present in instance + dep.second->validate(ptr / dep.first, instance, patch, e); // validate } } @@ -907,7 +966,7 @@ class array : public schema std::shared_ptr contains_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override { if (maxItems_.first && instance.size() > maxItems_.second) e.error(ptr, instance, "array has too many items"); @@ -926,7 +985,7 @@ class array : public schema size_t index = 0; if (items_schema_) for (auto &i : instance) { - items_schema_->validate(ptr / index, i, e); + items_schema_->validate(ptr / index, i, patch, e); index++; } else { @@ -943,7 +1002,7 @@ class array : public schema if (!item_validator) break; - item_validator->validate(ptr / index, i, e); + item_validator->validate(ptr / index, i, patch, e); } } @@ -951,7 +1010,7 @@ class array : public schema bool contained = false; for (auto &item : instance) { first_error_handler local_e; - contains_->validate(ptr, item, local_e); + contains_->validate(ptr, item, patch, local_e); if (!local_e) { contained = true; break; @@ -1127,16 +1186,22 @@ namespace json_schema json_validator::json_validator(schema_loader loader, format_checker format) - : root_(std::unique_ptr(new root_schema(loader, format))) + : root_(std::unique_ptr(new root_schema(std::move(loader), std::move(format)))) { } json_validator::json_validator(const json &schema, schema_loader loader, format_checker format) - : json_validator(loader, format) + : json_validator(std::move(loader), std::move(format)) { set_root_schema(schema); } +json_validator::json_validator(json &&schema, schema_loader loader, format_checker format) + : json_validator(std::move(loader), std::move(format)) +{ + set_root_schema(std::move(schema)); +} + // move constructor, destructor and move assignment operator can be defaulted here // where root_schema is a complete type json_validator::json_validator(json_validator &&) = default; @@ -1148,16 +1213,23 @@ void json_validator::set_root_schema(const json &schema) root_->set_root_schema(schema); } -void json_validator::validate(const json &instance) const +void json_validator::set_root_schema(json &&schema) { - throwing_error_handler err; - validate(instance, err); + root_->set_root_schema(std::move(schema)); } -void json_validator::validate(const json &instance, error_handler &err) const +json json_validator::validate(const json &instance) const +{ + throwing_error_handler err; + return validate(instance, err); +} + +json json_validator::validate(const json &instance, error_handler &err) const { json::json_pointer ptr; - root_->validate(ptr, instance, err); + json_patch patch{}; + root_->validate(ptr, instance, patch, err); + return patch; } } // namespace json_schema diff --git a/src/nlohmann/json-schema.hpp b/src/nlohmann/json-schema.hpp index 204f412..baa9719 100644 --- a/src/nlohmann/json-schema.hpp +++ b/src/nlohmann/json-schema.hpp @@ -71,14 +71,14 @@ public: update(uri); } - const std::string scheme() const { return scheme_; } - const std::string authority() const { return authority_; } - const std::string path() const { return path_; } + const std::string &scheme() const { return scheme_; } + const std::string &authority() const { return authority_; } + const std::string &path() const { return path_; } - const json::json_pointer pointer() const { return pointer_; } - const std::string identifier() const { return identifier_; } + const json::json_pointer &pointer() const { return pointer_; } + const std::string &identifier() const { return identifier_; } - const std::string fragment() const + std::string fragment() const { if (identifier_ == "") return pointer_; @@ -86,8 +86,8 @@ public: return identifier_; } - const std::string url() const { return location(); } - const std::string location() const; + std::string url() const { return location(); } + std::string location() const; static std::string escape(const std::string &); @@ -169,19 +169,27 @@ class JSON_SCHEMA_VALIDATOR_API json_validator public: json_validator(schema_loader = nullptr, format_checker = nullptr); - json_validator(json_validator &&); + json_validator(const json &, schema_loader = nullptr, format_checker = nullptr); - ~json_validator(); + json_validator(json &&, schema_loader = nullptr, format_checker = nullptr); + + json_validator(json_validator &&); json_validator &operator=(json_validator &&); + json_validator(json_validator const &) = delete; + json_validator &operator=(json_validator const &) = delete; + + ~json_validator(); + // insert and set the root-schema void set_root_schema(const json &); + void set_root_schema(json &&); // validate a json-document based on the root-schema - void validate(const json &) const; + json validate(const json &) const; // validate a json-document based on the root-schema with a custom error-handler - void validate(const json &, error_handler &) const; + json validate(const json &, error_handler &) const; }; } // namespace json_schema diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9ca65d7..3371cb9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,6 +35,10 @@ add_executable(issue-70-root-schema-constructor issue-70-root-schema-constructor target_link_libraries(issue-70-root-schema-constructor nlohmann_json_schema_validator) add_test(NAME issue-70-root-schema-constructor COMMAND issue-70-root-schema-constructor) +add_executable(issue-25-default-values issue-25-default-values.cpp) +target_link_libraries(issue-25-default-values nlohmann_json_schema_validator) +add_test(NAME issue-25-default-values COMMAND issue-25-default-values) + # Unit test for string format checks add_executable("string-format-check-test" "string-format-check-test.cpp") target_include_directories("string-format-check-test" PRIVATE "${PROJECT_SOURCE_DIR}/src/") diff --git a/test/issue-25-default-values.cpp b/test/issue-25-default-values.cpp new file mode 100644 index 0000000..f775281 --- /dev/null +++ b/test/issue-25-default-values.cpp @@ -0,0 +1,100 @@ +#include +#include + +using nlohmann::json; +using nlohmann::json_schema::json_validator; + +static const json person_schema = R"( +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "A person", + "properties": { + "name": { + "description": "Name", + "type": "string" + }, + "age": { + "description": "Age of the person", + "type": "number", + "minimum": 2, + "maximum": 200 + }, + "address":{ + "type": "object", + "properties":{ + "street":{ + "type": "string", + "default": "Abbey Road" + } + } + } + }, + "required": [ + "name", + "age" + ], + "additionalProperties": false, + "type": "object" +})"_json; + +int main(void) +{ + json_validator validator{}; + + // add address which is optional that should generate a diff containing a default street + json person_missing_address = R"({ + "name": "Hans", + "age": 69, + "address": {} +})"_json; + + validator.set_root_schema(person_schema); + + const auto default_patch = validator.validate(person_missing_address); + + if (!default_patch.is_array()) { + std::cerr << "Patch with defaults is expected to be an array" << std::endl; + return 1; + } + + if (default_patch.size() != 1) { + std::cerr << "Patch with defaults is expected to contain one opperation" << 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; + return 1; + } + + if (single_op["op"].get() != "add") { + std::cerr << "Patch with defaults is expected to contain add opperation" << std::endl; + return 1; + } + + if (!single_op.contains("path")) { + std::cerr << "Patch with defaults is expected to contain a path" << std::endl; + return 1; + } + + const auto &readPath = single_op["path"].get(); + if (readPath != "/address/street") { + std::cerr << "Patch with defaults contains wrong path. It is " << readPath << " and should be " + << "/address/street" << std::endl; + return 1; + } + + if (!single_op.contains("value")) { + std::cerr << "Patch with defaults is expected to contain a value" << std::endl; + return 1; + } + + if (single_op["value"].get() != "Abbey Road") { + std::cerr << "Patch with defaults contains wrong value" << std::endl; + return 1; + } + + return 0; +}