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;
}