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:
parent
247c2edbee
commit
3ec0e69a0b
@ -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
|
||||
|
||||
@ -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<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:
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -83,8 +83,8 @@ class root_schema : public schema
|
||||
std::shared_ptr<schema> root_;
|
||||
|
||||
struct schema_file {
|
||||
std::map<json::json_pointer, 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>> schemas;
|
||||
std::map<std::string, std::shared_ptr<schema_ref>> 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<schema> &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<schema> 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<schema_ref>(uri.to_string(), this)})
|
||||
{uri.fragment(), std::make_shared<schema_ref>(uri.to_string(), this)})
|
||||
->second;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
122
test/uri.cpp
122
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<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";
|
||||
// }
|
||||
//}
|
||||
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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user