Merge remote-tracking branch 'origin/binary_support_changes' into binary_support

This commit is contained in:
andrejlevkovitch 2020-05-29 22:24:12 +03:00
commit b56d1eac77
6 changed files with 190 additions and 63 deletions

View File

@ -1,12 +1,12 @@
project(nlohmann_json_schema_validator
LANGUAGES CXX)
LANGUAGES CXX)
set(PROJECT_VERSION 2.1.0)
set(PROJECT_VERSION 2.1.1)
cmake_minimum_required(VERSION 3.2)
option(BUILD_TESTS "Build tests" ON)
option(BUILD_EXAMPLES "Build examples" ON)
option(BUILD_TESTS "Build tests" ON)
option(BUILD_EXAMPLES "Build examples" ON)
# the library
add_library(nlohmann_json_schema_validator

View File

@ -105,6 +105,7 @@ class root_schema : public schema
{
schema_loader loader_;
format_checker format_check_;
content_checker content_check_;
std::shared_ptr<schema> root_;
@ -128,10 +129,18 @@ class root_schema : public schema
public:
root_schema(schema_loader &&loader,
format_checker &&format)
: schema(this), loader_(std::move(loader)), format_check_(std::move(format)) {}
format_checker &&format,
content_checker &&content)
: schema(this),
loader_(std::move(loader)),
format_check_(std::move(format)),
content_check_(std::move(content))
{
}
format_checker &format_check() { return format_check_; }
content_checker &content_check() { return content_check_; }
void insert(const json_uri &uri, const std::shared_ptr<schema> &s)
{
@ -478,7 +487,6 @@ public:
{"boolean", json::value_t::boolean},
{"integer", json::value_t::number_integer},
{"number", json::value_t::number_float},
{"binary", json::value_t::binary},
};
std::set<std::string> known_keywords;
@ -492,37 +500,17 @@ public:
case json::value_t::string: {
auto schema_type = attr.value().get<std::string>();
// add supporting validation binary types
if (schema_type == "string") {
auto found = sch.find("contentEncoding");
if (found != sch.end() && found->get<std::string>() == "binary") {
schema_type = "binary";
}
}
for (auto &t : schema_types)
if (t.first == schema_type)
type_[(uint8_t) t.second] = type_schema::make(sch, t.second, root, uris, known_keywords);
} break;
case json::value_t::array: // "type": ["type1", "type2"]
{
json type_array = attr.value();
auto has_string_type = std::find(type_array.begin(), type_array.end(), "string");
if (has_string_type != type_array.end()) {
auto encodingFound = sch.find("contentEncoding");
if (encodingFound != sch.end() && encodingFound.value() == "binary") {
type_array.emplace_back("binary");
}
}
for (auto &schema_type : type_array)
for (auto &schema_type : attr.value())
for (auto &t : schema_types)
if (t.first == schema_type)
type_[(uint8_t) t.second] = type_schema::make(sch, t.second, root, uris, known_keywords);
} break;
break;
default:
break;
@ -548,6 +536,11 @@ public:
// we stick with JSON-schema: use the integer-validator if instance-value is unsigned
type_[(uint8_t) json::value_t::number_unsigned] = type_[(uint8_t) json::value_t::number_integer];
// special for binary types
if (type_[(uint8_t) json::value_t::string]) {
type_[(uint8_t) json::value_t::binary] = type_[(uint8_t) json::value_t::string];
}
attr = sch.find("enum");
if (attr != sch.end()) {
enum_ = {true, attr.value()};
@ -618,11 +611,12 @@ class string : public schema
#endif
std::pair<bool, std::string> format_;
std::tuple<bool, std::string, std::string> content_{false, "", ""};
std::size_t utf8_length(const std::string &s) const
{
size_t len = 0;
for (const unsigned char &c : s)
for (unsigned char c : s)
if ((c & 0xc0) != 0x80)
len++;
return len;
@ -646,6 +640,24 @@ class string : public schema
}
}
if (std::get<0>(content_)) {
if (root_->content_check() == nullptr)
e.error(ptr, instance, std::string("a content checker was not provided but a contentEncoding or contentMediaType for this string have been present: '") + std::get<1>(content_) + "' '" + std::get<2>(content_) + "'");
else {
try {
root_->content_check()(std::get<1>(content_), std::get<2>(content_), instance);
} catch (const std::exception &ex) {
e.error(ptr, instance, std::string("content-checking failed: ") + ex.what());
}
}
} else if (instance.type() == json::value_t::binary) {
e.error(ptr, instance, "expected string, but get binary data");
}
if (instance.type() != json::value_t::string) {
return; // next checks only for strings
}
#ifndef NO_STD_REGEX
if (pattern_.first &&
!REGEX_NAMESPACE::regex_search(instance.get<std::string>(), pattern_.second))
@ -681,6 +693,37 @@ public:
sch.erase(attr);
}
attr = sch.find("contentEncoding");
if (attr != sch.end()) {
std::get<0>(content_) = true;
std::get<1>(content_) = attr.value().get<std::string>();
// special case for nlohmann::json-binary-types
//
// https://github.com/pboettch/json-schema-validator/pull/114
//
// We cannot use explicitly in a schema: {"type": "binary"} or
// "type": ["binary", "number"] we have to be implicit. For a
// schema where "contentEncoding" is set to "binary", an instance
// of type json::value_t::binary is accepted. If a
// contentEncoding-callback has to be provided and is called
// accordingly. For encoding=binary, no other type validations are done
sch.erase(attr);
}
attr = sch.find("contentMediaType");
if (attr != sch.end()) {
std::get<0>(content_) = true;
std::get<2>(content_) = attr.value().get<std::string>();
sch.erase(attr);
}
if (std::get<0>(content_) == true && root_->content_check() == nullptr) {
throw std::invalid_argument{"schema contains contentEncoding/contentMediaType but content checker was not set"};
}
#ifndef NO_STD_REGEX
attr = sch.find("pattern");
if (attr != sch.end()) {
@ -1110,21 +1153,6 @@ public:
}
};
/**\brief just a placeholder
*/
class binary : public schema
{
void validate(const json::json_pointer &, const json &, json_patch &, error_handler &) const override
{
}
public:
binary(json &, root_schema *root)
: schema(root)
{
}
};
std::shared_ptr<schema> type_schema::make(json &schema,
json::value_t type,
root_schema *root,
@ -1152,8 +1180,8 @@ std::shared_ptr<schema> type_schema::make(json &schema,
case json::value_t::discarded: // not a real type - silence please
break;
case json::value_t::binary: // can use for validate bson or other binary representation of json
return std::make_shared<binary>(schema, root);
case json::value_t::binary:
break;
}
return nullptr;
}
@ -1248,19 +1276,33 @@ namespace json_schema
{
json_validator::json_validator(schema_loader loader,
format_checker format)
: root_(std::unique_ptr<root_schema>(new root_schema(std::move(loader), std::move(format))))
format_checker format,
content_checker content)
: root_(std::unique_ptr<root_schema>(new root_schema(std::move(loader),
std::move(format),
std::move(content))))
{
}
json_validator::json_validator(const json &schema, schema_loader loader, format_checker format)
: json_validator(std::move(loader), std::move(format))
json_validator::json_validator(const json &schema,
schema_loader loader,
format_checker format,
content_checker content)
: json_validator(std::move(loader),
std::move(format),
std::move(content))
{
set_root_schema(schema);
}
json_validator::json_validator(json &&schema, schema_loader loader, format_checker format)
: json_validator(std::move(loader), std::move(format))
json_validator::json_validator(json &&schema,
schema_loader loader,
format_checker format,
content_checker content)
: json_validator(std::move(loader),
std::move(format),
std::move(content))
{
set_root_schema(std::move(schema));
}

