From 8e3f61ba7785abbd24a461d215a1e51b5bc74ad6 Mon Sep 17 00:00:00 2001 From: Lukasz Laszko Date: Tue, 28 Jan 2020 12:45:06 +0800 Subject: [PATCH 01/14] conan file added --- conanfile.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 conanfile.py diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 0000000..49f78bf --- /dev/null +++ b/conanfile.py @@ -0,0 +1,69 @@ +import os +import re +from conans import load, 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/lukaszlaszko/json-schema-validator' + 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/*', + ] + + _build_subfolder = 'build_subfolder' + + 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_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 += self.collect_libs(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 From 16491dfa1b3c867d5aad37b4056edbbdc90095b7 Mon Sep 17 00:00:00 2001 From: Lukasz Laszko Date: Tue, 28 Jan 2020 12:51:57 +0800 Subject: [PATCH 02/14] example app installation added --- CMakeLists.txt | 3 +++ conanfile.py | 1 + 2 files changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 80a77ec..20ea426 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,6 +129,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) diff --git a/conanfile.py b/conanfile.py index 49f78bf..bd56cbf 100644 --- a/conanfile.py +++ b/conanfile.py @@ -46,6 +46,7 @@ class JsonSchemaValidatorConan(ConanFile): 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() From 96f7c5af533cbefd108775d62f3a9e0959fa6dce Mon Sep 17 00:00:00 2001 From: Lukasz Laszko Date: Tue, 28 Jan 2020 12:54:19 +0800 Subject: [PATCH 03/14] license entry added to conanfile --- conanfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index bd56cbf..5865893 100644 --- a/conanfile.py +++ b/conanfile.py @@ -19,7 +19,8 @@ def get_version(): class JsonSchemaValidatorConan(ConanFile): name = 'JsonSchemaValidator' version = get_version() - url = 'https://github.com/lukaszlaszko/json-schema-validator' + url = 'https://github.com/pboettch/json-schema-validator' + license = 'MIT' settings = 'os', 'compiler', 'build_type', 'arch' options = { 'shared': [True, False], From 6b043ea56fe0c96f297501e7886292845346f27f Mon Sep 17 00:00:00 2001 From: Lukasz Laszko Date: Tue, 28 Jan 2020 12:59:41 +0800 Subject: [PATCH 04/14] unnecessary field removed --- conanfile.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/conanfile.py b/conanfile.py index 5865893..f98e421 100644 --- a/conanfile.py +++ b/conanfile.py @@ -38,8 +38,6 @@ class JsonSchemaValidatorConan(ConanFile): 'app/*', ] - _build_subfolder = 'build_subfolder' - requires = ( 'nlohmann_json/3.7.3' ) From 597691589736c132b8b12c93c399cefd02dd1c06 Mon Sep 17 00:00:00 2001 From: Lukasz Laszko Date: Tue, 28 Jan 2020 15:19:22 +0800 Subject: [PATCH 05/14] deprecated collect libs replaced --- conanfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conanfile.py b/conanfile.py index f98e421..707d494 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,6 +1,6 @@ import os import re -from conans import load, ConanFile, CMake +from conans import load, tools, ConanFile, CMake def get_version(): @@ -60,7 +60,7 @@ class JsonSchemaValidatorConan(ConanFile): libdir = os.path.join(self.package_folder, "lib") self.cpp_info.libdirs = [libdir] - self.cpp_info.libs += self.collect_libs(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)) From 2969393afa32591123e710b10be043937a0af18c Mon Sep 17 00:00:00 2001 From: Patrick Boettcher Date: Mon, 27 Jan 2020 11:06:24 +0100 Subject: [PATCH 06/14] fix / workaround for #79 --- CMakeLists.txt | 75 +++++++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 20ea426..5c809d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,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 +43,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 @@ -113,14 +121,16 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") endif() endif() -install(TARGETS nlohmann_json_schema_validator - EXPORT ${PROJECT_NAME}Targets - LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib - RUNTIME DESTINATION bin) +if(JSON_VALIDATOR_INSTALL) + install(TARGETS nlohmann_json_schema_validator + EXPORT ${PROJECT_NAME}Targets + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin) -install(FILES src/nlohmann/json-schema.hpp - DESTINATION include/nlohmann) + install(FILES src/nlohmann/json-schema.hpp + DESTINATION include/nlohmann) +endif() if (BUILD_EXAMPLES) # simple nlohmann_json_schema_validator-executable @@ -139,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() From e146b37a32f6bb2b8cd176ecf4bbd4210cd56de2 Mon Sep 17 00:00:00 2001 From: Sven Fink Date: Mon, 10 Feb 2020 08:00:07 +0100 Subject: [PATCH 07/14] Add virtual dtor to schema --- src/json-validator.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/json-validator.cpp b/src/json-validator.cpp index e10234f..96fa626 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -36,6 +36,8 @@ protected: root_schema *root_; public: + virtual ~schema() = default; + schema(root_schema *root) : root_(root) {} From 01e3dea71b1a752e4f2e6998c8a1839392ba94c7 Mon Sep 17 00:00:00 2001 From: Sven Fink Date: Mon, 10 Feb 2020 10:58:11 +0100 Subject: [PATCH 08/14] Avoid copy of json on set_root_schema If a json object containing a schema is moved into the json validator it should not be copied. At least the public interface of the validator class schould not force the copy. --- src/json-validator.cpp | 5 +++++ src/nlohmann/json-schema.hpp | 1 + 2 files changed, 6 insertions(+) diff --git a/src/json-validator.cpp b/src/json-validator.cpp index 96fa626..8fc4b5a 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -1150,6 +1150,11 @@ void json_validator::set_root_schema(const json &schema) root_->set_root_schema(schema); } +void json_validator::set_root_schema(json &&schema) +{ + root_->set_root_schema(std::move(schema)); +} + void json_validator::validate(const json &instance) const { throwing_error_handler err; diff --git a/src/nlohmann/json-schema.hpp b/src/nlohmann/json-schema.hpp index 204f412..d6e0ede 100644 --- a/src/nlohmann/json-schema.hpp +++ b/src/nlohmann/json-schema.hpp @@ -176,6 +176,7 @@ public: // 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; From d84e0a28d6aebe3a8f499bc7b80af37117b45d07 Mon Sep 17 00:00:00 2001 From: Sven Fink Date: Mon, 10 Feb 2020 13:51:48 +0100 Subject: [PATCH 09/14] Allow create validator from rvalue json --- src/json-validator.cpp | 10 ++++++++-- src/nlohmann/json-schema.hpp | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/json-validator.cpp b/src/json-validator.cpp index 8fc4b5a..b84c026 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -1129,16 +1129,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; diff --git a/src/nlohmann/json-schema.hpp b/src/nlohmann/json-schema.hpp index d6e0ede..a1794cf 100644 --- a/src/nlohmann/json-schema.hpp +++ b/src/nlohmann/json-schema.hpp @@ -171,6 +171,7 @@ 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 &&, schema_loader = nullptr, format_checker = nullptr); ~json_validator(); json_validator &operator=(json_validator &&); From cb892e1d20478b9621e917476a8647f3cfbba354 Mon Sep 17 00:00:00 2001 From: Sven Fink Date: Mon, 10 Feb 2020 13:53:10 +0100 Subject: [PATCH 10/14] Separate ctors of json validator --- src/nlohmann/json-schema.hpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/nlohmann/json-schema.hpp b/src/nlohmann/json-schema.hpp index a1794cf..63f8e10 100644 --- a/src/nlohmann/json-schema.hpp +++ b/src/nlohmann/json-schema.hpp @@ -169,12 +169,15 @@ 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 &&, schema_loader = nullptr, format_checker = nullptr); - ~json_validator(); + + json_validator(json_validator &&); json_validator &operator=(json_validator &&); + ~json_validator(); + // insert and set the root-schema void set_root_schema(const json &); void set_root_schema(json &&); From 7fbda9da0ea2731566551b2dae626dc29d2a51e0 Mon Sep 17 00:00:00 2001 From: Sven Fink Date: Mon, 10 Feb 2020 13:54:10 +0100 Subject: [PATCH 11/14] Apply rule of five on json_validator explicitly delete copy ctor and copy assignment --- src/nlohmann/json-schema.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/nlohmann/json-schema.hpp b/src/nlohmann/json-schema.hpp index 63f8e10..518a0c7 100644 --- a/src/nlohmann/json-schema.hpp +++ b/src/nlohmann/json-schema.hpp @@ -176,6 +176,9 @@ public: 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 From f20017306f3fb9f19d171e06f3573780eef7cfd7 Mon Sep 17 00:00:00 2001 From: Sven Fink Date: Mon, 10 Feb 2020 14:03:58 +0100 Subject: [PATCH 12/14] Avoid forced string copies --- src/json-uri.cpp | 4 ++-- src/nlohmann/json-schema.hpp | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) 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/nlohmann/json-schema.hpp b/src/nlohmann/json-schema.hpp index 518a0c7..2d18411 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 &); From 7264fa0a05d6ef3e0d3d25beac02fdf4a39280f1 Mon Sep 17 00:00:00 2001 From: Sven Fink Date: Mon, 10 Feb 2020 14:08:29 +0100 Subject: [PATCH 13/14] Internal root_schema may move callbacks --- src/json-validator.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/json-validator.cpp b/src/json-validator.cpp index b84c026..3a09957 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -103,9 +103,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_; } From cb95425f594838f8982923702a25e7275fcec243 Mon Sep 17 00:00:00 2001 From: Sven Fink Date: Mon, 2 Mar 2020 15:22:59 +0100 Subject: [PATCH 14/14] Add schema class that is aware of a default entry Uses json patch format for defaults: json patch (RFC 6902) makes it clearer in case of array contents. It is supported by nlohmann::json and can be applied with `patch` to fill up the json with defaults. --- CMakeLists.txt | 1 + app/readme.cpp | 38 +++++---- src/json-patch.cpp | 75 ++++++++++++++++++ src/json-patch.hpp | 38 +++++++++ src/json-validator.cpp | 131 ++++++++++++++++++++++--------- src/nlohmann/json-schema.hpp | 4 +- test/CMakeLists.txt | 4 + test/issue-25-default-values.cpp | 100 +++++++++++++++++++++++ 8 files changed, 339 insertions(+), 52 deletions(-) create mode 100644 src/json-patch.cpp create mode 100644 src/json-patch.hpp create mode 100644 test/issue-25-default-values.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c809d7..c4cc218 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 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/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-validator.cpp b/src/json-validator.cpp index 3a09957..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,6 +33,8 @@ using namespace nlohmann::json_schema; namespace { +static const json EmptyDefault{}; + class schema { protected: @@ -41,7 +46,12 @@ public: 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, @@ -54,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) {} @@ -210,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 @@ -250,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, @@ -280,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++; @@ -350,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_; @@ -362,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"); @@ -389,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); } } } @@ -452,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); @@ -544,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) { @@ -634,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 @@ -693,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"); @@ -706,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) @@ -716,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 @@ -740,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()) @@ -768,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"); @@ -783,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 @@ -798,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 } } @@ -909,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"); @@ -928,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 { @@ -945,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); } } @@ -953,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; @@ -1161,16 +1218,18 @@ void json_validator::set_root_schema(json &&schema) root_->set_root_schema(std::move(schema)); } -void json_validator::validate(const json &instance) const +json json_validator::validate(const json &instance) const { throwing_error_handler err; - validate(instance, err); + return validate(instance, err); } -void json_validator::validate(const json &instance, error_handler &err) const +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 2d18411..baa9719 100644 --- a/src/nlohmann/json-schema.hpp +++ b/src/nlohmann/json-schema.hpp @@ -186,10 +186,10 @@ public: 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; +}