validator: rework everything to handle local and remote refs
Added json-uri and json-pointer-classes and a schema-preparation-pass.
This commit is contained in:
parent
43bd5b8e7d
commit
b2240084fe
@ -21,15 +21,18 @@ if(NOT TARGET json-hpp)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# and one for the validator
|
# and one for the validator
|
||||||
add_library(json-schema-validator INTERFACE)
|
add_library(json-schema-validator
|
||||||
|
src/json-schema-draft4.json.cpp
|
||||||
|
src/json-uri.cpp
|
||||||
|
src/json-validator.cpp)
|
||||||
target_include_directories(json-schema-validator
|
target_include_directories(json-schema-validator
|
||||||
INTERFACE
|
PUBLIC
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src)
|
${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||||
target_compile_options(json-schema-validator
|
target_compile_options(json-schema-validator
|
||||||
INTERFACE
|
PUBLIC
|
||||||
-Wall -Wextra) # bad, better use something else based on compiler type
|
-Wall -Wextra) # bad, better use something else based on compiler type
|
||||||
target_link_libraries(json-schema-validator
|
target_link_libraries(json-schema-validator
|
||||||
INTERFACE
|
PUBLIC
|
||||||
json-hpp)
|
json-hpp)
|
||||||
|
|
||||||
install(
|
install(
|
||||||
@ -43,10 +46,6 @@ install(
|
|||||||
add_executable(json-schema-validate app/json-schema-validate.cpp)
|
add_executable(json-schema-validate app/json-schema-validate.cpp)
|
||||||
target_link_libraries(json-schema-validate json-schema-validator)
|
target_link_libraries(json-schema-validate json-schema-validator)
|
||||||
|
|
||||||
# json-schema-validator-tester
|
|
||||||
add_executable(json-schema-test app/json-schema-test.cpp)
|
|
||||||
target_link_libraries(json-schema-test json-schema-validator)
|
|
||||||
|
|
||||||
# test-zone
|
# test-zone
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
@ -56,6 +55,14 @@ find_path(JSON_SCHEMA_TEST_SUITE_PATH
|
|||||||
tests/draft4)
|
tests/draft4)
|
||||||
|
|
||||||
if(JSON_SCHEMA_TEST_SUITE_PATH)
|
if(JSON_SCHEMA_TEST_SUITE_PATH)
|
||||||
|
# json-schema-validator-tester
|
||||||
|
add_executable(json-schema-test app/json-schema-test.cpp)
|
||||||
|
target_link_libraries(json-schema-test json-schema-validator)
|
||||||
|
target_compile_definitions(json-schema-test
|
||||||
|
PRIVATE
|
||||||
|
JSON_SCHEMA_TEST_SUITE_PATH="${JSON_SCHEMA_TEST_SUITE_PATH}")
|
||||||
|
|
||||||
|
|
||||||
# create tests foreach test-file
|
# create tests foreach test-file
|
||||||
file(GLOB_RECURSE TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/draft4/*.json)
|
file(GLOB_RECURSE TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/draft4/*.json)
|
||||||
|
|
||||||
|
|||||||
@ -84,7 +84,7 @@ int main(void)
|
|||||||
There is an application which can be used for testing the validator with the
|
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).
|
[JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
|
||||||
|
|
||||||
Currently **82** of ~**308** tests are still failing, because simply not all keywords and
|
Currently **72** of ~**308** tests are still failing, because simply not all keywords and
|
||||||
their functionalities have been implemented. Some of the missing feature will
|
their functionalities have been implemented. Some of the missing feature will
|
||||||
require a rework. Some will only work with external libraries. (remote references)
|
require a rework. Some will only work with external libraries. (remote references)
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,48 @@
|
|||||||
#include "json-schema-validator.hpp"
|
/*
|
||||||
|
* Modern C++ JSON schema validator
|
||||||
|
*
|
||||||
|
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
|
||||||
|
*
|
||||||
|
* 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 <fstream>
|
||||||
|
|
||||||
using nlohmann::json;
|
using nlohmann::json;
|
||||||
using nlohmann::json_validator;
|
using nlohmann::json_uri;
|
||||||
|
using nlohmann::json_schema_draft4::json_validator;
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
json validation;
|
json validation; // a validation case following the JSON-test-suite-schema
|
||||||
|
|
||||||
|
std::map<std::string, std::string> external_schemas;
|
||||||
|
|
||||||
|
external_schemas["http://localhost:1234/integer.json"] =
|
||||||
|
JSON_SCHEMA_TEST_SUITE_PATH "/remotes/integer.json";
|
||||||
|
external_schemas["http://localhost:1234/subSchemas.json"] =
|
||||||
|
JSON_SCHEMA_TEST_SUITE_PATH "/remotes/subSchemas.json";
|
||||||
|
external_schemas["http://localhost:1234/folder/folderInteger.json"] =
|
||||||
|
JSON_SCHEMA_TEST_SUITE_PATH "/remotes/folder/folderInteger.json";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::cin >> validation;
|
std::cin >> validation;
|
||||||
@ -25,22 +62,64 @@ int main(void)
|
|||||||
|
|
||||||
const auto &schema = test_group["schema"];
|
const auto &schema = test_group["schema"];
|
||||||
|
|
||||||
|
json_validator validator;
|
||||||
|
do {
|
||||||
|
std::set<json_uri> undefined;
|
||||||
|
try {
|
||||||
|
undefined = validator.insert_schema(schema, json_uri("#"));
|
||||||
|
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
std::cout << " Test Case Exception (root-schema-inserting): " << e.what() << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undefined.size() == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
for (auto ref : undefined) {
|
||||||
|
std::cerr << "missing schema URL " << ref << " - trying to load it\n";
|
||||||
|
|
||||||
|
if (ref.to_string() == "http://json-schema.org/draft-04/schema#")
|
||||||
|
validator.insert_schema(nlohmann::json_schema_draft4::draft4_schema_builtin, ref);
|
||||||
|
else {
|
||||||
|
std::string fn = external_schemas[ref.url()];
|
||||||
|
|
||||||
|
std::fstream s(fn.c_str());
|
||||||
|
if (!s.good()) {
|
||||||
|
std::cerr << "could not open " << ref.url() << "\n";
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
json extra;
|
||||||
|
extra << s;
|
||||||
|
|
||||||
|
try {
|
||||||
|
validator.insert_schema(extra, ref.url());
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
std::cout << " Test Case Exception (schema-loading/inserting): " << e.what() << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} while (1);
|
||||||
|
|
||||||
for (auto &test_case : test_group["tests"]) {
|
for (auto &test_case : test_group["tests"]) {
|
||||||
std::cout << " Testing Case " << test_case["description"] << "\n";
|
std::cout << " Testing Case " << test_case["description"] << "\n";
|
||||||
|
|
||||||
bool valid = true;
|
bool valid = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
json_validator validator;
|
|
||||||
validator.set_schema("#", schema);
|
|
||||||
validator.validate(test_case["data"]);
|
validator.validate(test_case["data"]);
|
||||||
} catch (const std::out_of_range &e) {
|
} catch (const std::out_of_range &e) {
|
||||||
|
|
||||||
valid = false;
|
valid = false;
|
||||||
std::cout << " Test Case Exception (out of range): " << e.what() << "\n";
|
std::cout << " Test Case Exception (out of range): " << e.what() << "\n";
|
||||||
|
|
||||||
} catch (const std::invalid_argument &e) {
|
} catch (const std::invalid_argument &e) {
|
||||||
|
|
||||||
valid = false;
|
valid = false;
|
||||||
std::cout << " Test Case Exception (invalid argument): " << e.what() << "\n";
|
std::cout << " Test Case Exception (invalid argument): " << e.what() << "\n";
|
||||||
|
|
||||||
} catch (const std::logic_error &e) {
|
} catch (const std::logic_error &e) {
|
||||||
|
|
||||||
valid = !test_case["valid"]; /* force test-case failure */
|
valid = !test_case["valid"]; /* force test-case failure */
|
||||||
std::cout << " Not yet implemented: " << e.what() << "\n";
|
std::cout << " Not yet implemented: " << e.what() << "\n";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +1,66 @@
|
|||||||
#include "json-schema-validator.hpp"
|
/*
|
||||||
|
* Modern C++ JSON schema validator
|
||||||
|
*
|
||||||
|
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
|
||||||
|
*
|
||||||
|
* 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 <fstream>
|
#include <fstream>
|
||||||
|
#include <set>
|
||||||
#include <cstdlib>
|
|
||||||
|
|
||||||
using nlohmann::json;
|
using nlohmann::json;
|
||||||
using nlohmann::json_validator;
|
using nlohmann::json_uri;
|
||||||
|
using nlohmann::json_schema_draft4::json_validator;
|
||||||
|
|
||||||
static void usage(const char *name)
|
static void usage(const char *name)
|
||||||
{
|
{
|
||||||
std::cerr << "Usage: " << name << " <json-document> < <schema>\n";
|
std::cerr << "Usage: " << name << " <schema> < <document>\n";
|
||||||
exit(EXIT_FAILURE);
|
exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
resolver r(nlohmann::json_schema_draft4::root_schema,
|
||||||
|
nlohmann::json_schema_draft4::root_schema["id"]);
|
||||||
|
schema_refs_.insert(r.schema_refs.begin(), r.schema_refs.end());
|
||||||
|
assert(r.undefined_refs.size() == 0);
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
if (argc != 2)
|
if (argc != 2)
|
||||||
usage(argv[0]);
|
usage(argv[0]);
|
||||||
|
|
||||||
|
json_validator validator;
|
||||||
|
|
||||||
std::fstream f(argv[1]);
|
std::fstream f(argv[1]);
|
||||||
if (!f.good()) {
|
if (!f.good()) {
|
||||||
std::cerr << "could not open " << argv[1] << " for reading\n";
|
std::cerr << "could not open " << argv[1] << " for reading\n";
|
||||||
usage(argv[0]);
|
usage(argv[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1) Read the schema for the document you want to validate
|
||||||
json schema;
|
json schema;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
f >> schema;
|
f >> schema;
|
||||||
} catch (std::exception &e) {
|
} catch (std::exception &e) {
|
||||||
@ -33,19 +68,46 @@ int main(int argc, char *argv[])
|
|||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
json document;
|
// 2) insert this schema to the validator
|
||||||
|
// this resolves remote-schemas, sub-schemas and references
|
||||||
|
bool error = false;
|
||||||
|
do {
|
||||||
|
// inserting with json_uri("#") means this is the document's root-schema
|
||||||
|
auto missing_schemas = validator.insert_schema(schema, json_uri("#"));
|
||||||
|
|
||||||
|
// schema was inserted and all references have been fulfilled
|
||||||
|
if (missing_schemas.size() == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// schema was not inserted because it references unknown schemas
|
||||||
|
// 3) load missing schemas and insert them
|
||||||
|
for (auto ref : missing_schemas) {
|
||||||
|
std::cerr << "missing schema URL " << ref << " - trying to load it\n";
|
||||||
|
|
||||||
|
std::fstream lf(ref.path());
|
||||||
|
if (!lf.good()) {
|
||||||
|
std::cerr << "could not open " << ref.url() << "\n";
|
||||||
|
error = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
json extra;
|
||||||
try {
|
try {
|
||||||
std::cin >> document;
|
lf >> extra;
|
||||||
} catch (std::exception &e) {
|
} catch (std::exception &e) {
|
||||||
std::cerr << e.what() << " at " << f.tellp() << "\n";
|
std::cerr << e.what() << " at " << lf.tellp() << "\n";
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validator.insert_schema(extra, json_uri(ref.url()));
|
||||||
|
std::cerr << "OK";
|
||||||
|
}
|
||||||
|
} while (!error);
|
||||||
|
|
||||||
|
// 4) do the actual validation of the document
|
||||||
|
json document;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
json_validator validator;
|
document << std::cin;
|
||||||
validator.set_schema("#", schema);
|
|
||||||
validator.validate(document);
|
validator.validate(document);
|
||||||
} catch (std::exception &e) {
|
} catch (std::exception &e) {
|
||||||
std::cerr << "schema validation failed\n";
|
std::cerr << "schema validation failed\n";
|
||||||
@ -53,7 +115,7 @@ int main(int argc, char *argv[])
|
|||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cerr << std::setw(2) << document << "\n";
|
std::cerr << "document is valid\n";
|
||||||
|
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
157
src/json-schema-draft4.json.cpp
Normal file
157
src/json-schema-draft4.json.cpp
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
#include <json-schema.hpp>
|
||||||
|
|
||||||
|
namespace nlohmann::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",
|
||||||
|
"definitions": {
|
||||||
|
"schemaArray": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": { "$ref": "#" }
|
||||||
|
},
|
||||||
|
"positiveInteger": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"positiveIntegerDefault0": {
|
||||||
|
"allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
|
||||||
|
},
|
||||||
|
"simpleTypes": {
|
||||||
|
"enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
|
||||||
|
},
|
||||||
|
"stringArray": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"$schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": {},
|
||||||
|
"multipleOf": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"exclusiveMinimum": true
|
||||||
|
},
|
||||||
|
"maximum": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"exclusiveMaximum": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"minimum": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"exclusiveMinimum": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"maxLength": { "$ref": "#/definitions/positiveInteger" },
|
||||||
|
"minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
|
||||||
|
"pattern": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "regex"
|
||||||
|
},
|
||||||
|
"additionalItems": {
|
||||||
|
"anyOf": [
|
||||||
|
{ "type": "boolean" },
|
||||||
|
{ "$ref": "#" }
|
||||||
|
],
|
||||||
|
"default": {}
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{ "$ref": "#" },
|
||||||
|
{ "$ref": "#/definitions/schemaArray" }
|
||||||
|
],
|
||||||
|
"default": {}
|
||||||
|
},
|
||||||
|
"maxItems": { "$ref": "#/definitions/positiveInteger" },
|
||||||
|
"minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
|
||||||
|
"uniqueItems": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"maxProperties": { "$ref": "#/definitions/positiveInteger" },
|
||||||
|
"minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
|
||||||
|
"required": { "$ref": "#/definitions/stringArray" },
|
||||||
|
"additionalProperties": {
|
||||||
|
"anyOf": [
|
||||||
|
{ "type": "boolean" },
|
||||||
|
{ "$ref": "#" }
|
||||||
|
],
|
||||||
|
"default": {}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "$ref": "#" },
|
||||||
|
"default": {}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "$ref": "#" },
|
||||||
|
"default": {}
|
||||||
|
},
|
||||||
|
"patternProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "$ref": "#" },
|
||||||
|
"default": {}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"anyOf": [
|
||||||
|
{ "$ref": "#" },
|
||||||
|
{ "$ref": "#/definitions/stringArray" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enum": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"anyOf": [
|
||||||
|
{ "$ref": "#/definitions/simpleTypes" },
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/simpleTypes" },
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allOf": { "$ref": "#/definitions/schemaArray" },
|
||||||
|
"anyOf": { "$ref": "#/definitions/schemaArray" },
|
||||||
|
"oneOf": { "$ref": "#/definitions/schemaArray" },
|
||||||
|
"not": { "$ref": "#" }
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"exclusiveMaximum": [ "maximum" ],
|
||||||
|
"exclusiveMinimum": [ "minimum" ]
|
||||||
|
},
|
||||||
|
"default": {}
|
||||||
|
} )"_json;
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,548 +0,0 @@
|
|||||||
/*
|
|
||||||
* Modern C++ JSON schema validator
|
|
||||||
*
|
|
||||||
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
|
|
||||||
*
|
|
||||||
* 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_VALIDATOR_HPP__
|
|
||||||
#define NLOHMANN_JSON_VALIDATOR_HPP__
|
|
||||||
|
|
||||||
#include <json.hpp>
|
|
||||||
|
|
||||||
#include <regex>
|
|
||||||
#include <set>
|
|
||||||
|
|
||||||
// make yourself a home - welcome to nlohmann's namespace
|
|
||||||
namespace nlohmann
|
|
||||||
{
|
|
||||||
|
|
||||||
class json_validator
|
|
||||||
{
|
|
||||||
// insert default values items into object
|
|
||||||
// if the key is not present before checking their
|
|
||||||
// validity in regards to their schema
|
|
||||||
//
|
|
||||||
// breaks JSON-Schema-Test-Suite if true
|
|
||||||
// *PARTIALLY IMPLEMENTED* only for properties of objects
|
|
||||||
bool default_value_insertion = false;
|
|
||||||
|
|
||||||
// recursively insert default values and create parent objects if
|
|
||||||
// they would be empty
|
|
||||||
//
|
|
||||||
// breaks JSON-Schema-Test-Suite if true
|
|
||||||
// *NOT YET IMPLEMENTED* -> maybe the same as the above option, need more thoughts
|
|
||||||
bool recursive_default_value_insertion = false;
|
|
||||||
|
|
||||||
void not_yet_implemented(const json &schema, const std::string &field, const std::string &type)
|
|
||||||
{
|
|
||||||
if (schema.find(field) != schema.end())
|
|
||||||
throw std::logic_error(field + " for " + type + " is not yet implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate_type(const json &schema, const std::string &expected_type, const std::string &name)
|
|
||||||
{
|
|
||||||
const auto &type_it = schema.find("type");
|
|
||||||
if (type_it == schema.end())
|
|
||||||
/* TODO guess type for more safety,
|
|
||||||
* TODO use definitions
|
|
||||||
* TODO valid by not being defined? FIXME not clear - there are
|
|
||||||
* schema-test case which are not specifying a type */
|
|
||||||
return;
|
|
||||||
|
|
||||||
const auto &type_instance = type_it.value();
|
|
||||||
|
|
||||||
// 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())
|
|
||||||
return;
|
|
||||||
|
|
||||||
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)
|
|
||||||
return;
|
|
||||||
|
|
||||||
throw std::invalid_argument(type_instance.get<std::string>() + " is not a " + expected_type + " for " + name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate_enum(json &instance, const json &schema, const std::string &name)
|
|
||||||
{
|
|
||||||
const auto &enum_value = schema.find("enum");
|
|
||||||
if (enum_value == schema.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (std::find(enum_value.value().begin(), enum_value.value().end(), instance) != enum_value.value().end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::ostringstream s;
|
|
||||||
s << "invalid enum-value '" << instance << "' "
|
|
||||||
<< "for instance '" << name << "'. Candidates are " << enum_value.value() << ".";
|
|
||||||
|
|
||||||
throw std::invalid_argument(s.str());
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate_string(json &instance, const json &schema, const std::string &name)
|
|
||||||
{
|
|
||||||
// possibile but unhanled keywords
|
|
||||||
not_yet_implemented(schema, "format", "string");
|
|
||||||
not_yet_implemented(schema, "pattern", "string");
|
|
||||||
|
|
||||||
validate_type(schema, "string", name);
|
|
||||||
|
|
||||||
// minLength
|
|
||||||
auto attr = schema.find("minLength");
|
|
||||||
if (attr != schema.end())
|
|
||||||
if (instance.get<std::string>().size() < attr.value()) {
|
|
||||||
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 (instance.get<std::string>().size() > attr.value()) {
|
|
||||||
std::ostringstream s;
|
|
||||||
s << "'" << name << "' of value '" << instance << "' is too long as per maxLength ("
|
|
||||||
<< attr.value() << ")";
|
|
||||||
throw std::out_of_range(s.str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate_boolean(json & /*instance*/, const json &schema, const std::string &name)
|
|
||||||
{
|
|
||||||
validate_type(schema, "boolean", name);
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate_numeric(json &instance, const json &schema, const std::string &name)
|
|
||||||
{
|
|
||||||
double value = instance;
|
|
||||||
|
|
||||||
const auto &multipleOf = schema.find("multipleOf");
|
|
||||||
if (multipleOf != schema.end()) {
|
|
||||||
double rem = fmod(value, multipleOf.value());
|
|
||||||
if (rem != 0.0)
|
|
||||||
throw std::out_of_range(name + " is not a multiple ...");
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto &maximum = schema.find("maximum");
|
|
||||||
if (maximum != schema.end()) {
|
|
||||||
double maxi = maximum.value();
|
|
||||||
auto ex = std::out_of_range(name + " exceeds maximum of ...");
|
|
||||||
if (schema.find("exclusiveMaximum") != schema.end()) {
|
|
||||||
if (value >= maxi)
|
|
||||||
throw ex;
|
|
||||||
} else {
|
|
||||||
if (value > maxi)
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto &minimum = schema.find("minimum");
|
|
||||||
if (minimum != schema.end()) {
|
|
||||||
double mini = minimum.value();
|
|
||||||
auto ex = std::out_of_range(name + " exceeds minimum of ...");
|
|
||||||
if (schema.find("exclusiveMinimum") != schema.end()) {
|
|
||||||
if (value <= mini)
|
|
||||||
throw ex;
|
|
||||||
} else {
|
|
||||||
if (value < mini)
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate_integer(json &instance, const json &schema, const std::string &name)
|
|
||||||
{
|
|
||||||
validate_type(schema, "integer", name);
|
|
||||||
validate_numeric(instance, schema, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate_unsigned(json &instance, const json &schema, const std::string &name)
|
|
||||||
{
|
|
||||||
validate_type(schema, "integer", name);
|
|
||||||
validate_numeric(instance, schema, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate_float(json &instance, const json &schema, const std::string &name)
|
|
||||||
{
|
|
||||||
validate_type(schema, "number", name);
|
|
||||||
validate_numeric(instance, schema, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate_null(json & /*instance*/, const json &schema, const std::string &name)
|
|
||||||
{
|
|
||||||
validate_type(schema, "null", name);
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate_array(json &instance, const json &schema, const std::string &name)
|
|
||||||
{
|
|
||||||
validate_type(schema, "array", name);
|
|
||||||
|
|
||||||
// maxItems
|
|
||||||
const auto &maxItems = schema.find("maxItems");
|
|
||||||
if (maxItems != schema.end())
|
|
||||||
if (instance.size() > maxItems.value())
|
|
||||||
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())
|
|
||||||
throw std::out_of_range(name + " has too many items.");
|
|
||||||
|
|
||||||
// uniqueItems
|
|
||||||
const auto &uniqueItems = schema.find("uniqueItems");
|
|
||||||
if (uniqueItems != schema.end())
|
|
||||||
if (uniqueItems.value() == true) {
|
|
||||||
std::set<json> 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 == 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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate_object(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"];
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// maxProperties
|
|
||||||
const auto &maxProperties = schema.find("maxProperties");
|
|
||||||
if (maxProperties != schema.end())
|
|
||||||
if (instance.size() > maxProperties.value())
|
|
||||||
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())
|
|
||||||
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() == 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();
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool patternProperties_has_matched = false;
|
|
||||||
for (auto pp = patternProperties.begin();
|
|
||||||
pp != patternProperties.end(); ++pp) {
|
|
||||||
std::regex re(pp.key(), std::regex::ECMAScript);
|
|
||||||
|
|
||||||
if (std::regex_search(child.key(), re)) {
|
|
||||||
validate(child.value(), pp.value(), child_name);
|
|
||||||
patternProperties_has_matched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (patternProperties_has_matched)
|
|
||||||
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<std::string>() +
|
|
||||||
"' 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<std::string>());
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate(json &instance, const json &schema_, const std::string &name)
|
|
||||||
{
|
|
||||||
not_yet_implemented(schema_, "allOf", "all");
|
|
||||||
not_yet_implemented(schema_, "anyOf", "all");
|
|
||||||
not_yet_implemented(schema_, "oneOf", "all");
|
|
||||||
not_yet_implemented(schema_, "not", "all");
|
|
||||||
|
|
||||||
// std::cerr << instance << " VS\n";
|
|
||||||
// std::cerr << schema_ << "\n";
|
|
||||||
// std::cerr << "\n";
|
|
||||||
|
|
||||||
const json *schema = &schema_;
|
|
||||||
|
|
||||||
do {
|
|
||||||
const auto &ref = schema->find("$ref");
|
|
||||||
if (ref != schema->end()) {
|
|
||||||
std::string r = ref.value();
|
|
||||||
|
|
||||||
// do we have stored a schema which correspond to this reference
|
|
||||||
if (schema_references.find(r) == schema_references.end()) { // no
|
|
||||||
|
|
||||||
if (r[0] != '#')
|
|
||||||
throw std::logic_error("remote references are not yet implemented for ref " + r);
|
|
||||||
|
|
||||||
schema = schema_references["#"]; // root schema
|
|
||||||
|
|
||||||
// Aieee, need so much better parsing than this TODO
|
|
||||||
r = r.substr(1); // skip '#'
|
|
||||||
|
|
||||||
std::regex re("\\/([a-zA-Z0-9~%]+)");
|
|
||||||
for (auto match = std::sregex_iterator(r.begin(), r.end(), re), end = std::sregex_iterator();
|
|
||||||
match != end;
|
|
||||||
++match) {
|
|
||||||
|
|
||||||
std::string name = match->str().substr(1);
|
|
||||||
|
|
||||||
switch (schema->type()) {
|
|
||||||
case json::value_t::array: {
|
|
||||||
auto index = std::stoul(name);
|
|
||||||
if (index >= schema->size())
|
|
||||||
throw std::out_of_range("reference schema " + r + " is out of range for array-reference\n");
|
|
||||||
schema = &((*schema)[index]);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case json::value_t::object: {
|
|
||||||
const auto &sub = schema->find(name);
|
|
||||||
if (sub == schema->end())
|
|
||||||
throw std::invalid_argument("reference schema " + r + " not found for object\n");
|
|
||||||
|
|
||||||
schema = &(*sub);
|
|
||||||
} break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema_references[r] = schema;
|
|
||||||
} else
|
|
||||||
schema = schema_references[r];
|
|
||||||
} else
|
|
||||||
break;
|
|
||||||
} while (1); // loop in case of nested refs
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
std::map<std::string, const json *> schema_references;
|
|
||||||
|
|
||||||
void set_schema(const std::string &ref, const json &schema)
|
|
||||||
{
|
|
||||||
schema_references[ref] = &schema; /* replace or insert */
|
|
||||||
}
|
|
||||||
|
|
||||||
void validate(json &instance)
|
|
||||||
{
|
|
||||||
validate(instance, *schema_references["#"], "root");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif /* NLOHMANN_JSON_VALIDATOR_HPP__ */
|
|
||||||
201
src/json-schema.hpp
Normal file
201
src/json-schema.hpp
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
/*
|
||||||
|
* Modern C++ JSON schema validator
|
||||||
|
*
|
||||||
|
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
|
||||||
|
*
|
||||||
|
* 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__
|
||||||
|
|
||||||
|
#include <json.hpp>
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
// 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 json_pointer
|
||||||
|
{
|
||||||
|
std::string str_;
|
||||||
|
|
||||||
|
void from_string(const std::string &r);
|
||||||
|
|
||||||
|
public:
|
||||||
|
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
|
||||||
|
//
|
||||||
|
// New URIs can be derived from it using the derive()-method.
|
||||||
|
// This is useful for resolving refs or subschema-IDs in json-schemas.
|
||||||
|
//
|
||||||
|
// This is done implement the requirements described in section 8.2.
|
||||||
|
//
|
||||||
|
class json_uri
|
||||||
|
{
|
||||||
|
std::string urn_;
|
||||||
|
|
||||||
|
std::string proto_;
|
||||||
|
std::string hostname_;
|
||||||
|
std::string path_;
|
||||||
|
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);
|
||||||
|
|
||||||
|
std::tuple<std::string, std::string, std::string, std::string, std::string> tie() const
|
||||||
|
{
|
||||||
|
return std::tie(urn_, proto_, hostname_, path_, pointer_.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
json_uri(const std::string &uri)
|
||||||
|
{
|
||||||
|
from_string(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string protocol() const { return proto_; }
|
||||||
|
const std::string hostname() const { return hostname_; }
|
||||||
|
const std::string path() const { return path_; }
|
||||||
|
const json_pointer pointer() const { return pointer_; }
|
||||||
|
|
||||||
|
const std::string url() const;
|
||||||
|
|
||||||
|
json_uri derive(const std::string &uri) const
|
||||||
|
{
|
||||||
|
json_uri u = *this;
|
||||||
|
u.from_string(uri);
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
json_uri append(const std::string &field) const
|
||||||
|
{
|
||||||
|
json_uri u = *this;
|
||||||
|
u.pointer_.append("/" + field);
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string to_string() const;
|
||||||
|
|
||||||
|
friend bool operator<(const json_uri &l, const json_uri &r)
|
||||||
|
{
|
||||||
|
return l.tie() < r.tie();
|
||||||
|
}
|
||||||
|
|
||||||
|
friend bool operator==(const json_uri &l, const json_uri &r)
|
||||||
|
{
|
||||||
|
return l.tie() == r.tie();
|
||||||
|
}
|
||||||
|
|
||||||
|
friend std::ostream &operator<<(std::ostream &os, const json_uri &u);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
namespace json_schema_draft4
|
||||||
|
{
|
||||||
|
|
||||||
|
extern json draft4_schema_builtin;
|
||||||
|
|
||||||
|
class json_validator
|
||||||
|
{
|
||||||
|
std::vector<std::shared_ptr<json>> schema_store_;
|
||||||
|
std::shared_ptr<json> root_schema_;
|
||||||
|
|
||||||
|
std::map<json_uri, const json *> schema_refs_;
|
||||||
|
|
||||||
|
void not_yet_implemented(const json &schema, const std::string &field, const std::string &type);
|
||||||
|
|
||||||
|
void validate_type(const json &schema, const std::string &expected_type, const std::string &name);
|
||||||
|
void validate_enum(json &instance, const json &schema, const std::string &name);
|
||||||
|
void validate_numeric(json &instance, const json &schema, const std::string &name);
|
||||||
|
void validate(json &instance, const json &schema, const std::string &name);
|
||||||
|
|
||||||
|
void validate_string(json &instance, const json &schema, const std::string &name);
|
||||||
|
void validate_boolean(json &instance, const json &schema, const std::string &name);
|
||||||
|
void validate_integer(json &instance, const json &schema, const std::string &name);
|
||||||
|
void validate_unsigned(json &instance, const json &schema, const std::string &name);
|
||||||
|
void validate_float(json &instance, const json &schema, const std::string &name);
|
||||||
|
void validate_null(json &instance, const json &schema, const std::string &name);
|
||||||
|
void validate_array(json &instance, const json &schema, const std::string &name);
|
||||||
|
void validate_object(json &instance, const json &schema, const std::string &name);
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::set<json_uri> insert_schema(const json &input, json_uri id);
|
||||||
|
|
||||||
|
void validate(json &instance);
|
||||||
|
|
||||||
|
// insert default values items into object
|
||||||
|
// if the key is not present before checking their
|
||||||
|
// validity in regards to their schema
|
||||||
|
//
|
||||||
|
// breaks JSON-Schema-Test-Suite if true
|
||||||
|
// *PARTIALLY IMPLEMENTED* only for properties of objects
|
||||||
|
bool default_value_insertion = false;
|
||||||
|
|
||||||
|
// recursively insert default values and create parent objects if
|
||||||
|
// they would be empty
|
||||||
|
//
|
||||||
|
// breaks JSON-Schema-Test-Suite if true
|
||||||
|
// *NOT YET IMPLEMENTED* -> maybe the same as the above option, need more thoughts
|
||||||
|
bool recursive_default_value_insertion = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // json_schema_draft4
|
||||||
|
} // nlohmann
|
||||||
|
|
||||||
|
#endif /* NLOHMANN_JSON_SCHEMA_HPP__ */
|
||||||
127
src/json-uri.cpp
Normal file
127
src/json-uri.cpp
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Modern C++ JSON schema validator
|
||||||
|
*
|
||||||
|
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
|
||||||
|
*
|
||||||
|
* 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"
|
||||||
|
|
||||||
|
namespace nlohmann {
|
||||||
|
|
||||||
|
void json_pointer::from_string(const std::string &r)
|
||||||
|
{
|
||||||
|
str_ = "#";
|
||||||
|
|
||||||
|
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
|
||||||
|
auto pointer_separator = uri.find('#');
|
||||||
|
if (pointer_separator != std::string::npos) // and extract the JSON-pointer-string if found
|
||||||
|
pointer = uri.substr(pointer_separator);
|
||||||
|
|
||||||
|
// the rest is an URL
|
||||||
|
std::string url = uri.substr(0, pointer_separator);
|
||||||
|
if (url.size()) { // if an URL is part of the URI
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
pointer_ = json_pointer("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointer.size() > 0)
|
||||||
|
pointer_ = pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string json_uri::url() const
|
||||||
|
{
|
||||||
|
std::stringstream s;
|
||||||
|
|
||||||
|
if (proto_.size() > 0)
|
||||||
|
s << proto_ << "://";
|
||||||
|
|
||||||
|
s << hostname_
|
||||||
|
<< path_;
|
||||||
|
|
||||||
|
return s.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string json_uri::to_string() const
|
||||||
|
{
|
||||||
|
std::stringstream s;
|
||||||
|
|
||||||
|
s << urn_
|
||||||
|
<< url()
|
||||||
|
<< pointer_.to_string();
|
||||||
|
|
||||||
|
return s.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream &operator<<(std::ostream &os, const json_uri &u)
|
||||||
|
{
|
||||||
|
return os << u.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
610
src/json-validator.cpp
Normal file
610
src/json-validator.cpp
Normal file
@ -0,0 +1,610 @@
|
|||||||
|
/*
|
||||||
|
* Modern C++ JSON schema validator
|
||||||
|
*
|
||||||
|
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
|
||||||
|
*
|
||||||
|
* 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 <regex>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
using nlohmann::json;
|
||||||
|
using nlohmann::json_uri;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class resolver
|
||||||
|
{
|
||||||
|
void resolve(json &schema, json_uri id)
|
||||||
|
{
|
||||||
|
auto fid = schema.find("id");
|
||||||
|
|
||||||
|
if (fid != schema.end() &&
|
||||||
|
fid.value().type() == json::value_t::string)
|
||||||
|
id = id.derive(fid.value());
|
||||||
|
|
||||||
|
if (schema_refs.find(id) != schema_refs.end())
|
||||||
|
throw std::invalid_argument("schema " + id.to_string() + " already present in local resolver");
|
||||||
|
|
||||||
|
// store a raw pointer to this (sub-)schema references by its absolute json_uri
|
||||||
|
// this (sub-)schema is part of a schema stored inside schema_store_
|
||||||
|
schema_refs[id] = &schema;
|
||||||
|
|
||||||
|
for (auto i = schema.begin(), end = schema.end(); i != end; ++i) {
|
||||||
|
if (i.key() == "default") /* default value can be objects, but are not schemas */
|
||||||
|
continue;
|
||||||
|
|
||||||
|
switch (i.value().type()) {
|
||||||
|
|
||||||
|
case json::value_t::object: // child is object, it is a schema
|
||||||
|
resolve(i.value(), id.append(i.key()));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case json::value_t::array: {
|
||||||
|
std::size_t index = 0;
|
||||||
|
auto child_id = id.append(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++;
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<json_uri> refs;
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::set<json_uri> undefined_refs;
|
||||||
|
|
||||||
|
std::map<json_uri, const json *> schema_refs;
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
resolve(schema, id);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
namespace nlohmann {
|
||||||
|
namespace json_schema_draft4
|
||||||
|
{
|
||||||
|
|
||||||
|
std::set<json_uri> json_validator::insert_schema(const json &input, json_uri id)
|
||||||
|
{
|
||||||
|
// allocate create a copy for later storage - if resolving reference works
|
||||||
|
std::shared_ptr<json> schema = std::make_shared<json>(input);
|
||||||
|
|
||||||
|
// resolve all local schemas and references
|
||||||
|
resolver r(*schema, id);
|
||||||
|
|
||||||
|
// check whether all undefined schema references can be resolved with existing ones
|
||||||
|
std::set<json_uri> undefined;
|
||||||
|
for (auto &ref : r.undefined_refs)
|
||||||
|
if (schema_refs_.find(ref) == schema_refs_.end()) { // exact schema reference not found
|
||||||
|
undefined.insert(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
// anything cannot be resolved, inform the user and make him/her load additional schemas
|
||||||
|
// before retrying
|
||||||
|
if (undefined.size() > 0)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
// 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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
|
||||||
|
// store the document root-schema
|
||||||
|
if (id == json_uri("#"))
|
||||||
|
root_schema_ = schema;
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::not_yet_implemented(const json &schema, const std::string &field, const std::string &type)
|
||||||
|
{
|
||||||
|
if (schema.find(field) != schema.end())
|
||||||
|
throw std::logic_error(field + " for " + type + " is not yet implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate_type(const json &schema, const std::string &expected_type, const std::string &name)
|
||||||
|
{
|
||||||
|
const auto &type_it = schema.find("type");
|
||||||
|
if (type_it == schema.end())
|
||||||
|
/* TODO guess type for more safety,
|
||||||
|
* TODO use definitions
|
||||||
|
* TODO valid by not being defined? FIXME not clear - there are
|
||||||
|
* schema-test case which are not specifying a type */
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto &type_instance = type_it.value();
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
return;
|
||||||
|
|
||||||
|
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)
|
||||||
|
return;
|
||||||
|
|
||||||
|
throw std::invalid_argument(type_instance.get<std::string>() + " is not a " + expected_type + " for " + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate_enum(json &instance, const json &schema, const std::string &name)
|
||||||
|
{
|
||||||
|
const auto &enum_value = schema.find("enum");
|
||||||
|
if (enum_value == schema.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (std::find(enum_value.value().begin(), enum_value.value().end(), instance) != enum_value.value().end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::ostringstream s;
|
||||||
|
s << "invalid enum-value '" << instance << "' "
|
||||||
|
<< "for instance '" << name << "'. Candidates are " << enum_value.value() << ".";
|
||||||
|
|
||||||
|
throw std::invalid_argument(s.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate_string(json &instance, const json &schema, const std::string &name)
|
||||||
|
{
|
||||||
|
// possibile but unhanled keywords
|
||||||
|
not_yet_implemented(schema, "format", "string");
|
||||||
|
not_yet_implemented(schema, "pattern", "string");
|
||||||
|
|
||||||
|
validate_type(schema, "string", name);
|
||||||
|
|
||||||
|
// minLength
|
||||||
|
auto attr = schema.find("minLength");
|
||||||
|
if (attr != schema.end())
|
||||||
|
if (instance.get<std::string>().size() < attr.value()) {
|
||||||
|
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 (instance.get<std::string>().size() > attr.value()) {
|
||||||
|
std::ostringstream s;
|
||||||
|
s << "'" << name << "' of value '" << instance << "' is too long as per maxLength ("
|
||||||
|
<< attr.value() << ")";
|
||||||
|
throw std::out_of_range(s.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate_boolean(json & /*instance*/, const json &schema, const std::string &name)
|
||||||
|
{
|
||||||
|
validate_type(schema, "boolean", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate_numeric(json &instance, const json &schema, const std::string &name)
|
||||||
|
{
|
||||||
|
double value = instance;
|
||||||
|
|
||||||
|
const auto &multipleOf = schema.find("multipleOf");
|
||||||
|
if (multipleOf != schema.end()) {
|
||||||
|
double rem = fmod(value, multipleOf.value());
|
||||||
|
if (rem != 0.0)
|
||||||
|
throw std::out_of_range(name + " is not a multiple ...");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &maximum = schema.find("maximum");
|
||||||
|
if (maximum != schema.end()) {
|
||||||
|
double maxi = maximum.value();
|
||||||
|
auto ex = std::out_of_range(name + " exceeds maximum of ...");
|
||||||
|
if (schema.find("exclusiveMaximum") != schema.end()) {
|
||||||
|
if (value >= maxi)
|
||||||
|
throw ex;
|
||||||
|
} else {
|
||||||
|
if (value > maxi)
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &minimum = schema.find("minimum");
|
||||||
|
if (minimum != schema.end()) {
|
||||||
|
double mini = minimum.value();
|
||||||
|
auto ex = std::out_of_range(name + " exceeds minimum of ...");
|
||||||
|
if (schema.find("exclusiveMinimum") != schema.end()) {
|
||||||
|
if (value <= mini)
|
||||||
|
throw ex;
|
||||||
|
} else {
|
||||||
|
if (value < mini)
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate_integer(json &instance, const json &schema, const std::string &name)
|
||||||
|
{
|
||||||
|
validate_type(schema, "integer", name);
|
||||||
|
validate_numeric(instance, schema, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate_unsigned(json &instance, const json &schema, const std::string &name)
|
||||||
|
{
|
||||||
|
validate_type(schema, "integer", name);
|
||||||
|
validate_numeric(instance, schema, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate_float(json &instance, const json &schema, const std::string &name)
|
||||||
|
{
|
||||||
|
validate_type(schema, "number", name);
|
||||||
|
validate_numeric(instance, schema, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate_null(json & /*instance*/, const json &schema, const std::string &name)
|
||||||
|
{
|
||||||
|
validate_type(schema, "null", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate_array(json &instance, const json &schema, const std::string &name)
|
||||||
|
{
|
||||||
|
validate_type(schema, "array", name);
|
||||||
|
|
||||||
|
// maxItems
|
||||||
|
const auto &maxItems = schema.find("maxItems");
|
||||||
|
if (maxItems != schema.end())
|
||||||
|
if (instance.size() > maxItems.value())
|
||||||
|
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())
|
||||||
|
throw std::out_of_range(name + " has too many items.");
|
||||||
|
|
||||||
|
// uniqueItems
|
||||||
|
const auto &uniqueItems = schema.find("uniqueItems");
|
||||||
|
if (uniqueItems != schema.end())
|
||||||
|
if (uniqueItems.value() == true) {
|
||||||
|
std::set<json> 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 == 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate_object(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"];
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxProperties
|
||||||
|
const auto &maxProperties = schema.find("maxProperties");
|
||||||
|
if (maxProperties != schema.end())
|
||||||
|
if (instance.size() > maxProperties.value())
|
||||||
|
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())
|
||||||
|
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() == 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();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool patternProperties_has_matched = false;
|
||||||
|
for (auto pp = patternProperties.begin();
|
||||||
|
pp != patternProperties.end(); ++pp) {
|
||||||
|
std::regex re(pp.key(), std::regex::ECMAScript);
|
||||||
|
|
||||||
|
if (std::regex_search(child.key(), re)) {
|
||||||
|
validate(child.value(), pp.value(), child_name);
|
||||||
|
patternProperties_has_matched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (patternProperties_has_matched)
|
||||||
|
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<std::string>() +
|
||||||
|
"' 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<std::string>());
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate(json &instance, const json &schema_, const std::string &name)
|
||||||
|
{
|
||||||
|
not_yet_implemented(schema_, "allOf", "all");
|
||||||
|
not_yet_implemented(schema_, "anyOf", "all");
|
||||||
|
not_yet_implemented(schema_, "oneOf", "all");
|
||||||
|
not_yet_implemented(schema_, "not", "all");
|
||||||
|
|
||||||
|
const json *schema = &schema_;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const auto &ref = schema->find("$ref");
|
||||||
|
if (ref != schema->end()) {
|
||||||
|
auto it = schema_refs_.find(ref.value());
|
||||||
|
|
||||||
|
if (it == schema_refs_.end())
|
||||||
|
throw std::invalid_argument("schema reference " + ref.value().get<std::string>() + " not found. Make sure all schemas have been inserted before validation.");
|
||||||
|
|
||||||
|
schema = it->second;
|
||||||
|
} else
|
||||||
|
break;
|
||||||
|
} while (1); // loop in case of nested refs
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void json_validator::validate(json &instance)
|
||||||
|
{
|
||||||
|
if (root_schema_ == nullptr)
|
||||||
|
throw std::invalid_argument("no root-schema has been inserted. Cannot validate an instance without it.");
|
||||||
|
|
||||||
|
validate(instance, *root_schema_, "root");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
test/id.schema.json
Normal file
29
test/id.schema.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"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" } ]
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user