From 3ec0e69a0bce7524f12a865b153b71d0d7ef2484 Mon Sep 17 00:00:00 2001 From: Patrick Boettcher Date: Wed, 23 Oct 2019 14:44:51 +0200 Subject: [PATCH] Add support for plain-name-fragment Plain-name-fragments are something like file.json#plain plain here is not a JSON-pointer but a Location-independent Identifier or plain-name. Fixes #72 --- README.md | 3 - src/json-schema.hpp | 27 +++- src/json-uri.cpp | 36 ++++-- src/json-validator.cpp | 30 ++--- .../tests/draft7/ref.json | 84 ++++++++++++ test/uri.cpp | 122 ++++++------------ 6 files changed, 179 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index ea7b846..d7bafa5 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,6 @@ Numerical validation uses nlohmann integer, unsigned and floating point types, d the schema type is "integer" or "number". Bignum (i.e. arbitrary precision and range) is not supported at this time. -Currently JSON-URI with "plain name fragments" are not supported: 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.6.0** of NLohmann's diff --git a/src/json-schema.hpp b/src/json-schema.hpp index 3bd319a..b69e7f8 100644 --- a/src/json-schema.hpp +++ b/src/json-schema.hpp @@ -48,10 +48,12 @@ class JSON_SCHEMA_VALIDATOR_API json_uri { std::string urn_; - std::string proto_; - std::string hostname_; + std::string scheme_; + std::string authority_; std::string path_; - json::json_pointer pointer_; + + json::json_pointer pointer_; // fragment part if JSON-Pointer + std::string identifier_; // fragment part if Locatation Independent ID protected: // decodes a JSON uri and replaces all or part of the currently stored values @@ -59,7 +61,8 @@ protected: std::tuple tie() const { - return std::tie(urn_, proto_, hostname_, path_, pointer_); + return std::tie(urn_, scheme_, authority_, path_, + identifier_ != "" ? identifier_ : pointer_); } public: @@ -68,11 +71,20 @@ public: update(uri); } - const std::string protocol() const { return proto_; } - const std::string hostname() const { return hostname_; } + const std::string scheme() const { return scheme_; } + const std::string authority() const { return authority_; } const std::string path() const { return path_; } const json::json_pointer pointer() const { return pointer_; } + const std::string identifier() const { return identifier_; } + + const std::string fragment() const + { + if (identifier_ == "") + return pointer_; + else + return identifier_; + } const std::string url() const { return location(); } const std::string location() const; @@ -91,6 +103,9 @@ public: // append a pointer-field to the pointer-part of this uri json_uri append(const std::string &field) const { + if (identifier_ != "") + return *this; + json_uri u = *this; u.pointer_ /= field; return u; diff --git a/src/json-uri.cpp b/src/json-uri.cpp index c0e37cb..8b76615 100644 --- a/src/json-uri.cpp +++ b/src/json-uri.cpp @@ -45,15 +45,14 @@ void json_uri::update(const std::string &uri) 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_ = ""; + scheme_ = ""; + authority_ = ""; path_ = ""; } else { // it is an URL @@ -65,13 +64,13 @@ void json_uri::update(const std::string &uri) urn_ = ""; // clear URN-member if URL is parsed - proto_ = location.substr(pos, proto - pos); + scheme_ = 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 authority = location.find("/", pos); + if (authority != std::string::npos) { // and the hostname (no proto without hostname) + authority_ = location.substr(pos, authority - pos); + pos = authority; } } @@ -91,7 +90,13 @@ void json_uri::update(const std::string &uri) } } - pointer_ = json::json_pointer(pointer); + pointer_ = ""_json_pointer; + identifier_ = ""; + + if (pointer[0] == '/') + pointer_ = json::json_pointer(pointer); + else + identifier_ = pointer; } const std::string json_uri::location() const @@ -101,10 +106,10 @@ const std::string json_uri::location() const std::stringstream s; - if (proto_.size() > 0) - s << proto_ << "://"; + if (scheme_.size() > 0) + s << scheme_ << "://"; - s << hostname_ + s << authority_ << path_; return s.str(); @@ -114,7 +119,12 @@ std::string json_uri::to_string() const { std::stringstream s; - s << location() << " # " << pointer_.to_string(); + s << location() << " # "; + + if (identifier_ == "") + s << pointer_.to_string(); + else + s << identifier_; return s.str(); } diff --git a/src/json-validator.cpp b/src/json-validator.cpp index b28d326..b1d91ec 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -83,8 +83,8 @@ class root_schema : public schema std::shared_ptr root_; struct schema_file { - std::map> schemas; - std::map> unresolved; // contains all unresolved references from any other file seen during parsing + std::map> schemas; + std::map> unresolved; // contains all unresolved references from any other file seen during parsing json unknown_keywords; }; @@ -110,16 +110,16 @@ public: void insert(const json_uri &uri, const std::shared_ptr &s) { 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))) { + auto schema = file.schemas.lower_bound(uri.fragment()); + if (schema != file.schemas.end() && !(file.schemas.key_comp()(uri.fragment(), schema->first))) { throw std::invalid_argument("schema with " + uri.to_string() + " already inserted"); return; } - file.schemas.insert({uri.pointer(), s}); + file.schemas.insert({uri.fragment(), s}); // was someone referencing this newly inserted schema? - auto unresolved = file.unresolved.find(uri.pointer()); + auto unresolved = file.unresolved.find(uri.fragment()); if (unresolved != file.unresolved.end()) { unresolved->second->set_target(s); file.unresolved.erase(unresolved); @@ -130,14 +130,14 @@ public: { auto &file = get_or_create_file(uri.location()); auto new_uri = uri.append(key); - auto pointer = new_uri.pointer(); + auto fragment = new_uri.fragment(); // 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); + auto unresolved = file.unresolved.find(fragment); if (unresolved != file.unresolved.end()) schema::make(value, this, {}, {{new_uri}}); else // no, nothing ref'd it - file.unknown_keywords[pointer] = value; + file.unknown_keywords[fragment] = value; } std::shared_ptr get_or_create_ref(const json_uri &uri) @@ -145,26 +145,26 @@ public: auto &file = get_or_create_file(uri.location()); // existing schema - auto schema = file.schemas.find(uri.pointer()); + auto schema = file.schemas.find(uri.fragment()); 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 &subschema = file.unknown_keywords.at(uri.fragment()); auto s = schema::make(subschema, this, {}, {{uri}}); - file.unknown_keywords.erase(uri.pointer()); + file.unknown_keywords.erase(uri.fragment()); return s; } catch (...) { } // get or create a schema_ref - auto r = file.unresolved.lower_bound(uri.pointer()); - if (r != file.unresolved.end() && !(file.unresolved.key_comp()(uri.pointer(), r->first))) { + auto r = file.unresolved.lower_bound(uri.fragment()); + if (r != file.unresolved.end() && !(file.unresolved.key_comp()(uri.fragment(), r->first))) { return r->second; } else { return file.unresolved.insert(r, - {uri.pointer(), std::make_shared(uri.to_string(), this)}) + {uri.fragment(), std::make_shared(uri.to_string(), this)}) ->second; } } diff --git a/test/JSON-Schema-Test-Suite/tests/draft7/ref.json b/test/JSON-Schema-Test-Suite/tests/draft7/ref.json index 3651dac..230c6d0 100644 --- a/test/JSON-Schema-Test-Suite/tests/draft7/ref.json +++ b/test/JSON-Schema-Test-Suite/tests/draft7/ref.json @@ -355,5 +355,89 @@ "valid": false } ] + }, + { + "description": "Location-independent identifier", + "schema": { + "allOf": [{ + "$ref": "#foo" + }], + "definitions": { + "A": { + "$id": "#foo", + "type": "integer" + } + } + }, + "tests": [ + { + "data": 1, + "description": "match", + "valid": true + }, + { + "data": "a", + "description": "mismatch", + "valid": false + } + ] + }, + { + "description": "Location-independent identifier with absolute URI", + "schema": { + "allOf": [{ + "$ref": "http://localhost:1234/bar#foo" + }], + "definitions": { + "A": { + "$id": "http://localhost:1234/bar#foo", + "type": "integer" + } + } + }, + "tests": [ + { + "data": 1, + "description": "match", + "valid": true + }, + { + "data": "a", + "description": "mismatch", + "valid": false + } + ] + }, + { + "description": "Location-independent identifier with base URI change in subschema", + "schema": { + "$id": "http://localhost:1234/root", + "allOf": [{ + "$ref": "http://localhost:1234/nested.json#foo" + }], + "definitions": { + "A": { + "$id": "nested.json", + "definitions": { + "B": { + "$id": "#foo", + "type": "integer" + } + } + } + } + }, + "tests": [ + { + "data": 1, + "description": "match", + "valid": true + }, + { + "data": "a", + "description": "mismatch", + "valid": false + } + ] } ] diff --git a/test/uri.cpp b/test/uri.cpp index 9cb1c21..01097d1 100644 --- a/test/uri.cpp +++ b/test/uri.cpp @@ -16,95 +16,18 @@ 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"( +static void EXPECT_EQ(const std::string &a, const std::string &b) { - "$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" } - } - } + if (a != b) { + std::cerr << "Failed: '" << a << "' != '" << b << "'\n"; + errors++; } } -)"_json; -// resolve all refs -class store +static void EXPECT_EQ(const nlohmann::json_uri &a, const std::string &b) { -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"; -// } -//} + EXPECT_EQ(a.to_string(), b); +} static void paths(json_uri start, const std::string &full, @@ -135,6 +58,31 @@ static void paths(json_uri start, EXPECT_EQ(g, no_path + "/new.json # "); } +static void pointer_plain_name(json_uri start, + const std::string &full, + const std::string &full_path, + const std::string &no_path) +{ + auto a = start.derive("#/json/path"); + EXPECT_EQ(a, full + " # /json/path"); + + a = start.derive("#/json/special_%22"); + EXPECT_EQ(a, full + " # /json/special_\""); + + a = a.derive("#foo"); + EXPECT_EQ(a, full + " # foo"); + + a = a.derive("#foo/looks_like_json/poiner/but/isnt"); + EXPECT_EQ(a, full + " # foo/looks_like_json/poiner/but/isnt"); + EXPECT_EQ(a.identifier(), "foo/looks_like_json/poiner/but/isnt"); + EXPECT_EQ(a.pointer(), ""); + + a = a.derive("#/looks_like_json/poiner/and/it/is"); + EXPECT_EQ(a, full + " # /looks_like_json/poiner/and/it/is"); + EXPECT_EQ(a.pointer(), "/looks_like_json/poiner/and/it/is"); + EXPECT_EQ(a.identifier(), ""); +} + int main(void) { json_uri empty(""); @@ -149,8 +97,10 @@ int main(void) "http://json-schema.org/draft-07", "http://json-schema.org"); - // plain name fragments instead of JSON-pointers, are not supported, yet - //store_test(); + pointer_plain_name(http, + "http://json-schema.org/draft-07/schema", + "http://json-schema.org/draft-07", + "http://json-schema.org"); return errors; }