diff --git a/.travis.yml b/.travis.yml index edb2981..9bd8a92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -60,7 +60,7 @@ script: - $CXX --version # put json.hpp to nlohmann - - mkdir -p nlohmann && wget https://github.com/nlohmann/json/releases/download/v3.5.0/json.hpp -O nlohmann/json.hpp + - mkdir -p nlohmann && wget https://github.com/nlohmann/json/releases/download/v3.6.0/json.hpp -O nlohmann/json.hpp # compile and execute unit tests - mkdir -p build && cd build diff --git a/CMakeLists.txt b/CMakeLists.txt index b6afc34..c209683 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,7 +46,8 @@ set_target_properties(json-schema-validator install(TARGETS json-schema-validator LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib) + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin) install(DIRECTORY src/ DESTINATION include @@ -59,12 +60,13 @@ target_include_directories(json-schema-validator target_compile_features(json-schema-validator PUBLIC cxx_range_for) # for C++11 - flags -# Enable more compiler warnings, except when using Visual Studio compiler -if(NOT MSVC) + +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") target_compile_options(json-schema-validator PRIVATE -Wall -Wextra) endif() + target_link_libraries(json-schema-validator PUBLIC json-hpp) diff --git a/README.md b/README.md index 65c5692..02026b8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This is a C++ library for validating JSON documents based on a [draft-7 of JSON Schema Validation](http://json-schema.org/schema). First a disclaimer: *It is work in progress and -contributions or hints or discussions are welcome.* Even though a 2.0.0 release is immenent. +contributions or hints or discussions are welcome.* Even though a 2.0.0 release is imminent. Niels Lohmann et al develop a great JSON parser for C++ called [JSON for Modern C++](https://github.com/nlohmann/json). This validator is based on this @@ -21,7 +21,7 @@ is rather simple. # New in version 2 -Although significant changes have been coorporate to the 2 version +Although significant changes have been done for the 2nd version (a complete rewrite) the API is compatible with the 1.0.0 release. Except for the namespace which is now `nlohmann::json_schema. @@ -29,7 +29,7 @@ Version **2** supports JSON schema draft 7, whereas 1 was supporting draft 4 only. Please update your schemas. The primary change in 2 is the way a schema is used. While in version 1 the schema was -kept as a JSON-document and used again and again during validation, in versin 2 the schema +kept as a JSON-document and used again and again during validation, in version 2 the schema is parsed into compiled C++ objects which are then used during validation. There are surely still optimizations to be done, but validation speed has improved by factor 100 or more. @@ -42,9 +42,9 @@ messages if a JSON-document/instance does not comply to its schema. By default this is done with exceptions thrown at the users with a helpful message telling what's wrong with the document while validating. -With **2.0.0** the user can pass a `json_scheam::basic_error_handler` derived object -along with the instance to validate to receive a each time a validation error occurs -and decice what to do (throwing, counting, collecting). +With **2.0.0** the user can pass a `json_scheam::basic_error_handler`-derived +object along with the instance to validate to receive a callback each time a +validation error occurs and decide what to do (throwing, counting, collecting). Another goal was to use Niels Lohmann's JSON-library. This is why the validator lives in his namespace. @@ -55,12 +55,12 @@ Numerical validation uses nlohmann integer, unsigned and floating point types, d the schema type is "integer" or "number". Bignum (i.e. arbitrary precision and range) is not supported at this time. -Currently JSON-URI with "plain name fragments" are not supported. So referring to an URI +Currently JSON-URI with "plain name fragments" are not supported: referring to an URI with `$ref: "file.json#plain"` will not work. # How to use -The current state of the build-system needs at least version **3.5.0** of NLohmann's +The current state of the build-system needs at least version **3.6.0** of NLohmann's JSON library. It is looking for the `json.hpp` within a `nlohmann/`-path. When build the library you need to provide the path to the directory where the include-file @@ -167,7 +167,7 @@ int main() std::cout << "About to validate this person:\n" << std::setw(2) << person << std::endl; try { - validator.validate(person); // validate the document + validator.validate(person); // validate the document - uses the default throwing error-handler std::cout << "Validation succeeded\n"; } catch (const std::exception &e) { std::cerr << "Validation failed, here is why: " << e.what() << "\n"; @@ -190,7 +190,7 @@ int main() << std::setw(2) << person << std::endl; custom_error_handler err; - validator.validate(person, err); // validate the document - uses the default throwing error-handler + validator.validate(person, err); // validate the document if (err) std::cerr << "Validation failed\n"; diff --git a/app/readme.cpp b/app/readme.cpp index 9122f48..227a560 100644 --- a/app/readme.cpp +++ b/app/readme.cpp @@ -55,7 +55,7 @@ int main() std::cout << "About to validate this person:\n" << std::setw(2) << person << std::endl; try { - validator.validate(person); // validate the document + validator.validate(person); // validate the document - uses the default throwing error-handler std::cout << "Validation succeeded\n"; } catch (const std::exception &e) { std::cerr << "Validation failed, here is why: " << e.what() << "\n"; @@ -78,7 +78,7 @@ int main() << std::setw(2) << person << std::endl; custom_error_handler err; - validator.validate(person, err); // validate the document - uses the default throwing error-handler + validator.validate(person, err); // validate the document if (err) std::cerr << "Validation failed\n"; diff --git a/src/json-schema.hpp b/src/json-schema.hpp index e0a1410..3f2fff7 100644 --- a/src/json-schema.hpp +++ b/src/json-schema.hpp @@ -24,11 +24,11 @@ #include #ifdef NLOHMANN_JSON_VERSION_MAJOR -# if NLOHMANN_JSON_VERSION_MAJOR < 3 || NLOHMANN_JSON_VERSION_MINOR < 5 || NLOHMANN_JSON_VERSION_PATCH < 1 -# error "Please use this library with NLohmann's JSON version 3.5.1 or higher" +# if (NLOHMANN_JSON_VERSION_MAJOR * 10000 + NLOHMANN_JSON_VERSION_MINOR * 100 + NLOHMANN_JSON_VERSION_PATCH) < 30600 +# error "Please use this library with NLohmann's JSON version 3.6.0 or higher" # endif #else -# error "expected existing NLOHMANN_JSON_VERSION_MAJOR preproc variable, please update to NLohmann's JSON 3.5.1" +# error "expected existing NLOHMANN_JSON_VERSION_MAJOR preproc variable, please update to NLohmann's JSON 3.6.0" #endif // make yourself a home - welcome to nlohmann's namespace @@ -51,7 +51,7 @@ class JSON_SCHEMA_VALIDATOR_API json_uri std::string proto_; std::string hostname_; std::string path_; - nlohmann::json::json_pointer pointer_; + json::json_pointer pointer_; protected: // decodes a JSON uri and replaces all or part of the currently stored values @@ -72,7 +72,7 @@ public: const std::string hostname() const { return hostname_; } const std::string path() const { return path_; } - const nlohmann::json::json_pointer pointer() const { return pointer_; } + const json::json_pointer pointer() const { return pointer_; } const std::string url() const { return location(); } const std::string location() const; @@ -92,7 +92,7 @@ public: json_uri append(const std::string &field) const { json_uri u = *this; - u.pointer_.push_back(field); + u.pointer_ /= field; return u; } @@ -121,7 +121,7 @@ class basic_error_handler bool error_{false}; public: - virtual void error(const nlohmann::json::json_pointer & /*path*/, const json & /* instance */, const std::string & /*message*/) + virtual void error(const json::json_pointer & /*path*/, const json & /* instance */, const std::string & /*message*/) { error_ = true; } diff --git a/src/json-uri.cpp b/src/json-uri.cpp index f8d6cbe..c0e37cb 100644 --- a/src/json-uri.cpp +++ b/src/json-uri.cpp @@ -91,7 +91,7 @@ void json_uri::update(const std::string &uri) } } - pointer_ = nlohmann::json::json_pointer(pointer); + pointer_ = json::json_pointer(pointer); } const std::string json_uri::location() const diff --git a/src/json-validator.cpp b/src/json-validator.cpp index bcad52d..3b60bfc 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -39,7 +39,7 @@ public: schema(root_schema *root) : root_(root) {} - virtual void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const = 0; + virtual void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const = 0; static std::shared_ptr make(json &schema, root_schema *root, @@ -52,7 +52,7 @@ class schema_ref : public schema const std::string id_; std::shared_ptr target_; - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final { if (target_) target_->validate(ptr, instance, e); @@ -83,8 +83,8 @@ class root_schema : public schema std::shared_ptr root_; struct schema_file { - std::map> schemas; - std::map> unresolved; // contains all unresolved references from any other file seen during parsing + std::map> schemas; + std::map> unresolved; // contains all unresolved references from any other file seen during parsing json unknown_keywords; }; @@ -211,7 +211,7 @@ public: } while (1); } - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final { if (root_) root_->validate(ptr, instance, e); @@ -230,7 +230,7 @@ class logical_not : public schema { std::shared_ptr subschema_; - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final { basic_error_handler err; subschema_->validate(ptr, instance, err); @@ -260,7 +260,7 @@ class logical_combination : public schema { std::vector> subschemata_; - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final { size_t count = 0; @@ -268,27 +268,21 @@ class logical_combination : public schema basic_error_handler err; s->validate(ptr, instance, err); - if (err) { - //sub_schema_err << " one schema failed because: " << e.what() << "\n"; - if (combine_logic == allOf) { - e.error(ptr, instance, "at least one schema has failed, but all of them are required to validate."); - return; - } - } else + if (!err) count++; - if (combine_logic == oneOf && count > 1) { - e.error(ptr, instance, "more than one schema has succeeded, but exactly one of them is required to validate."); - return; - } - if (combine_logic == anyOf && count == 1) + if (is_validate_complete(instance, ptr, e, err, count)) return; } - if ((combine_logic == anyOf || combine_logic == oneOf) && count == 0) + if (count == 0) e.error(ptr, instance, "no validation 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 &, basic_error_handler &, bool, size_t); + public: logical_combination(json &sch, root_schema *root, @@ -296,24 +290,43 @@ public: : schema(root) { size_t c = 0; - std::string key; - switch (combine_logic) { - case allOf: - key = "allOf"; - break; - case oneOf: - key = "oneOf"; - break; - case anyOf: - key = "anyOf"; - break; - } - for (auto &subschema : sch) subschemata_.push_back(schema::make(subschema, root, {key, std::to_string(c++)}, uris)); + + // value of allOf, anyOf, and oneOf "MUST be a non-empty array" + // TODO error/throw? when subschemata_.empty() } }; +template <> +const std::string logical_combination::key = "allOf"; +template <> +const std::string logical_combination::key = "anyOf"; +template <> +const std::string logical_combination::key = "oneOf"; + +template <> +bool logical_combination::is_validate_complete(const json &instance, const json::json_pointer &ptr, basic_error_handler &e, bool err, size_t) +{ + if (err) + e.error(ptr, instance, "at least one schema has failed, but all of them are required to validate."); + return err; +} + +template <> +bool logical_combination::is_validate_complete(const json &, const json::json_pointer &, basic_error_handler &, bool, size_t count) +{ + return count == 1; +} + +template <> +bool logical_combination::is_validate_complete(const json &instance, const json::json_pointer &ptr, basic_error_handler &e, bool, size_t count) +{ + if (count > 1) + e.error(ptr, instance, "more than one schema has succeeded, but exactly one of them is required to validate."); + return count > 1; +} + class type_schema : public schema { std::vector> type_; @@ -328,7 +341,7 @@ class type_schema : public schema std::shared_ptr if_, then_, else_; - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override final + void validate(const json::json_pointer &ptr, const json &instance, basic_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()]; @@ -340,8 +353,8 @@ class type_schema : public schema if (enum_.first) { bool seen_in_enum = false; - for (auto &e : enum_.second) - if (instance == e) { + for (auto &v : enum_.second) + if (instance == v) { seen_in_enum = true; break; } @@ -509,7 +522,7 @@ class string : public schema return len; } - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { if (minLength_.first) { if (utf8_length(instance) < minLength_.second) { @@ -591,15 +604,15 @@ class numeric : public schema std::pair multipleOf_{false, 0}; - // multipleOf - if the rest of the division is 0 -> OK - bool violates_multiple_of(json::number_float_t x) const + // multipleOf - if the remainder of the division is 0 -> OK + bool violates_multiple_of(T x) const { - json::number_integer_t n = static_cast(x / multipleOf_.second); - double res = (x - n * multipleOf_.second); - return fabs(res) > std::numeric_limits::epsilon(); + double res = std::remainder(x, multipleOf_.second); + double eps = std::nextafter(x, 0) - x; + return std::fabs(res) > std::fabs(eps); } - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { T value = instance; // conversion of json to value_type @@ -658,7 +671,7 @@ public: class null : public schema { - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { if (!instance.is_null()) e.error(ptr, instance, "expected to be null"); @@ -671,7 +684,7 @@ public: class boolean_type : public schema { - void validate(const nlohmann::json::json_pointer &, const json &, basic_error_handler &) const override {} + void validate(const json::json_pointer &, const json &, basic_error_handler &) const override {} public: boolean_type(json &, root_schema *root) @@ -681,7 +694,7 @@ public: class boolean : public schema { bool true_; - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { if (!true_) { // false schema // empty array @@ -705,7 +718,7 @@ class required : public schema { const std::vector required_; - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override final + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override final { for (auto &r : required_) if (instance.find(r) == instance.end()) @@ -733,7 +746,7 @@ class object : public schema std::shared_ptr propertyNames_; - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { if (maxProperties_.first && instance.size() > maxProperties_.second) e.error(ptr, instance, "too many properties."); @@ -771,7 +784,7 @@ class object : public schema for (auto &dep : dependencies_) { auto prop = instance.find(dep.first); - if (prop != instance.end()) // if dependency-property is present in instance + if (prop != instance.end()) // if dependency-property is present in instance dep.second->validate(ptr / dep.first, instance, e); // validate } } @@ -867,7 +880,7 @@ class array : public schema std::shared_ptr contains_; - void validate(const nlohmann::json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { if (maxItems_.first && instance.size() > maxItems_.second) e.error(ptr, instance, "has too many items."); @@ -1109,7 +1122,7 @@ void json_validator::validate(const json &instance) void json_validator::validate(const json &instance, basic_error_handler &err) { - nlohmann::json::json_pointer ptr; + json::json_pointer ptr; root_->validate(ptr, instance, err); } diff --git a/test/issue-48/CMakeLists.txt b/test/issue-48/CMakeLists.txt new file mode 100644 index 0000000..832d374 --- /dev/null +++ b/test/issue-48/CMakeLists.txt @@ -0,0 +1,3 @@ +add_test_simple_schema(Issue::48 + ${CMAKE_CURRENT_SOURCE_DIR}/schema.json + ${CMAKE_CURRENT_SOURCE_DIR}/instance.json) diff --git a/test/issue-48/instance.json b/test/issue-48/instance.json new file mode 100644 index 0000000..5625e59 --- /dev/null +++ b/test/issue-48/instance.json @@ -0,0 +1 @@ +1.2 diff --git a/test/issue-48/schema.json b/test/issue-48/schema.json new file mode 100644 index 0000000..71782fc --- /dev/null +++ b/test/issue-48/schema.json @@ -0,0 +1,3 @@ +{ + "multipleOf": 0.1 +}