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
This commit is contained in:
Patrick Boettcher 2019-10-23 14:44:51 +02:00
parent 247c2edbee
commit 3ec0e69a0b
6 changed files with 179 additions and 123 deletions

View File

@ -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 the schema type is "integer" or "number". Bignum (i.e. arbitrary precision and
range) is not supported at this time. 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 # How to use
The current state of the build-system needs at least version **3.6.0** of NLohmann's The current state of the build-system needs at least version **3.6.0** of NLohmann's

View File

@ -48,10 +48,12 @@ class JSON_SCHEMA_VALIDATOR_API json_uri
{ {
std::string urn_; std::string urn_;
std::string proto_; std::string scheme_;
std::string hostname_; std::string authority_;
std::string path_; 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: protected:
// decodes a JSON uri and replaces all or part of the currently stored values // decodes a JSON uri and replaces all or part of the currently stored values
@ -59,7 +61,8 @@ protected:
std::tuple<std::string, std::string, std::string, std::string, std::string> tie() const std::tuple<std::string, std::string, std::string, std::string, std::string> tie() const
{ {
return std::tie(urn_, proto_, hostname_, path_, pointer_); return std::tie(urn_, scheme_, authority_, path_,
identifier_ != "" ? identifier_ : pointer_);
} }
public: public:
@ -68,11 +71,20 @@ public:
update(uri); update(uri);
} }
const std::string protocol() const { return proto_; } const std::string scheme() const { return scheme_; }
const std::string hostname() const { return hostname_; } const std::string authority() const { return authority_; }
const std::string path() const { return path_; } const std::string path() const { return path_; }
const json::json_pointer pointer() const { return pointer_; } 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 url() const { return location(); }
const std::string location() const; const std::string location() const;
@ -91,6 +103,9 @@ public:
// append a pointer-field to the pointer-part of this uri // append a pointer-field to the pointer-part of this uri
json_uri append(const std::string &field) const json_uri append(const std::string &field) const
{ {
if (identifier_ != "")
return *this;
json_uri u = *this; json_uri u = *this;
u.pointer_ /= field; u.pointer_ /= field;
return u; return u;

View File

@ -45,15 +45,14 @@ void json_uri::update(const std::string &uri)
auto location = uri.substr(0, pointer_separator); auto location = uri.substr(0, pointer_separator);
if (location.size()) { // a location part has been found 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 it is an URN take it as it is
if (location.find("urn:") == 0) { if (location.find("urn:") == 0) {
urn_ = location; urn_ = location;
// and clear URL members // and clear URL members
proto_ = ""; scheme_ = "";
hostname_ = ""; authority_ = "";
path_ = ""; path_ = "";
} else { // it is an URL } 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 urn_ = ""; // clear URN-member if URL is parsed
proto_ = location.substr(pos, proto - pos); scheme_ = location.substr(pos, proto - pos);
pos = 3 + proto; // 3 == "://" pos = 3 + proto; // 3 == "://"
auto hostname = location.find("/", pos); auto authority = location.find("/", pos);
if (hostname != std::string::npos) { // and the hostname (no proto without hostname) if (authority != std::string::npos) { // and the hostname (no proto without hostname)
hostname_ = location.substr(pos, hostname - pos); authority_ = location.substr(pos, authority - pos);
pos = hostname; pos = authority;
} }
} }
@ -91,7 +90,13 @@ void json_uri::update(const std::string &uri)
} }
} }
pointer_ = ""_json_pointer;
identifier_ = "";
if (pointer[0] == '/')
pointer_ = json::json_pointer(pointer); pointer_ = json::json_pointer(pointer);
else
identifier_ = pointer;
} }
const std::string json_uri::location() const const std::string json_uri::location() const
@ -101,10 +106,10 @@ const std::string json_uri::location() const
std::stringstream s; std::stringstream s;
if (proto_.size() > 0) if (scheme_.size() > 0)
s << proto_ << "://"; s << scheme_ << "://";
s << hostname_ s << authority_
<< path_; << path_;
return s.str(); return s.str();
@ -114,7 +119,12 @@ std::string json_uri::to_string() const
{ {
std::stringstream s; std::stringstream s;
s << location() << " # " << pointer_.to_string(); s << location() << " # ";
if (identifier_ == "")
s << pointer_.to_string();
else
s << identifier_;
return s.str(); return s.str();
} }

View File

