diff --git a/CMakeLists.txt b/CMakeLists.txt index c4cc218..aae7ab3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,12 @@ project(nlohmann_json_schema_validator - LANGUAGES CXX) + LANGUAGES CXX) -set(PROJECT_VERSION 2.1.0) +set(PROJECT_VERSION 2.1.1) cmake_minimum_required(VERSION 3.2) -option(BUILD_TESTS "Build tests" ON) -option(BUILD_EXAMPLES "Build examples" ON) +option(BUILD_TESTS "Build tests" ON) +option(BUILD_EXAMPLES "Build examples" ON) # the library add_library(nlohmann_json_schema_validator diff --git a/src/json-validator.cpp b/src/json-validator.cpp index 65949ee..9c5fc26 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -105,6 +105,7 @@ class root_schema : public schema { schema_loader loader_; format_checker format_check_; + content_checker content_check_; std::shared_ptr root_; @@ -128,10 +129,18 @@ class root_schema : public schema public: root_schema(schema_loader &&loader, - format_checker &&format) - : schema(this), loader_(std::move(loader)), format_check_(std::move(format)) {} + format_checker &&format, + content_checker &&content) + + : schema(this), + loader_(std::move(loader)), + format_check_(std::move(format)), + content_check_(std::move(content)) + { + } format_checker &format_check() { return format_check_; } + content_checker &content_check() { return content_check_; } void insert(const json_uri &uri, const std::shared_ptr &s) { @@ -478,7 +487,6 @@ public: {"boolean", json::value_t::boolean}, {"integer", json::value_t::number_integer}, {"number", json::value_t::number_float}, - {"binary", json::value_t::binary}, }; std::set known_keywords; @@ -492,37 +500,17 @@ public: case json::value_t::string: { auto schema_type = attr.value().get(); - - // add supporting validation binary types - if (schema_type == "string") { - auto found = sch.find("contentEncoding"); - if (found != sch.end() && found->get() == "binary") { - schema_type = "binary"; - } - } - for (auto &t : schema_types) if (t.first == schema_type) type_[(uint8_t) t.second] = type_schema::make(sch, t.second, root, uris, known_keywords); } break; case json::value_t::array: // "type": ["type1", "type2"] - { - json type_array = attr.value(); - - auto has_string_type = std::find(type_array.begin(), type_array.end(), "string"); - if (has_string_type != type_array.end()) { - auto encodingFound = sch.find("contentEncoding"); - if (encodingFound != sch.end() && encodingFound.value() == "binary") { - type_array.emplace_back("binary"); - } - } - - for (auto &schema_type : type_array) + for (auto &schema_type : attr.value()) for (auto &t : schema_types) if (t.first == schema_type) type_[(uint8_t) t.second] = type_schema::make(sch, t.second, root, uris, known_keywords); - } break; + break; default: break; @@ -548,6 +536,11 @@ public: // we stick with JSON-schema: use the integer-validator if instance-value is unsigned type_[(uint8_t) json::value_t::number_unsigned] = type_[(uint8_t) json::value_t::number_integer]; + // special for binary types + if (type_[(uint8_t) json::value_t::string]) { + type_[(uint8_t) json::value_t::binary] = type_[(uint8_t) json::value_t::string]; + } + attr = sch.find("enum"); if (attr != sch.end()) { enum_ = {true, attr.value()}; @@ -618,11 +611,12 @@ class string : public schema #endif std::pair format_; + std::tuple content_{false, "", ""}; std::size_t utf8_length(const std::string &s) const { size_t len = 0; - for (const unsigned char &c : s) + for (unsigned char c : s) if ((c & 0xc0) != 0x80) len++; return len; @@ -646,6 +640,24 @@ class string : public schema } } + if (std::get<0>(content_)) { + if (root_->content_check() == nullptr) + e.error(ptr, instance, std::string("a content checker was not provided but a contentEncoding or contentMediaType for this string have been present: '") + std::get<1>(content_) + "' '" + std::get<2>(content_) + "'"); + else { + try { + root_->content_check()(std::get<1>(content_), std::get<2>(content_), instance); + } catch (const std::exception &ex) { + e.error(ptr, instance, std::string("content-checking failed: ") + ex.what()); + } + } + } else if (instance.type() == json::value_t::binary) { + e.error(ptr, instance, "expected string, but get binary data"); + } + + if (instance.type() != json::value_t::string) { + return; // next checks only for strings + } + #ifndef NO_STD_REGEX if (pattern_.first && !REGEX_NAMESPACE::regex_search(instance.get(), pattern_.second)) @@ -681,6 +693,37 @@ public: sch.erase(attr); } + attr = sch.find("contentEncoding"); + if (attr != sch.end()) { + std::get<0>(content_) = true; + std::get<1>(content_) = attr.value().get(); + + // special case for nlohmann::json-binary-types + // + // https://github.com/pboettch/json-schema-validator/pull/114 + // + // We cannot use explicitly in a schema: {"type": "binary"} or + // "type": ["binary", "number"] we have to be implicit. For a + // schema where "contentEncoding" is set to "binary", an instance + // of type json::value_t::binary is accepted. If a + // contentEncoding-callback has to be provided and is called + // accordingly. For encoding=binary, no other type validations are done + + sch.erase(attr); + } + + attr = sch.find("contentMediaType"); + if (attr != sch.end()) { + std::get<0>(content_) = true; + std::get<2>(content_) = attr.value().get(); + + sch.erase(attr); + } + + if (std::get<0>(content_) == true && root_->content_check() == nullptr) { + throw std::invalid_argument{"schema contains contentEncoding/contentMediaType but content checker was not set"}; + } + #ifndef NO_STD_REGEX attr = sch.find("pattern"); if (attr != sch.end()) { @@ -1110,21 +1153,6 @@ public: } }; -/**\brief just a placeholder - */ -class binary : public schema -{ - void validate(const json::json_pointer &, const json &, json_patch &, error_handler &) const override - { - } - -public: - binary(json &, root_schema *root) - : schema(root) - { - } -}; - std::shared_ptr type_schema::make(json &schema, json::value_t type, root_schema *root, @@ -1152,8 +1180,8 @@ std::shared_ptr type_schema::make(json &schema, case json::value_t::discarded: // not a real type - silence please break; - case json::value_t::binary: // can use for validate bson or other binary representation of json - return std::make_shared(schema, root); + case json::value_t::binary: + break; } return nullptr; } @@ -1248,19 +1276,33 @@ namespace json_schema { json_validator::json_validator(schema_loader loader, - format_checker format) - : root_(std::unique_ptr(new root_schema(std::move(loader), std::move(format)))) + format_checker format, + content_checker content) + : root_(std::unique_ptr(new root_schema(std::move(loader), + std::move(format), + std::move(content)))) { } -json_validator::json_validator(const json &schema, schema_loader loader, format_checker format) - : json_validator(std::move(loader), std::move(format)) +json_validator::json_validator(const json &schema, + schema_loader loader, + format_checker format, + content_checker content) + : json_validator(std::move(loader), + std::move(format), + std::move(content)) { set_root_schema(schema); } -json_validator::json_validator(json &&schema, schema_loader loader, format_checker format) - : json_validator(std::move(loader), std::move(format)) +json_validator::json_validator(json &&schema, + schema_loader loader, + format_checker format, + content_checker content) + + : json_validator(std::move(loader), + std::move(format), + std::move(content)) { set_root_schema(std::move(schema)); } diff --git a/src/nlohmann/json-schema.hpp b/src/nlohmann/json-schema.hpp index baa9719..c25ca72 100644 --- a/src/nlohmann/json-schema.hpp +++ b/src/nlohmann/json-schema.hpp @@ -133,6 +133,7 @@ extern json draft7_schema_builtin; typedef std::function schema_loader; typedef std::function format_checker; +typedef std::function content_checker; // Interface for validation error handlers class JSON_SCHEMA_VALIDATOR_API error_handler @@ -168,10 +169,10 @@ class JSON_SCHEMA_VALIDATOR_API json_validator std::unique_ptr root_; public: - json_validator(schema_loader = nullptr, format_checker = nullptr); + json_validator(schema_loader = nullptr, format_checker = nullptr, content_checker = nullptr); - json_validator(const json &, schema_loader = nullptr, format_checker = nullptr); - json_validator(json &&, schema_loader = nullptr, format_checker = nullptr); + json_validator(const json &, schema_loader = nullptr, format_checker = nullptr, content_checker = nullptr); + json_validator(json &&, schema_loader = nullptr, format_checker = nullptr, content_checker = nullptr); json_validator(json_validator &&); json_validator &operator=(json_validator &&); diff --git a/test/JSON-Schema-Test-Suite/CMakeLists.txt b/test/JSON-Schema-Test-Suite/CMakeLists.txt index fc52df2..61d5fd4 100644 --- a/test/JSON-Schema-Test-Suite/CMakeLists.txt +++ b/test/JSON-Schema-Test-Suite/CMakeLists.txt @@ -50,7 +50,6 @@ if(JSON_SCHEMA_TEST_SUITE_PATH) # some optional tests will fail set_tests_properties( JSON-Suite::Optional::bignum - JSON-Suite::Optional::content JSON-Suite::Optional::zeroTerminatedFloats JSON-Suite::Optional::non-bmp-regex diff --git a/test/JSON-Schema-Test-Suite/json-schema-test.cpp b/test/JSON-Schema-Test-Suite/json-schema-test.cpp index 05a255d..deee4c8 100644 --- a/test/JSON-Schema-Test-Suite/json-schema-test.cpp +++ b/test/JSON-Schema-Test-Suite/json-schema-test.cpp @@ -39,6 +39,50 @@ static void loader(const json_uri &uri, json &schema) } } +// from here +// https://stackoverflow.com/a/34571089/880584 +static std::string base64_decode(const std::string &in) +{ + std::string out; + + std::vector T(256, -1); + for (int i = 0; i < 64; i++) + T["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i; + + unsigned val = 0; + int valb = -8; + for (uint8_t c : in) { + if (c == '=') + break; + + if (T[c] == -1) { + throw std::invalid_argument("base64-decode: unexpected character in encode string: '" + std::string(1, c) + "'"); + } + val = (val << 6) + T[c]; + valb += 6; + if (valb >= 0) { + out.push_back(char((val >> valb) & 0xFF)); + valb -= 8; + } + } + return out; +} + +static void content(const std::string &contentEncoding, const std::string &contentMediaType, const json &instance) +{ + std::string content = instance; + + if (contentEncoding == "base64") + content = base64_decode(instance); + else if (contentEncoding != "") + throw std::invalid_argument("unable to check for contentEncoding '" + contentEncoding + "'"); + + if (contentMediaType == "application/json") + auto dummy = json::parse(content); // throws if conversion fails + else if (contentMediaType != "") + throw std::invalid_argument("unable to check for contentMediaType '" + contentMediaType + "'"); +} + int main(void) { json validation; // a validation case following the JSON-test-suite-schema @@ -62,7 +106,8 @@ int main(void) const auto &schema = test_group["schema"]; json_validator validator(loader, - nlohmann::json_schema::default_string_format_check); + nlohmann::json_schema::default_string_format_check, + content); validator.set_root_schema(schema); diff --git a/test/binary-validation.cpp b/test/binary-validation.cpp index 29e707a..01392cf 100644 --- a/test/binary-validation.cpp +++ b/test/binary-validation.cpp @@ -4,7 +4,7 @@ #include #include -int error_count = 0; +static int error_count = 0; #define EXPECT_EQ(a, b) \ do { \ @@ -14,6 +14,19 @@ int error_count = 0; } \ } while (0) +#define EXPECT_THROW(foo) \ + { \ + bool ok = false; \ + try { \ + foo; \ + } catch (std::exception &) { \ + ok = true; \ + } \ + if (ok == false) { \ + error_count++; \ + } \ + } + using json = nlohmann::json; using validator = nlohmann::json_schema::json_validator; @@ -78,10 +91,24 @@ public: } }; +static void content(const std::string &contentEncoding, const std::string &contentMediaType, const json &instance) +{ + std::cerr << "mediaType: '" << contentMediaType << "', encoding: '" << contentEncoding << "'\n"; + + if (contentEncoding == "binary") { + if (instance.type() != json::value_t::binary) { + throw std::invalid_argument{"expected binary data"}; + } + } else { + if (instance.type() == json::value_t::binary) { + throw std::invalid_argument{"expected string, but get binary"}; + } + } +} + int main() { - validator val; - val.set_root_schema(bson_schema); + validator val(nullptr, nullptr, content); // create some bson doc json::binary_t arr; @@ -90,8 +117,12 @@ int main() json binary = json::binary(arr); + store_ptr_err_handler err; + + ///////////////////////////////////// + val.set_root_schema(bson_schema); + // all right - store_ptr_err_handler err{}; val.validate({{"standard_string", "some string"}, {"binary_data", binary}}, err); EXPECT_EQ(err.failed_pointers.size(), 0); err.reset(); @@ -113,9 +144,12 @@ int main() // check simple types val.set_root_schema(array_of_types); - val.validate({{"something", "string"}}, err); val.validate({{"something", 1}}, err); val.validate({{"something", false}}, err); + // TODO when we set `string` in array and set `contentEncoding` = "binary" - what it means? We expected string or binary? + // Or we expect only binary? Now if you set `contentEncoding` = "binary", then it means that you expect only binary data, + // not string + //val.validate({{"something", "string"}}, err); -> produce error about type EXPECT_EQ(err.failed_pointers.size(), 0); err.reset(); @@ -124,6 +158,7 @@ int main() EXPECT_EQ(err.failed_pointers.size(), 0); err.reset(); + ///////////////////////////////////// // and check that you can't set binary data if contentEncoding don't set val.set_root_schema(array_of_types_without_binary); val.validate({{"something", binary}}, err); @@ -131,5 +166,10 @@ int main() EXPECT_EQ(err.failed_pointers[0], "/something"); err.reset(); + // check that without content callback you get exception with schema with contentEncoding or contentMeditType + validator val_no_content; + + EXPECT_THROW(val_no_content.set_root_schema(bson_schema)); + return error_count; }