From 66e8f13f72b3944507e1b5e7e96e00dfbe6dd58e Mon Sep 17 00:00:00 2001 From: Patrick Boettcher Date: Tue, 22 May 2018 18:02:52 +0200 Subject: [PATCH] URIs/URLs/URNs: fix multiple URI for one (sub-)schema This fixes other things as well: - handle transition from URN to URI correclty - URL constructions by handling '/' correctly Fixes #9 --- src/json-schema.hpp | 15 ++-- src/json-uri.cpp | 88 +++++++++++-------- src/json-validator.cpp | 59 ++++++++----- test/JSON-Schema-Test-Suite/CMakeLists.txt | 6 -- .../json-schema-test.cpp | 2 +- .../remotes/folder/folderInteger.json | 3 + .../remotes/integer.json | 3 + test/JSON-Schema-Test-Suite/remotes/name.json | 11 +++ .../remotes/subSchemas.json | 8 ++ 9 files changed, 125 insertions(+), 70 deletions(-) create mode 100644 test/JSON-Schema-Test-Suite/remotes/folder/folderInteger.json create mode 100644 test/JSON-Schema-Test-Suite/remotes/integer.json create mode 100644 test/JSON-Schema-Test-Suite/remotes/name.json create mode 100644 test/JSON-Schema-Test-Suite/remotes/subSchemas.json diff --git a/src/json-schema.hpp b/src/json-schema.hpp index d38a308..8df0df9 100644 --- a/src/json-schema.hpp +++ b/src/json-schema.hpp @@ -106,7 +106,7 @@ class JSON_SCHEMA_VALIDATOR_API json_uri 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 { @@ -116,7 +116,7 @@ protected: public: json_uri(const std::string &uri) { - from_string(uri); + update(uri); } const std::string protocol() const { return proto_; } @@ -124,7 +124,8 @@ public: const std::string path() const { return path_; } const local_json_pointer pointer() const { return pointer_; } - const std::string url() const; + 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 &); @@ -135,7 +136,7 @@ public: json_uri derive(const std::string &uri) const { json_uri u = *this; - u.from_string(uri); + u.update(uri); return u; } @@ -179,7 +180,7 @@ class JSON_SCHEMA_VALIDATOR_API json_validator 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 validate_string(const json &instance, const json &schema, const std::string &name); void insert_schema(const json &input, const json_uri &id); @@ -197,7 +198,7 @@ public: void validate(const json &instance); }; -} // json_schema_draft4 -} // nlohmann +} // namespace json_schema_draft4 +} // namespace nlohmann #endif /* NLOHMANN_JSON_SCHEMA_HPP__ */ diff --git a/src/json-uri.cpp b/src/json-uri.cpp index 5b737d3..dc35bc8 100644 --- a/src/json-uri.cpp +++ b/src/json-uri.cpp @@ -50,54 +50,73 @@ void local_json_pointer::from_string(const std::string &r) } while (pos != std::string::npos); } -void json_uri::from_string(const std::string &uri) +void json_uri::update(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 document-root - 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 + if (pointer_separator != std::string::npos) // and extract the 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 + auto location = uri.substr(0, pointer_separator); - 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 == "://" + if (location.size()) { // a location part has been found + pointer_ = local_json_pointer(""); // if a location is given, the pointer is emptied - 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 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 && path_.back() != '/') // the URL contained only a path and the current path has no / at the end, it is an absolute path + path_ = '/' + path; + else // otherwise it is a subfolder + path_.append(path); } - - // 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_ = local_json_pointer(""); } + // if there was a pointer part store it internally if (pointer.size() > 0) 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) @@ -113,8 +132,7 @@ std::string json_uri::to_string() const { std::stringstream s; - s << urn_ - << url() + s << location() << pointer_.to_string(); return s.str(); @@ -184,4 +202,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..4ec5fb9 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -45,23 +45,29 @@ namespace class resolver { - void resolve(json &schema, json_uri id) + void resolve(json &schema, std::vector base_uris) { // look for the id-field in this schema auto fid = schema.find("id"); // 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 + fid.value().type() == json::value_t::string) { + // resolve to a full id with URL + path based on last base_uri-added for this node + auto id = base_uris.back().derive(fid.value()); + if (std::find(base_uris.begin(), base_uris.end(), id) == base_uris.end()) + base_uris.push_back(id); + } - // already existing - error - 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 referenced by its absolute json_uri + // store a raw pointer to this (sub-)schema referenced by all of its absolute json_uris // 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; + for (auto &u : base_uris) { + // already existing - error + if (schema_refs.find(u) != schema_refs.end()) + throw std::invalid_argument("schema " + u.to_string() + " already present in local resolver"); + + schema_refs[u] = &schema; + } for (auto i = schema.begin(), end = schema.end(); i != end; ++i) { // FIXME: this inhibits the user adding properties with the key "default" @@ -70,23 +76,38 @@ class resolver switch (i.value().type()) { - case json::value_t::object: // child is object, it is a schema - resolve(i.value(), id.append(json_uri::escape(i.key()))); - break; + case json::value_t::object: { // child is object, it is a schema + std::vector subschema_uris = base_uris; + + // add key to all of the URIs + for (auto &s : subschema_uris) + s = s.append(nlohmann::json_uri::escape(i.key())); + + resolve(i.value(), subschema_uris); + } break; case json::value_t::array: { + std::vector subschema_uris = base_uris; + for (auto &s : subschema_uris) + s = s.append(nlohmann::json_uri::escape(i.key())); + 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))); + if (v.type() == json::value_t::object) { // array element is object + + std::vector subschema_item_uris = subschema_uris; + for (auto &s : subschema_item_uris) + s = s.append(std::to_string(index)); + resolve(v, subschema_item_uris); + } index++; } } break; case json::value_t::string: if (i.key() == "$ref") { - json_uri ref = id.derive(i.value()); + // use last inserted URI to derive the $ref-element + auto ref = base_uris.back().derive(i.value()); i.value() = ref.to_string(); refs.insert(ref); } @@ -107,12 +128,8 @@ public: 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); + resolve(schema, {{id}}); // refs now contains all references // diff --git a/test/JSON-Schema-Test-Suite/CMakeLists.txt b/test/JSON-Schema-Test-Suite/CMakeLists.txt index 4153b5b..e2bc193 100644 --- a/test/JSON-Schema-Test-Suite/CMakeLists.txt +++ b/test/JSON-Schema-Test-Suite/CMakeLists.txt @@ -38,12 +38,6 @@ 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 diff --git a/test/JSON-Schema-Test-Suite/json-schema-test.cpp b/test/JSON-Schema-Test-Suite/json-schema-test.cpp index 9059471..9dc7502 100644 --- a/test/JSON-Schema-Test-Suite/json-schema-test.cpp +++ b/test/JSON-Schema-Test-Suite/json-schema-test.cpp @@ -26,8 +26,8 @@ #include "json-schema.hpp" #include -#include #include +#include using nlohmann::json; using nlohmann::json_uri; 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