@ -83,8 +83,8 @@ class root_schema : public schema
std::shared_ptr<schema> root_; std::shared_ptr<schema> root_;
struct schema_file { struct schema_file {
std::map<json::json_pointer, std::shared_ptr<schema>> schemas; std::map<std::string, std::shared_ptr<schema>> schemas;
std::map<json::json_pointer, std::shared_ptr<schema_ref>> unresolved; // contains all unresolved references from any other file seen during parsing std::map<std::string, std::shared_ptr<schema_ref>> unresolved; // contains all unresolved references from any other file seen during parsing
json unknown_keywords; json unknown_keywords;
}; };
@ -110,16 +110,16 @@ public:
void insert(const json_uri &uri, const std::shared_ptr<schema> &s) void insert(const json_uri &uri, const std::shared_ptr<schema> &s)
{ {
auto &file = get_or_create_file(uri.location()); auto &file = get_or_create_file(uri.location());
auto schema = file.schemas.lower_bound(uri.pointer()); auto schema = file.schemas.lower_bound(uri.fragment());
if (schema != file.schemas.end() && !(file.schemas.key_comp()(uri.pointer(), schema->first))) { if (schema != file.schemas.end() && !(file.schemas.key_comp()(uri.fragment(), schema->first))) {
throw std::invalid_argument("schema with " + uri.to_string() + " already inserted"); throw std::invalid_argument("schema with " + uri.to_string() + " already inserted");
return; return;
} }
file.schemas.insert({uri.pointer(), s}); file.schemas.insert({uri.fragment(), s});
// was someone referencing this newly inserted schema? // 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()) { if (unresolved != file.unresolved.end()) {
unresolved->second->set_target(s); unresolved->second->set_target(s);
file.unresolved.erase(unresolved); file.unresolved.erase(unresolved);
@ -130,14 +130,14 @@ public:
{ {
auto &file = get_or_create_file(uri.location()); auto &file = get_or_create_file(uri.location());
auto new_uri = uri.append(key); 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 // 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()) if (unresolved != file.unresolved.end())
schema::make(value, this, {}, {{new_uri}}); schema::make(value, this, {}, {{new_uri}});
else // no, nothing ref'd it else // no, nothing ref'd it
file.unknown_keywords[pointer] = value; file.unknown_keywords[fragment] = value;
} }
std::shared_ptr<schema> get_or_create_ref(const json_uri &uri) std::shared_ptr<schema> get_or_create_ref(const json_uri &uri)
@ -145,26 +145,26 @@ public:
auto &file = get_or_create_file(uri.location()); auto &file = get_or_create_file(uri.location());
// existing schema // existing schema
auto schema = file.schemas.find(uri.pointer()); auto schema = file.schemas.find(uri.fragment());
if (schema != file.schemas.end()) if (schema != file.schemas.end())
return schema->second; return schema->second;
// referencing an unknown keyword, turn it into schema // referencing an unknown keyword, turn it into schema
try { try {
auto &subschema = file.unknown_keywords.at(uri.pointer()); auto &subschema = file.unknown_keywords.at(uri.fragment());
auto s = schema::make(subschema, this, {}, {{uri}}); auto s = schema::make(subschema, this, {}, {{uri}});
file.unknown_keywords.erase(uri.pointer()); file.unknown_keywords.erase(uri.fragment());
return s; return s;
} catch (...) { } catch (...) {
} }
// get or create a schema_ref // get or create a schema_ref
auto r = file.unresolved.lower_bound(uri.pointer()); auto r = file.unresolved.lower_bound(uri.fragment());
if (r != file.unresolved.end() && !(file.unresolved.key_comp()(uri.pointer(), r->first))) { if (r != file.unresolved.end() && !(file.unresolved.key_comp()(uri.fragment(), r->first))) {
return r->second; return r->second;
} else { } else {
return file.unresolved.insert(r, return file.unresolved.insert(r,
{uri.pointer(), std::make_shared<schema_ref>(uri.to_string(), this)}) {uri.fragment(), std::make_shared<schema_ref>(uri.to_string(), this)})
->second; ->second;
} }
} }

View File

@ -355,5 +355,89 @@
"valid": false "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
}
]
} }
] ]

View File

@ -16,95 +16,18 @@ using nlohmann::json_uri;
static int errors; static int errors;
#define EXPECT_EQ(a, b) \ static void EXPECT_EQ(const std::string &a, const std::string &b)
do { \
if (a.to_string() != b) { \
std::cerr << "Failed: '" << a << "' != '" << b << "'\n"; \
errors++; \
} \
} while (0)
// test-schema taken from spec with modifications
auto schema = R"(
{ {
"$id": "http://example.com/root.json", if (a != b) {
"definitions": { std::cerr << "Failed: '" << a << "' != '" << b << "'\n";
"A": { "$id": "#foo" }, errors++;
"B": {
"$id": "other.json",
"definitions": {
"X": { "$id": "#bar" },
"Y": { "$id": "t/inner.json" }
}
},
"C": {
"$id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f",
"definitions": {
"Z": { "$id": "#bar" },
"9": { "$id": "http://example.com/drole.json" }
}
}
} }
} }
)"_json;
// resolve all refs static void EXPECT_EQ(const nlohmann::json_uri &a, const std::string &b)
class store
{ {
public: EXPECT_EQ(a.to_string(), b);
std::vector<json> schemas_; }
std::map<nlohmann::json_uri, const json *> schema_store_;
public:
void insert_schema(json &s, std::vector<nlohmann::json_uri> 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<nlohmann::json_uri> subschema_uri = base_uris;
for (auto &ss : subschema_uri)
ss = ss.append(nlohmann::json_uri::escape(i.key()));
insert_schema(i.value(), subschema_uri);
} break;
case json::value_t::string:
// this schema is a reference
if (i.key() == "$ref") {
auto id = base_uris.back().derive(i.value());
i.value() = id.to_string();
}
break;
default:
break;
}
}
}
};
//static void store_test(void)
//{
// store s;
//
// s.insert_schema(schema, {{""}});
//
// for (auto &i : s.schema_store_) {
// std::cerr << i.first << " " << *i.second << "\n";
// }
//}
static void paths(json_uri start, static void paths(json_uri start,
const std::string &full, const std::string &full,
@ -135,6 +58,31 @@ static void paths(json_uri start,
EXPECT_EQ(g, no_path + "/new.json # "); 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) int main(void)
{ {
json_uri empty(""); json_uri empty("");
@ -149,8 +97,10 @@ int main(void)
"http://json-schema.org/draft-07", "http://json-schema.org/draft-07",
"http://json-schema.org"); "http://json-schema.org");
// plain name fragments instead of JSON-pointers, are not supported, yet pointer_plain_name(http,
//store_test(); "http://json-schema.org/draft-07/schema",
"http://json-schema.org/draft-07",
"http://json-schema.org");
return errors; return errors;
} }