diff --git a/.clang-format b/.clang-format index 647604d..715ac57 100644 --- a/.clang-format +++ b/.clang-format @@ -7,9 +7,11 @@ AllowShortFunctionsOnASingleLine: Inline BreakBeforeBraces: Linux ColumnLimit: 0 ConstructorInitializerAllOnOneLineOrOnePerLine: true -IndentWidth: 2 -ObjCBlockIndentWidth: 2 +IndentWidth: 4 +IndentPPDirectives: AfterHash +ObjCBlockIndentWidth: 0 SpaceAfterCStyleCast: true -TabWidth: 2 +TabWidth: 4 +AccessModifierOffset: -4 UseTab: ForIndentation ... diff --git a/.travis.yml b/.travis.yml index e4ee62f..edb2981 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.1.2/json.hpp -O nlohmann/json.hpp + - mkdir -p nlohmann && wget https://github.com/nlohmann/json/releases/download/v3.5.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 b2a77e7..e407979 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ endif() # and one for the validator add_library(json-schema-validator - src/json-schema-draft4.json.cpp + src/json-schema-draft7.json.cpp src/json-uri.cpp src/json-validator.cpp) @@ -95,8 +95,13 @@ if (BUILD_EXAMPLES) # simple json-schema-validator-executable add_executable(json-schema-validate app/json-schema-validate.cpp) target_link_libraries(json-schema-validate json-schema-validator) + + add_executable(readme app/readme.cpp) + target_link_libraries(readme json-schema-validator) endif() +#add_subdirectory(ng) + if (BUILD_TESTS) # test-zone enable_testing() diff --git a/README.md b/README.md index a1a1f95..691af12 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,68 @@ [![Build Status](https://travis-ci.org/pboettch/json-schema-validator.svg?branch=master)](https://travis-ci.org/pboettch/json-schema-validator) -# Modern C++ JSON schema validator +# JSON schema validator for JSON for Modern C++ # What is it? This is a C++ library for validating JSON documents based on a [JSON Schema](http://json-schema.org/) which itself should validate with -[draft-4 of JSON Schema Validation](http://json-schema.org/schema). +[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.* +contributions or hints or discussions are welcome.* Even though a 2.0.0 release is immenent. 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 library, hence the name. -The name is for the moment purely marketing, because there is, IMHO, not so much -modern C++ inside. There is plenty of space to make it more modern. - External documentation is missing as well. However the API of the validator -will be rather simple. +is rather simple. + +# New in version 2 + +Although significant changes have been coorporate to the 2 version +(a complete rewrite) the API is compatible with the 1.0.0 release. Except for +the namespace which is now `nlohmann::json_schema. + +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 +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. + +In JSON-schema one sub-schema can be # Design goals The main goal of this validator is to produce *human-comprehensible* error -messages if a JSON-document/instance does not comply with its schema. This is -done with exceptions thrown at the users with a helpful message telling what's -wrong with the document while validating. +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 passed 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). Another goal was to use Niels Lohmann's JSON-library. This is why the validator lives in his namespace. # Weaknesses -Schema-reference resolution is not recursivity-proven: If there is a nested -cross-schema reference, it will not stop. (Though I haven't tested it) - -Numerical validation uses `int64_t`, `uint64_t` or `double`, depending on if +Numerical validation uses nlohmann integer, unsigned and floating point types, depending on if the schema type is "integer" or "number". Bignum (i.e. arbitrary precision and range) is not supported at this time. -Unsigned integer validation will only take place if the following two conditions are true: -- The nlohmann `type()` of the json object under validation is `nlohmann::json::value_t::number_unsigned` -- The schema specifies a numerical minimum greater than or equal to 0 +Currently JSON-URI with "plain name fragments" are not supported. So 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.1.1** of NLohmann's +The current state of the build-system needs at least version **3.5.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 @@ -66,7 +81,7 @@ cmake .. \ -DNLOHMANN_JSON_DIR=nlohmann/json.hpp \ -DJSON_SCHEMA_TEST_SUITE_PATH= # optional make # install -ctest # if test-suite has been given +ctest # run unit, non-regression and test-suite tests ``` ### As a subdirectory from within @@ -93,7 +108,6 @@ In your initial call to cmake simply add: ```bash cmake -DBUILD_SHARED_LIBS=ON ``` - ## Code See also `app/json-schema-validate.cpp`. @@ -104,13 +118,12 @@ See also `app/json-schema-validate.cpp`. #include "json-schema.hpp" using nlohmann::json; -using nlohmann::json_uri; -using nlohmann::json_schema_draft4::json_validator; +using nlohmann::json_schema::json_validator; // The schema is defined based upon a string literal static json person_schema = R"( { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "title": "A person", "properties": { "name": { @@ -137,55 +150,74 @@ static json person_schema = R"( static json bad_person = {{"age", 42}}; static json good_person = {{"name", "Albert"}, {"age", 42}}; -int main(){ +int main() +{ + /* json-parse the schema */ - /* json-parse the schema */ + json_validator validator; // create validator - json_validator validator; // create validator + try { + validator.set_root_schema(person_schema); // insert root-schema + } catch (const std::exception &e) { + std::cerr << "Validation of schema failed, here is why: " << e.what() << "\n"; + return EXIT_FAILURE; + } - try { - validator.set_root_schema(person_schema); // insert root-schema - } catch (const std::exception &e) { - std::cerr << "Validation of schema failed, here is why: " << e.what() << "\n"; - return EXIT_FAILURE; - } + /* json-parse the people - API of 1.0.0, default throwing error handler */ - /* json-parse the people */ + for (auto &person : {bad_person, good_person}) { + std::cout << "About to validate this person:\n" + << std::setw(2) << person << std::endl; + try { + validator.validate(person); // validate the document + std::cout << "Validation succeeded\n"; + } catch (const std::exception &e) { + std::cerr << "Validation failed, here is why: " << e.what() << "\n"; + } + } - for (auto &person : {bad_person, good_person}) - { - std::cout << "About to validate this person:\n" << std::setw(2) << person << std::endl; - try { - validator.validate(person); // validate the document - std::cout << "Validation succeeded\n"; - } catch (const std::exception &e) { - std::cerr << "Validation failed, here is why: " << e.what() << "\n"; - } - } - return EXIT_SUCCESS; + /* json-parse the people - with custom error handler */ + class custom_error_handler : public nlohmann::json_schema::basic_error_handler + { + void error(const std::string &path, const json &instance, const std::string &message) override + { + nlohmann::json_schema::basic_error_handler::error(path, instance, message); + std::cerr << "ERROR: '" << path << "' - '" << instance << "': " << message << "\n"; + } + }; + + + for (auto &person : {bad_person, good_person}) { + std::cout << "About to validate this person:\n" + << std::setw(2) << person << std::endl; + + custom_error_handler err; + validator.validate(person, err); // validate the document - uses the default throwing error-handler + + if (err) + std::cerr << "Validation failed\n"; + else + std::cout << "Validation succeeded\n"; + } + + return EXIT_SUCCESS; } - ``` # Compliance There is an application which can be used for testing the validator with the [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). +In order to simplify the testing, the test-suite is included in the repository. If you have cloned this repository providing a path the repository-root via the cmake-variable `JSON_SCHEMA_TEST_SUITE_PATH` will enable the test-target(s). All required tests are **OK**. -**12** optional tests of **305** total (required + optional) tests are failing: - -- 10 of them are `format`-strings which are not supported. -- big numbers are not working (2) - # Additional features ## Default values The goal is to create an empty document, based on schema-defined default-values, recursively populated. - diff --git a/app/json-schema-validate.cpp b/app/json-schema-validate.cpp index 055f19b..b6699e3 100644 --- a/app/json-schema-validate.cpp +++ b/app/json-schema-validate.cpp @@ -1,27 +1,10 @@ /* - * Modern C++ JSON schema validator + * JSON schema validator for JSON for modern C++ * - * Licensed under the MIT License . + * Copyright (c) 2016-2019 Patrick Boettcher . * - * Copyright (c) 2016 Patrick Boettcher . + * SPDX-License-Identifier: MIT * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom - * the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN - * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT - * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR - * THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include @@ -30,7 +13,7 @@ using nlohmann::json; using nlohmann::json_uri; -using nlohmann::json_schema_draft4::json_validator; +using nlohmann::json_schema::json_validator; static void usage(const char *name) { @@ -47,9 +30,10 @@ static void usage(const char *name) static void loader(const json_uri &uri, json &schema) { - std::fstream lf("." + uri.path()); + std::string filename = "./" + uri.path(); + std::fstream lf(filename); if (!lf.good()) - throw std::invalid_argument("could not open " + uri.url() + " tried with " + uri.path()); + throw std::invalid_argument("could not open " + uri.url() + " tried with " + filename); try { lf >> schema; @@ -58,6 +42,15 @@ static void loader(const json_uri &uri, json &schema) } } +class custom_error_handler : public nlohmann::json_schema::basic_error_handler +{ + void error(const std::string &path, const json &instance, const std::string &message) override + { + nlohmann::json_schema::basic_error_handler::error(path, instance, message); + std::cerr << "ERROR: '" << path << "' - '" << instance << "': " << message << "\n"; + } +}; + int main(int argc, char *argv[]) { if (argc != 2) @@ -95,10 +88,16 @@ int main(int argc, char *argv[]) try { std::cin >> document; - validator.validate(document); } catch (std::exception &e) { + std::cerr << "json parsing failed: " << e.what() << " at offset: " << std::cin.tellg() << "\n"; + return EXIT_FAILURE; + } + + custom_error_handler err; + validator.validate(document, err); + + if (err) { std::cerr << "schema validation failed\n"; - std::cerr << e.what() << " at offset: " << std::cin.tellg() << "\n"; return EXIT_FAILURE; } diff --git a/app/readme.cpp b/app/readme.cpp new file mode 100644 index 0000000..a25d652 --- /dev/null +++ b/app/readme.cpp @@ -0,0 +1,90 @@ +#include +#include + +#include "json-schema.hpp" + +using nlohmann::json; +using nlohmann::json_schema::json_validator; + +// The schema is defined based upon a string literal +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 + } + }, + "required": [ + "name", + "age" + ], + "type": "object" +} + +)"_json; + +// The people are defined with brace initialization +static json bad_person = {{"age", 42}}; +static json good_person = {{"name", "Albert"}, {"age", 42}}; + +int main() +{ + /* json-parse the schema */ + + json_validator validator; // create validator + + try { + validator.set_root_schema(person_schema); // insert root-schema + } catch (const std::exception &e) { + std::cerr << "Validation of schema failed, here is why: " << e.what() << "\n"; + return EXIT_FAILURE; + } + + /* json-parse the people - API of 1.0.0, default throwing error handler */ + + for (auto &person : {bad_person, good_person}) { + std::cout << "About to validate this person:\n" + << std::setw(2) << person << std::endl; + try { + validator.validate(person); // validate the document + std::cout << "Validation succeeded\n"; + } catch (const std::exception &e) { + std::cerr << "Validation failed, here is why: " << e.what() << "\n"; + } + } + + /* json-parse the people - with custom error handler */ + class custom_error_handler : public nlohmann::json_schema::basic_error_handler + { + void error(const std::string &path, const json &instance, const std::string &message) override + { + nlohmann::json_schema::basic_error_handler::error(path, instance, message); + std::cerr << "ERROR: '" << path << "' - '" << instance << "': " << message << "\n"; + } + }; + + + for (auto &person : {bad_person, good_person}) { + std::cout << "About to validate this person:\n" + << std::setw(2) << person << std::endl; + + custom_error_handler err; + validator.validate(person, err); // validate the document - uses the default throwing error-handler + + if (err) + std::cerr << "Validation failed\n"; + else + std::cout << "Validation succeeded\n"; + } + + return EXIT_SUCCESS; +} diff --git a/src/json-schema-draft4.json.cpp b/schema similarity index 57% rename from src/json-schema-draft4.json.cpp rename to schema index 43bab6f..5bee90e 100644 --- a/src/json-schema-draft4.json.cpp +++ b/schema @@ -1,109 +1,114 @@ -#include - -namespace nlohmann { -namespace json_schema_draft4 -{ - -json draft4_schema_builtin = R"( { - "id": "http://json-schema.org/draft-04/schema#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Core schema meta-schema", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", "definitions": { "schemaArray": { "type": "array", "minItems": 1, "items": { "$ref": "#" } }, - "positiveInteger": { + "nonNegativeInteger": { "type": "integer", "minimum": 0 }, - "positiveIntegerDefault0": { - "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] }, "simpleTypes": { - "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] }, "stringArray": { "type": "array", "items": { "type": "string" }, - "minItems": 1, - "uniqueItems": true + "uniqueItems": true, + "default": [] } }, - "type": "object", + "type": ["object", "boolean"], "properties": { - "id": { + "$id": { "type": "string", - "format": "uri" + "format": "uri-reference" }, "$schema": { "type": "string", "format": "uri" }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, "title": { "type": "string" }, "description": { "type": "string" }, - "default": {}, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, "multipleOf": { "type": "number", - "minimum": 0, - "exclusiveMinimum": true + "exclusiveMinimum": 0 }, "maximum": { "type": "number" }, "exclusiveMaximum": { - "type": "boolean", - "default": false + "type": "number" }, "minimum": { "type": "number" }, "exclusiveMinimum": { - "type": "boolean", - "default": false + "type": "number" }, - "maxLength": { "$ref": "#/definitions/positiveInteger" }, - "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "pattern": { "type": "string", "format": "regex" }, - "additionalItems": { - "anyOf": [ - { "type": "boolean" }, - { "$ref": "#" } - ], - "default": {} - }, + "additionalItems": { "$ref": "#" }, "items": { "anyOf": [ { "$ref": "#" }, { "$ref": "#/definitions/schemaArray" } ], - "default": {} + "default": true }, - "maxItems": { "$ref": "#/definitions/positiveInteger" }, - "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "uniqueItems": { "type": "boolean", "default": false }, - "maxProperties": { "$ref": "#/definitions/positiveInteger" }, - "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "required": { "$ref": "#/definitions/stringArray" }, - "additionalProperties": { - "anyOf": [ - { "type": "boolean" }, - { "$ref": "#" } - ], - "default": {} - }, + "additionalProperties": { "$ref": "#" }, "definitions": { "type": "object", "additionalProperties": { "$ref": "#" }, @@ -117,6 +122,7 @@ json draft4_schema_builtin = R"( { "patternProperties": { "type": "object", "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, "default": {} }, "dependencies": { @@ -128,8 +134,11 @@ json draft4_schema_builtin = R"( { ] } }, + "propertyNames": { "$ref": "#" }, + "const": true, "enum": { "type": "array", + "items": true, "minItems": 1, "uniqueItems": true }, @@ -144,17 +153,16 @@ json draft4_schema_builtin = R"( { } ] }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": {"$ref": "#"}, + "then": {"$ref": "#"}, + "else": {"$ref": "#"}, "allOf": { "$ref": "#/definitions/schemaArray" }, "anyOf": { "$ref": "#/definitions/schemaArray" }, "oneOf": { "$ref": "#/definitions/schemaArray" }, "not": { "$ref": "#" } }, - "dependencies": { - "exclusiveMaximum": [ "maximum" ], - "exclusiveMinimum": [ "minimum" ] - }, - "default": {} -} )"_json; - -} + "default": true } diff --git a/src/json-schema-draft7.json.cpp b/src/json-schema-draft7.json.cpp new file mode 100644 index 0000000..b680e2c --- /dev/null +++ b/src/json-schema-draft7.json.cpp @@ -0,0 +1,185 @@ +/* + * JSON schema validator for JSON for modern C++ + * + * Copyright (c) 2016-2019 Patrick Boettcher . + * + * SPDX-License-Identifier: MIT + * + */ +#include + +namespace nlohmann +{ +namespace json_schema +{ + +json draft7_schema_builtin = R"( { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": true + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": { "$ref": "#" }, + "then": { "$ref": "#" }, + "else": { "$ref": "#" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true +} )"_json; +} +} // namespace nlohmann diff --git a/src/json-schema.hpp b/src/json-schema.hpp index d38a308..46dc600 100644 --- a/src/json-schema.hpp +++ b/src/json-schema.hpp @@ -1,91 +1,40 @@ /* - * Modern C++ JSON schema validator + * JSON schema validator for JSON for modern C++ * - * Licensed under the MIT License . + * Copyright (c) 2016-2019 Patrick Boettcher . * - * Copyright (c) 2016 Patrick Boettcher . + * SPDX-License-Identifier: MIT * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom - * the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN - * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT - * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR - * THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef NLOHMANN_JSON_SCHEMA_HPP__ #define NLOHMANN_JSON_SCHEMA_HPP__ #ifdef _WIN32 -# if defined(JSON_SCHEMA_VALIDATOR_EXPORTS) -# define JSON_SCHEMA_VALIDATOR_API __declspec(dllexport) -# elif defined(JSON_SCHEMA_VALIDATOR_IMPORTS) -# define JSON_SCHEMA_VALIDATOR_API __declspec(dllimport) -# else -# define JSON_SCHEMA_VALIDATOR_API -# endif +# if defined(JSON_SCHEMA_VALIDATOR_EXPORTS) +# define JSON_SCHEMA_VALIDATOR_API __declspec(dllexport) +# elif defined(JSON_SCHEMA_VALIDATOR_IMPORTS) +# define JSON_SCHEMA_VALIDATOR_API __declspec(dllimport) +# else +# define JSON_SCHEMA_VALIDATOR_API +# endif #else -# define JSON_SCHEMA_VALIDATOR_API +# define JSON_SCHEMA_VALIDATOR_API #endif #include +#ifdef NLOHMANN_JSON_VERSION_MAJOR +# if NLOHMANN_JSON_VERSION_MAJOR < 3 || NLOHMANN_JSON_VERSION_MINOR < 5 || NLOHMANN_JSON_VERSION_PATCH < 0 +# error "Please use this library with NLohmann's JSON version 3.5.0 or higher" +# endif +#else +# error "expected existing NLOHMANN_JSON_VERSION_MAJOR preproc variable, please update to NLohmann's JSON 3.5.0" +#endif + // make yourself a home - welcome to nlohmann's namespace namespace nlohmann { -// a class representing a JSON-pointer RFC6901 -// -// examples of JSON pointers -// -// # - root of the current document -// #item - refers to the object which is identified ("id") by `item` -// in the current document -// #/path/to/element -// - refers to the element in /path/to from the root-document -// -// -// The json_pointer-class stores everything in a string, which might seem bizarre -// as parsing is done from a string to a string, but from_string() is also -// doing some formatting. -// -// TODO -// ~ and % - codec -// needs testing and clarification regarding the '#' at the beginning - -class local_json_pointer -{ - std::string str_; - - void from_string(const std::string &r); - -public: - local_json_pointer(const std::string &s = "") - { - from_string(s); - } - - void append(const std::string &elem) - { - str_.append(elem); - } - - const std::string &to_string() const - { - return str_; - } -}; - // A class representing a JSON-URI for schemas derived from // section 8 of JSON Schema: A Media Type for Describing JSON Documents // draft-wright-json-schema-00 @@ -102,32 +51,32 @@ class JSON_SCHEMA_VALIDATOR_API json_uri std::string proto_; std::string hostname_; std::string path_; - local_json_pointer pointer_; + nlohmann::json::json_pointer pointer_; protected: // decodes a JSON uri and replaces all or part of the currently stored values - void from_string(const std::string &uri); + void update(const std::string &uri); std::tuple tie() const { - return std::tie(urn_, proto_, hostname_, path_, pointer_.to_string()); + return std::tie(urn_, proto_, hostname_, path_, pointer_); } public: json_uri(const std::string &uri) { - from_string(uri); + update(uri); } const std::string protocol() const { return proto_; } const std::string hostname() const { return hostname_; } const std::string path() const { return path_; } - const local_json_pointer pointer() const { return pointer_; } - const std::string url() const; + const nlohmann::json::json_pointer pointer() const { return pointer_; } + + const std::string url() const { return location(); } + const std::string location() const; - // decode and encode strings for ~ and % escape sequences - static std::string unescape(const std::string &); static std::string escape(const std::string &); // create a new json_uri based in this one and the given uri @@ -135,7 +84,7 @@ public: json_uri derive(const std::string &uri) const { json_uri u = *this; - u.from_string(uri); + u.update(uri); return u; } @@ -143,7 +92,7 @@ public: json_uri append(const std::string &field) const { json_uri u = *this; - u.pointer_.append("/" + field); + u.pointer_ = nlohmann::json::json_pointer(u.pointer_.to_string() + '/' + escape(field)); return u; } @@ -162,42 +111,47 @@ public: friend std::ostream &operator<<(std::ostream &os, const json_uri &u); }; -namespace json_schema_draft4 +namespace json_schema { -extern json draft4_schema_builtin; +extern json draft7_schema_builtin; + +class basic_error_handler +{ + bool error_{false}; + +public: + virtual void error(const std::string & /*path*/, const json & /* instance */, const std::string & /*message*/) + { + error_ = true; + } + + void reset() { error_ = false; } + operator bool() const { return error_; } +}; + +class root_schema; class JSON_SCHEMA_VALIDATOR_API json_validator { - std::vector> schema_store_; - std::shared_ptr root_schema_; - std::function schema_loader_ = nullptr; - std::function format_check_ = nullptr; - - std::map schema_refs_; - - void validate(const json &instance, const json &schema_, const std::string &name); - void validate_array(const json &instance, const json &schema_, const std::string &name); - void validate_object(const json &instance, const json &schema_, const std::string &name); - void validate_string(const json &instance, const json &schema, const std::string &name); - - void insert_schema(const json &input, const json_uri &id); + std::unique_ptr root_; public: json_validator(std::function loader = nullptr, - std::function format = nullptr) - : schema_loader_(loader), format_check_(format) - { - } + std::function format = nullptr); + ~json_validator(); - // insert and set a root-schema + // insert and set thea root-schema void set_root_schema(const json &); // validate a json-document based on the root-schema - void validate(const json &instance); + void validate(const json &); + + // validate a json-document based on the root-schema with a custom error-handler + void validate(const json &, basic_error_handler &); }; -} // json_schema_draft4 -} // nlohmann +} // namespace json_schema +} // namespace nlohmann #endif /* NLOHMANN_JSON_SCHEMA_HPP__ */ diff --git a/src/json-uri.cpp b/src/json-uri.cpp index 6a3136f..f8d6cbe 100644 --- a/src/json-uri.cpp +++ b/src/json-uri.cpp @@ -1,27 +1,10 @@ /* - * Modern C++ JSON schema validator + * JSON schema validator for JSON for modern C++ * - * Licensed under the MIT License . + * Copyright (c) 2016-2019 Patrick Boettcher . * - * Copyright (c) 2016 Patrick Boettcher . + * SPDX-License-Identifier: MIT * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom - * the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN - * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT - * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR - * THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "json-schema.hpp" @@ -30,76 +13,92 @@ namespace nlohmann { -void local_json_pointer::from_string(const std::string &r) +void json_uri::update(const std::string &uri) { - str_ = "#"; + std::string pointer = ""; // default pointer is document-root - if (r.size() == 0) - return; - - if (r[0] != '#') - throw std::invalid_argument("not a valid JSON pointer - missing # at the beginning"); - - if (r.size() == 1) - return; - - std::size_t pos = 1; - - do { - std::size_t next = r.find('/', pos + 1); - str_.append(r.substr(pos, next - pos)); - pos = next; - } while (pos != std::string::npos); -} - -void json_uri::from_string(const std::string &uri) -{ - // if it is an urn take it as it is - maybe there is more to be done - if (uri.find("urn:") == 0) { - urn_ = uri; - return; - } - - std::string pointer = "#"; // default pointer is the root - - // first split the URI into URL and JSON-pointer + // first split the URI into location and pointer auto pointer_separator = uri.find('#'); - if (pointer_separator != std::string::npos) // and extract the JSON-pointer-string if found - pointer = uri.substr(pointer_separator); + if (pointer_separator != std::string::npos) { // and extract the pointer-string if found + pointer = uri.substr(pointer_separator + 1); // remove # - // the rest is an URL - std::string url = uri.substr(0, pointer_separator); - if (url.size()) { // if an URL is part of the URI + // unescape %-values IOW, decode JSON-URI-formatted JSON-pointer + std::size_t pos = pointer.size() - 1; + do { + pos = pointer.rfind('%', pos); + if (pos == std::string::npos) + break; - std::size_t pos = 0; - auto proto = url.find("://", pos); - if (proto != std::string::npos) { // extract the protocol - proto_ = url.substr(pos, proto - pos); - pos = 3 + proto; // 3 == "://" - - auto hostname = url.find("/", pos); - if (hostname != std::string::npos) { // and the hostname (no proto without hostname) - hostname_ = url.substr(pos, hostname - pos); - pos = hostname; + if (pos >= pointer.size() - 2) { + pos--; + continue; } - } - // the rest is the path - auto path = url.substr(pos); - if (path[0] == '/') // if it starts with a / it is root-path - path_ = path; - else // otherwise it is a subfolder - path_.append(path); + std::string hex = pointer.substr(pos + 1, 2); + char ascii = (char) std::strtoul(hex.c_str(), nullptr, 16); + pointer.replace(pos, 3, 1, ascii); - pointer_ = local_json_pointer(""); + pos--; + } while (1); } - if (pointer.size() > 0) - pointer_ = pointer; + auto location = uri.substr(0, pointer_separator); + + if (location.size()) { // a location part has been found + pointer_ = ""_json_pointer; // if a location is given, the pointer is emptied + + // if it is an URN take it as it is + if (location.find("urn:") == 0) { + urn_ = location; + + // and clear URL members + proto_ = ""; + hostname_ = ""; + path_ = ""; + + } else { // it is an URL + + // split URL in protocol, hostname and path + std::size_t pos = 0; + auto proto = location.find("://", pos); + if (proto != std::string::npos) { // extract the protocol + + urn_ = ""; // clear URN-member if URL is parsed + + proto_ = location.substr(pos, proto - pos); + pos = 3 + proto; // 3 == "://" + + auto hostname = location.find("/", pos); + if (hostname != std::string::npos) { // and the hostname (no proto without hostname) + hostname_ = location.substr(pos, hostname - pos); + pos = hostname; + } + } + + auto path = location.substr(pos); + + // URNs cannot of have paths + if (urn_.size() && path.size()) + throw std::invalid_argument("Cannot add a path (" + path + ") to an URN URI (" + urn_ + ")"); + + if (path[0] == '/') // if it starts with a / it is root-path + path_ = path; + else if (pos == 0) { // the URL contained only a path and the current path has no / at the end, strip last element until / and append + auto last_slash = path_.rfind('/'); + path_ = path_.substr(0, last_slash) + '/' + path; + } else // otherwise it is a subfolder + path_.append(path); + } + } + + pointer_ = nlohmann::json::json_pointer(pointer); } -const std::string json_uri::url() const +const std::string json_uri::location() const { + if (urn_.size()) + return urn_; + std::stringstream s; if (proto_.size() > 0) @@ -115,9 +114,7 @@ std::string json_uri::to_string() const { std::stringstream s; - s << urn_ - << url() - << pointer_.to_string(); + s << location() << " # " << pointer_.to_string(); return s.str(); } @@ -127,48 +124,11 @@ std::ostream &operator<<(std::ostream &os, const json_uri &u) return os << u.to_string(); } -std::string json_uri::unescape(const std::string &src) -{ - std::string l = src; - std::size_t pos = src.size() - 1; - - do { - pos = l.rfind('~', pos); - - if (pos == std::string::npos) - break; - - if (pos < l.size() - 1) { - switch (l[pos + 1]) { - case '0': - l.replace(pos, 2, "~"); - break; - - case '1': - l.replace(pos, 2, "/"); - break; - - default: - break; - } - } - - if (pos == 0) - break; - pos--; - } while (pos != std::string::npos); - - // TODO - percent handling - - return l; -} - std::string json_uri::escape(const std::string &src) { std::vector> chars = { {"~", "~0"}, - {"/", "~1"}, - {"%", "%25"}}; + {"/", "~1"}}; std::string l = src; @@ -186,4 +146,4 @@ std::string json_uri::escape(const std::string &src) return l; } -} // nlohmann +} // namespace nlohmann diff --git a/src/json-validator.cpp b/src/json-validator.cpp index 5bfc809..e902052 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -1,782 +1,1100 @@ /* - * Modern C++ JSON schema validator + * JSON schema validator for JSON for modern C++ * - * Licensed under the MIT License . + * Copyright (c) 2016-2019 Patrick Boettcher . * - * Copyright (c) 2016 Patrick Boettcher . + * SPDX-License-Identifier: MIT * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom - * the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN - * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT - * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR - * THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include +#include #include using nlohmann::json; using nlohmann::json_uri; +using nlohmann::json_schema::root_schema; +using namespace nlohmann::json_schema; #ifdef JSON_SCHEMA_BOOST_REGEX - #include - #define REGEX_NAMESPACE boost +# include +# define REGEX_NAMESPACE boost #elif defined(JSON_SCHEMA_NO_REGEX) - #define NO_STD_REGEX +# define NO_STD_REGEX #else - #include - #define REGEX_NAMESPACE std +# include +# define REGEX_NAMESPACE std #endif namespace { -class resolver +class schema { - void resolve(json &schema, json_uri id) +protected: + root_schema *root_; + +public: + schema(root_schema *root) + : root_(root) {} + + virtual void validate(const json &instance, basic_error_handler &e) const = 0; + + static std::shared_ptr make(json &schema, + root_schema *root, + const std::vector &key, + std::vector uris); +}; + +class logical_not : public schema +{ + std::shared_ptr subschema_; + + void validate(const json &instance, basic_error_handler &e) const final { - // look for the id-field in this schema - auto fid = schema.find("id"); + basic_error_handler err; + subschema_->validate(instance, err); - // found? - if (fid != schema.end() && - fid.value().type() == json::value_t::string) - id = id.derive(fid.value()); // resolve to a full id with URL + path based on the parent + if (!err) + e.error("", instance, "instance is valid, whereas it should NOT be as required by schema"); + } - // already existing - error - if (schema_refs.find(id) != schema_refs.end()) - throw std::invalid_argument("schema " + id.to_string() + " already present in local resolver"); +public: + logical_not(json &sch, + root_schema *root, + const std::vector &uris) + : schema(root) + { + subschema_ = schema::make(sch, root, {"not"}, uris); + } +}; - // store a raw pointer to this (sub-)schema referenced by its absolute json_uri - // this (sub-)schema is part of a schema stored inside schema_store_ so we can use the a raw-pointer-ref - schema_refs[id] = &schema; +enum logical_combination_types { + allOf, + anyOf, + oneOf +}; - for (auto i = schema.begin(), end = schema.end(); i != end; ++i) { - // FIXME: this inhibits the user adding properties with the key "default" - if (i.key() == "default") /* default value can be objects, but are not schemas */ - continue; +template +class logical_combination : public schema +{ + std::vector> subschemata_; - switch (i.value().type()) { + void validate(const json &instance, basic_error_handler &e) const final + { + size_t count = 0; - case json::value_t::object: // child is object, it is a schema - resolve(i.value(), id.append(json_uri::escape(i.key()))); - break; + for (auto &s : subschemata_) { + basic_error_handler err; + s->validate(instance, err); - case json::value_t::array: { - std::size_t index = 0; - auto child_id = id.append(json_uri::escape(i.key())); - for (auto &v : i.value()) { - if (v.type() == json::value_t::object) // array element is object - resolve(v, child_id.append(std::to_string(index))); - index++; + if (err) { + //sub_schema_err << " one schema failed because: " << e.what() << "\n"; + if (combine_logic == allOf) { + e.error("", instance, "at least one schema has failed, but ALLOF them are required to validate."); + return; } + } else + count++; + + if (combine_logic == oneOf && count > 1) { + e.error("", instance, "more than one schema has succeeded, but only ONEOF them is required to validate."); + return; + } + if (combine_logic == anyOf && count == 1) + return; + } + + if ((combine_logic == anyOf || combine_logic == oneOf) && count == 0) + e.error("", instance, "no validation has succeeded but ANYOF/ONEOF them is required to validate."); + } + +public: + logical_combination(json &sch, + root_schema *root, + const std::vector &uris) + : 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)); + } +}; + +class type_schema : public schema +{ + std::vector> type_; + std::pair enum_, const_; + std::vector> logic_; + + static std::shared_ptr make(json &schema, + json::value_t type, + root_schema *, + const std::vector &, + std::set &); + + std::shared_ptr if_, then_, else_; + + void validate(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()]; + + if (type) + type->validate(instance, e); + else + e.error("", instance, "unexpected instance type"); + + if (enum_.first) { + bool seen_in_enum = false; + for (auto &e : enum_.second) + if (instance == e) { + seen_in_enum = true; + break; + } + + if (!seen_in_enum) + e.error("", instance, "instance not found in required enum"); + } + + if (const_.first && + const_.second != instance) + e.error("", instance, "instance not const"); + + for (auto l : logic_) + l->validate(instance, e); + + if (if_) { + basic_error_handler err; + + if_->validate(instance, err); + if (!err) { + if (then_) + then_->validate(instance, e); + } else { + if (else_) + else_->validate(instance, e); + } + } + } + +public: + type_schema(json &sch, + root_schema *root, + const std::vector &uris) + : schema(root), type_((uint8_t) json::value_t::discarded + 1) + { + // association between JSON-schema-type and NLohmann-types + static const std::vector> schema_types = { + {"null", json::value_t::null}, + {"object", json::value_t::object}, + {"array", json::value_t::array}, + {"string", json::value_t::string}, + {"boolean", json::value_t::boolean}, + {"integer", json::value_t::number_integer}, + {"integer", json::value_t::number_unsigned}, + {"number", json::value_t::number_float}, + }; + + std::set known_keywords; + + auto attr = sch.find("type"); + if (attr == sch.end()) // no type field means all sub-types possible + for (auto &t : schema_types) + type_[(uint8_t) t.second] = type_schema::make(sch, t.second, root, uris, known_keywords); + else { + switch (attr.value().type()) { // "type": "type" + + case json::value_t::string: { + auto schema_type = attr.value().get(); + 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::string: - if (i.key() == "$ref") { - json_uri ref = id.derive(i.value()); - i.value() = ref.to_string(); - refs.insert(ref); - } + case json::value_t::array: // "type": ["type1", "type2"] + 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; default: break; } + + sch.erase(attr); } - } - std::set refs; + for (auto &key : known_keywords) + sch.erase(key); -public: - std::set undefined_refs; + // with nlohmann::json floats can be seen as unsigned or integer - reuse the number-validator for + // integer values as well, if they have not been specified + if (type_[(uint8_t) json::value_t::number_float] && !type_[(uint8_t) json::value_t::number_integer]) + type_[(uint8_t) json::value_t::number_integer] = + type_[(uint8_t) json::value_t::number_unsigned] = + type_[(uint8_t) json::value_t::number_float]; - std::map schema_refs; + attr = sch.find("enum"); + if (attr != sch.end()) { + enum_ = {true, attr.value()}; + sch.erase(attr); + } - resolver(json &schema, json_uri id) - { - // if schema has an id use it as name and to retrieve the namespace (URL) - auto fid = schema.find("id"); - if (fid != schema.end()) - id = id.derive(fid.value()); + attr = sch.find("const"); + if (attr != sch.end()) { + const_ = {true, attr.value()}; + sch.erase(attr); + } - resolve(schema, id); + attr = sch.find("not"); + if (attr != sch.end()) { + logic_.push_back(std::make_shared(attr.value(), root, uris)); + sch.erase(attr); + } - // refs now contains all references - // - // local references should be resolvable inside the same URL - // - // undefined_refs will only contain external references - for (auto r : refs) { - if (schema_refs.find(r) == schema_refs.end()) { - if (r.url() == id.url()) // same url means referencing a sub-schema - // of the same document, which has not been found - throw std::invalid_argument("sub-schema " + r.pointer().to_string() + - " in schema " + id.to_string() + " not found"); - undefined_refs.insert(r.url()); + attr = sch.find("allOf"); + if (attr != sch.end()) { + logic_.push_back(std::make_shared>(attr.value(), root, uris)); + sch.erase(attr); + } + + attr = sch.find("anyOf"); + if (attr != sch.end()) { + logic_.push_back(std::make_shared>(attr.value(), root, uris)); + sch.erase(attr); + } + + attr = sch.find("oneOf"); + if (attr != sch.end()) { + logic_.push_back(std::make_shared>(attr.value(), root, uris)); + sch.erase(attr); + } + + attr = sch.find("if"); + if (attr != sch.end()) { + auto attr_then = sch.find("then"); + auto attr_else = sch.find("else"); + + if (attr_then != sch.end() || attr_else != sch.end()) { + if_ = schema::make(attr.value(), root, {"if"}, uris); + + if (attr_then != sch.end()) { + then_ = schema::make(attr_then.value(), root, {"then"}, uris); + sch.erase(attr_then); + } + + if (attr_else != sch.end()) { + else_ = schema::make(attr_else.value(), root, {"else"}, uris); + sch.erase(attr_else); + } } + sch.erase(attr); } } }; -void validate_type(const json &schema, const std::string &expected_type, const std::string &name) +class string : public schema { - const auto &type_it = schema.find("type"); - if (type_it == schema.end()) - /* TODO something needs to be done here, I think */ - return; + std::pair maxLength_{false, 0}; + std::pair minLength_{false, 0}; - const auto &type_instance = type_it.value(); +#ifndef NO_STD_REGEX + std::pair pattern_{false, REGEX_NAMESPACE::regex()}; + std::string patternString_; +#endif - // any of the types in this array - if (type_instance.type() == json::value_t::array) { - if ((std::find(type_instance.begin(), - type_instance.end(), - expected_type) != type_instance.end()) || - (expected_type == "integer" && - std::find(type_instance.begin(), - type_instance.end(), - "number") != type_instance.end())) - return; + std::pair format_; + std::function format_check_ = nullptr; - std::ostringstream s; - s << expected_type << " is not any of " << type_instance << " for " << name; - throw std::invalid_argument(s.str()); - - } else { // type_instance is a string - if (type_instance == expected_type || - (type_instance == "number" && expected_type == "integer")) - return; - - throw std::invalid_argument(name + " is " + expected_type + - ", but required type is " + type_instance.get()); + std::size_t utf8_length(const std::string &s) const + { + size_t len = 0; + for (const unsigned char &c : s) + if ((c & 0xc0) != 0x80) + len++; + return len; } -} -void validate_enum(const json &instance, const json &schema, const std::string &name) -{ - const auto &enum_value = schema.find("enum"); - if (enum_value == schema.end()) - return; + void validate(const json &instance, basic_error_handler &e) const override + { + if (minLength_.first) { + if (utf8_length(instance) < minLength_.second) { + std::ostringstream s; + s << "'" << instance << "' is too short as per minLength (" << minLength_.second << ")"; + e.error("", instance, s.str()); + } + } - if (std::find(enum_value.value().begin(), enum_value.value().end(), instance) != enum_value.value().end()) - return; + if (maxLength_.first) { + if (utf8_length(instance) > maxLength_.second) { + std::ostringstream s; + s << "'" << instance << "' is too long as per maxLength (" << maxLength_.second << ")"; + e.error("", instance, s.str()); + } + } - std::ostringstream s; - s << "invalid enum-value '" << instance << "' " - << "for instance '" << name << "'. Candidates are " << enum_value.value() << "."; +#ifndef NO_STD_REGEX + if (pattern_.first && + !REGEX_NAMESPACE::regex_search(instance.get(), pattern_.second)) + e.error("", instance, instance.get() + " does not match regex pattern: " + patternString_); +#endif - throw std::invalid_argument(s.str()); -} - -void validate_boolean(const json & /*instance*/, const json &schema, const std::string &name) -{ - validate_type(schema, "boolean", name); -} - -template -bool violates_numeric_maximum(T max, T value, bool exclusive) -{ - if (exclusive) - return value >= max; - - return value > max; -} - -template -bool violates_numeric_minimum(T min, T value, bool exclusive) -{ - if (exclusive) - return value <= min; - - return value < min; -} - -// multipleOf - if the rest of the division is 0 -> OK -bool violates_multiple_of(json::number_float_t x, json::number_float_t y) -{ - json::number_integer_t n = static_cast(x / y); - double res = (x - n * y); - return fabs(res) > std::numeric_limits::epsilon(); -} - -template -void validate_numeric(const json &instance, const json &schema, const std::string &name) -{ - T value = instance; - - if (value != 0) { // zero is multiple of everything - const auto &multipleOf = schema.find("multipleOf"); - - if (multipleOf != schema.end()) { - double multiple = multipleOf.value(); - double value_float = value; - - if (violates_multiple_of(value_float, multiple)) - throw std::out_of_range(name + " is not a multiple of " + std::to_string(multiple)); + if (format_.first) { + if (format_check_ == nullptr) + e.error("", instance, std::string("A format checker was not provided but a format-attribute for this string is present. ") + " cannot be validated for " + format_.second); + else + format_check_(format_.second, instance); } } - const auto &maximum = schema.find("maximum"); - if (maximum != schema.end()) { - T maxi = maximum.value(); +public: + string(json &sch, root_schema *root) + : schema(root) + { + auto attr = sch.find("maxLength"); + if (attr != sch.end()) { + maxLength_ = {true, attr.value()}; + sch.erase(attr); + } - const auto &excl = schema.find("exclusiveMaximum"); - bool exclusive = (excl != schema.end()) ? excl.value().get() : false; + attr = sch.find("minLength"); + if (attr != sch.end()) { + minLength_ = {true, attr.value()}; + sch.erase(attr); + } - if (violates_numeric_maximum(maxi, value, exclusive)) - throw std::out_of_range(name + " exceeds maximum of " + std::to_string(maxi)); +#ifndef NO_STD_REGEX + attr = sch.find("pattern"); + if (attr != sch.end()) { + patternString_ = attr.value(); + pattern_ = {true, REGEX_NAMESPACE::regex(attr.value().get(), + REGEX_NAMESPACE::regex::ECMAScript)}; + sch.erase(attr); + } +#endif + + attr = sch.find("format"); + if (attr != sch.end()) { + format_ = {true, attr.value()}; + sch.erase(attr); + } + } +}; + +template +class numeric : public schema +{ + std::pair maximum_{false, 0}; + std::pair minimum_{false, 0}; + + bool exclusiveMaximum_ = false; + bool exclusiveMinimum_ = false; + + 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 + { + json::number_integer_t n = static_cast(x / multipleOf_.second); + double res = (x - n * multipleOf_.second); + return fabs(res) > std::numeric_limits::epsilon(); } - const auto &minimum = schema.find("minimum"); - if (minimum != schema.end()) { - T mini = minimum.value(); + void validate(const json &instance, basic_error_handler &e) const override + { + T value = instance; // conversion of json to value_type - const auto &excl = schema.find("exclusiveMinimum"); - bool exclusive = (excl != schema.end()) ? excl.value().get() : false; + if (multipleOf_.first && value != 0) // zero is multiple of everything + if (violates_multiple_of(value)) + e.error("", instance, "is not a multiple of " + std::to_string(multipleOf_.second)); - if (violates_numeric_minimum(mini, value, exclusive)) - throw std::out_of_range(name + " is below minimum of " + std::to_string(mini)); + if (maximum_.first) + if ((exclusiveMaximum_ && value >= maximum_.second) || + value > maximum_.second) + e.error("", instance, "exceeds maximum of " + std::to_string(maximum_.second)); + + if (minimum_.first) + if ((exclusiveMinimum_ && value <= minimum_.second) || + value < minimum_.second) + e.error("", instance, "is below minimum of " + std::to_string(minimum_.second)); } -} -void validate_integer(const json &instance, const json &schema, const std::string &name) +public: + numeric(const json &sch, root_schema *root, std::set &kw) + : schema(root) + { + auto attr = sch.find("maximum"); + if (attr != sch.end()) { + maximum_ = {true, attr.value()}; + kw.insert("maximum"); + } + + attr = sch.find("minimum"); + if (attr != sch.end()) { + minimum_ = {true, attr.value()}; + kw.insert("minimum"); + } + + attr = sch.find("exclusiveMaximum"); + if (attr != sch.end()) { + exclusiveMaximum_ = true; + maximum_ = {true, attr.value()}; + kw.insert("exclusiveMaximum"); + } + + attr = sch.find("exclusiveMinimum"); + if (attr != sch.end()) { + minimum_ = {true, attr.value()}; + exclusiveMinimum_ = true; + kw.insert("exclusiveMinimum"); + } + + attr = sch.find("multipleOf"); + if (attr != sch.end()) { + multipleOf_ = {true, attr.value()}; + kw.insert("multipleOf"); + } + } +}; + +class null : public schema { - validate_type(schema, "integer", name); - //TODO: Validate schema values are json::value_t::number_integer/unsigned? + void validate(const json &instance, basic_error_handler &e) const override + { + if (!instance.is_null()) + e.error("", instance, "expected to be null"); + } - validate_numeric(instance, schema, name); -} +public: + null(json &, root_schema *root) + : schema(root) {} +}; -bool is_unsigned(const json &schema) +class boolean_type : public schema { - const auto &minimum = schema.find("minimum"); + void validate(const json &, basic_error_handler &) const override {} - // Number is expected to be unsigned if a minimum >= 0 is set - return minimum != schema.end() && minimum.value() >= 0; -} +public: + boolean_type(json &, root_schema *root) + : schema(root) {} +}; -void validate_unsigned(const json &instance, const json &schema, const std::string &name) +class boolean : public schema { - validate_type(schema, "integer", name); - //TODO: Validate schema values are json::value_t::unsigned? + bool true_; + void validate(const json &instance, basic_error_handler &e) const override + { + if (!true_) { // false schema + // empty array + //switch (instance.type()) { + //case json::value_t::array: + // if (instance.size() != 0) // valid false-schema + // e.error("", instance, "false-schema required empty array"); + // return; + //} - //Is there a better way to determine whether an unsigned comparison should take place? - if (is_unsigned(schema)) - validate_numeric(instance, schema, name); - else - validate_numeric(instance, schema, name); -} + e.error("", instance, "instance invalid as par false-schema"); + } + } -void validate_float(const json &instance, const json &schema, const std::string &name) +public: + boolean(json &sch, root_schema *root) + : schema(root), true_(sch) {} +}; + +class required : public schema { - validate_type(schema, "number", name); - //TODO: Validate schema values are json::value_t::number_float? + const std::vector required_; - validate_numeric(instance, schema, name); -} + void validate(const json &instance, basic_error_handler &e) const override final + { + for (auto &r : required_) + if (instance.find(r) == instance.end()) + e.error("", instance, "required property '" + r + "' not found in object as a dependency"); + } -void validate_null(const json & /*instance*/, const json &schema, const std::string &name) +public: + required(const std::vector &r, root_schema *root) + : schema(root), required_(r) {} +}; + +class object : public schema { - validate_type(schema, "null", name); -} + std::pair maxProperties_{false, 0}; + std::pair minProperties_{false, 0}; + std::vector required_; -} // anonymous namespace + std::map> properties_; +#ifndef NO_STD_REGEX + std::vector>> patternProperties_; +#endif + std::shared_ptr additionalProperties_; + + std::map> dependencies_; + + std::shared_ptr propertyNames_; + + void validate(const json &instance, basic_error_handler &e) const override + { + if (maxProperties_.first && instance.size() > maxProperties_.second) + e.error("", instance, "too many properties."); + + if (minProperties_.first && instance.size() < minProperties_.second) + e.error("", instance, "too few properties."); + + for (auto &r : required_) + if (instance.find(r) == instance.end()) + e.error("", instance, "required property '" + r + "' not found in object '"); + + // for each property in instance + for (auto &p : instance.items()) { + if (propertyNames_) + propertyNames_->validate(p.key(), 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(p.value(), e); + } + + // check all matching patternProperties + for (auto &schema_pp : patternProperties_) + if (REGEX_NAMESPACE::regex_search(p.key(), schema_pp.first)) { + a_prop_or_pattern_matched = true; + schema_pp.second->validate(p.value(), e); + } + // check additionalProperties as a last resort + if (!a_prop_or_pattern_matched && additionalProperties_) + additionalProperties_->validate(p.value(), e); + } + + for (auto &dep : dependencies_) { + auto prop = instance.find(dep.first); + if (prop != instance.end()) // if dependency-property is present in instance + dep.second->validate(instance, e); // validate + } + } + +public: + object(json &sch, + root_schema *root, + const std::vector &uris) + : schema(root) + { + auto attr = sch.find("maxProperties"); + if (attr != sch.end()) { + maxProperties_ = {true, attr.value()}; + sch.erase(attr); + } + + attr = sch.find("minProperties"); + if (attr != sch.end()) { + minProperties_ = {true, attr.value()}; + sch.erase(attr); + } + + attr = sch.find("required"); + if (attr != sch.end()) { + required_ = attr.value().get>(); + sch.erase(attr); + } + + attr = sch.find("properties"); + if (attr != sch.end()) { + for (auto prop : attr.value().items()) + properties_.insert( + std::make_pair( + prop.key(), + schema::make(prop.value(), root, {"properties", prop.key()}, uris))); + sch.erase(attr); + } + +#ifndef NO_STD_REGEX + attr = sch.find("patternProperties"); + if (attr != sch.end()) { + for (auto prop : attr.value().items()) + patternProperties_.push_back( + std::make_pair( + REGEX_NAMESPACE::regex(prop.key(), REGEX_NAMESPACE::regex::ECMAScript), + schema::make(prop.value(), root, {prop.key()}, uris))); + sch.erase(attr); + } +#endif + + attr = sch.find("additionalProperties"); + if (attr != sch.end()) { + additionalProperties_ = schema::make(attr.value(), root, {"additionalProperties"}, uris); + sch.erase(attr); + } + + attr = sch.find("dependencies"); + if (attr != sch.end()) { + for (auto &dep : attr.value().items()) + switch (dep.value().type()) { + case json::value_t::array: + dependencies_.emplace(dep.key(), + std::make_shared( + dep.value().get>(), root)); + break; + + default: + dependencies_.emplace(dep.key(), + schema::make(dep.value(), root, {"dependencies", dep.key()}, uris)); + break; + } + sch.erase(attr); + } + + attr = sch.find("propertyNames"); + if (attr != sch.end()) { + propertyNames_ = schema::make(attr.value(), root, {"propertyNames"}, uris); + sch.erase(attr); + } + } +}; + +class array : public schema +{ + std::pair maxItems_{false, 0}; + std::pair minItems_{false, 0}; + bool uniqueItems_ = false; + + std::shared_ptr items_schema_; + + std::vector> items_; + std::shared_ptr additionalItems_; + + std::shared_ptr contains_; + + void validate(const json &instance, basic_error_handler &e) const override + { + if (maxItems_.first && instance.size() > maxItems_.second) + e.error("", instance, "has too many items."); + + if (minItems_.first && instance.size() < minItems_.second) + e.error("", instance, "has too few items."); + + if (uniqueItems_) { + for (auto it = instance.cbegin(); it != instance.cend(); ++it) { + auto v = std::find(it + 1, instance.end(), *it); + if (v != instance.end()) + e.error("", instance, "items have to be unique for this array."); + } + } + + if (items_schema_) + for (auto &i : instance) + items_schema_->validate(i, e); + else { + auto item = items_.cbegin(); + for (auto &i : instance) { + std::shared_ptr item_validator; + if (item == items_.cend()) + item_validator = additionalItems_; + else { + item_validator = *item; + item++; + } + + if (!item_validator) + break; + + item_validator->validate(i, e); + } + } + + if (contains_) { + bool contained = false; + for (auto &item : instance) { + basic_error_handler local_e; + contains_->validate(item, local_e); + if (!local_e) { + contained = true; + break; + } + } + if (!contained) + e.error("", instance, "array does not contain required element as per 'contains'"); + } + } + +public: + array(json &sch, root_schema *root, const std::vector &uris) + : schema(root) + { + auto attr = sch.find("maxItems"); + if (attr != sch.end()) { + maxItems_ = {true, attr.value()}; + sch.erase(attr); + } + + attr = sch.find("minItems"); + if (attr != sch.end()) { + minItems_ = {true, attr.value()}; + sch.erase(attr); + } + + attr = sch.find("uniqueItems"); + if (attr != sch.end()) { + uniqueItems_ = attr.value(); + sch.erase(attr); + } + + attr = sch.find("items"); + if (attr != sch.end()) { + + if (attr.value().type() == json::value_t::array) { + size_t c = 0; + for (auto &subsch : attr.value()) + items_.push_back(schema::make(subsch, root, {"items", std::to_string(c++)}, uris)); + + auto attr_add = sch.find("additionalItems"); + if (attr_add != sch.end()) { + additionalItems_ = schema::make(attr_add.value(), root, {"additionalItems"}, uris); + sch.erase(attr_add); + } + + } else if (attr.value().type() == json::value_t::object || + attr.value().type() == json::value_t::boolean) + items_schema_ = schema::make(attr.value(), root, {"items"}, uris); + + sch.erase(attr); + } + + attr = sch.find("contains"); + if (attr != sch.end()) { + contains_ = schema::make(attr.value(), root, {"contains"}, uris); + sch.erase(attr); + } + } +}; + +class schema_ref : public schema +{ + const std::string id_; + std::shared_ptr target_; + + void validate(const json &instance, basic_error_handler &e) const final + { + if (target_) + target_->validate(instance, e); + else + e.error("", instance, "unresolved schema-reference " + id_); + } + +public: + schema_ref(const std::string &id, root_schema *root) + : schema(root), id_(id) {} + + const std::string &id() const { return id_; } + void set_target(std::shared_ptr target) { target_ = target; } +}; + +std::shared_ptr type_schema::make(json &schema, + json::value_t type, + root_schema *root, + const std::vector &uris, + std::set &kw) +{ + switch (type) { + case json::value_t::null: + return std::make_shared(schema, root); + case json::value_t::number_unsigned: + return std::make_shared>(schema, root, kw); + case json::value_t::number_integer: + return std::make_shared>(schema, root, kw); + case json::value_t::number_float: + return std::make_shared>(schema, root, kw); + case json::value_t::string: + return std::make_shared(schema, root); + case json::value_t::boolean: + return std::make_shared(schema, root); + case json::value_t::object: + return std::make_shared(schema, root, uris); + case json::value_t::array: + return std::make_shared(schema, root, uris); + + case json::value_t::discarded: // not a real type - silence please + break; + } + return nullptr; +} +} // namespace namespace nlohmann { -namespace json_schema_draft4 +namespace json_schema { -void json_validator::insert_schema(const json &input, const json_uri &id) +class root_schema : public schema { - // allocate create a copy for later storage - if resolving reference works - std::shared_ptr schema = std::make_shared(input); + std::function loader_; + std::function format_; - do { - // resolve all local schemas and references - resolver r(*schema, id); + std::shared_ptr root_; - // check whether all undefined schema references can be resolved with existing ones - std::set undefined; - for (auto &ref : r.undefined_refs) - if (schema_refs_.find(ref) == schema_refs_.end()) // exact schema reference not found - undefined.insert(ref); + struct schema_file { + std::map> schemas; + std::map> unresolved; // contains all unresolved references from any other file seen during parsing + json unknown_keywords; + }; - if (undefined.size() == 0) { // no undefined references - // now insert all schema-references - // check whether all schema-references are new - for (auto &sref : r.schema_refs) { - if (schema_refs_.find(sref.first) != schema_refs_.end()) - throw std::invalid_argument("schema " + sref.first.to_string() + " already present in validator."); + // location as key + std::map files_; + + schema_file &get_or_create_file(const std::string &loc) + { + auto file = files_.lower_bound(loc); + if (file != files_.end() && !(files_.key_comp()(loc, file->first))) + return file->second; + else + return files_.insert(file, {loc, {}})->second; + } + +public: + root_schema(std::function loader, + std::function format) + : schema(this), loader_(loader), format_(format) {} + + void insert(const json_uri &uri, const std::shared_ptr &s) + { + // std::cout << "adding schema '" << uri << "' '" << uri.location() << "'\n"; + + auto &file = get_or_create_file(uri.location()); + auto schema = file.schemas.lower_bound(uri.pointer()); + if (schema != file.schemas.end() && !(file.schemas.key_comp()(uri.pointer(), schema->first))) { + throw std::invalid_argument("schema with " + uri.to_string() + " already inserted\n"); + return; + } + + file.schemas.insert({uri.pointer(), s}); + + // was someone referencing this newly inserted schema? + auto unresolved = file.unresolved.find(uri.pointer()); + // std::cout << "resolving schemas looking for '" << uri.pointer() << "' in " << uri.location() << "\n"; + if (unresolved != file.unresolved.end()) { + // std::cout << " --> resolved!!\n"; + unresolved->second->set_target(s); + file.unresolved.erase(unresolved); + } + } + + void insert_unknown_keyword(const json_uri &uri, const std::string &key, json &value) + { + auto &file = get_or_create_file(uri.location()); + auto new_uri = uri.append(key); + auto pointer = new_uri.pointer(); + + // std::cout << "inserting unknown " << new_uri << " '" << pointer << "'\n"; + + // is there a reference looking for this unknown-keyword, which is thus no longer a unknown keyword but a schema + auto unresolved = file.unresolved.find(pointer); + if (unresolved != file.unresolved.end()) + schema::make(value, this, {}, {{new_uri}}); + else // no, nothing ref'd it + file.unknown_keywords[pointer] = value; + } + + std::shared_ptr get_or_create_ref(const json_uri &uri) + { + auto &file = get_or_create_file(uri.location()); + + // existing schema + auto schema = file.schemas.find(uri.pointer()); + if (schema != file.schemas.end()) + return schema->second; + + // referencing an unknown keyword, turn it into schema + try { + auto &subschema = file.unknown_keywords.at(uri.pointer()); + auto s = schema::make(subschema, this, {}, {{uri}}); + file.unknown_keywords.erase(uri.pointer()); + return s; + } catch (...) { + } + + // get or create a schema_ref + // std::cout << "using or creating a reference to " << uri << "\n"; + auto r = file.unresolved.lower_bound(uri.pointer()); + if (r != file.unresolved.end() && !(file.unresolved.key_comp()(uri.pointer(), r->first))) { + // std::cout << " --> using existing ref\n"; + return r->second; + } else { + // std::cout << " --> creating a new ref\n"; + return file.unresolved.insert(r, + {uri.pointer(), std::make_shared(uri.to_string(), this)}) + ->second; + } + } + + void set_root_schema(json schema) + { + root_ = schema::make(schema, this, {}, {{"#"}}); + + // load all files which have not yet been loaded + do { + bool new_schema_loaded = false; + + // files_ is modified during parsing, iterators are invalidated + std::vector locations; + for (auto &file : files_) + locations.push_back(file.first); + + for (auto &loc : locations) { + if (files_[loc].schemas.size() == 0) { // nothing has been loaded for this file + if (loader_) { + json sch; + + loader_(loc, sch); + + schema::make(sch, this, {}, {{loc}}); + new_schema_loaded = true; + } else { + throw std::invalid_argument("external schema reference '" + loc + "' needs loading, but no loader callback given\n"); + } + } } - // no undefined references and no duplicated schema - store the schema - schema_store_.push_back(schema); - // and insert all references - schema_refs_.insert(r.schema_refs.begin(), r.schema_refs.end()); + if (!new_schema_loaded) // if no new schema loaded, no need to try again + break; + } while (1); + } - break; + void validate(const json &instance, basic_error_handler &e) const final + { + if (root_) + root_->validate(instance, e); + else + e.error("", "", "no root schema has yet been set for validating an instance."); + } +}; + +} // namespace json_schema +} // namespace nlohmann + +namespace +{ + +std::shared_ptr schema::make(json &schema, + root_schema *root, + const std::vector &keys, + std::vector uris) +{ + // append to all URIs the keys for this sub-schema + for (auto &key : keys) + for (auto &uri : uris) + uri = uri.append(json_uri::escape(key)); + + std::shared_ptr<::schema> sch; + + // boolean schema + if (schema.type() == json::value_t::boolean) + sch = std::make_shared(schema, root); + else if (schema.type() == json::value_t::object) { + + auto attr = schema.find("$id"); // if $id is present, this schema can be referenced by this ID + // as an additional URI + if (attr != schema.end()) { + if (std::find(uris.begin(), + uris.end(), + attr.value().get()) == uris.end()) + uris.push_back(uris.back().derive(attr.value())); // so add it to the list if it is not there already + schema.erase(attr); } - if (schema_loader_ == nullptr) - throw std::invalid_argument("schema contains undefined references to other schemas, needed schema-loader."); - - for (auto undef : undefined) { - json ext; - - // check whether a recursive-call has already insert this schema in the meantime - if (schema_refs_.find(undef) != schema_refs_.end()) - continue; - - schema_loader_(undef, ext); - insert_schema(ext, undef.url()); // recursively call insert_schema to fill in new external references + attr = schema.find("definitions"); + if (attr != schema.end()) { + for (auto &def : attr.value().items()) + schema::make(def.value(), root, {"definitions", def.key()}, uris); + schema.erase(attr); } - } while (1); - // store the document root-schema - if (id == json_uri("#")) - root_schema_ = schema; + attr = schema.find("$ref"); + if (attr != schema.end()) { // this schema is a reference + // the last one on the uri-stack is the last id seen before coming here, + // so this is the origial URI for this reference, the $ref-value has thus be resolved from it + auto id = uris.back().derive(attr.value()); + sch = root->get_or_create_ref(id); + schema.erase(attr); + } else { + sch = std::make_shared(schema, root, uris); + } + + schema.erase("$schema"); + schema.erase("default"); + schema.erase("title"); + schema.erase("description"); + } else { + return nullptr; // TODO error/throw? when schema is invalid + } + + for (auto &uri : uris) { // for all URI reference this schema + root->insert(uri, sch); + + if (schema.type() == json::value_t::object) + for (auto &u : schema.items()) + root->insert_unknown_keyword(uri, u.key(), u.value()); + } + return sch; } -void json_validator::validate(const json &instance) +class throwing_error_handler : public basic_error_handler { - if (root_schema_ == nullptr) - throw std::invalid_argument("no root-schema has been inserted. Cannot validate an instance without it."); + void error(const std::string &path, const json &instance, const std::string &message) + { + throw std::invalid_argument(std::string("At ") + path + " of " + instance.dump() + " - " + message + "\n"); + } +}; - validate(instance, *root_schema_, "root"); +} // namespace + +namespace nlohmann +{ +namespace json_schema +{ + +json_validator::json_validator(std::function loader, + std::function format) + : root_(std::unique_ptr(new root_schema(loader, format))) +{ +} + +json_validator::~json_validator() +{ } void json_validator::set_root_schema(const json &schema) { - insert_schema(schema, json_uri("#")); + root_->set_root_schema(schema); } -void json_validator::validate(const json &instance, const json &schema_, const std::string &name) +void json_validator::validate(const json &instance) { - const json *schema = &schema_; - - // $ref resolution - do { - const auto &ref = schema->find("$ref"); - if (ref == schema->end()) - break; - - auto it = schema_refs_.find(ref.value().get()); - - if (it == schema_refs_.end()) - throw std::invalid_argument("schema reference " + ref.value().get() + " not found. Make sure all schemas have been inserted before validation."); - - schema = it->second; - } while (1); // loop in case of nested refs - - // not - const auto attr = schema->find("not"); - if (attr != schema->end()) { - bool ok; - - try { - validate(instance, attr.value(), name); - ok = false; - } catch (std::exception &) { - ok = true; - } - if (!ok) - throw std::invalid_argument("schema match for " + name + " but a not-match is defined by schema."); - return; // return here - not cannot be mixed with based-schemas? - } - - // allOf, anyOf, oneOf - const json *combined_schemas = nullptr; - enum { - none, - allOf, - anyOf, - oneOf - } combine_logic = none; - - { - const auto &attr = schema->find("allOf"); - if (attr != schema->end()) { - combine_logic = allOf; - combined_schemas = &attr.value(); - } - } - { - const auto &attr = schema->find("anyOf"); - if (attr != schema->end()) { - combine_logic = anyOf; - combined_schemas = &attr.value(); - } - } - { - const auto &attr = schema->find("oneOf"); - if (attr != schema->end()) { - combine_logic = oneOf; - combined_schemas = &attr.value(); - } - } - - if (combine_logic != none) { - std::size_t count = 0; - std::ostringstream sub_schema_err; - - for (const auto &s : *combined_schemas) { - try { - validate(instance, s, name); - count++; - } catch (std::exception &e) { - sub_schema_err << " one schema failed because: " << e.what() << "\n"; - - if (combine_logic == allOf) - throw std::out_of_range("At least one schema has failed for " + name + " where allOf them were requested.\n" + sub_schema_err.str()); - } - if (combine_logic == oneOf && count > 1) - throw std::out_of_range("More than one schema has succeeded for " + name + " where only oneOf them was requested.\n" + sub_schema_err.str()); - } - if ((combine_logic == anyOf || combine_logic == oneOf) && count == 0) - throw std::out_of_range("No schema has succeeded for " + name + " but anyOf/oneOf them should have worked.\n" + sub_schema_err.str()); - } - - // check (base) schema - validate_enum(instance, *schema, name); - - switch (instance.type()) { - case json::value_t::object: - validate_object(instance, *schema, name); - break; - - case json::value_t::array: - validate_array(instance, *schema, name); - break; - - case json::value_t::string: - validate_string(instance, *schema, name); - break; - - case json::value_t::number_unsigned: - validate_unsigned(instance, *schema, name); - break; - - case json::value_t::number_integer: - validate_integer(instance, *schema, name); - break; - - case json::value_t::number_float: - validate_float(instance, *schema, name); - break; - - case json::value_t::boolean: - validate_boolean(instance, *schema, name); - break; - - case json::value_t::null: - validate_null(instance, *schema, name); - break; - - default: - assert(0 && "unexpected instance type for validation"); - break; - } + throwing_error_handler err; + validate(instance, err); } -void json_validator::validate_array(const json &instance, const json &schema, const std::string &name) +void json_validator::validate(const json &instance, basic_error_handler &err) { - validate_type(schema, "array", name); - - // maxItems - const auto &maxItems = schema.find("maxItems"); - if (maxItems != schema.end()) - if (instance.size() > maxItems.value().get()) - throw std::out_of_range(name + " has too many items."); - - // minItems - const auto &minItems = schema.find("minItems"); - if (minItems != schema.end()) - if (instance.size() < minItems.value().get()) - throw std::out_of_range(name + " has too few items."); - - // uniqueItems - const auto &uniqueItems = schema.find("uniqueItems"); - if (uniqueItems != schema.end()) - if (uniqueItems.value().get() == true) { - std::set array_to_set; - for (auto v : instance) { - auto ret = array_to_set.insert(v); - if (ret.second == false) - throw std::out_of_range(name + " should have only unique items."); - } - } - - // items and additionalItems - // default to empty schemas - auto items_iter = schema.find("items"); - json items = {}; - if (items_iter != schema.end()) - items = items_iter.value(); - - auto additionalItems_iter = schema.find("additionalItems"); - json additionalItems = {}; - if (additionalItems_iter != schema.end()) - additionalItems = additionalItems_iter.value(); - - size_t i = 0; - bool validation_done = false; - - for (auto &value : instance) { - std::string sub_name = name + "[" + std::to_string(i) + "]"; - - switch (items.type()) { - - case json::value_t::array: - - if (i < items.size()) - validate(value, items[i], sub_name); - else { - switch (additionalItems.type()) { // items is an array - // we need to take into consideration additionalItems - case json::value_t::object: - validate(value, additionalItems, sub_name); - break; - - case json::value_t::boolean: - if (additionalItems.get() == false) - throw std::out_of_range("additional values in array are not allowed for " + sub_name); - else - validation_done = true; - break; - - default: - break; - } - } - - break; - - case json::value_t::object: // items is a schema - validate(value, items, sub_name); - break; - - default: - break; - } - if (validation_done) - break; - - i++; - } + root_->validate(instance, err); } -void json_validator::validate_object(const json &instance, const json &schema, const std::string &name) -{ - validate_type(schema, "object", name); - - json properties = {}; - if (schema.find("properties") != schema.end()) - properties = schema["properties"]; - -#if 0 - // check for default values of properties - // and insert them into this object, if they don't exists - // works only for object properties for the moment - if (default_value_insertion) - for (auto it = properties.begin(); it != properties.end(); ++it) { - - const auto &default_value = it.value().find("default"); - if (default_value == it.value().end()) - continue; /* no default value -> continue */ - - if (instance.find(it.key()) != instance.end()) - continue; /* value is present */ - - /* create element from default value */ - instance[it.key()] = default_value.value(); - } -#endif - // maxProperties - const auto &maxProperties = schema.find("maxProperties"); - if (maxProperties != schema.end()) - if (instance.size() > maxProperties.value().get()) - throw std::out_of_range(name + " has too many properties."); - - // minProperties - const auto &minProperties = schema.find("minProperties"); - if (minProperties != schema.end()) - if (instance.size() < minProperties.value().get()) - throw std::out_of_range(name + " has too few properties."); - - // additionalProperties - enum { - True, - False, - Object - } additionalProperties = True; - - const auto &additionalPropertiesVal = schema.find("additionalProperties"); - if (additionalPropertiesVal != schema.end()) { - if (additionalPropertiesVal.value().type() == json::value_t::boolean) - additionalProperties = additionalPropertiesVal.value().get() == true ? True : False; - else - additionalProperties = Object; - } - - // patternProperties - json patternProperties = {}; - if (schema.find("patternProperties") != schema.end()) - patternProperties = schema["patternProperties"]; - - // check all elements in object - for (auto child = instance.begin(); child != instance.end(); ++child) { - std::string child_name = name + "." + child.key(); - - bool property_or_patternProperties_has_validated = false; - // is this a property which is described in the schema - const auto &object_prop = properties.find(child.key()); - if (object_prop != properties.end()) { - // validate the element with its schema - validate(child.value(), object_prop.value(), child_name); - property_or_patternProperties_has_validated = true; - } - - for (auto pp = patternProperties.begin(); - pp != patternProperties.end(); ++pp) { -#ifndef NO_STD_REGEX - REGEX_NAMESPACE::regex re(pp.key(), REGEX_NAMESPACE::regex::ECMAScript); - - if (REGEX_NAMESPACE::regex_search(child.key(), re)) { - validate(child.value(), pp.value(), child_name); - property_or_patternProperties_has_validated = true; - } -#else - // accept everything in case of a patternProperty - property_or_patternProperties_has_validated = true; - break; -#endif - } - - if (property_or_patternProperties_has_validated) - continue; - - switch (additionalProperties) { - case True: - break; - - case Object: - validate(child.value(), additionalPropertiesVal.value(), child_name); - break; - - case False: - throw std::invalid_argument("unknown property '" + child.key() + "' in object '" + name + "'"); - break; - }; - } - - // required - const auto &required = schema.find("required"); - if (required != schema.end()) - for (const auto &element : required.value()) { - if (instance.find(element) == instance.end()) { - throw std::invalid_argument("required element '" + element.get() + - "' not found in object '" + name + "'"); - } - } - - // dependencies - const auto &dependencies = schema.find("dependencies"); - if (dependencies == schema.end()) - return; - - for (auto dep = dependencies.value().cbegin(); - dep != dependencies.value().cend(); - ++dep) { - - // property not present in this instance - next - if (instance.find(dep.key()) == instance.end()) - continue; - - std::string sub_name = name + ".dependency-of-" + dep.key(); - - switch (dep.value().type()) { - - case json::value_t::object: - validate(instance, dep.value(), sub_name); - break; - - case json::value_t::array: - for (const auto &prop : dep.value()) - if (instance.find(prop) == instance.end()) - throw std::invalid_argument("failed dependency for " + sub_name + ". Need property " + prop.get()); - break; - - default: - break; - } - } -} - -static std::size_t utf8_length(const std::string &s) -{ - size_t len = 0; - for (const unsigned char &c : s) - if ((c & 0xc0) != 0x80) - len++; - return len; -} - -void json_validator::validate_string(const json &instance, const json &schema, const std::string &name) -{ - validate_type(schema, "string", name); - - // minLength - auto attr = schema.find("minLength"); - if (attr != schema.end()) - if (utf8_length(instance) < attr.value().get()) { - std::ostringstream s; - s << "'" << name << "' of value '" << instance << "' is too short as per minLength (" - << attr.value() << ")"; - throw std::out_of_range(s.str()); - } - - // maxLength - attr = schema.find("maxLength"); - if (attr != schema.end()) - if (utf8_length(instance) > attr.value().get()) { - std::ostringstream s; - s << "'" << name << "' of value '" << instance << "' is too long as per maxLength (" - << attr.value() << ")"; - throw std::out_of_range(s.str()); - } - -#ifndef NO_STD_REGEX - // pattern - attr = schema.find("pattern"); - if (attr != schema.end()) { - REGEX_NAMESPACE::regex re(attr.value().get(), REGEX_NAMESPACE::regex::ECMAScript); - if (!REGEX_NAMESPACE::regex_search(instance.get(), re)) - throw std::invalid_argument(instance.get() + " does not match regex pattern: " + attr.value().get() + " for " + name); - } -#endif - - // format - attr = schema.find("format"); - if (attr != schema.end()) { - if (format_check_ == nullptr) - throw std::logic_error("A format checker was not provided but a format-attribute for this string is present. " + - name + " cannot be validated for " + attr.value().get()); - format_check_(attr.value(), instance); - } -} -} // namespace json_schema_draft4 +} // namespace json_schema } // namespace nlohmann diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a9072f4..b1b1c01 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,7 +7,8 @@ function(add_test_simple_schema name schema instance) COMMAND ${PIPE_IN_TEST_SCRIPT} $ ${schema} - ${instance}) + ${instance} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) endfunction() file(GLOB TEST_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/*) @@ -17,3 +18,8 @@ foreach(DIR ${TEST_DIRS}) add_subdirectory(${DIR}) endif() endforeach() + +add_executable(uri uri.cpp) +target_link_libraries(uri json-schema-validator) + +add_test(NAME uri COMMAND uri) diff --git a/test/JSON-Schema-Test-Suite/CMakeLists.txt b/test/JSON-Schema-Test-Suite/CMakeLists.txt index 4153b5b..56e2bc1 100644 --- a/test/JSON-Schema-Test-Suite/CMakeLists.txt +++ b/test/JSON-Schema-Test-Suite/CMakeLists.txt @@ -1,9 +1,11 @@ set(JSON_SCHEMA_TEST_PREFIX "JSON-Suite" CACHE STRING "prefix for JSON-tests added to ctest") +set(DRAFT "draft7") + # find schema-test-suite find_path(JSON_SCHEMA_TEST_SUITE_PATH NAMES - tests/draft4) + tests/${DRAFT}) if (NOT JSON_SCHEMA_TEST_SUITE_PATH) message(STATUS "Set JSON_SCHEMA_TEST_SUITE_PATH to a path in which JSON-Schema-Test-Suite is located (github.com/json-schema-org/JSON-Schema-Test-Suite). Using internal test-suite which might be out of date.") @@ -21,7 +23,7 @@ if(JSON_SCHEMA_TEST_SUITE_PATH) option(JSON_SCHEMA_ENABLE_OPTIONAL_TESTS "Enable optional tests of the JSONSchema Test Suite" ON) # create tests foreach test-file - file(GLOB TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/draft4/*.json) + file(GLOB TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/${DRAFT}/*.json) foreach(TEST_FILE ${TEST_FILES}) get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE) @@ -30,7 +32,7 @@ if(JSON_SCHEMA_TEST_SUITE_PATH) endforeach() if (JSON_SCHEMA_ENABLE_OPTIONAL_TESTS) - file(GLOB OPT_TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/draft4/optional/*.json) + file(GLOB OPT_TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/${DRAFT}/optional/*.json) foreach(TEST_FILE ${OPT_TEST_FILES}) get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE) @@ -38,18 +40,13 @@ if(JSON_SCHEMA_TEST_SUITE_PATH) COMMAND ${PIPE_IN_TEST_SCRIPT} $ ${TEST_FILE}) endforeach() - # XXX Unfortunately URLs are not very well handled yet, accept those tests which fail - set_tests_properties(JSON-Suite::ref - JSON-Suite::refRemote - PROPERTIES - WILL_FAIL ON) - # some optional tests will fail as well. - set_tests_properties(JSON-Suite::Optional::bignum - JSON-Suite::Optional::ecmascript-regex - JSON-Suite::Optional::format - PROPERTIES - WILL_FAIL ON) + set_tests_properties( + JSON-Suite::Optional::bignum + JSON-Suite::Optional::content + JSON-Suite::Optional::zeroTerminatedFloats + PROPERTIES + WILL_FAIL ON) endif() else() endif() diff --git a/test/JSON-Schema-Test-Suite/json-schema-test.cpp b/test/JSON-Schema-Test-Suite/json-schema-test.cpp index 9059471..67564c0 100644 --- a/test/JSON-Schema-Test-Suite/json-schema-test.cpp +++ b/test/JSON-Schema-Test-Suite/json-schema-test.cpp @@ -1,37 +1,20 @@ /* - * Modern C++ JSON schema validator + * JSON schema validator for JSON for modern C++ * - * Licensed under the MIT License . + * Copyright (c) 2016-2019 Patrick Boettcher . * - * Copyright (c) 2016 Patrick Boettcher . + * SPDX-License-Identifier: MIT * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom - * the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN - * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT - * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR - * THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "json-schema.hpp" #include -#include #include +#include using nlohmann::json; using nlohmann::json_uri; -using nlohmann::json_schema_draft4::json_validator; +using nlohmann::json_schema::json_validator; static void format_check(const std::string &format, const std::string &value) { @@ -61,8 +44,8 @@ static void format_check(const std::string &format, const std::string &value) static void loader(const json_uri &uri, json &schema) { - if (uri.to_string() == "http://json-schema.org/draft-04/schema#") { - schema = nlohmann::json_schema_draft4::draft4_schema_builtin; + if (uri.location() == "http://json-schema.org/draft-07/schema") { + schema = nlohmann::json_schema::draft7_schema_builtin; return; } diff --git a/test/JSON-Schema-Test-Suite/remotes/folder/folderInteger.json b/test/JSON-Schema-Test-Suite/remotes/folder/folderInteger.json new file mode 100644 index 0000000..dbe5c75 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/remotes/folder/folderInteger.json @@ -0,0 +1,3 @@ +{ + "type": "integer" +} \ No newline at end of file diff --git a/test/JSON-Schema-Test-Suite/remotes/integer.json b/test/JSON-Schema-Test-Suite/remotes/integer.json new file mode 100644 index 0000000..dbe5c75 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/remotes/integer.json @@ -0,0 +1,3 @@ +{ + "type": "integer" +} \ No newline at end of file diff --git a/test/JSON-Schema-Test-Suite/remotes/name.json b/test/JSON-Schema-Test-Suite/remotes/name.json new file mode 100644 index 0000000..19ba093 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/remotes/name.json @@ -0,0 +1,11 @@ +{ + "definitions": { + "orNull": { + "anyOf": [ + {"type": "null"}, + {"$ref": "#"} + ] + } + }, + "type": "string" +} diff --git a/test/JSON-Schema-Test-Suite/remotes/subSchemas.json b/test/JSON-Schema-Test-Suite/remotes/subSchemas.json new file mode 100644 index 0000000..8b6d8f8 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/remotes/subSchemas.json @@ -0,0 +1,8 @@ +{ + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/integer" + } +} \ No newline at end of file diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/maximum.json b/test/JSON-Schema-Test-Suite/tests/draft4/maximum.json deleted file mode 100644 index 02581f6..0000000 --- a/test/JSON-Schema-Test-Suite/tests/draft4/maximum.json +++ /dev/null @@ -1,73 +0,0 @@ -[ - { - "description": "maximum validation", - "schema": {"maximum": 3.0}, - "tests": [ - { - "description": "below the maximum is valid", - "data": 2.6, - "valid": true - }, - { - "description": "boundary point is valid", - "data": 3.0, - "valid": true - }, - { - "description": "above the maximum is invalid", - "data": 3.5, - "valid": false - }, - { - "description": "ignores non-numbers", - "data": "x", - "valid": true - } - ] - }, - { - "description": "maximum validation (explicit false exclusivity)", - "schema": {"maximum": 3.0, "exclusiveMaximum": false}, - "tests": [ - { - "description": "below the maximum is valid", - "data": 2.6, - "valid": true - }, - { - "description": "boundary point is valid", - "data": 3.0, - "valid": true - }, - { - "description": "above the maximum is invalid", - "data": 3.5, - "valid": false - }, - { - "description": "ignores non-numbers", - "data": "x", - "valid": true - } - ] - }, - { - "description": "exclusiveMaximum validation", - "schema": { - "maximum": 3.0, - "exclusiveMaximum": true - }, - "tests": [ - { - "description": "below the maximum is still valid", - "data": 2.2, - "valid": true - }, - { - "description": "boundary point is invalid", - "data": 3.0, - "valid": false - } - ] - } -] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/minimum.json b/test/JSON-Schema-Test-Suite/tests/draft4/minimum.json deleted file mode 100644 index 98f08d5..0000000 --- a/test/JSON-Schema-Test-Suite/tests/draft4/minimum.json +++ /dev/null @@ -1,73 +0,0 @@ -[ - { - "description": "minimum validation", - "schema": {"minimum": 1.1}, - "tests": [ - { - "description": "above the minimum is valid", - "data": 2.6, - "valid": true - }, - { - "description": "boundary point is valid", - "data": 1.1, - "valid": true - }, - { - "description": "below the minimum is invalid", - "data": 0.6, - "valid": false - }, - { - "description": "ignores non-numbers", - "data": "x", - "valid": true - } - ] - }, - { - "description": "minimum validation (explicit false exclusivity)", - "schema": {"minimum": 1.1, "exclusiveMinimum": false}, - "tests": [ - { - "description": "above the minimum is valid", - "data": 2.6, - "valid": true - }, - { - "description": "boundary point is valid", - "data": 1.1, - "valid": true - }, - { - "description": "below the minimum is invalid", - "data": 0.6, - "valid": false - }, - { - "description": "ignores non-numbers", - "data": "x", - "valid": true - } - ] - }, - { - "description": "exclusiveMinimum validation", - "schema": { - "minimum": 1.1, - "exclusiveMinimum": true - }, - "tests": [ - { - "description": "above the minimum is still valid", - "data": 1.2, - "valid": true - }, - { - "description": "boundary point is invalid", - "data": 1.1, - "valid": false - } - ] - } -] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/optional/format.json b/test/JSON-Schema-Test-Suite/tests/draft4/optional/format.json deleted file mode 100644 index 32db8de..0000000 --- a/test/JSON-Schema-Test-Suite/tests/draft4/optional/format.json +++ /dev/null @@ -1,223 +0,0 @@ -[ - { - "description": "validation of date-time strings", - "schema": {"format": "date-time"}, - "tests": [ - { - "description": "a valid date-time string", - "data": "1963-06-19T08:30:06.283185Z", - "valid": true - }, - { - "description": "an invalid date-time string", - "data": "06/19/1963 08:30:06 PST", - "valid": false - }, - { - "description": "only RFC3339 not all of ISO 8601 are valid", - "data": "2013-350T01:01:01", - "valid": false - } - ] - }, - { - "description": "validation of URIs", - "schema": {"format": "uri"}, - "tests": [ - { - "description": "a valid URL with anchor tag", - "data": "http://foo.bar/?baz=qux#quux", - "valid": true - }, - { - "description": "a valid URL with anchor tag and parantheses", - "data": "http://foo.com/blah_(wikipedia)_blah#cite-1", - "valid": true - }, - { - "description": "a valid URL with URL-encoded stuff", - "data": "http://foo.bar/?q=Test%20URL-encoded%20stuff", - "valid": true - }, - { - "description": "a valid puny-coded URL ", - "data": "http://xn--nw2a.xn--j6w193g/", - "valid": true - }, - { - "description": "a valid URL with many special characters", - "data": "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", - "valid": true - }, - { - "description": "a valid URL based on IPv4", - "data": "http://223.255.255.254", - "valid": true - }, - { - "description": "a valid URL with ftp scheme", - "data": "ftp://ftp.is.co.za/rfc/rfc1808.txt", - "valid": true - }, - { - "description": "a valid URL for a simple text file", - "data": "http://www.ietf.org/rfc/rfc2396.txt", - "valid": true - }, - { - "description": "a valid URL ", - "data": "ldap://[2001:db8::7]/c=GB?objectClass?one", - "valid": true - }, - { - "description": "a valid mailto URI", - "data": "mailto:John.Doe@example.com", - "valid": true - }, - { - "description": "a valid newsgroup URI", - "data": "news:comp.infosystems.www.servers.unix", - "valid": true - }, - { - "description": "a valid tel URI", - "data": "tel:+1-816-555-1212", - "valid": true - }, - { - "description": "a valid URN", - "data": "urn:oasis:names:specification:docbook:dtd:xml:4.1.2", - "valid": true - }, - { - "description": "an invalid protocol-relative URI Reference", - "data": "//foo.bar/?baz=qux#quux", - "valid": false - }, - { - "description": "an invalid relative URI Reference", - "data": "/abc", - "valid": false - }, - { - "description": "an invalid URI", - "data": "\\\\WINDOWS\\fileshare", - "valid": false - }, - { - "description": "an invalid URI though valid URI reference", - "data": "abc", - "valid": false - }, - { - "description": "an invalid URI with spaces", - "data": "http:// shouldfail.com", - "valid": false - }, - { - "description": "an invalid URI with spaces and missing scheme", - "data": ":// should fail", - "valid": false - } - ] - }, - { - "description": "validation of e-mail addresses", - "schema": {"format": "email"}, - "tests": [ - { - "description": "a valid e-mail address", - "data": "joe.bloggs@example.com", - "valid": true - }, - { - "description": "an invalid e-mail address", - "data": "2962", - "valid": false - } - ] - }, - { - "description": "validation of IP addresses", - "schema": {"format": "ipv4"}, - "tests": [ - { - "description": "a valid IP address", - "data": "192.168.0.1", - "valid": true - }, - { - "description": "an IP address with too many components", - "data": "127.0.0.0.1", - "valid": false - }, - { - "description": "an IP address with out-of-range values", - "data": "256.256.256.256", - "valid": false - }, - { - "description": "an IP address without 4 components", - "data": "127.0", - "valid": false - }, - { - "description": "an IP address as an integer", - "data": "0x7f000001", - "valid": false - } - ] - }, - { - "description": "validation of IPv6 addresses", - "schema": {"format": "ipv6"}, - "tests": [ - { - "description": "a valid IPv6 address", - "data": "::1", - "valid": true - }, - { - "description": "an IPv6 address with out-of-range values", - "data": "12345::", - "valid": false - }, - { - "description": "an IPv6 address with too many components", - "data": "1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1", - "valid": false - }, - { - "description": "an IPv6 address containing illegal characters", - "data": "::laptop", - "valid": false - } - ] - }, - { - "description": "validation of host names", - "schema": {"format": "hostname"}, - "tests": [ - { - "description": "a valid host name", - "data": "www.example.com", - "valid": true - }, - { - "description": "a host name starting with an illegal character", - "data": "-a-host-name-that-starts-with--", - "valid": false - }, - { - "description": "a host name containing illegal characters", - "data": "not_a_valid_host_name", - "valid": false - }, - { - "description": "a host name with a component too long", - "data": "a-vvvvvvvvvvvvvvvveeeeeeeeeeeeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyyyyy-long-host-name-component", - "valid": false - } - ] - } -] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/additionalItems.json b/test/JSON-Schema-Test-Suite/tests/draft7/additionalItems.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/additionalItems.json rename to test/JSON-Schema-Test-Suite/tests/draft7/additionalItems.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/additionalProperties.json b/test/JSON-Schema-Test-Suite/tests/draft7/additionalProperties.json similarity index 74% rename from test/JSON-Schema-Test-Suite/tests/draft4/additionalProperties.json rename to test/JSON-Schema-Test-Suite/tests/draft7/additionalProperties.json index 90d7607..ffeac6b 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/additionalProperties.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/additionalProperties.json @@ -40,6 +40,25 @@ } ] }, + { + "description": "non-ASCII pattern with additionalProperties", + "schema": { + "patternProperties": {"^á": {}}, + "additionalProperties": false + }, + "tests": [ + { + "description": "matching the pattern is valid", + "data": {"ármányos": 2}, + "valid": true + }, + { + "description": "not matching the pattern is invalid", + "data": {"élmény": 2}, + "valid": false + } + ] + }, { "description": "additionalProperties allows a schema which should validate", @@ -94,5 +113,21 @@ "valid": true } ] + }, + { + "description": "additionalProperties should not look in applicators", + "schema": { + "allOf": [ + {"properties": {"foo": {}}} + ], + "additionalProperties": {"type": "boolean"} + }, + "tests": [ + { + "description": "properties defined in allOf are not allowed", + "data": {"foo": 1, "bar": true}, + "valid": false + } + ] } ] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/allOf.json b/test/JSON-Schema-Test-Suite/tests/draft7/allOf.json similarity index 77% rename from test/JSON-Schema-Test-Suite/tests/draft4/allOf.json rename to test/JSON-Schema-Test-Suite/tests/draft7/allOf.json index bbb5f89..00c016c 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/allOf.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/allOf.json @@ -108,5 +108,38 @@ "valid": false } ] + }, + { + "description": "allOf with boolean schemas, all true", + "schema": {"allOf": [true, true]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "allOf with boolean schemas, some false", + "schema": {"allOf": [true, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "allOf with boolean schemas, all false", + "schema": {"allOf": [false, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] } ] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/anyOf.json b/test/JSON-Schema-Test-Suite/tests/draft7/anyOf.json similarity index 76% rename from test/JSON-Schema-Test-Suite/tests/draft4/anyOf.json rename to test/JSON-Schema-Test-Suite/tests/draft7/anyOf.json index 6c8b251..4d05a9e 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/anyOf.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/anyOf.json @@ -65,6 +65,39 @@ } ] }, + { + "description": "anyOf with boolean schemas, all true", + "schema": {"anyOf": [true, true]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "anyOf with boolean schemas, some true", + "schema": {"anyOf": [true, false]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "anyOf with boolean schemas, all false", + "schema": {"anyOf": [false, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, { "description": "anyOf complex types", "schema": { diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/boolean_schema.json b/test/JSON-Schema-Test-Suite/tests/draft7/boolean_schema.json new file mode 100644 index 0000000..6d40f23 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/boolean_schema.json @@ -0,0 +1,104 @@ +[ + { + "description": "boolean schema 'true'", + "schema": true, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "boolean schema 'false'", + "schema": false, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/const.json b/test/JSON-Schema-Test-Suite/tests/draft7/const.json new file mode 100644 index 0000000..0fe00f2 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/const.json @@ -0,0 +1,86 @@ +[ + { + "description": "const validation", + "schema": {"const": 2}, + "tests": [ + { + "description": "same value is valid", + "data": 2, + "valid": true + }, + { + "description": "another value is invalid", + "data": 5, + "valid": false + }, + { + "description": "another type is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "const with object", + "schema": {"const": {"foo": "bar", "baz": "bax"}}, + "tests": [ + { + "description": "same object is valid", + "data": {"foo": "bar", "baz": "bax"}, + "valid": true + }, + { + "description": "same object with different property order is valid", + "data": {"baz": "bax", "foo": "bar"}, + "valid": true + }, + { + "description": "another object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "another type is invalid", + "data": [1, 2], + "valid": false + } + ] + }, + { + "description": "const with array", + "schema": {"const": [{ "foo": "bar" }]}, + "tests": [ + { + "description": "same array is valid", + "data": [{"foo": "bar"}], + "valid": true + }, + { + "description": "another array item is invalid", + "data": [2], + "valid": false + }, + { + "description": "array with additional items is invalid", + "data": [1, 2, 3], + "valid": false + } + ] + }, + { + "description": "const with null", + "schema": {"const": null}, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "not null is invalid", + "data": 0, + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/contains.json b/test/JSON-Schema-Test-Suite/tests/draft7/contains.json new file mode 100644 index 0000000..b7ae5a2 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/contains.json @@ -0,0 +1,95 @@ +[ + { + "description": "contains keyword validation", + "schema": { + "contains": {"minimum": 5} + }, + "tests": [ + { + "description": "array with item matching schema (5) is valid", + "data": [3, 4, 5], + "valid": true + }, + { + "description": "array with item matching schema (6) is valid", + "data": [3, 4, 6], + "valid": true + }, + { + "description": "array with two items matching schema (5, 6) is valid", + "data": [3, 4, 5, 6], + "valid": true + }, + { + "description": "array without items matching schema is invalid", + "data": [2, 3, 4], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + }, + { + "description": "not array is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "contains keyword with const keyword", + "schema": { + "contains": { "const": 5 } + }, + "tests": [ + { + "description": "array with item 5 is valid", + "data": [3, 4, 5], + "valid": true + }, + { + "description": "array with two items 5 is valid", + "data": [3, 4, 5, 5], + "valid": true + }, + { + "description": "array without item 5 is invalid", + "data": [1, 2, 3, 4], + "valid": false + } + ] + }, + { + "description": "contains keyword with boolean schema true", + "schema": {"contains": true}, + "tests": [ + { + "description": "any non-empty array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "contains keyword with boolean schema false", + "schema": {"contains": false}, + "tests": [ + { + "description": "any non-empty array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/default.json b/test/JSON-Schema-Test-Suite/tests/draft7/default.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/default.json rename to test/JSON-Schema-Test-Suite/tests/draft7/default.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/definitions.json b/test/JSON-Schema-Test-Suite/tests/draft7/definitions.json similarity index 85% rename from test/JSON-Schema-Test-Suite/tests/draft4/definitions.json rename to test/JSON-Schema-Test-Suite/tests/draft7/definitions.json index cf935a3..4360406 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/definitions.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/definitions.json @@ -1,7 +1,7 @@ [ { "description": "valid definition", - "schema": {"$ref": "http://json-schema.org/draft-04/schema#"}, + "schema": {"$ref": "http://json-schema.org/draft-07/schema#"}, "tests": [ { "description": "valid definition schema", @@ -16,7 +16,7 @@ }, { "description": "invalid definition", - "schema": {"$ref": "http://json-schema.org/draft-04/schema#"}, + "schema": {"$ref": "http://json-schema.org/draft-07/schema#"}, "tests": [ { "description": "invalid definition schema", diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/dependencies.json b/test/JSON-Schema-Test-Suite/tests/draft7/dependencies.json similarity index 71% rename from test/JSON-Schema-Test-Suite/tests/draft4/dependencies.json rename to test/JSON-Schema-Test-Suite/tests/draft7/dependencies.json index 38effa1..80e552f 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/dependencies.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/dependencies.json @@ -42,6 +42,24 @@ } ] }, + { + "description": "dependencies with empty array", + "schema": { + "dependencies": {"bar": []} + }, + "tests": [ + { + "description": "empty object", + "data": {}, + "valid": true + }, + { + "description": "object with one property", + "data": {"bar": 2}, + "valid": true + } + ] + }, { "description": "multiple dependencies", "schema": { @@ -119,5 +137,36 @@ "valid": false } ] + }, + { + "description": "dependencies with boolean subschemas", + "schema": { + "dependencies": { + "foo": true, + "bar": false + } + }, + "tests": [ + { + "description": "object with property having schema true is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with property having schema false is invalid", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "object with both properties is invalid", + "data": {"foo": 1, "bar": 2}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] } ] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/enum.json b/test/JSON-Schema-Test-Suite/tests/draft7/enum.json similarity index 90% rename from test/JSON-Schema-Test-Suite/tests/draft4/enum.json rename to test/JSON-Schema-Test-Suite/tests/draft7/enum.json index f124436..8fb9d7a 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/enum.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/enum.json @@ -39,13 +39,13 @@ { "description": "enums in properties", "schema": { - "type":"object", - "properties": { - "foo": {"enum":["foo"]}, - "bar": {"enum":["bar"]} - }, - "required": ["bar"] - }, + "type":"object", + "properties": { + "foo": {"enum":["foo"]}, + "bar": {"enum":["bar"]} + }, + "required": ["bar"] + }, "tests": [ { "description": "both properties are valid", diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/exclusiveMaximum.json b/test/JSON-Schema-Test-Suite/tests/draft7/exclusiveMaximum.json new file mode 100644 index 0000000..dc3cd70 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/exclusiveMaximum.json @@ -0,0 +1,30 @@ +[ + { + "description": "exclusiveMaximum validation", + "schema": { + "exclusiveMaximum": 3.0 + }, + "tests": [ + { + "description": "below the exclusiveMaximum is valid", + "data": 2.2, + "valid": true + }, + { + "description": "boundary point is invalid", + "data": 3.0, + "valid": false + }, + { + "description": "above the exclusiveMaximum is invalid", + "data": 3.5, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/exclusiveMinimum.json b/test/JSON-Schema-Test-Suite/tests/draft7/exclusiveMinimum.json new file mode 100644 index 0000000..b38d7ec --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/exclusiveMinimum.json @@ -0,0 +1,30 @@ +[ + { + "description": "exclusiveMinimum validation", + "schema": { + "exclusiveMinimum": 1.1 + }, + "tests": [ + { + "description": "above the exclusiveMinimum is valid", + "data": 1.2, + "valid": true + }, + { + "description": "boundary point is invalid", + "data": 1.1, + "valid": false + }, + { + "description": "below the exclusiveMinimum is invalid", + "data": 0.6, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/if-then-else.json b/test/JSON-Schema-Test-Suite/tests/draft7/if-then-else.json new file mode 100644 index 0000000..37a229c --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/if-then-else.json @@ -0,0 +1,188 @@ +[ + { + "description": "ignore if without then or else", + "schema": { + "if": { + "const": 0 + } + }, + "tests": [ + { + "description": "valid when valid against lone if", + "data": 0, + "valid": true + }, + { + "description": "valid when invalid against lone if", + "data": "hello", + "valid": true + } + ] + }, + { + "description": "ignore then without if", + "schema": { + "then": { + "const": 0 + } + }, + "tests": [ + { + "description": "valid when valid against lone then", + "data": 0, + "valid": true + }, + { + "description": "valid when invalid against lone then", + "data": "hello", + "valid": true + } + ] + }, + { + "description": "ignore else without if", + "schema": { + "else": { + "const": 0 + } + }, + "tests": [ + { + "description": "valid when valid against lone else", + "data": 0, + "valid": true + }, + { + "description": "valid when invalid against lone else", + "data": "hello", + "valid": true + } + ] + }, + { + "description": "if and then without else", + "schema": { + "if": { + "exclusiveMaximum": 0 + }, + "then": { + "minimum": -10 + } + }, + "tests": [ + { + "description": "valid through then", + "data": -1, + "valid": true + }, + { + "description": "invalid through then", + "data": -100, + "valid": false + }, + { + "description": "valid when if test fails", + "data": 3, + "valid": true + } + ] + }, + { + "description": "if and else without then", + "schema": { + "if": { + "exclusiveMaximum": 0 + }, + "else": { + "multipleOf": 2 + } + }, + "tests": [ + { + "description": "valid when if test passes", + "data": -1, + "valid": true + }, + { + "description": "valid through else", + "data": 4, + "valid": true + }, + { + "description": "invalid through else", + "data": 3, + "valid": false + } + ] + }, + { + "description": "validate against correct branch, then vs else", + "schema": { + "if": { + "exclusiveMaximum": 0 + }, + "then": { + "minimum": -10 + }, + "else": { + "multipleOf": 2 + } + }, + "tests": [ + { + "description": "valid through then", + "data": -1, + "valid": true + }, + { + "description": "invalid through then", + "data": -100, + "valid": false + }, + { + "description": "valid through else", + "data": 4, + "valid": true + }, + { + "description": "invalid through else", + "data": 3, + "valid": false + } + ] + }, + { + "description": "non-interference across combined schemas", + "schema": { + "allOf": [ + { + "if": { + "exclusiveMaximum": 0 + } + }, + { + "then": { + "minimum": -10 + } + }, + { + "else": { + "multipleOf": 2 + } + } + ] + }, + "tests": [ + { + "description": "valid, but woud have been invalid through then", + "data": -100, + "valid": true + }, + { + "description": "valid, but would have been invalid through else", + "data": 3, + "valid": true + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/items.json b/test/JSON-Schema-Test-Suite/tests/draft7/items.json similarity index 58% rename from test/JSON-Schema-Test-Suite/tests/draft4/items.json rename to test/JSON-Schema-Test-Suite/tests/draft7/items.json index 6a4e648..13a6a11 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/items.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/items.json @@ -74,5 +74,60 @@ "valid": true } ] + }, + { + "description": "items with boolean schema (true)", + "schema": {"items": true}, + "tests": [ + { + "description": "any array is valid", + "data": [ 1, "foo", true ], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "items with boolean schema (false)", + "schema": {"items": false}, + "tests": [ + { + "description": "any non-empty array is invalid", + "data": [ 1, "foo", true ], + "valid": false + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "items with boolean schemas", + "schema": { + "items": [true, false] + }, + "tests": [ + { + "description": "array with one item is valid", + "data": [ 1 ], + "valid": true + }, + { + "description": "array with two items is invalid", + "data": [ 1, "foo" ], + "valid": false + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] } ] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/maxItems.json b/test/JSON-Schema-Test-Suite/tests/draft7/maxItems.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/maxItems.json rename to test/JSON-Schema-Test-Suite/tests/draft7/maxItems.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/maxLength.json b/test/JSON-Schema-Test-Suite/tests/draft7/maxLength.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/maxLength.json rename to test/JSON-Schema-Test-Suite/tests/draft7/maxLength.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/maxProperties.json b/test/JSON-Schema-Test-Suite/tests/draft7/maxProperties.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/maxProperties.json rename to test/JSON-Schema-Test-Suite/tests/draft7/maxProperties.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/maximum.json b/test/JSON-Schema-Test-Suite/tests/draft7/maximum.json new file mode 100644 index 0000000..8150984 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/maximum.json @@ -0,0 +1,28 @@ +[ + { + "description": "maximum validation", + "schema": {"maximum": 3.0}, + "tests": [ + { + "description": "below the maximum is valid", + "data": 2.6, + "valid": true + }, + { + "description": "boundary point is valid", + "data": 3.0, + "valid": true + }, + { + "description": "above the maximum is invalid", + "data": 3.5, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/minItems.json b/test/JSON-Schema-Test-Suite/tests/draft7/minItems.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/minItems.json rename to test/JSON-Schema-Test-Suite/tests/draft7/minItems.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/minLength.json b/test/JSON-Schema-Test-Suite/tests/draft7/minLength.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/minLength.json rename to test/JSON-Schema-Test-Suite/tests/draft7/minLength.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/minProperties.json b/test/JSON-Schema-Test-Suite/tests/draft7/minProperties.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/minProperties.json rename to test/JSON-Schema-Test-Suite/tests/draft7/minProperties.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/minimum.json b/test/JSON-Schema-Test-Suite/tests/draft7/minimum.json new file mode 100644 index 0000000..bd1e95b --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/minimum.json @@ -0,0 +1,28 @@ +[ + { + "description": "minimum validation", + "schema": {"minimum": 1.1}, + "tests": [ + { + "description": "above the minimum is valid", + "data": 2.6, + "valid": true + }, + { + "description": "boundary point is valid", + "data": 1.1, + "valid": true + }, + { + "description": "below the minimum is invalid", + "data": 0.6, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/multipleOf.json b/test/JSON-Schema-Test-Suite/tests/draft7/multipleOf.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/multipleOf.json rename to test/JSON-Schema-Test-Suite/tests/draft7/multipleOf.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/not.json b/test/JSON-Schema-Test-Suite/tests/draft7/not.json similarity index 80% rename from test/JSON-Schema-Test-Suite/tests/draft4/not.json rename to test/JSON-Schema-Test-Suite/tests/draft7/not.json index cbb7f46..98de0ed 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/not.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/not.json @@ -91,6 +91,27 @@ "valid": true } ] + }, + { + "description": "not with boolean schema true", + "schema": {"not": true}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "not with boolean schema false", + "schema": {"not": false}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] } - ] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/oneOf.json b/test/JSON-Schema-Test-Suite/tests/draft7/oneOf.json similarity index 69% rename from test/JSON-Schema-Test-Suite/tests/draft4/oneOf.json rename to test/JSON-Schema-Test-Suite/tests/draft7/oneOf.json index 3a03ded..bc4295c 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/oneOf.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/oneOf.json @@ -65,6 +65,50 @@ } ] }, + { + "description": "oneOf with boolean schemas, all true", + "schema": {"oneOf": [true, true, true]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf with boolean schemas, one true", + "schema": {"oneOf": [true, false, false]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "oneOf with boolean schemas, more than one true", + "schema": {"oneOf": [true, true, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf with boolean schemas, all false", + "schema": {"oneOf": [false, false, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, { "description": "oneOf complex types", "schema": { diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/optional/bignum.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/bignum.json similarity index 93% rename from test/JSON-Schema-Test-Suite/tests/draft4/optional/bignum.json rename to test/JSON-Schema-Test-Suite/tests/draft7/optional/bignum.json index ccc7c17..fac275e 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/optional/bignum.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/bignum.json @@ -68,8 +68,7 @@ { "description": "float comparison with high precision", "schema": { - "maximum": 972783798187987123879878123.18878137, - "exclusiveMaximum": true + "exclusiveMaximum": 972783798187987123879878123.18878137 }, "tests": [ { @@ -93,8 +92,7 @@ { "description": "float comparison with high precision on negative numbers", "schema": { - "minimum": -972783798187987123879878123.18878137, - "exclusiveMinimum": true + "exclusiveMinimum": -972783798187987123879878123.18878137 }, "tests": [ { diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/content.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/content.json new file mode 100644 index 0000000..3f5a743 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/content.json @@ -0,0 +1,77 @@ +[ + { + "description": "validation of string-encoded content based on media type", + "schema": { + "contentMediaType": "application/json" + }, + "tests": [ + { + "description": "a valid JSON document", + "data": "{\"foo\": \"bar\"}", + "valid": true + }, + { + "description": "an invalid JSON document", + "data": "{:}", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + } + ] + }, + { + "description": "validation of binary string-encoding", + "schema": { + "contentEncoding": "base64" + }, + "tests": [ + { + "description": "a valid base64 string", + "data": "eyJmb28iOiAiYmFyIn0K", + "valid": true + }, + { + "description": "an invalid base64 string (% is not a valid character)", + "data": "eyJmb28iOi%iYmFyIn0K", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + } + ] + }, + { + "description": "validation of binary-encoded media type documents", + "schema": { + "contentMediaType": "application/json", + "contentEncoding": "base64" + }, + "tests": [ + { + "description": "a valid base64-encoded JSON document", + "data": "eyJmb28iOiAiYmFyIn0K", + "valid": true + }, + { + "description": "a validly-encoded invalid JSON document", + "data": "ezp9Cg==", + "valid": false + }, + { + "description": "an invalid base64 string that is valid JSON", + "data": "{}", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/optional/ecmascript-regex.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/ecmascript-regex.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/optional/ecmascript-regex.json rename to test/JSON-Schema-Test-Suite/tests/draft7/optional/ecmascript-regex.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/date-time.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/date-time.json new file mode 100644 index 0000000..dfccee6 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/date-time.json @@ -0,0 +1,53 @@ +[ + { + "description": "validation of date-time strings", + "schema": {"format": "date-time"}, + "tests": [ + { + "description": "a valid date-time string", + "data": "1963-06-19T08:30:06.283185Z", + "valid": true + }, + { + "description": "a valid date-time string without second fraction", + "data": "1963-06-19T08:30:06Z", + "valid": true + }, + { + "description": "a valid date-time string with plus offset", + "data": "1937-01-01T12:00:27.87+00:20", + "valid": true + }, + { + "description": "a valid date-time string with minus offset", + "data": "1990-12-31T15:59:50.123-08:00", + "valid": true + }, + { + "description": "a invalid day in date-time string", + "data": "1990-02-31T15:59:60.123-08:00", + "valid": false + }, + { + "description": "an invalid offset in date-time string", + "data": "1990-12-31T15:59:60-24:00", + "valid": false + }, + { + "description": "an invalid date-time string", + "data": "06/19/1963 08:30:06 PST", + "valid": false + }, + { + "description": "case-insensitive T and Z", + "data": "1963-06-19t08:30:06.283185z", + "valid": true + }, + { + "description": "only RFC3339 not all of ISO 8601 are valid", + "data": "2013-350T01:01:01", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/date.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/date.json new file mode 100644 index 0000000..cd23baa --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/date.json @@ -0,0 +1,23 @@ +[ + { + "description": "validation of date strings", + "schema": {"format": "date"}, + "tests": [ + { + "description": "a valid date string", + "data": "1963-06-19", + "valid": true + }, + { + "description": "an invalid date-time string", + "data": "06/19/1963", + "valid": false + }, + { + "description": "only RFC3339 not all of ISO 8601 are valid", + "data": "2013-350", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/email.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/email.json new file mode 100644 index 0000000..c837c84 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/email.json @@ -0,0 +1,18 @@ +[ + { + "description": "validation of e-mail addresses", + "schema": {"format": "email"}, + "tests": [ + { + "description": "a valid e-mail address", + "data": "joe.bloggs@example.com", + "valid": true + }, + { + "description": "an invalid e-mail address", + "data": "2962", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/hostname.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/hostname.json new file mode 100644 index 0000000..d22e57d --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/hostname.json @@ -0,0 +1,33 @@ +[ + { + "description": "validation of host names", + "schema": {"format": "hostname"}, + "tests": [ + { + "description": "a valid host name", + "data": "www.example.com", + "valid": true + }, + { + "description": "a valid punycoded IDN hostname", + "data": "xn--4gbwdl.xn--wgbh1c", + "valid": true + }, + { + "description": "a host name starting with an illegal character", + "data": "-a-host-name-that-starts-with--", + "valid": false + }, + { + "description": "a host name containing illegal characters", + "data": "not_a_valid_host_name", + "valid": false + }, + { + "description": "a host name with a component too long", + "data": "a-vvvvvvvvvvvvvvvveeeeeeeeeeeeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyyyyy-long-host-name-component", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-email.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-email.json new file mode 100644 index 0000000..637409e --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-email.json @@ -0,0 +1,18 @@ +[ + { + "description": "validation of an internationalized e-mail addresses", + "schema": {"format": "idn-email"}, + "tests": [ + { + "description": "a valid idn e-mail (example@example.test in Hangul)", + "data": "실례@실례.테스트", + "valid": true + }, + { + "description": "an invalid idn e-mail address", + "data": "2962", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-hostname.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-hostname.json new file mode 100644 index 0000000..3291820 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-hostname.json @@ -0,0 +1,28 @@ +[ + { + "description": "validation of internationalized host names", + "schema": {"format": "idn-hostname"}, + "tests": [ + { + "description": "a valid host name (example.test in Hangul)", + "data": "실례.테스트", + "valid": true + }, + { + "description": "illegal first char U+302E Hangul single dot tone mark", + "data": "〮실례.테스트", + "valid": false + }, + { + "description": "contains illegal char U+302E Hangul single dot tone mark", + "data": "실〮례.테스트", + "valid": false + }, + { + "description": "a host name with a component too long", + "data": "실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실례례테스트례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례테스트례례실례.테스트", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv4.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv4.json new file mode 100644 index 0000000..661148a --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv4.json @@ -0,0 +1,33 @@ +[ + { + "description": "validation of IP addresses", + "schema": {"format": "ipv4"}, + "tests": [ + { + "description": "a valid IP address", + "data": "192.168.0.1", + "valid": true + }, + { + "description": "an IP address with too many components", + "data": "127.0.0.0.1", + "valid": false + }, + { + "description": "an IP address with out-of-range values", + "data": "256.256.256.256", + "valid": false + }, + { + "description": "an IP address without 4 components", + "data": "127.0", + "valid": false + }, + { + "description": "an IP address as an integer", + "data": "0x7f000001", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv6.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv6.json new file mode 100644 index 0000000..f67559b --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv6.json @@ -0,0 +1,28 @@ +[ + { + "description": "validation of IPv6 addresses", + "schema": {"format": "ipv6"}, + "tests": [ + { + "description": "a valid IPv6 address", + "data": "::1", + "valid": true + }, + { + "description": "an IPv6 address with out-of-range values", + "data": "12345::", + "valid": false + }, + { + "description": "an IPv6 address with too many components", + "data": "1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1", + "valid": false + }, + { + "description": "an IPv6 address containing illegal characters", + "data": "::laptop", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri-reference.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri-reference.json new file mode 100644 index 0000000..1fd779c --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri-reference.json @@ -0,0 +1,43 @@ +[ + { + "description": "validation of IRI References", + "schema": {"format": "iri-reference"}, + "tests": [ + { + "description": "a valid IRI", + "data": "http://ƒøø.ßår/?∂éœ=πîx#πîüx", + "valid": true + }, + { + "description": "a valid protocol-relative IRI Reference", + "data": "//ƒøø.ßår/?∂éœ=πîx#πîüx", + "valid": true + }, + { + "description": "a valid relative IRI Reference", + "data": "/âππ", + "valid": true + }, + { + "description": "an invalid IRI Reference", + "data": "\\\\WINDOWS\\filëßåré", + "valid": false + }, + { + "description": "a valid IRI Reference", + "data": "âππ", + "valid": true + }, + { + "description": "a valid IRI fragment", + "data": "#ƒrägmênt", + "valid": true + }, + { + "description": "an invalid IRI fragment", + "data": "#ƒräg\\mênt", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri.json new file mode 100644 index 0000000..ed54094 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri.json @@ -0,0 +1,53 @@ +[ + { + "description": "validation of IRIs", + "schema": {"format": "iri"}, + "tests": [ + { + "description": "a valid IRI with anchor tag", + "data": "http://ƒøø.ßår/?∂éœ=πîx#πîüx", + "valid": true + }, + { + "description": "a valid IRI with anchor tag and parantheses", + "data": "http://ƒøø.com/blah_(wîkïpédiå)_blah#ßité-1", + "valid": true + }, + { + "description": "a valid IRI with URL-encoded stuff", + "data": "http://ƒøø.ßår/?q=Test%20URL-encoded%20stuff", + "valid": true + }, + { + "description": "a valid IRI with many special characters", + "data": "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", + "valid": true + }, + { + "description": "a valid IRI based on IPv6", + "data": "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + "valid": true + }, + { + "description": "an invalid IRI based on IPv6", + "data": "http://2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "valid": false + }, + { + "description": "an invalid relative IRI Reference", + "data": "/abc", + "valid": false + }, + { + "description": "an invalid IRI", + "data": "\\\\WINDOWS\\filëßåré", + "valid": false + }, + { + "description": "an invalid IRI though valid IRI reference", + "data": "âππ", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/json-pointer.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/json-pointer.json new file mode 100644 index 0000000..65c2f06 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/json-pointer.json @@ -0,0 +1,168 @@ +[ + { + "description": "validation of JSON-pointers (JSON String Representation)", + "schema": {"format": "json-pointer"}, + "tests": [ + { + "description": "a valid JSON-pointer", + "data": "/foo/bar~0/baz~1/%a", + "valid": true + }, + { + "description": "not a valid JSON-pointer (~ not escaped)", + "data": "/foo/bar~", + "valid": false + }, + { + "description": "valid JSON-pointer with empty segment", + "data": "/foo//bar", + "valid": true + }, + { + "description": "valid JSON-pointer with the last empty segment", + "data": "/foo/bar/", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #1", + "data": "", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #2", + "data": "/foo", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #3", + "data": "/foo/0", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #4", + "data": "/", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #5", + "data": "/a~1b", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #6", + "data": "/c%d", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #7", + "data": "/e^f", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #8", + "data": "/g|h", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #9", + "data": "/i\\j", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #10", + "data": "/k\"l", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #11", + "data": "/ ", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #12", + "data": "/m~0n", + "valid": true + }, + { + "description": "valid JSON-pointer used adding to the last array position", + "data": "/foo/-", + "valid": true + }, + { + "description": "valid JSON-pointer (- used as object member name)", + "data": "/foo/-/bar", + "valid": true + }, + { + "description": "valid JSON-pointer (multiple escaped characters)", + "data": "/~1~0~0~1~1", + "valid": true + }, + { + "description": "valid JSON-pointer (escaped with fraction part) #1", + "data": "/~1.1", + "valid": true + }, + { + "description": "valid JSON-pointer (escaped with fraction part) #2", + "data": "/~0.1", + "valid": true + }, + { + "description": "not a valid JSON-pointer (URI Fragment Identifier) #1", + "data": "#", + "valid": false + }, + { + "description": "not a valid JSON-pointer (URI Fragment Identifier) #2", + "data": "#/", + "valid": false + }, + { + "description": "not a valid JSON-pointer (URI Fragment Identifier) #3", + "data": "#a", + "valid": false + }, + { + "description": "not a valid JSON-pointer (some escaped, but not all) #1", + "data": "/~0~", + "valid": false + }, + { + "description": "not a valid JSON-pointer (some escaped, but not all) #2", + "data": "/~0/~", + "valid": false + }, + { + "description": "not a valid JSON-pointer (wrong escape character) #1", + "data": "/~2", + "valid": false + }, + { + "description": "not a valid JSON-pointer (wrong escape character) #2", + "data": "/~-1", + "valid": false + }, + { + "description": "not a valid JSON-pointer (multiple characters not escaped)", + "data": "/~~", + "valid": false + }, + { + "description": "not a valid JSON-pointer (isn't empty nor starts with /) #1", + "data": "a", + "valid": false + }, + { + "description": "not a valid JSON-pointer (isn't empty nor starts with /) #2", + "data": "0", + "valid": false + }, + { + "description": "not a valid JSON-pointer (isn't empty nor starts with /) #3", + "data": "a/a", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/regex.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/regex.json new file mode 100644 index 0000000..d99d021 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/regex.json @@ -0,0 +1,18 @@ +[ + { + "description": "validation of regular expressions", + "schema": {"format": "regex"}, + "tests": [ + { + "description": "a valid regular expression", + "data": "([abc])+\\s+$", + "valid": true + }, + { + "description": "a regular expression with unclosed parens is invalid", + "data": "^(abc]", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/relative-json-pointer.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/relative-json-pointer.json new file mode 100644 index 0000000..ceeb743 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/relative-json-pointer.json @@ -0,0 +1,33 @@ +[ + { + "description": "validation of Relative JSON Pointers (RJP)", + "schema": {"format": "relative-json-pointer"}, + "tests": [ + { + "description": "a valid upwards RJP", + "data": "1", + "valid": true + }, + { + "description": "a valid downwards RJP", + "data": "0/foo/bar", + "valid": true + }, + { + "description": "a valid up and then down RJP, with array index", + "data": "2/0/baz/1/zip", + "valid": true + }, + { + "description": "a valid RJP taking the member or index name", + "data": "0#", + "valid": true + }, + { + "description": "an invalid RJP that is a valid JSON Pointer", + "data": "/foo/bar", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/time.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/time.json new file mode 100644 index 0000000..4ec8a01 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/time.json @@ -0,0 +1,23 @@ +[ + { + "description": "validation of time strings", + "schema": {"format": "time"}, + "tests": [ + { + "description": "a valid time string", + "data": "08:30:06.283185Z", + "valid": true + }, + { + "description": "an invalid time string", + "data": "08:30:06 PST", + "valid": false + }, + { + "description": "only RFC3339 not all of ISO 8601 are valid", + "data": "01:01:01,1111", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-reference.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-reference.json new file mode 100644 index 0000000..e4c9eef --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-reference.json @@ -0,0 +1,43 @@ +[ + { + "description": "validation of URI References", + "schema": {"format": "uri-reference"}, + "tests": [ + { + "description": "a valid URI", + "data": "http://foo.bar/?baz=qux#quux", + "valid": true + }, + { + "description": "a valid protocol-relative URI Reference", + "data": "//foo.bar/?baz=qux#quux", + "valid": true + }, + { + "description": "a valid relative URI Reference", + "data": "/abc", + "valid": true + }, + { + "description": "an invalid URI Reference", + "data": "\\\\WINDOWS\\fileshare", + "valid": false + }, + { + "description": "a valid URI Reference", + "data": "abc", + "valid": true + }, + { + "description": "a valid URI fragment", + "data": "#fragment", + "valid": true + }, + { + "description": "an invalid URI fragment", + "data": "#frag\\ment", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-template.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-template.json new file mode 100644 index 0000000..d8396a5 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-template.json @@ -0,0 +1,30 @@ +[ + { + "description": "format: uri-template", + "schema": { + "format": "uri-template" + }, + "tests": [ + { + "description": "a valid uri-template", + "data": "http://example.com/dictionary/{term:1}/{term}", + "valid": true + }, + { + "description": "an invalid uri-template", + "data": "http://example.com/dictionary/{term:1}/{term", + "valid": false + }, + { + "description": "a valid uri-template without variables", + "data": "http://example.com/dictionary", + "valid": true + }, + { + "description": "a valid relative uri-template", + "data": "dictionary/{term:1}/{term}", + "valid": true + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri.json new file mode 100644 index 0000000..25cc40c --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri.json @@ -0,0 +1,103 @@ +[ + { + "description": "validation of URIs", + "schema": {"format": "uri"}, + "tests": [ + { + "description": "a valid URL with anchor tag", + "data": "http://foo.bar/?baz=qux#quux", + "valid": true + }, + { + "description": "a valid URL with anchor tag and parantheses", + "data": "http://foo.com/blah_(wikipedia)_blah#cite-1", + "valid": true + }, + { + "description": "a valid URL with URL-encoded stuff", + "data": "http://foo.bar/?q=Test%20URL-encoded%20stuff", + "valid": true + }, + { + "description": "a valid puny-coded URL ", + "data": "http://xn--nw2a.xn--j6w193g/", + "valid": true + }, + { + "description": "a valid URL with many special characters", + "data": "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", + "valid": true + }, + { + "description": "a valid URL based on IPv4", + "data": "http://223.255.255.254", + "valid": true + }, + { + "description": "a valid URL with ftp scheme", + "data": "ftp://ftp.is.co.za/rfc/rfc1808.txt", + "valid": true + }, + { + "description": "a valid URL for a simple text file", + "data": "http://www.ietf.org/rfc/rfc2396.txt", + "valid": true + }, + { + "description": "a valid URL ", + "data": "ldap://[2001:db8::7]/c=GB?objectClass?one", + "valid": true + }, + { + "description": "a valid mailto URI", + "data": "mailto:John.Doe@example.com", + "valid": true + }, + { + "description": "a valid newsgroup URI", + "data": "news:comp.infosystems.www.servers.unix", + "valid": true + }, + { + "description": "a valid tel URI", + "data": "tel:+1-816-555-1212", + "valid": true + }, + { + "description": "a valid URN", + "data": "urn:oasis:names:specification:docbook:dtd:xml:4.1.2", + "valid": true + }, + { + "description": "an invalid protocol-relative URI Reference", + "data": "//foo.bar/?baz=qux#quux", + "valid": false + }, + { + "description": "an invalid relative URI Reference", + "data": "/abc", + "valid": false + }, + { + "description": "an invalid URI", + "data": "\\\\WINDOWS\\fileshare", + "valid": false + }, + { + "description": "an invalid URI though valid URI reference", + "data": "abc", + "valid": false + }, + { + "description": "an invalid URI with spaces", + "data": "http:// shouldfail.com", + "valid": false + }, + { + "description": "an invalid URI with spaces and missing scheme", + "data": ":// should fail", + "valid": false + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/optional/zeroTerminatedFloats.json b/test/JSON-Schema-Test-Suite/tests/draft7/optional/zeroTerminatedFloats.json similarity index 68% rename from test/JSON-Schema-Test-Suite/tests/draft4/optional/zeroTerminatedFloats.json rename to test/JSON-Schema-Test-Suite/tests/draft7/optional/zeroTerminatedFloats.json index 9b50ea2..1bcdf96 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/optional/zeroTerminatedFloats.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/optional/zeroTerminatedFloats.json @@ -6,9 +6,9 @@ }, "tests": [ { - "description": "a float is not an integer even without fractional part", + "description": "a float without fractional part is an integer", "data": 1.0, - "valid": false + "valid": true } ] } diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/pattern.json b/test/JSON-Schema-Test-Suite/tests/draft7/pattern.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/pattern.json rename to test/JSON-Schema-Test-Suite/tests/draft7/pattern.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/patternProperties.json b/test/JSON-Schema-Test-Suite/tests/draft7/patternProperties.json similarity index 78% rename from test/JSON-Schema-Test-Suite/tests/draft4/patternProperties.json rename to test/JSON-Schema-Test-Suite/tests/draft7/patternProperties.json index 5f741df..1d04a16 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/patternProperties.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/patternProperties.json @@ -30,12 +30,12 @@ }, { "description": "ignores arrays", - "data": [], + "data": ["foo"], "valid": true }, { "description": "ignores strings", - "data": "", + "data": "foo", "valid": true }, { @@ -116,5 +116,36 @@ "valid": false } ] + }, + { + "description": "patternProperties with boolean schemas", + "schema": { + "patternProperties": { + "f.*": true, + "b.*": false + } + }, + "tests": [ + { + "description": "object with property matching schema true is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with property matching schema false is invalid", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "object with both properties is invalid", + "data": {"foo": 1, "bar": 2}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] } ] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/properties.json b/test/JSON-Schema-Test-Suite/tests/draft7/properties.json similarity index 77% rename from test/JSON-Schema-Test-Suite/tests/draft4/properties.json rename to test/JSON-Schema-Test-Suite/tests/draft7/properties.json index a830c67..c8ad719 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/properties.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/properties.json @@ -93,5 +93,36 @@ "valid": false } ] + }, + { + "description": "properties with boolean schema", + "schema": { + "properties": { + "foo": true, + "bar": false + } + }, + "tests": [ + { + "description": "no property present is valid", + "data": {}, + "valid": true + }, + { + "description": "only 'true' property present is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "only 'false' property present is invalid", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "both properties present is invalid", + "data": {"foo": 1, "bar": 2}, + "valid": false + } + ] } ] diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/propertyNames.json b/test/JSON-Schema-Test-Suite/tests/draft7/propertyNames.json new file mode 100644 index 0000000..8423690 --- /dev/null +++ b/test/JSON-Schema-Test-Suite/tests/draft7/propertyNames.json @@ -0,0 +1,78 @@ +[ + { + "description": "propertyNames validation", + "schema": { + "propertyNames": {"maxLength": 3} + }, + "tests": [ + { + "description": "all property names valid", + "data": { + "f": {}, + "foo": {} + }, + "valid": true + }, + { + "description": "some property names invalid", + "data": { + "foo": {}, + "foobar": {} + }, + "valid": false + }, + { + "description": "object without properties is valid", + "data": {}, + "valid": true + }, + { + "description": "ignores arrays", + "data": [1, 2, 3, 4], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobar", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "propertyNames with boolean schema true", + "schema": {"propertyNames": true}, + "tests": [ + { + "description": "object with any properties is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames with boolean schema false", + "schema": {"propertyNames": false}, + "tests": [ + { + "description": "object with any properties is invalid", + "data": {"foo": 1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + } +] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/ref.json b/test/JSON-Schema-Test-Suite/tests/draft7/ref.json similarity index 89% rename from test/JSON-Schema-Test-Suite/tests/draft4/ref.json rename to test/JSON-Schema-Test-Suite/tests/draft7/ref.json index b43a277..7579507 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/ref.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/ref.json @@ -175,7 +175,7 @@ }, { "description": "remote ref, containing refs itself", - "schema": {"$ref": "http://json-schema.org/draft-04/schema#"}, + "schema": {"$ref": "http://json-schema.org/draft-07/schema#"}, "tests": [ { "description": "remote ref valid", @@ -209,10 +209,42 @@ } ] }, + { + "description": "$ref to boolean schema true", + "schema": { + "$ref": "#/definitions/bool", + "definitions": { + "bool": true + } + }, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "$ref to boolean schema false", + "schema": { + "$ref": "#/definitions/bool", + "definitions": { + "bool": false + } + }, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, { "description": "Recursive references between schemas", "schema": { - "id": "http://localhost:1234/tree", + "$id": "http://localhost:1234/tree", "description": "tree of nodes", "type": "object", "properties": { @@ -225,7 +257,7 @@ "required": ["meta", "nodes"], "definitions": { "node": { - "id": "http://localhost:1234/node", + "$id": "http://localhost:1234/node", "description": "node", "type": "object", "properties": { @@ -239,7 +271,7 @@ "tests": [ { "description": "valid tree", - "data": { + "data": { "meta": "root", "nodes": [ { @@ -268,7 +300,7 @@ }, { "description": "invalid tree", - "data": { + "data": { "meta": "root", "nodes": [ { diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/refRemote.json b/test/JSON-Schema-Test-Suite/tests/draft7/refRemote.json similarity index 92% rename from test/JSON-Schema-Test-Suite/tests/draft4/refRemote.json rename to test/JSON-Schema-Test-Suite/tests/draft7/refRemote.json index 8611fad..819d326 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/refRemote.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/refRemote.json @@ -52,9 +52,9 @@ { "description": "base URI change", "schema": { - "id": "http://localhost:1234/", + "$id": "http://localhost:1234/", "items": { - "id": "folder/", + "$id": "folder/", "items": {"$ref": "folderInteger.json"} } }, @@ -74,14 +74,14 @@ { "description": "base URI change - change folder", "schema": { - "id": "http://localhost:1234/scope_change_defs1.json", + "$id": "http://localhost:1234/scope_change_defs1.json", "type" : "object", "properties": { "list": {"$ref": "#/definitions/baz"} }, "definitions": { "baz": { - "id": "folder/", + "$id": "folder/", "type": "array", "items": {"$ref": "folderInteger.json"} } @@ -103,14 +103,14 @@ { "description": "base URI change - change folder in subschema", "schema": { - "id": "http://localhost:1234/scope_change_defs2.json", + "$id": "http://localhost:1234/scope_change_defs2.json", "type" : "object", "properties": { "list": {"$ref": "#/definitions/baz/definitions/bar"} }, "definitions": { "baz": { - "id": "folder/", + "$id": "folder/", "definitions": { "bar": { "type": "array", @@ -136,7 +136,7 @@ { "description": "root ref in remote ref", "schema": { - "id": "http://localhost:1234/object", + "$id": "http://localhost:1234/object", "type": "object", "properties": { "name": {"$ref": "name.json#/definitions/orNull"} diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/required.json b/test/JSON-Schema-Test-Suite/tests/draft7/required.json similarity index 78% rename from test/JSON-Schema-Test-Suite/tests/draft4/required.json rename to test/JSON-Schema-Test-Suite/tests/draft7/required.json index 1e2a4f0..bd96907 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft4/required.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/required.json @@ -50,5 +50,21 @@ "valid": true } ] + }, + { + "description": "required with empty array", + "schema": { + "properties": { + "foo": {} + }, + "required": [] + }, + "tests": [ + { + "description": "property not required", + "data": {}, + "valid": true + } + ] } ] diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/type.json b/test/JSON-Schema-Test-Suite/tests/draft7/type.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/type.json rename to test/JSON-Schema-Test-Suite/tests/draft7/type.json diff --git a/test/JSON-Schema-Test-Suite/tests/draft4/uniqueItems.json b/test/JSON-Schema-Test-Suite/tests/draft7/uniqueItems.json similarity index 100% rename from test/JSON-Schema-Test-Suite/tests/draft4/uniqueItems.json rename to test/JSON-Schema-Test-Suite/tests/draft7/uniqueItems.json diff --git a/test/id-ref.cpp b/test/id-ref.cpp new file mode 100644 index 0000000..339a3c2 --- /dev/null +++ b/test/id-ref.cpp @@ -0,0 +1,146 @@ +#include + +#include "../src/json-schema.hpp" + +#include + +using nlohmann::json; + +auto schema_draft = R"( + { + "$id": "http://example.com/root.json", + "definitions": { + "A": { "$id": "#foo" }, + "B": { + "$id": "other.json", + "definitions": { + "X": { "$id": "#bar" }, + "Y": { "$id": "t/inner.json" } + } + }, + "C": { + + "$id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f", + "definitions": { + "Z": { "$id": "#bar" }, + "9": { "$id": "http://example.com/drole.json" } + } + } + } + } +)"_json; + +/* # (document root) + + http://example.com/root.json + http://example.com/root.json# + + #/definitions/A + + http://example.com/root.json#foo + http://example.com/root.json#/definitions/A + + #/definitions/B + + http://example.com/other.json + http://example.com/other.json# + http://example.com/root.json#/definitions/B + + #/definitions/B/definitions/X + + http://example.com/other.json#bar + http://example.com/other.json#/definitions/X + http://example.com/root.json#/definitions/B/definitions/X + + #/definitions/B/definitions/Y + + http://example.com/t/inner.json + http://example.com/t/inner.json# + http://example.com/other.json#/definitions/Y + http://example.com/root.json#/definitions/B/definitions/Y + + #/definitions/C + + urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f + urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f# + http://example.com/root.json#/definitions/C + */ + +auto schema = R"( +{ + "id": "http://localhost:1234/scope_change_defs2.json", + "type" : "object", + "properties": { + "list": {"$ref": "#/definitions/baz/definitions/bar"} + }, + "definitions": { + "baz": { + "id": "folder/", + "definitions": { + "bar": { + "type": "array", + "items": {"$ref": "folderInteger.json"} + } + } + } + } +})"_json; + +class json_schema_validator +{ +public: + std::vector schemas_; + std::map schema_store_; + +public: + void insert_schema(json &s, std::vector base_uris) + { + auto id = s.find("$id"); + if (id != s.end()) + base_uris.push_back(base_uris.back().derive(id.value())); + + for (auto &u : base_uris) + schema_store_[u] = &s; + + for (auto i = s.begin(); + i != s.end(); + ++i) { + + switch (i.value().type()) { + case json::value_t::object: { // child is object, thus a schema + std::vector subschema_uri = base_uris; + + for (auto &ss : subschema_uri) + ss = ss.append(nlohmann::json_uri::escape(i.key())); + + insert_schema(i.value(), subschema_uri); + } break; + + case json::value_t::string: + // this schema is a reference + if (i.key() == "$ref") { + auto id = base_uris.back().derive(i.value()); + i.value() = id.to_string(); + } + + break; + + default: + break; + } + } + } +}; + +int main(void) +{ + json_schema_validator store; + + store.insert_schema(schema_draft, {{"#"}}); + + for (auto &i : store.schema_store_) { + std::cerr << i.first << " " << *i.second << "\n"; + } + + return 0; +} diff --git a/test/id.schema.json b/test/id.schema.json deleted file mode 100644 index dda0e09..0000000 --- a/test/id.schema.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "id": "http://example.com/root.json", - "definitions": { - "A": { "id": "#foo" }, - "B": { - "id": "other.json", - "definitions": { - "X": { "id": "#bar" }, - "Y": { "id": "t/inner.json" } - } - }, - "C": { - "id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f" - }, - "single": { - "id": "#item", - "type": "integer" - }, - "a": {"type": "integer"}, - "b": {"$ref": "#/definitions/a"}, - "c": {"$ref": "#/definitions/b"}, - "remote": { "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" } - }, - - "items": { - "type": "array", - "items": [ { "$ref": "#item" }, { "$ref": "other.json#bar" } ] - } -} diff --git a/test/issue-9/CMakeLists.txt b/test/issue-9/CMakeLists.txt new file mode 100644 index 0000000..41b56a2 --- /dev/null +++ b/test/issue-9/CMakeLists.txt @@ -0,0 +1,3 @@ +add_test_simple_schema(Issue::9 + ${CMAKE_CURRENT_SOURCE_DIR}/base.json + ${CMAKE_CURRENT_SOURCE_DIR}/instance.json) diff --git a/test/issue-9/bar.json b/test/issue-9/bar.json new file mode 100644 index 0000000..a9b8dbc --- /dev/null +++ b/test/issue-9/bar.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes bar", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/test/issue-9/base.json b/test/issue-9/base.json new file mode 100644 index 0000000..8faa877 --- /dev/null +++ b/test/issue-9/base.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes foo", + "type": "object", + "allOf": [ + { "$ref": "bar.json" }, + { "$ref": "foo/foo.json" } + ] +} diff --git a/test/issue-9/foo/baz/baz.json b/test/issue-9/foo/baz/baz.json new file mode 100644 index 0000000..1ed89d2 --- /dev/null +++ b/test/issue-9/foo/baz/baz.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes baz", + "$ref": "qux/qux.json" +} diff --git a/test/issue-9/foo/baz/qux/qux.json b/test/issue-9/foo/baz/qux/qux.json new file mode 100644 index 0000000..707d062 --- /dev/null +++ b/test/issue-9/foo/baz/qux/qux.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes qux", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/test/issue-9/foo/foo.json b/test/issue-9/foo/foo.json new file mode 100644 index 0000000..298f9c8 --- /dev/null +++ b/test/issue-9/foo/foo.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes foo", + "$ref": "baz/baz.json" +} diff --git a/test/issue-9/instance.json b/test/issue-9/instance.json new file mode 100644 index 0000000..1193e8e --- /dev/null +++ b/test/issue-9/instance.json @@ -0,0 +1,3 @@ +{ + "name": "name" +} diff --git a/test/test-schema.sh b/test/test-schema.sh deleted file mode 100755 index cec4a18..0000000 --- a/test/test-schema.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -PWD=$(realpath `dirname $0`/../JSON-Schema-Test-Suite/tests/draft4) - -TESTS=`find $PWD | grep json$` - -FAILCOUNT=0 - -for T in $TESTS -do - ./json-schema-test < $T - FAILCOUNT=$(($FAILCOUNT + $?)) -done - -echo $FAILCOUNT tests failed - diff --git a/test/uri.cpp b/test/uri.cpp new file mode 100644 index 0000000..9cb1c21 --- /dev/null +++ b/test/uri.cpp @@ -0,0 +1,156 @@ +/* + * JSON schema validator for JSON for modern C++ + * + * Copyright (c) 2016-2019 Patrick Boettcher . + * + * SPDX-License-Identifier: MIT + * + */ +#include +#include + +#include + +using nlohmann::json; +using nlohmann::json_uri; + +static int errors; + +#define EXPECT_EQ(a, b) \ + do { \ + if (a.to_string() != b) { \ + std::cerr << "Failed: '" << a << "' != '" << b << "'\n"; \ + errors++; \ + } \ + } while (0) + +// test-schema taken from spec with modifications +auto schema = R"( +{ + "$id": "http://example.com/root.json", + "definitions": { + "A": { "$id": "#foo" }, + "B": { + "$id": "other.json", + "definitions": { + "X": { "$id": "#bar" }, + "Y": { "$id": "t/inner.json" } + } + }, + "C": { + "$id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f", + "definitions": { + "Z": { "$id": "#bar" }, + "9": { "$id": "http://example.com/drole.json" } + } + } + } +} +)"_json; + +// resolve all refs +class store +{ +public: + std::vector schemas_; + std::map schema_store_; + +public: + void insert_schema(json &s, std::vector base_uris) + { + auto id = s.find("$id"); + if (id != s.end()) + base_uris.push_back(base_uris.back().derive(id.value())); + + for (auto &u : base_uris) + schema_store_[u] = &s; + + for (auto i = s.begin(); + i != s.end(); + ++i) { + + switch (i.value().type()) { + case json::value_t::object: { // child is object, thus a schema + std::vector subschema_uri = base_uris; + + for (auto &ss : subschema_uri) + ss = ss.append(nlohmann::json_uri::escape(i.key())); + + insert_schema(i.value(), subschema_uri); + } break; + + case json::value_t::string: + // this schema is a reference + if (i.key() == "$ref") { + auto id = base_uris.back().derive(i.value()); + i.value() = id.to_string(); + } + + break; + + default: + break; + } + } + } +}; + +//static void store_test(void) +//{ +// store s; +// +// s.insert_schema(schema, {{""}}); +// +// for (auto &i : s.schema_store_) { +// std::cerr << i.first << " " << *i.second << "\n"; +// } +//} + +static void paths(json_uri start, + const std::string &full, + const std::string &full_path, + const std::string &no_path) +{ + EXPECT_EQ(start, full + " # "); + + auto a = start.derive("other.json"); + EXPECT_EQ(a, full_path + "/other.json # "); + + auto b = a.derive("base.json"); + EXPECT_EQ(b, full_path + "/base.json # "); + + auto c = b.derive("subdir/base.json"); + EXPECT_EQ(c, full_path + "/subdir/base.json # "); + + auto d = c.derive("subdir2/base.json"); + EXPECT_EQ(d, full_path + "/subdir/subdir2/base.json # "); + + auto e = c.derive("/subdir2/base.json"); + EXPECT_EQ(e, no_path + "/subdir2/base.json # "); + + auto f = c.derive("new.json"); + EXPECT_EQ(f, full_path + "/subdir/new.json # "); + + auto g = c.derive("/new.json"); + EXPECT_EQ(g, no_path + "/new.json # "); +} + +int main(void) +{ + json_uri empty(""); + paths(empty, + "", + "", + ""); + + json_uri http("http://json-schema.org/draft-07/schema#"); + paths(http, + "http://json-schema.org/draft-07/schema", + "http://json-schema.org/draft-07", + "http://json-schema.org"); + + // plain name fragments instead of JSON-pointers, are not supported, yet + //store_test(); + + return errors; +}