View File

@ -133,6 +133,7 @@ extern json draft7_schema_builtin;
typedef std::function<void(const json_uri & /*id*/, json & /*value*/)> schema_loader;
typedef std::function<void(const std::string & /*format*/, const std::string & /*value*/)> format_checker;
typedef std::function<void(const std::string & /*contentEncoding*/, const std::string & /*contentMediaType*/, const json & /*instance*/)> content_checker;
// Interface for validation error handlers
class JSON_SCHEMA_VALIDATOR_API error_handler
@ -168,10 +169,10 @@ class JSON_SCHEMA_VALIDATOR_API json_validator
std::unique_ptr<root_schema> root_;
public:
json_validator(schema_loader = nullptr, format_checker = nullptr);
json_validator(schema_loader = nullptr, format_checker = nullptr, content_checker = nullptr);
json_validator(const json &, schema_loader = nullptr, format_checker = nullptr);
json_validator(json &&, schema_loader = nullptr, format_checker = nullptr);
json_validator(const json &, schema_loader = nullptr, format_checker = nullptr, content_checker = nullptr);
json_validator(json &&, schema_loader = nullptr, format_checker = nullptr, content_checker = nullptr);
json_validator(json_validator &&);
json_validator &operator=(json_validator &&);

View File

@ -50,7 +50,6 @@ if(JSON_SCHEMA_TEST_SUITE_PATH)
# some optional tests will fail
set_tests_properties(
JSON-Suite::Optional::bignum
JSON-Suite::Optional::content
JSON-Suite::Optional::zeroTerminatedFloats
JSON-Suite::Optional::non-bmp-regex

View File

@ -39,6 +39,50 @@ static void loader(const json_uri &uri, json &schema)
}
}
// from here
// https://stackoverflow.com/a/34571089/880584
static std::string base64_decode(const std::string &in)
{
std::string out;
std::vector<int> T(256, -1);
for (int i = 0; i < 64; i++)
T["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i;
unsigned val = 0;
int valb = -8;
for (uint8_t c : in) {
if (c == '=')
break;
if (T[c] == -1) {
throw std::invalid_argument("base64-decode: unexpected character in encode string: '" + std::string(1, c) + "'");
}
val = (val << 6) + T[c];
valb += 6;
if (valb >= 0) {
out.push_back(char((val >> valb) & 0xFF));
valb -= 8;
}
}
return out;
}
static void content(const std::string &contentEncoding, const std::string &contentMediaType, const json &instance)
{
std::string content = instance;
if (contentEncoding == "base64")
content = base64_decode(instance);
else if (contentEncoding != "")
throw std::invalid_argument("unable to check for contentEncoding '" + contentEncoding + "'");
if (contentMediaType == "application/json")
auto dummy = json::parse(content); // throws if conversion fails
else if (contentMediaType != "")
throw std::invalid_argument("unable to check for contentMediaType '" + contentMediaType + "'");
}
int main(void)
{
json validation; // a validation case following the JSON-test-suite-schema
@ -62,7 +106,8 @@ int main(void)
const auto &schema = test_group["schema"];
json_validator validator(loader,
nlohmann::json_schema::default_string_format_check);
nlohmann::json_schema::default_string_format_check,
content);
validator.set_root_schema(schema);

View File

@ -4,7 +4,7 @@
#include <nlohmann/json-schema.hpp>
#include <nlohmann/json.hpp>
int error_count = 0;
static int error_count = 0;
#define EXPECT_EQ(a, b) \
do { \
@ -14,6 +14,19 @@ int error_count = 0;
} \
} while (0)
#define EXPECT_THROW(foo) \
{ \
bool ok = false; \
try { \
foo; \
} catch (std::exception &) { \
ok = true; \
} \
if (ok == false) { \
error_count++; \
} \
}
using json = nlohmann::json;
using validator = nlohmann::json_schema::json_validator;
@ -78,10 +91,24 @@ public:
}
};
static void content(const std::string &contentEncoding, const std::string &contentMediaType, const json &instance)
{
std::cerr << "mediaType: '" << contentMediaType << "', encoding: '" << contentEncoding << "'\n";
if (contentEncoding == "binary") {
if (instance.type() != json::value_t::binary) {
throw std::invalid_argument{"expected binary data"};
}
} else {
if (instance.type() == json::value_t::binary) {
throw std::invalid_argument{"expected string, but get binary"};
}
}
}
int main()
{
validator val;
val.set_root_schema(bson_schema);
validator val(nullptr, nullptr, content);
// create some bson doc
json::binary_t arr;
@ -90,8 +117,12 @@ int main()
json binary = json::binary(arr);
store_ptr_err_handler err;
/////////////////////////////////////
val.set_root_schema(bson_schema);
// all right
store_ptr_err_handler err{};
val.validate({{"standard_string", "some string"}, {"binary_data", binary}}, err);
EXPECT_EQ(err.failed_pointers.size(), 0);
err.reset();
@ -113,9 +144,12 @@ int main()
// check simple types
val.set_root_schema(array_of_types);
val.validate({{"something", "string"}}, err);
val.validate({{"something", 1}}, err);
val.validate({{"something", false}}, err);
// TODO when we set `string` in array and set `contentEncoding` = "binary" - what it means? We expected string or binary?
// Or we expect only binary? Now if you set `contentEncoding` = "binary", then it means that you expect only binary data,
// not string
//val.validate({{"something", "string"}}, err); -> produce error about type
EXPECT_EQ(err.failed_pointers.size(), 0);
err.reset();
@ -124,6 +158,7 @@ int main()
EXPECT_EQ(err.failed_pointers.size(), 0);
err.reset();
/////////////////////////////////////
// and check that you can't set binary data if contentEncoding don't set
val.set_root_schema(array_of_types_without_binary);
val.validate({{"something", binary}}, err);
@ -131,5 +166,10 @@ int main()
EXPECT_EQ(err.failed_pointers[0], "/something");
err.reset();
// check that without content callback you get exception with schema with contentEncoding or contentMeditType
validator val_no_content;
EXPECT_THROW(val_no_content.set_root_schema(bson_schema));
return error_count;
}