diff options
author | Richard <q@1bpm.net> | 2022-09-04 00:32:56 +0100 |
---|---|---|
committer | Richard <q@1bpm.net> | 2022-09-04 00:32:56 +0100 |
commit | 1d055261b4144dbf86b2658437015b15d4dd9bff (patch) | |
tree | 6049b19d1bf953a650383de1a5e438b8b82679f6 /include/jsoncons_ext/jsonschema | |
download | csound-json-1d055261b4144dbf86b2658437015b15d4dd9bff.tar.gz csound-json-1d055261b4144dbf86b2658437015b15d4dd9bff.tar.bz2 csound-json-1d055261b4144dbf86b2658437015b15d4dd9bff.zip |
initial
Diffstat (limited to 'include/jsoncons_ext/jsonschema')
-rw-r--r-- | include/jsoncons_ext/jsonschema/format_validator.hpp | 968 | ||||
-rw-r--r-- | include/jsoncons_ext/jsonschema/json_validator.hpp | 120 | ||||
-rw-r--r-- | include/jsoncons_ext/jsonschema/jsonschema.hpp | 13 | ||||
-rw-r--r-- | include/jsoncons_ext/jsonschema/jsonschema_error.hpp | 105 | ||||
-rw-r--r-- | include/jsoncons_ext/jsonschema/jsonschema_version.hpp | 18 | ||||
-rw-r--r-- | include/jsoncons_ext/jsonschema/keyword_validator.hpp | 1745 | ||||
-rw-r--r-- | include/jsoncons_ext/jsonschema/keyword_validator_factory.hpp | 556 | ||||
-rw-r--r-- | include/jsoncons_ext/jsonschema/schema_draft7.hpp | 198 | ||||
-rw-r--r-- | include/jsoncons_ext/jsonschema/schema_location.hpp | 200 | ||||
-rw-r--r-- | include/jsoncons_ext/jsonschema/schema_version.hpp | 35 | ||||
-rw-r--r-- | include/jsoncons_ext/jsonschema/subschema.hpp | 144 |
11 files changed, 4102 insertions, 0 deletions
diff --git a/include/jsoncons_ext/jsonschema/format_validator.hpp b/include/jsoncons_ext/jsonschema/format_validator.hpp new file mode 100644 index 0000000..312bf41 --- /dev/null +++ b/include/jsoncons_ext/jsonschema/format_validator.hpp @@ -0,0 +1,968 @@ +// Copyright 2020 Daniel Parker +// Distributed under the Boost license, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// See https://github.com/danielaparker/jsoncons for latest version + +#ifndef JSONCONS_JSONSCHEMA_FORMAT_VALIDATOR_HPP +#define JSONCONS_JSONSCHEMA_FORMAT_VALIDATOR_HPP + +#include <jsoncons/config/jsoncons_config.hpp> +#include <jsoncons/uri.hpp> +#include <jsoncons/json.hpp> +#include <jsoncons_ext/jsonpointer/jsonpointer.hpp> +#include <jsoncons_ext/jsonschema/subschema.hpp> +#include <cassert> +#include <set> +#include <sstream> +#include <iostream> +#include <cassert> +#if defined(JSONCONS_HAS_STD_REGEX) +#include <regex> +#endif + +namespace jsoncons { +namespace jsonschema { + + inline + bool is_atext( char c) + { + switch (c) + { + case '!': + case '#': + case '$': + case '%': + case '&': + case '\'': + case '*': + case '+': + case '-': + case '/': + case '=': + case '?': + case '^': + case '_': + case '`': + case '{': + case '|': + case '}': + case '~': + return true; + default: + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + } + + inline + bool is_dtext( char c) + { + return (c >= 33 && c <= 90) || (c >= 94 && c <= 126); + } + + // RFC 5322, section 3.4.1 + inline + bool validate_email_rfc5322(const std::string& s) + { + enum class state_t {local_part,atom,dot_atom,quoted_string,amp,domain}; + + state_t state = state_t::local_part; + std::size_t part_length = 0; + + for (char c : s) + { + switch (state) + { + case state_t::local_part: + { + if (is_atext(c)) + { + state = state_t::atom; + } + else if (c == '"') + { + state = state_t::quoted_string; + } + else + { + return false; + } + break; + } + case state_t::dot_atom: + { + if (is_atext(c)) + { + ++part_length; + state = state_t::atom; + } + else + return false; + break; + } + case state_t::atom: + { + switch (c) + { + case '@': + state = state_t::domain; + part_length = 0; + break; + case '.': + state = state_t::dot_atom; + ++part_length; + break; + default: + if (is_atext(c)) + ++part_length; + else + return false; + break; + } + break; + } + case state_t::quoted_string: + { + if (c == '\"') + { + state = state_t::amp; + } + else + { + ++part_length; + } + break; + } + case state_t::amp: + { + if (c == '@') + { + state = state_t::domain; + part_length = 0; + } + else + { + return false; + } + break; + } + case state_t::domain: + { + if (is_dtext(c)) + { + ++part_length; + } + else + { + return false; + } + break; + } + } + } + + return state == state_t::domain && part_length > 0; + } + + // RFC 2673, Section 3.2 + + inline + bool validate_ipv6_rfc2373(const std::string& s) + { + enum class state_t{start,expect_hexdig_or_unspecified, + hexdig, decdig,expect_unspecified, unspecified}; + + state_t state = state_t::start; + + std::size_t digit_count = 0; + std::size_t piece_count = 0; + std::size_t piece_count2 = 0; + bool has_unspecified = false; + std::size_t dec_value = 0; + + for (std::size_t i = 0; i < s.length(); ++i) + { + char c = s[i]; + switch (state) + { + case state_t::start: + { + switch (c) + { + case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8': case '9': + case 'A':case 'B':case 'C':case 'D':case 'E':case 'F': + case 'a':case 'b':case 'c':case 'd':case 'e':case 'f': + state = state_t::hexdig; + ++digit_count; + piece_count = 0; + break; + case ':': + if (!has_unspecified) + { + state = state_t::expect_unspecified; + } + else + { + return false; + } + break; + default: + return false; + } + break; + } + case state_t::expect_hexdig_or_unspecified: + { + switch (c) + { + case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8': case '9': + dec_value = dec_value*10 + static_cast<std::size_t>(c - '0'); // just in case this piece is followed by a dot + state = state_t::hexdig; + ++digit_count; + break; + case 'A':case 'B':case 'C':case 'D':case 'E':case 'F': + case 'a':case 'b':case 'c':case 'd':case 'e':case 'f': + state = state_t::hexdig; + ++digit_count; + break; + case ':': + if (!has_unspecified) + { + has_unspecified = true; + state = state_t::unspecified; + } + else + { + return false; + } + break; + default: + return false; + } + break; + } + case state_t::expect_unspecified: + { + if (c == ':') + { + has_unspecified = true; + state = state_t::unspecified; + } + else + { + return false; + } + break; + } + case state_t::hexdig: + { + switch (c) + { + case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8': case '9': + case 'A':case 'B':case 'C':case 'D':case 'E':case 'F': + case 'a':case 'b':case 'c':case 'd':case 'e':case 'f': + ++digit_count; + break; + case ':': + if (digit_count <= 4) + { + ++piece_count; + digit_count = 0; + dec_value = 0; + state = state_t::expect_hexdig_or_unspecified; + } + else + { + return false; + } + break; + case '.': + if (piece_count == 6 || has_unspecified) + { + ++piece_count2; + state = state_t::decdig; + dec_value = 0; + } + else + { + return false; + } + break; + default: + return false; + } + break; + } + case state_t::decdig: + { + switch (c) + { + case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8': case '9': + dec_value = dec_value*10 + static_cast<std::size_t>(c - '0'); + ++digit_count; + break; + case '.': + if (dec_value > 0xff) + { + return false; + } + digit_count = 0; + dec_value = 0; + ++piece_count2; + break; + default: + return false; + } + break; + } + case state_t::unspecified: + { + switch (c) + { + case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8': case '9': + case 'A':case 'B':case 'C':case 'D':case 'E':case 'F': + case 'a':case 'b':case 'c':case 'd':case 'e':case 'f': + state = state_t::hexdig; + ++digit_count; + break; + default: + return false; + } + break; + } + default: + return false; + } + } + + switch (state) + { + case state_t::unspecified: + return piece_count <= 8; + case state_t::hexdig: + if (digit_count <= 4) + { + ++piece_count; + return digit_count > 0 && (piece_count == 8 || (has_unspecified && piece_count <= 8)); + } + else + { + return false; + } + case state_t::decdig: + ++piece_count2; + if (dec_value > 0xff) + { + return false; + } + return digit_count > 0 && piece_count2 == 4; + default: + return false; + } + } + + // RFC 2673, Section 3.2 + + inline + bool validate_ipv4_rfc2673(const std::string& s) + { + enum class state_t {expect_indicator_or_dotted_quad,decbyte, + bindig, octdig, hexdig}; + + state_t state = state_t::expect_indicator_or_dotted_quad; + + std::size_t digit_count = 0; + std::size_t decbyte_count = 0; + std::size_t value = 0; + + for (std::size_t i = 0; i < s.length(); ++i) + { + char c = s[i]; + switch (state) + { + case state_t::expect_indicator_or_dotted_quad: + { + switch (c) + { + case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8': case '9': + state = state_t::decbyte; + decbyte_count = 0; + digit_count = 1; + value = 0; + break; + case 'b': + state = state_t::bindig; + digit_count = 0; + break; + case 'o': + state = state_t::octdig; + digit_count = 0; + break; + case 'x': + state = state_t::hexdig; + digit_count = 0; + break; + default: + return false; + } + break; + } + case state_t::bindig: + { + if (digit_count >= 256) + { + return false; + } + switch (c) + { + case '0':case '1': + ++digit_count; + break; + default: + return false; + } + break; + } + case state_t::octdig: + { + if (digit_count >= 86) + { + return false; + } + switch (c) + { + case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7': + ++digit_count; + break; + default: + return false; + } + break; + } + case state_t::hexdig: + { + if (digit_count >= 64) + { + return false; + } + switch (c) + { + case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8': case '9': + case 'A':case 'B':case 'C':case 'D':case 'E':case 'F': + case 'a':case 'b':case 'c':case 'd':case 'e':case 'f': + ++digit_count; + break; + default: + return false; + } + break; + } + case state_t::decbyte: + { + if (decbyte_count >= 4) + { + return false; + } + switch (c) + { + case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8': case '9': + { + if (digit_count >= 3) + { + return false; + } + ++digit_count; + value = value*10 + static_cast<std::size_t>(c - '0'); + if (value > 255) + { + return false; + } + break; + } + case '.': + if (decbyte_count > 3) + { + return false; + } + ++decbyte_count; + digit_count = 0; + value = 0; + break; + default: + return false; + } + break; + } + default: + return false; + } + } + + switch (state) + { + case state_t::decbyte: + if (digit_count > 0) + { + ++decbyte_count; + } + else + { + return false; + } + return (decbyte_count == 4) ? true : false; + case state_t::bindig: + return digit_count > 0 ? true : false; + case state_t::octdig: + return digit_count > 0 ? true : false; + case state_t::hexdig: + return digit_count > 0 ? true : false; + default: + return false; + } + } + + // RFC 1034, Section 3.1 + inline + bool validate_hostname_rfc1034(const std::string& hostname) + { + enum class state_t {start_label,expect_letter_or_digit_or_hyphen_or_dot}; + + state_t state = state_t::start_label; + std::size_t length = hostname.length() - 1; + std::size_t label_length = 0; + + for (std::size_t i = 0; i < length; ++i) + { + char c = hostname[i]; + switch (state) + { + case state_t::start_label: + { + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) + { + ++label_length; + state = state_t::expect_letter_or_digit_or_hyphen_or_dot; + } + else + { + return false; + } + break; + } + case state_t::expect_letter_or_digit_or_hyphen_or_dot: + { + if (c == '.') + { + label_length = 0; + state = state_t::start_label; + } + else if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c < '9') || c == '-')) + { + return false; + } + if (++label_length > 63) + { + return false; + } + break; + } + } + } + + char last = hostname.back(); + if (!((last >= 'a' && last <= 'z') || (last >= 'A' && last <= 'Z') || (last >= '0' && last < '9'))) + { + return false; + } + return true; + } + + inline + bool is_leap_year(std::size_t year) + { + return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); + } + + inline + std::size_t days_in_month(std::size_t year, std::size_t month) + { + switch (month) + { + case 1: return 31; + case 2: return is_leap_year(year) ? 29 : 28; + case 3: return 31; + case 4: return 30; + case 5: return 31; + case 6: return 30; + case 7: return 31; + case 8: return 31; + case 9: return 30; + case 10: return 31; + case 11: return 30; + case 12: return 31; + default: + JSONCONS_UNREACHABLE(); + break; + } + } + + enum class date_time_type {date_time,date,time}; + // RFC 3339, Section 5.6 + inline + bool validate_date_time_rfc3339(const std::string& s, date_time_type type) + { + enum class state_t {fullyear,month,mday,hour,minute,second,secfrac,z,offset_hour,offset_minute}; + + std::size_t piece_length = 0; + std::size_t year = 0; + std::size_t month = 0; + std::size_t mday = 0; + std::size_t value = 0; + state_t state = (type == date_time_type::time) ? state_t::hour : state_t::fullyear; + + for (char c : s) + { + switch (state) + { + case state_t::fullyear: + { + if (piece_length < 4 && (c >= '0' && c <= '9')) + { + piece_length++; + year = year*10 + static_cast<std::size_t>(c - '0'); + } + else if (c == '-' && piece_length == 4) + { + state = state_t::month; + piece_length = 0; + } + else + { + return false; + } + break; + } + case state_t::month: + { + if (piece_length < 2 && (c >= '0' && c <= '9')) + { + piece_length++; + month = month*10 + static_cast<std::size_t>(c - '0'); + } + else if (c == '-' && piece_length == 2 && (month >=1 && month <= 12)) + { + state = state_t::mday; + piece_length = 0; + } + else + { + return false; + } + break; + } + case state_t::mday: + { + if (piece_length < 2 && (c >= '0' && c <= '9')) + { + piece_length++; + mday = mday *10 + static_cast<std::size_t>(c - '0'); + } + else if ((c == 'T' || c == 't') && piece_length == 2 && (mday <= days_in_month(year, month))) + { + piece_length = 0; + state = state_t::hour; + } + else + { + return false; + } + break; + } + case state_t::hour: + { + if (piece_length < 2 && (c >= '0' && c <= '9')) + { + piece_length++; + value = value*10 + static_cast<std::size_t>(c - '0'); + } + else if (c == ':' && piece_length == 2 && (/*value >=0 && */ value <= 23)) + { + state = state_t::minute; + value = 0; + piece_length = 0; + } + else + { + return false; + } + break; + } + case state_t::minute: + { + if (piece_length < 2 && (c >= '0' && c <= '9')) + { + piece_length++; + value = value*10 + static_cast<std::size_t>(c - '0'); + } + else if (c == ':' && piece_length == 2 && (/*value >=0 && */value <= 59)) + { + state = state_t::second; + value = 0; + piece_length = 0; + } + else + { + return false; + } + break; + } + case state_t::second: + { + if (piece_length < 2 && (c >= '0' && c <= '9')) + { + piece_length++; + value = value*10 + static_cast<std::size_t>(c - '0'); + } + else if (piece_length == 2 && (/*value >=0 && */value <= 60)) // 00-58, 00-59, 00-60 based on leap second rules + { + switch (c) + { + case '.': + value = 0; + state = state_t::secfrac; + break; + case '+': + case '-': + value = 0; + piece_length = 0; + state = state_t::offset_hour; + break; + case 'Z': + case 'z': + state = state_t::z; + break; + default: + return false; + } + } + else + { + return false; + } + break; + } + case state_t::secfrac: + { + if (c >= '0' && c <= '9') + { + value = value*10 + static_cast<std::size_t>(c - '0'); + } + else + { + switch (c) + { + case '+': + case '-': + value = 0; + piece_length = 0; + state = state_t::offset_hour; + break; + case 'Z': + case 'z': + state = state_t::z; + break; + default: + return false; + } + } + break; + } + case state_t::offset_hour: + { + if (piece_length < 2 && (c >= '0' && c <= '9')) + { + piece_length++; + value = value*10 + static_cast<std::size_t>(c - '0'); + } + else if (c == ':' && piece_length == 2 && (/*value >=0 && */value <= 23)) + { + value = 0; + piece_length = 0; + state = state_t::offset_minute; + } + else + { + return false; + } + break; + } + case state_t::offset_minute: + { + if (piece_length < 2 && (c >= '0' && c <= '9')) + { + piece_length++; + value = value*10 + static_cast<std::size_t>(c - '0'); + } + else if (c == ':' && piece_length == 2 && (/*value >=0 && */value <= 59)) + { + value = 0; + piece_length = 0; + } + else + { + return false; + } + break; + } + case state_t::z: + return false; + } + } + + if (type == date_time_type::date) + { + return state == state_t::mday && piece_length == 2 && (mday >= 1 && mday <= days_in_month(year, month)); + } + else + { + return state == state_t::offset_minute || state == state_t::z || state == state_t::secfrac; + } + } + + // format checkers + using format_checker = std::function<void(const std::string& absolute_keyword_location, + const jsonpointer::json_pointer& instance_location, + const std::string&, + error_reporter& reporter)>; + + inline + void rfc3339_date_check(const std::string& absolute_keyword_location, + const jsonpointer::json_pointer& instance_location, + const std::string& value, + error_reporter& reporter) + { + if (!validate_date_time_rfc3339(value,date_time_type::date)) + { + reporter.error(validation_output("date", + absolute_keyword_location, + instance_location.to_uri_fragment(), + "\"" + value + "\" is not a RFC 3339 date string")); + } + } + + inline + void rfc3339_time_check(const std::string& absolute_keyword_location, + const jsonpointer::json_pointer& instance_location, + const std::string &value, + error_reporter& reporter) + { + if (!validate_date_time_rfc3339(value, date_time_type::time)) + { + reporter.error(validation_output("time", + absolute_keyword_location, + instance_location.to_uri_fragment(), + "\"" + value + "\" is not a RFC 3339 time string")); + } + } + + inline + void rfc3339_date_time_check(const std::string& absolute_keyword_location, + const jsonpointer::json_pointer& instance_location, + const std::string &value, + error_reporter& reporter) + { + if (!validate_date_time_rfc3339(value, date_time_type::date_time)) + { + reporter.error(validation_output("date-time", + absolute_keyword_location, + instance_location.to_uri_fragment(), + "\"" + value + "\" is not a RFC 3339 date-time string")); + } + } + + inline + void email_check(const std::string& absolute_keyword_location, + const jsonpointer::json_pointer& instance_location, + const std::string& value, + error_reporter& reporter) + { + if (!validate_email_rfc5322(value)) + { + reporter.error(validation_output("email", + absolute_keyword_location, + instance_location.to_uri_fragment(), + "\"" + value + "\" is not a valid email address as defined by RFC 5322")); + } + } + + inline + void hostname_check(const std::string& absolute_keyword_location, + const jsonpointer::json_pointer& instance_location, + const std::string& value, + error_reporter& reporter) + { + if (!validate_hostname_rfc1034(value)) + { + reporter.error(validation_output("hostname", + absolute_keyword_location, + instance_location.to_uri_fragment(), + "\"" + value + "\" is not a valid hostname as defined by RFC 3986 Appendix A")); + } + } + + inline + void ipv4_check(const std::string& absolute_keyword_location, + const jsonpointer::json_pointer& instance_location, + const std::string& value, + error_reporter& reporter) + { + if (!validate_ipv4_rfc2673(value)) + { + reporter.error(validation_output("ipv4", + absolute_keyword_location, + instance_location.to_uri_fragment(), + "\"" + value + "\" is not a valid IPv4 address as defined by RFC 2673")); + } + } + + inline + void ipv6_check(const std::string& absolute_keyword_location, + const jsonpointer::json_pointer& instance_location, + const std::string& value, + error_reporter& reporter) + { + if (!validate_ipv6_rfc2373(value)) + { + reporter.error(validation_output("ipv6", + absolute_keyword_location, + instance_location.to_uri_fragment(), + "\"" + value + "\" is not a valid IPv6 address as defined by RFC 2373")); + } + } + + inline + void regex_check(const std::string& absolute_keyword_location, + const jsonpointer::json_pointer& instance_location, + const std::string& value, + error_reporter& reporter) + { +#if defined(JSONCONS_HAS_STD_REGEX) + try + { + std::regex re(value, std::regex::ECMAScript); + } + catch (const std::exception& e) + { + reporter.error(validation_output("pattern", + absolute_keyword_location, + instance_location.to_uri_fragment(), + "\"" + value + "\" is not a valid ECMAScript regular expression. " + e.what())); + } +#endif + } + +} // namespace jsonschema +} // namespace jsoncons + +#endif // JSONCONS_JSONSCHEMA_FORMAT_CHECKERS_HPP diff --git a/include/jsoncons_ext/jsonschema/json_validator.hpp b/include/jsoncons_ext/jsonschema/json_validator.hpp new file mode 100644 index 0000000..87bec58 --- /dev/null +++ b/include/jsoncons_ext/jsonschema/json_validator.hpp @@ -0,0 +1,120 @@ +// Copyright 2020 Daniel Parker +// Distributed under the Boost license, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// See https://github.com/danielaparker/jsoncons for latest version + +#ifndef JSONCONS_JSONSCHEMA_JSON_VALIDATOR_HPP +#define JSONCONS_JSONSCHEMA_JSON_VALIDATOR_HPP + +#include <jsoncons/config/jsoncons_config.hpp> +#include <jsoncons/uri.hpp> +#include <jsoncons/json.hpp> +#include <jsoncons_ext/jsonpointer/jsonpointer.hpp> +#include <jsoncons_ext/jsonschema/keyword_validator_factory.hpp> +#include <cassert> +#include <set> +#include <sstream> +#include <iostream> +#include <cassert> +#include <functional> + +namespace jsoncons { +namespace jsonschema { + + class throwing_error_reporter : public error_reporter + { + void do_error(const validation_output& o) override + { + JSONCONS_THROW(validation_error(o.message())); + } + }; + + class fail_early_reporter : public error_reporter + { + void do_error(const validation_output&) override + { + } + public: + fail_early_reporter() + : error_reporter(true) + { + } + }; + + using error_reporter_t = std::function<void(const validation_output& o)>; + + struct error_reporter_adaptor : public error_reporter + { + error_reporter_t reporter_; + + error_reporter_adaptor(const error_reporter_t& reporter) + : reporter_(reporter) + { + } + private: + void do_error(const validation_output& e) override + { + reporter_(e); + } + }; + + template <class Json> + class json_validator + { + std::shared_ptr<json_schema<Json>> root_; + + public: + json_validator(std::shared_ptr<json_schema<Json>> root) + : root_(root) + { + } + + json_validator(json_validator &&) = default; + json_validator &operator=(json_validator &&) = default; + + json_validator(json_validator const &) = delete; + json_validator &operator=(json_validator const &) = delete; + + ~json_validator() = default; + + // Validate input JSON against a JSON Schema with a default throwing error reporter + Json validate(const Json& instance) const + { + throwing_error_reporter reporter; + jsonpointer::json_pointer instance_location("#"); + Json patch(json_array_arg); + + root_->validate(instance, instance_location, reporter, patch); + return patch; + } + + // Validate input JSON against a JSON Schema + bool is_valid(const Json& instance) const + { + fail_early_reporter reporter; + jsonpointer::json_pointer instance_location("#"); + Json patch(json_array_arg); + + root_->validate(instance, instance_location, reporter, patch); + return reporter.error_count() == 0; + } + + // Validate input JSON against a JSON Schema with a provided error reporter + template <class Reporter> + typename std::enable_if<type_traits::is_unary_function_object_exact<Reporter,void,validation_output>::value,Json>::type + validate(const Json& instance, const Reporter& reporter) const + { + jsonpointer::json_pointer instance_location("#"); + Json patch(json_array_arg); + + error_reporter_adaptor adaptor(reporter); + root_->validate(instance, instance_location, adaptor, patch); + return patch; + } + }; + +} // namespace jsonschema +} // namespace jsoncons + +#endif // JSONCONS_JSONSCHEMA_JSON_VALIDATOR_HPP diff --git a/include/jsoncons_ext/jsonschema/jsonschema.hpp b/include/jsoncons_ext/jsonschema/jsonschema.hpp new file mode 100644 index 0000000..e2c4210 --- /dev/null +++ b/include/jsoncons_ext/jsonschema/jsonschema.hpp @@ -0,0 +1,13 @@ +// Copyright 2020 Daniel Parker +// Distributed under the Boost license, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// See https://github.com/danielaparker/jsoncons for latest version + +#ifndef JSONCONS_JSONSCHEMA_JSONSCHEMA_HPP +#define JSONCONS_JSONSCHEMA_JSONSCHEMA_HPP + +#include <jsoncons_ext/jsonschema/keyword_validator.hpp> +#include <jsoncons_ext/jsonschema/json_validator.hpp> + +#endif // JSONCONS_JSONSCHEMA_JSONSCHEMA_HPP diff --git a/include/jsoncons_ext/jsonschema/jsonschema_error.hpp b/include/jsoncons_ext/jsonschema/jsonschema_error.hpp new file mode 100644 index 0000000..7cb1061 --- /dev/null +++ b/include/jsoncons_ext/jsonschema/jsonschema_error.hpp @@ -0,0 +1,105 @@ +/// Copyright 2020 Daniel Parker +// Distributed under the Boost license, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// See https://github.com/danielaparker/jsoncons for latest version + +#ifndef JSONCONS_JSONSCHEMA_JSONSCHEMA_ERROR_HPP +#define JSONCONS_JSONSCHEMA_JSONSCHEMA_ERROR_HPP + +#include <jsoncons/json_exception.hpp> +#include <system_error> + +namespace jsoncons { +namespace jsonschema { + + class schema_error : public std::runtime_error, public virtual json_exception + { + public: + schema_error(const std::string& message) + : std::runtime_error(message) + { + } + + const char* what() const noexcept override + { + return std::runtime_error::what(); + } + }; + + class validation_error : public std::runtime_error, public virtual json_exception + { + public: + validation_error(const std::string& message) + : std::runtime_error(message) + { + } + + const char* what() const noexcept override + { + return std::runtime_error::what(); + } + }; + + class validation_output + { + std::string keyword_; + std::string absolute_keyword_location_; + std::string instance_location_; + std::string message_; + std::vector<validation_output> nested_errors_; + public: + validation_output(std::string keyword, + std::string absolute_keyword_location, + std::string instance_location, + std::string message) + : keyword_(std::move(keyword)), + absolute_keyword_location_(std::move(absolute_keyword_location)), + instance_location_(std::move(instance_location)), + message_(std::move(message)) + { + } + + validation_output(const std::string& keyword, + const std::string& absolute_keyword_location, + const std::string& instance_location, + const std::string& message, + const std::vector<validation_output>& nested_errors) + : keyword_(keyword), + absolute_keyword_location_(absolute_keyword_location), + instance_location_(instance_location), + message_(message), + nested_errors_(nested_errors) + { + } + + const std::string& instance_location() const + { + return instance_location_; + } + + const std::string& message() const + { + return message_; + } + + const std::string& absolute_keyword_location() const + { + return absolute_keyword_location_; + } + + const std::string& keyword() const + { + return keyword_; + } + + const std::vector<validation_output>& nested_errors() const + { + return nested_errors_; + } + }; + +} // namespace jsonschema +} // namespace jsoncons + +#endif // JSONCONS_JSONSCHEMA_JSONSCHEMA_ERROR_HPP diff --git a/include/jsoncons_ext/jsonschema/jsonschema_version.hpp b/include/jsoncons_ext/jsonschema/jsonschema_version.hpp new file mode 100644 index 0000000..bf0afff --- /dev/null +++ b/include/jsoncons_ext/jsonschema/jsonschema_version.hpp @@ -0,0 +1,18 @@ +// Copyright 2021 Daniel Parker +// Distributed under the Boost license, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// See https://github.com/danielaparker/jsoncons for latest version + +#ifndef JSONCONS_JSONSCHEMA_JSONSCHEMA_VERSION_HPP +#define JSONCONS_JSONSCHEMA_JSONSCHEMA_VERSION_HPP + +#include <jsoncons/json.hpp> + +namespace jsoncons { +namespace jsonschema { + +} // namespace jsonschema +} // namespace jsoncons + +#endif // JSONCONS_JSONSCHEMA_JSONSCHEMA_VERSION_HPP diff --git a/include/jsoncons_ext/jsonschema/keyword_validator.hpp b/include/jsoncons_ext/jsonschema/keyword_validator.hpp new file mode 100644 index 0000000..249c7d0 --- /dev/null +++ b/include/jsoncons_ext/jsonschema/keyword_validator.hpp @@ -0,0 +1,1745 @@ +// Copyright 2020 Daniel Parker +// Distributed under the Boost license, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// See https://github.com/danielaparker/jsoncons for latest version + +#ifndef JSONCONS_JSONSCHEMA_KEYWORD_VALIDATOR_HPP +#define JSONCONS_JSONSCHEMA_KEYWORD_VALIDATOR_HPP + +#include <jsoncons/config/jsoncons_config.hpp> +#include <jsoncons/uri.hpp> +#include <jsoncons/json.hpp> +#include <jsoncons_ext/jsonpointer/jsonpointer.hpp> +#include <jsoncons_ext/jsonschema/subschema.hpp> +#include <jsoncons_ext/jsonschema/format_validator.hpp> +#include <cassert> +#include <set> +#include <sstream> +#include <iostream> +#include <cassert> +#if defined(JSONCONS_HAS_STD_REGEX) +#include <regex> +#endif + +namespace jsoncons { +namespace jsonschema { + + template <class Json> + class abstract_keyword_validator_factory + { + public: + using validator_pointer = typename keyword_validator<Json>::self_pointer; + + virtual ~abstract_keyword_validator_factory() = default; + + virtual validator_pointer make_keyword_validator(const Json& schema, + const std::vector<schema_location>& uris, + const std::vector<std::string>& keys) = 0; + virtual validator_pointer make_required_validator(const std::vector<schema_location>& uris, + const std::vector<std::string>& items) = 0; + + virtual validator_pointer make_null_validator(const std::vector<schema_location>& uris) = 0; + + virtual validator_pointer make_true_validator(const std::vector<schema_location>& uris) = 0; + + virtual validator_pointer make_false_validator(const std::vector<schema_location>& uris) = 0; + + virtual validator_pointer make_object_validator(const Json& sch, + const std::vector<schema_location>& uris) = 0; + + virtual validator_pointer make_array_validator(const Json& sch, + const std::vector<schema_location>& uris) = 0; + + virtual validator_pointer make_string_validator(const Json& sch, + const std::vector<schema_location>& uris) = 0; + + virtual validator_pointer make_boolean_validator(const std::vector<schema_location>& uris) = 0; + + virtual validator_pointer make_integer_validator(const Json& sch, + const std::vector<schema_location>& uris, + std::set<std::string>& keywords) = 0; + + virtual validator_pointer make_number_validator(const Json& sch, + const std::vector<schema_location>& uris, + std::set<std::string>& keywords) = 0; + + virtual validator_pointer make_not_validator(const Json& schema, + const std::vector<schema_location>& uris) = 0; + + virtual validator_pointer make_all_of_validator(const Json& schema, + const std::vector<schema_location>& uris) = 0; + + virtual validator_pointer make_any_of_validator(const Json& schema, + const std::vector<schema_location>& uris) = 0; + + virtual validator_pointer make_one_of_validator(const Json& schema, + const std::vector<schema_location>& uris) = 0; + + virtual validator_pointer make_type_validator(const Json& schema, + const std::vector<schema_location>& uris) = 0; + }; + + struct collecting_error_reporter : public error_reporter + { + std::vector<validation_output> errors; + + private: + void do_error(const validation_output& o) override + { + errors.push_back(o); + } + }; + + // string keyword_validator + + inline + std::string make_absolute_keyword_location(const std::vector<schema_location>& uris, + const std::string& keyword) + { + for (auto it = uris.rbegin(); it != uris.rend(); ++it) + { + if (!it->has_identifier() && it->is_absolute()) + { + return it->append(keyword).string(); + } + } + return ""; + } + + template <class Json> + class string_validator : public keyword_validator<Json> + { + jsoncons::optional<std::size_t> max_length_; + std::string max_length_location_; + jsoncons::optional<std::size_t> min_length_; + std::string min_length_location_; + + #if defined(JSONCONS_HAS_STD_REGEX) + jsoncons::optional<std::regex> pattern_; + std::string pattern_string_; + std::string pattern_location_; + #endif + + format_checker format_check_; + std::string format_location_; + + jsoncons::optional<std::string> content_encoding_; + std::string content_encoding_location_; + jsoncons::optional<std::string> content_media_type_; + std::string content_media_type_location_; + + public: + string_validator(const Json& sch, const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : ""), max_length_(), min_length_(), + #if defined(JSONCONS_HAS_STD_REGEX) + pattern_(), + #endif + content_encoding_(), content_media_type_() + { + auto it = sch.find("maxLength"); + if (it != sch.object_range().end()) + { + max_length_ = it->value().template as<std::size_t>(); + max_length_location_ = make_absolute_keyword_location(uris, "maxLength"); + } + + it = sch.find("minLength"); + if (it != sch.object_range().end()) + { + min_length_ = it->value().template as<std::size_t>(); + min_length_location_ = make_absolute_keyword_location(uris, "minLength"); + } + + it = sch.find("contentEncoding"); + if (it != sch.object_range().end()) + { + content_encoding_ = it->value().template as<std::string>(); + content_encoding_location_ = make_absolute_keyword_location(uris, "contentEncoding"); + // If "contentEncoding" is set to "binary", a Json value + // of type json_type::byte_string_value is accepted. + } + + it = sch.find("contentMediaType"); + if (it != sch.object_range().end()) + { + content_media_type_ = it->value().template as<std::string>(); + content_media_type_location_ = make_absolute_keyword_location(uris, "contentMediaType"); + } + + #if defined(JSONCONS_HAS_STD_REGEX) + it = sch.find("pattern"); + if (it != sch.object_range().end()) + { + pattern_string_ = it->value().template as<std::string>(); + pattern_ = std::regex(it->value().template as<std::string>(),std::regex::ECMAScript); + pattern_location_ = make_absolute_keyword_location(uris, "pattern"); + } + #endif + + it = sch.find("format"); + if (it != sch.object_range().end()) + { + format_location_ = make_absolute_keyword_location(uris, "format"); + std::string format = it->value().template as<std::string>(); + if (format == "date-time") + { + format_check_ = rfc3339_date_time_check; + } + else if (format == "date") + { + format_check_ = rfc3339_date_check; + } + else if (format == "time") + { + format_check_ = rfc3339_time_check; + } + else if (format == "email") + { + format_check_ = email_check; + } + else if (format == "hostname") + { + format_check_ = hostname_check; + } + else if (format == "ipv4") + { + format_check_ = ipv4_check; + } + else if (format == "ipv6") + { + format_check_ = ipv6_check; + } + else if (format == "regex") + { + format_check_ = regex_check; + } + else + { + // Not supported - ignore + } + } + } + + private: + + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json&) const override + { + std::string content; + if (content_encoding_) + { + if (*content_encoding_ == "base64") + { + auto s = instance.template as<jsoncons::string_view>(); + auto retval = jsoncons::decode_base64(s.begin(), s.end(), content); + if (retval.ec != jsoncons::conv_errc::success) + { + reporter.error(validation_output("contentEncoding", + content_encoding_location_, + instance_location.to_uri_fragment(), + "Content is not a base64 string")); + if (reporter.fail_early()) + { + return; + } + } + } + else if (!content_encoding_->empty()) + { + reporter.error(validation_output("contentEncoding", + content_encoding_location_, + instance_location.to_uri_fragment(), + "unable to check for contentEncoding '" + *content_encoding_ + "'")); + if (reporter.fail_early()) + { + return; + } + } + } + else + { + content = instance.template as<std::string>(); + } + + if (content_media_type_) + { + if (content_media_type_ == "application/Json") + { + json_string_reader reader(content); + std::error_code ec; + reader.read(ec); + + if (ec) + { + reporter.error(validation_output("contentMediaType", + content_media_type_location_, + instance_location.to_uri_fragment(), + std::string("Content is not JSON: ") + ec.message())); + } + } + } + else if (instance.type() == json_type::byte_string_value) + { + reporter.error(validation_output("contentMediaType", + content_media_type_location_, + instance_location.to_uri_fragment(), + "Expected string, but is byte string")); + if (reporter.fail_early()) + { + return; + } + } + + if (instance.type() != json_type::string_value) + { + return; + } + + if (min_length_) + { + std::size_t length = unicode_traits::count_codepoints(content.data(), content.size()); + if (length < *min_length_) + { + reporter.error(validation_output("minLength", + min_length_location_, + instance_location.to_uri_fragment(), + std::string("Expected minLength: ") + std::to_string(*min_length_) + + ", actual: " + std::to_string(length))); + if (reporter.fail_early()) + { + return; + } + } + } + + if (max_length_) + { + std::size_t length = unicode_traits::count_codepoints(content.data(), content.size()); + if (length > *max_length_) + { + reporter.error(validation_output("maxLength", + max_length_location_, + instance_location.to_uri_fragment(), + std::string("Expected maxLength: ") + std::to_string(*max_length_) + + ", actual: " + std::to_string(length))); + if (reporter.fail_early()) + { + return; + } + } + } + + #if defined(JSONCONS_HAS_STD_REGEX) + if (pattern_) + { + if (!std::regex_search(content, *pattern_)) + { + std::string message("String \""); + message.append(instance.template as<std::string>()); + message.append("\" does not match pattern \""); + message.append(pattern_string_); + message.append("\""); + reporter.error(validation_output("pattern", + pattern_location_, + instance_location.to_uri_fragment(), + std::move(message))); + if (reporter.fail_early()) + { + return; + } + } + } + + #endif + + if (format_check_ != nullptr) + { + format_check_(format_location_, instance_location, content, reporter); + if (reporter.error_count() > 0 && reporter.fail_early()) + { + return; + } + } + } + }; + + // not_validator + + template <class Json> + class not_validator : public keyword_validator<Json> + { + using validator_pointer = typename keyword_validator<Json>::self_pointer; + + validator_pointer rule_; + + public: + not_validator(abstract_keyword_validator_factory<Json>* builder, + const Json& sch, + const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : "") + { + rule_ = builder->make_keyword_validator(sch, uris, {"not"}); + } + + private: + + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json& patch) const final + { + collecting_error_reporter local_reporter; + rule_->validate(instance, instance_location, local_reporter, patch); + + if (local_reporter.errors.empty()) + { + reporter.error(validation_output("not", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "Instance must not be valid against schema")); + } + } + + jsoncons::optional<Json> get_default_value(const jsonpointer::json_pointer& instance_location, + const Json& instance, + error_reporter& reporter) const override + { + return rule_->get_default_value(instance_location, instance, reporter); + } + }; + + template <class Json> + struct all_of_criterion + { + static const std::string& key() + { + static const std::string k("allOf"); + return k; + } + + static bool is_complete(const Json&, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + const collecting_error_reporter& local_reporter, + std::size_t) + { + if (!local_reporter.errors.empty()) + reporter.error(validation_output("allOf", + "", + instance_location.to_uri_fragment(), + "At least one schema failed to match, but all are required to match. ", + local_reporter.errors)); + return !local_reporter.errors.empty(); + } + }; + + template <class Json> + struct any_of_criterion + { + static const std::string& key() + { + static const std::string k("anyOf"); + return k; + } + + static bool is_complete(const Json&, + const jsonpointer::json_pointer&, + error_reporter&, + const collecting_error_reporter&, + std::size_t count) + { + return count == 1; + } + }; + + template <class Json> + struct one_of_criterion + { + static const std::string& key() + { + static const std::string k("oneOf"); + return k; + } + + static bool is_complete(const Json&, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + const collecting_error_reporter&, + std::size_t count) + { + if (count > 1) + { + std::string message(std::to_string(count)); + message.append(" subschemas matched, but exactly one is required to match"); + reporter.error(validation_output("oneOf", + "", + instance_location.to_uri_fragment(), + std::move(message))); + } + return count > 1; + } + }; + + template <class Json,class Criterion> + class combining_validator : public keyword_validator<Json> + { + using validator_pointer = typename keyword_validator<Json>::self_pointer; + + std::vector<validator_pointer> subschemas_; + + public: + combining_validator(abstract_keyword_validator_factory<Json>* builder, + const Json& sch, + const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : "") + { + size_t c = 0; + for (const auto& subsch : sch.array_range()) + { + subschemas_.push_back(builder->make_keyword_validator(subsch, uris, {Criterion::key(), std::to_string(c++)})); + } + + // Validate value of allOf, anyOf, and oneOf "MUST be a non-empty array" + } + + private: + + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json& patch) const final + { + size_t count = 0; + + collecting_error_reporter local_reporter; + for (auto& s : subschemas_) + { + std::size_t mark = local_reporter.errors.size(); + s->validate(instance, instance_location, local_reporter, patch); + if (mark == local_reporter.errors.size()) + count++; + + if (Criterion::is_complete(instance, instance_location, reporter, local_reporter, count)) + return; + } + + if (count == 0) + { + reporter.error(validation_output("combined", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "No schema matched, but one of them is required to match", + local_reporter.errors)); + } + } + }; + + template <class T, class Json> + T get_number(const Json& val, const string_view& keyword) + { + if (!val.is_number()) + { + std::string message(keyword); + message.append(" must be a number value"); + JSONCONS_THROW(schema_error(message)); + } + return val.template as<T>(); + } + + template <class Json,class T> + class numeric_validator_base : public keyword_validator<Json> + { + jsoncons::optional<T> maximum_; + std::string absolute_maximum_location_; + jsoncons::optional<T> minimum_; + std::string absolute_minimum_location_; + jsoncons::optional<T> exclusive_maximum_; + std::string absolute_exclusive_maximum_location_; + jsoncons::optional<T> exclusive_minimum_; + std::string absolute_exclusive_minimum_location_; + jsoncons::optional<double> multiple_of_; + std::string absolute_multiple_of_location_; + + public: + numeric_validator_base(const Json& sch, + const std::vector<schema_location>& uris, + std::set<std::string>& keywords) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : ""), + maximum_(), minimum_(),exclusive_maximum_(), exclusive_minimum_(), multiple_of_() + { + auto it = sch.find("maximum"); + if (it != sch.object_range().end()) + { + maximum_ = get_number<T>(it->value(), "maximum"); + absolute_maximum_location_ = make_absolute_keyword_location(uris,"maximum"); + keywords.insert("maximum"); + } + + it = sch.find("minimum"); + if (it != sch.object_range().end()) + { + minimum_ = get_number<T>(it->value(), "minimum"); + absolute_minimum_location_ = make_absolute_keyword_location(uris,"minimum"); + keywords.insert("minimum"); + } + + it = sch.find("exclusiveMaximum"); + if (it != sch.object_range().end()) + { + exclusive_maximum_ = get_number<T>(it->value(), "exclusiveMaximum"); + absolute_exclusive_maximum_location_ = make_absolute_keyword_location(uris,"exclusiveMaximum"); + keywords.insert("exclusiveMaximum"); + } + + it = sch.find("exclusiveMinimum"); + if (it != sch.object_range().end()) + { + exclusive_minimum_ = get_number<T>(it->value(), "exclusiveMinimum"); + absolute_exclusive_minimum_location_ = make_absolute_keyword_location(uris,"exclusiveMinimum"); + keywords.insert("exclusiveMinimum"); + } + + it = sch.find("multipleOf"); + if (it != sch.object_range().end()) + { + multiple_of_ = get_number<double>(it->value(), "multipleOf"); + absolute_multiple_of_location_ = make_absolute_keyword_location(uris,"multipleOf"); + keywords.insert("multipleOf"); + } + } + + protected: + + void apply_kewords(T value, + const jsonpointer::json_pointer& instance_location, + const Json& instance, + error_reporter& reporter) const + { + if (multiple_of_ && value != 0) // exclude zero + { + if (!is_multiple_of(value, *multiple_of_)) + { + reporter.error(validation_output("multipleOf", + absolute_multiple_of_location_, + instance_location.to_uri_fragment(), + instance.template as<std::string>() + " is not a multiple of " + std::to_string(*multiple_of_))); + if (reporter.fail_early()) + { + return; + } + } + } + + if (maximum_) + { + if (value > *maximum_) + { + reporter.error(validation_output("maximum", + absolute_maximum_location_, + instance_location.to_uri_fragment(), + instance.template as<std::string>() + " exceeds maximum of " + std::to_string(*maximum_))); + if (reporter.fail_early()) + { + return; + } + } + } + + if (minimum_) + { + if (value < *minimum_) + { + reporter.error(validation_output("minimum", + absolute_minimum_location_, + instance_location.to_uri_fragment(), + instance.template as<std::string>() + " is below minimum of " + std::to_string(*minimum_))); + if (reporter.fail_early()) + { + return; + } + } + } + + if (exclusive_maximum_) + { + if (value >= *exclusive_maximum_) + { + reporter.error(validation_output("exclusiveMaximum", + absolute_exclusive_maximum_location_, + instance_location.to_uri_fragment(), + instance.template as<std::string>() + " exceeds maximum of " + std::to_string(*exclusive_maximum_))); + if (reporter.fail_early()) + { + return; + } + } + } + + if (exclusive_minimum_) + { + if (value <= *exclusive_minimum_) + { + reporter.error(validation_output("exclusiveMinimum", + absolute_exclusive_minimum_location_, + instance_location.to_uri_fragment(), + instance.template as<std::string>() + " is below minimum of " + std::to_string(*exclusive_minimum_))); + if (reporter.fail_early()) + { + return; + } + } + } + } + private: + static bool is_multiple_of(T x, double multiple_of) + { + double rem = std::remainder(x, multiple_of); + double eps = std::nextafter(x, 0) - x; + return std::fabs(rem) < std::fabs(eps); + } + }; + + template <class Json> + class integer_validator : public numeric_validator_base<Json,int64_t> + { + public: + integer_validator(const Json& sch, + const std::vector<schema_location>& uris, + std::set<std::string>& keywords) + : numeric_validator_base<Json, int64_t>(sch, uris, keywords) + { + } + private: + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json&) const + { + if (!(instance.template is_integer<int64_t>() || (instance.is_double() && static_cast<double>(instance.template as<int64_t>()) == instance.template as<double>()))) + { + reporter.error(validation_output("integer", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "Instance is not an integer")); + if (reporter.fail_early()) + { + return; + } + } + int64_t value = instance.template as<int64_t>(); + this->apply_kewords(value, instance_location, instance, reporter); + } + }; + + template <class Json> + class number_validator : public numeric_validator_base<Json,double> + { + public: + number_validator(const Json& sch, + const std::vector<schema_location>& uris, + std::set<std::string>& keywords) + : numeric_validator_base<Json, double>(sch, uris, keywords) + { + } + private: + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json&) const + { + if (!(instance.template is_integer<int64_t>() || instance.is_double())) + { + reporter.error(validation_output("number", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "Instance is not a number")); + if (reporter.fail_early()) + { + return; + } + } + double value = instance.template as<double>(); + this->apply_kewords(value, instance_location, instance, reporter); + } + }; + + // null_validator + + template <class Json> + class null_validator : public keyword_validator<Json> + { + public: + null_validator(const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : "") + { + } + private: + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json&) const override + { + if (!instance.is_null()) + { + reporter.error(validation_output("null", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "Expected to be null")); + } + } + }; + + template <class Json> + class boolean_validator : public keyword_validator<Json> + { + public: + boolean_validator(const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : "") + { + } + private: + void do_validate(const Json&, + const jsonpointer::json_pointer&, + error_reporter&, + Json&) const override + { + } + + }; + + template <class Json> + class true_validator : public keyword_validator<Json> + { + public: + true_validator(const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : "") + { + } + private: + void do_validate(const Json&, + const jsonpointer::json_pointer&, + error_reporter&, + Json&) const override + { + } + }; + + template <class Json> + class false_validator : public keyword_validator<Json> + { + public: + false_validator(const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : "") + { + } + private: + void do_validate(const Json&, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json&) const override + { + reporter.error(validation_output("false", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "False schema always fails")); + } + }; + + template <class Json> + class required_validator : public keyword_validator<Json> + { + using validator_pointer = typename keyword_validator<Json>::self_pointer; + + std::vector<std::string> items_; + + public: + required_validator(const std::vector<schema_location>& uris, + const std::vector<std::string>& items) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : ""), items_(items) {} + required_validator(const std::string& absolute_keyword_location, const std::vector<std::string>& items) + : keyword_validator<Json>(absolute_keyword_location), items_(items) {} + + required_validator(const required_validator&) = delete; + required_validator(required_validator&&) = default; + required_validator& operator=(const required_validator&) = delete; + required_validator& operator=(required_validator&&) = default; + private: + + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json&) const override final + { + for (const auto& key : items_) + { + if (instance.find(key) == instance.object_range().end()) + { + reporter.error(validation_output("required", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "Required property \"" + key + "\" not found")); + if (reporter.fail_early()) + { + return; + } + } + } + } + }; + + template <class Json> + class object_validator : public keyword_validator<Json> + { + using validator_pointer = typename keyword_validator<Json>::self_pointer; + + jsoncons::optional<std::size_t> max_properties_; + std::string absolute_max_properties_location_; + jsoncons::optional<std::size_t> min_properties_; + std::string absolute_min_properties_location_; + jsoncons::optional<required_validator<Json>> required_; + + std::map<std::string, validator_pointer> properties_; + #if defined(JSONCONS_HAS_STD_REGEX) + std::vector<std::pair<std::regex, validator_pointer>> pattern_properties_; + #endif + validator_pointer additional_properties_; + + std::map<std::string, validator_pointer> dependencies_; + + validator_pointer property_name_validator_; + + public: + object_validator(abstract_keyword_validator_factory<Json>* builder, + const Json& sch, + const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : ""), + max_properties_(), min_properties_(), + additional_properties_(nullptr), + property_name_validator_(nullptr) + { + auto it = sch.find("maxProperties"); + if (it != sch.object_range().end()) + { + max_properties_ = it->value().template as<std::size_t>(); + absolute_max_properties_location_ = make_absolute_keyword_location(uris, "maxProperties"); + } + + it = sch.find("minProperties"); + if (it != sch.object_range().end()) + { + min_properties_ = it->value().template as<std::size_t>(); + absolute_min_properties_location_ = make_absolute_keyword_location(uris, "minProperties"); + } + + it = sch.find("required"); + if (it != sch.object_range().end()) + { + auto location = make_absolute_keyword_location(uris, "required"); + required_ = required_validator<Json>(location, + it->value().template as<std::vector<std::string>>()); + } + + it = sch.find("properties"); + if (it != sch.object_range().end()) + { + for (const auto& prop : it->value().object_range()) + properties_.emplace( + std::make_pair( + prop.key(), + builder->make_keyword_validator(prop.value(), uris, {"properties", prop.key()}))); + } + + #if defined(JSONCONS_HAS_STD_REGEX) + it = sch.find("patternProperties"); + if (it != sch.object_range().end()) + { + for (const auto& prop : it->value().object_range()) + pattern_properties_.emplace_back( + std::make_pair( + std::regex(prop.key(), std::regex::ECMAScript), + builder->make_keyword_validator(prop.value(), uris, {prop.key()}))); + } + #endif + + it = sch.find("additionalProperties"); + if (it != sch.object_range().end()) + { + additional_properties_ = builder->make_keyword_validator(it->value(), uris, {"additionalProperties"}); + } + + it = sch.find("dependencies"); + if (it != sch.object_range().end()) + { + for (const auto& dep : it->value().object_range()) + { + switch (dep.value().type()) + { + case json_type::array_value: + { + auto location = make_absolute_keyword_location(uris, "dependencies"); + dependencies_.emplace(dep.key(), + builder->make_required_validator({location}, + dep.value().template as<std::vector<std::string>>())); + break; + } + default: + { + dependencies_.emplace(dep.key(), + builder->make_keyword_validator(dep.value(), uris, {"dependencies", dep.key()})); + break; + } + } + } + } + + auto property_names_it = sch.find("propertyNames"); + if (property_names_it != sch.object_range().end()) + { + property_name_validator_ = builder->make_keyword_validator(property_names_it->value(), uris, {"propertyNames"}); + } + } + private: + + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json& patch) const override + { + if (max_properties_ && instance.size() > *max_properties_) + { + std::string message("Maximum properties: " + std::to_string(*max_properties_)); + message.append(", found: " + std::to_string(instance.size())); + reporter.error(validation_output("maxProperties", + absolute_max_properties_location_, + instance_location.to_uri_fragment(), + std::move(message))); + if (reporter.fail_early()) + { + return; + } + } + + if (min_properties_ && instance.size() < *min_properties_) + { + std::string message("Minimum properties: " + std::to_string(*min_properties_)); + message.append(", found: " + std::to_string(instance.size())); + reporter.error(validation_output("minProperties", + absolute_min_properties_location_, + instance_location.to_uri_fragment(), + std::move(message))); + if (reporter.fail_early()) + { + return; + } + } + + if (required_) + required_->validate(instance, instance_location, reporter, patch); + + for (const auto& property : instance.object_range()) + { + if (property_name_validator_) + property_name_validator_->validate(property.key(), instance_location, reporter, patch); + + bool a_prop_or_pattern_matched = false; + auto properties_it = properties_.find(property.key()); + + // check if it is in "properties" + if (properties_it != properties_.end()) + { + a_prop_or_pattern_matched = true; + jsonpointer::json_pointer pointer(instance_location); + pointer /= property.key(); + properties_it->second->validate(property.value(), pointer, reporter, patch); + } + + #if defined(JSONCONS_HAS_STD_REGEX) + + // check all matching "patternProperties" + for (auto& schema_pp : pattern_properties_) + if (std::regex_search(property.key(), schema_pp.first)) + { + a_prop_or_pattern_matched = true; + jsonpointer::json_pointer pointer(instance_location); + pointer /= property.key(); + schema_pp.second->validate(property.value(), pointer, reporter, patch); + } + #endif + + // finally, check "additionalProperties" + if (!a_prop_or_pattern_matched && additional_properties_) + { + collecting_error_reporter local_reporter; + + jsonpointer::json_pointer pointer(instance_location); + pointer /= property.key(); + additional_properties_->validate(property.value(), pointer, local_reporter, patch); + if (!local_reporter.errors.empty()) + { + reporter.error(validation_output("additionalProperties", + additional_properties_->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "Additional property \"" + property.key() + "\" found but was invalid.")); + if (reporter.fail_early()) + { + return; + } + } + } + } + + // reverse search + for (auto const& prop : properties_) + { + const auto finding = instance.find(prop.first); + if (finding == instance.object_range().end()) + { + // If property is not in instance + auto default_value = prop.second->get_default_value(instance_location, instance, reporter); + if (default_value) + { + // If default value is available, update patch + jsonpointer::json_pointer pointer(instance_location); + pointer /= prop.first; + + update_patch(patch, pointer, std::move(*default_value)); + } + } + } + + for (const auto& dep : dependencies_) + { + auto prop = instance.find(dep.first); + if (prop != instance.object_range().end()) + { + // if dependency-property is present in instance + jsonpointer::json_pointer pointer(instance_location); + pointer /= dep.first; + dep.second->validate(instance, pointer, reporter, patch); // validate + } + } + } + + void update_patch(Json& patch, const jsonpointer::json_pointer& instance_location, Json&& default_value) const + { + Json j; + j.try_emplace("op", "add"); + j.try_emplace("path", instance_location.to_uri_fragment()); + j.try_emplace("value", std::forward<Json>(default_value)); + patch.push_back(std::move(j)); + } + }; + + // array_validator + + template <class Json> + class array_validator : public keyword_validator<Json> + { + using validator_pointer = typename keyword_validator<Json>::self_pointer; + + jsoncons::optional<std::size_t> max_items_; + std::string absolute_max_items_location_; + jsoncons::optional<std::size_t> min_items_; + std::string absolute_min_items_location_; + bool unique_items_ = false; + validator_pointer items_validator_; + std::vector<validator_pointer> item_validators_; + validator_pointer additional_items_validator_; + validator_pointer contains_validator_; + + public: + array_validator(abstract_keyword_validator_factory<Json>* builder, + const Json& sch, + const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : ""), + max_items_(), min_items_(), items_validator_(nullptr), additional_items_validator_(nullptr), contains_validator_(nullptr) + { + { + auto it = sch.find("maxItems"); + if (it != sch.object_range().end()) + { + max_items_ = it->value().template as<std::size_t>(); + absolute_max_items_location_ = make_absolute_keyword_location(uris, "maxItems"); + } + } + + { + auto it = sch.find("minItems"); + if (it != sch.object_range().end()) + { + min_items_ = it->value().template as<std::size_t>(); + absolute_min_items_location_ = make_absolute_keyword_location(uris, "minItems"); + } + } + + { + auto it = sch.find("uniqueItems"); + if (it != sch.object_range().end()) + { + unique_items_ = it->value().template as<bool>(); + } + } + + { + auto it = sch.find("items"); + if (it != sch.object_range().end()) + { + + if (it->value().type() == json_type::array_value) + { + size_t c = 0; + for (const auto& subsch : it->value().array_range()) + item_validators_.push_back(builder->make_keyword_validator(subsch, uris, {"items", std::to_string(c++)})); + + auto attr_add = sch.find("additionalItems"); + if (attr_add != sch.object_range().end()) + { + additional_items_validator_ = builder->make_keyword_validator(attr_add->value(), uris, {"additionalItems"}); + } + + } + else if (it->value().type() == json_type::object_value || + it->value().type() == json_type::bool_value) + { + items_validator_ = builder->make_keyword_validator(it->value(), uris, {"items"}); + } + + } + } + + { + auto it = sch.find("contains"); + if (it != sch.object_range().end()) + { + contains_validator_ = builder->make_keyword_validator(it->value(), uris, {"contains"}); + } + } + } + private: + + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json& patch) const override + { + if (max_items_) + { + if (instance.size() > *max_items_) + { + std::string message("Expected maximum item count: " + std::to_string(*max_items_)); + message.append(", found: " + std::to_string(instance.size())); + reporter.error(validation_output("maxItems", + absolute_max_items_location_, + instance_location.to_uri_fragment(), + std::move(message))); + if (reporter.fail_early()) + { + return; + } + } + } + + if (min_items_) + { + if (instance.size() < *min_items_) + { + std::string message("Expected minimum item count: " + std::to_string(*min_items_)); + message.append(", found: " + std::to_string(instance.size())); + reporter.error(validation_output("minItems", + absolute_min_items_location_, + instance_location.to_uri_fragment(), + std::move(message))); + if (reporter.fail_early()) + { + return; + } + } + } + + if (unique_items_) + { + if (!array_has_unique_items(instance)) + { + reporter.error(validation_output("uniqueItems", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "Array items are not unique")); + if (reporter.fail_early()) + { + return; + } + } + } + + size_t index = 0; + if (items_validator_) + { + for (const auto& i : instance.array_range()) + { + jsonpointer::json_pointer pointer(instance_location); + pointer /= index; + items_validator_->validate(i, pointer, reporter, patch); + index++; + } + } + else + { + auto validator_it = item_validators_.cbegin(); + for (const auto& item : instance.array_range()) + { + validator_pointer item_validator = nullptr; + if (validator_it != item_validators_.cend()) + { + item_validator = *validator_it; + ++validator_it; + } + else if (additional_items_validator_ != nullptr) + { + item_validator = additional_items_validator_; + } + else + break; + + jsonpointer::json_pointer pointer(instance_location); + pointer /= index; + item_validator->validate(item, pointer, reporter, patch); + } + } + + if (contains_validator_) + { + bool contained = false; + collecting_error_reporter local_reporter; + for (const auto& item : instance.array_range()) + { + std::size_t mark = local_reporter.errors.size(); + contains_validator_->validate(item, instance_location, local_reporter, patch); + if (mark == local_reporter.errors.size()) + { + contained = true; + break; + } + } + if (!contained) + { + reporter.error(validation_output("contains", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "Expected at least one array item to match \"contains\" schema", + local_reporter.errors)); + if (reporter.fail_early()) + { + return; + } + } + } + } + + static bool array_has_unique_items(const Json& a) + { + for (auto it = a.array_range().begin(); it != a.array_range().end(); ++it) + { + for (auto jt = it+1; jt != a.array_range().end(); ++jt) + { + if (*it == *jt) + { + return false; // contains duplicates + } + } + } + return true; // elements are unique + } + }; + + template <class Json> + class conditional_validator : public keyword_validator<Json> + { + using validator_pointer = typename keyword_validator<Json>::self_pointer; + + validator_pointer if_validator_; + validator_pointer then_validator_; + validator_pointer else_validator_; + + public: + conditional_validator(abstract_keyword_validator_factory<Json>* builder, + const Json& sch_if, + const Json& sch, + const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : ""), if_validator_(nullptr), then_validator_(nullptr), else_validator_(nullptr) + { + auto then_it = sch.find("then"); + auto else_it = sch.find("else"); + + if (then_it != sch.object_range().end() || else_it != sch.object_range().end()) + { + if_validator_ = builder->make_keyword_validator(sch_if, uris, {"if"}); + + if (then_it != sch.object_range().end()) + { + then_validator_ = builder->make_keyword_validator(then_it->value(), uris, {"then"}); + } + + if (else_it != sch.object_range().end()) + { + else_validator_ = builder->make_keyword_validator(else_it->value(), uris, {"else"}); + } + } + } + private: + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json& patch) const final + { + if (if_validator_) + { + collecting_error_reporter local_reporter; + + if_validator_->validate(instance, instance_location, local_reporter, patch); + if (local_reporter.errors.empty()) + { + if (then_validator_) + then_validator_->validate(instance, instance_location, reporter, patch); + } + else + { + if (else_validator_) + else_validator_->validate(instance, instance_location, reporter, patch); + } + } + } + }; + + // enum_validator + + template <class Json> + class enum_validator : public keyword_validator<Json> + { + Json enum_validator_; + + public: + enum_validator(const Json& sch, + const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : ""), enum_validator_(sch) + { + } + private: + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json&) const final + { + bool in_range = false; + for (const auto& item : enum_validator_.array_range()) + { + if (item == instance) + { + in_range = true; + break; + } + } + + if (!in_range) + { + reporter.error(validation_output("enum", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + instance.template as<std::string>() + " is not a valid enum value")); + if (reporter.fail_early()) + { + return; + } + } + } + }; + + // const_keyword + + template <class Json> + class const_keyword : public keyword_validator<Json> + { + Json const_validator_; + + public: + const_keyword(const Json& sch, const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : ""), const_validator_(sch) + { + } + private: + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json&) const final + { + if (const_validator_ != instance) + reporter.error(validation_output("const", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "Instance is not const")); + } + }; + + template <class Json> + class type_validator : public keyword_validator<Json> + { + using validator_pointer = typename keyword_validator<Json>::self_pointer; + + Json default_value_; + std::vector<validator_pointer> type_mapping_; + jsoncons::optional<enum_validator<Json>> enum_validator_; + jsoncons::optional<const_keyword<Json>> const_validator_; + std::vector<validator_pointer> combined_validators_; + jsoncons::optional<conditional_validator<Json>> conditional_validator_; + std::vector<std::string> expected_types_; + + public: + type_validator(const type_validator&) = delete; + type_validator& operator=(const type_validator&) = delete; + type_validator(type_validator&&) = default; + type_validator& operator=(type_validator&&) = default; + + type_validator(abstract_keyword_validator_factory<Json>* builder, + const Json& sch, + const std::vector<schema_location>& uris) + : keyword_validator<Json>((!uris.empty() && uris.back().is_absolute()) ? uris.back().string() : ""), default_value_(jsoncons::null_type()), + type_mapping_((uint8_t)(json_type::object_value)+1), + enum_validator_(), const_validator_() + { + //std::cout << uris.size() << " uris: "; + //for (const auto& uri : uris) + //{ + // std::cout << uri.string() << ", "; + //} + //std::cout << "\n"; + std::set<std::string> known_keywords; + + auto it = sch.find("type"); + if (it == sch.object_range().end()) + { + initialize_type_mapping(builder, "", sch, uris, known_keywords); + } + else + { + switch (it->value().type()) + { + case json_type::string_value: + { + auto type = it->value().template as<std::string>(); + initialize_type_mapping(builder, type, sch, uris, known_keywords); + expected_types_.emplace_back(std::move(type)); + break; + } + + case json_type::array_value: // "type": ["type1", "type2"] + { + for (const auto& item : it->value().array_range()) + { + auto type = item.template as<std::string>(); + initialize_type_mapping(builder, type, sch, uris, known_keywords); + expected_types_.emplace_back(std::move(type)); + } + break; + } + default: + break; + } + } + + const auto default_it = sch.find("default"); + if (default_it != sch.object_range().end()) + { + default_value_ = default_it->value(); + } + + it = sch.find("enum"); + if (it != sch.object_range().end()) + { + enum_validator_ = enum_validator<Json >(it->value(), uris); + } + + it = sch.find("const"); + if (it != sch.object_range().end()) + { + const_validator_ = const_keyword<Json>(it->value(), uris); + } + + it = sch.find("not"); + if (it != sch.object_range().end()) + { + combined_validators_.push_back(builder->make_not_validator(it->value(), uris)); + } + + it = sch.find("allOf"); + if (it != sch.object_range().end()) + { + combined_validators_.push_back(builder->make_all_of_validator(it->value(), uris)); + } + + it = sch.find("anyOf"); + if (it != sch.object_range().end()) + { + combined_validators_.push_back(builder->make_any_of_validator(it->value(), uris)); + } + + it = sch.find("oneOf"); + if (it != sch.object_range().end()) + { + combined_validators_.push_back(builder->make_one_of_validator(it->value(), uris)); + } + + it = sch.find("if"); + if (it != sch.object_range().end()) + { + conditional_validator_ = conditional_validator<Json>(builder, it->value(), sch, uris); + } + } + private: + + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json& patch) const override final + { + auto type = type_mapping_[(uint8_t) instance.type()]; + + if (type) + type->validate(instance, instance_location, reporter, patch); + else + { + std::ostringstream ss; + ss << "Expected "; + for (std::size_t i = 0; i < expected_types_.size(); ++i) + { + if (i > 0) + { + ss << ", "; + if (i+1 == expected_types_.size()) + { + ss << "or "; + } + } + ss << expected_types_[i]; + } + ss << ", found " << instance.type(); + + reporter.error(validation_output("type", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + ss.str())); + if (reporter.fail_early()) + { + return; + } + } + + if (enum_validator_) + { + enum_validator_->validate(instance, instance_location, reporter, patch); + if (reporter.error_count() > 0 && reporter.fail_early()) + { + return; + } + } + + if (const_validator_) + { + const_validator_->validate(instance, instance_location, reporter, patch); + if (reporter.error_count() > 0 && reporter.fail_early()) + { + return; + } + } + + for (const auto& validator : combined_validators_) + { + validator->validate(instance, instance_location, reporter, patch); + if (reporter.error_count() > 0 && reporter.fail_early()) + { + return; + } + } + + + if (conditional_validator_) + { + conditional_validator_->validate(instance, instance_location, reporter, patch); + if (reporter.error_count() > 0 && reporter.fail_early()) + { + return; + } + } + } + + jsoncons::optional<Json> get_default_value(const jsonpointer::json_pointer&, + const Json&, + error_reporter&) const override + { + return default_value_; + } + + void initialize_type_mapping(abstract_keyword_validator_factory<Json>* builder, + const std::string& type, + const Json& sch, + const std::vector<schema_location>& uris, + std::set<std::string>& keywords) + { + if (type == "null") + { + type_mapping_[(uint8_t)json_type::null_value] = builder->make_null_validator(uris); + } + else if (type == "object") + { + type_mapping_[(uint8_t)json_type::object_value] = builder->make_object_validator(sch, uris); + } + else if (type == "array") + { + type_mapping_[(uint8_t)json_type::array_value] = builder->make_array_validator(sch, uris); + } + else if (type == "string") + { + type_mapping_[(uint8_t)json_type::string_value] = builder->make_string_validator(sch, uris); + // For binary types + type_mapping_[(uint8_t) json_type::byte_string_value] = type_mapping_[(uint8_t) json_type::string_value]; + } + else if (type == "boolean") + { + type_mapping_[(uint8_t)json_type::bool_value] = builder->make_boolean_validator(uris); + } + else if (type == "integer") + { + type_mapping_[(uint8_t)json_type::int64_value] = builder->make_integer_validator(sch, uris, keywords); + type_mapping_[(uint8_t)json_type::uint64_value] = type_mapping_[(uint8_t)json_type::int64_value]; + type_mapping_[(uint8_t)json_type::double_value] = type_mapping_[(uint8_t)json_type::int64_value]; + } + else if (type == "number") + { + type_mapping_[(uint8_t)json_type::double_value] = builder->make_number_validator(sch, uris, keywords); + type_mapping_[(uint8_t)json_type::int64_value] = type_mapping_[(uint8_t)json_type::double_value]; + type_mapping_[(uint8_t)json_type::uint64_value] = type_mapping_[(uint8_t)json_type::double_value]; + } + else if (type.empty()) + { + type_mapping_[(uint8_t)json_type::null_value] = builder->make_null_validator(uris); + type_mapping_[(uint8_t)json_type::object_value] = builder->make_object_validator(sch, uris); + type_mapping_[(uint8_t)json_type::array_value] = builder->make_array_validator(sch, uris); + type_mapping_[(uint8_t)json_type::string_value] = builder->make_string_validator(sch, uris); + // For binary types + type_mapping_[(uint8_t) json_type::byte_string_value] = type_mapping_[(uint8_t) json_type::string_value]; + type_mapping_[(uint8_t)json_type::bool_value] = builder->make_boolean_validator(uris); + type_mapping_[(uint8_t)json_type::int64_value] = builder->make_integer_validator(sch, uris, keywords); + type_mapping_[(uint8_t)json_type::uint64_value] = type_mapping_[(uint8_t)json_type::int64_value]; + type_mapping_[(uint8_t)json_type::double_value] = type_mapping_[(uint8_t)json_type::int64_value]; + type_mapping_[(uint8_t)json_type::double_value] = builder->make_number_validator(sch, uris, keywords); + type_mapping_[(uint8_t)json_type::int64_value] = type_mapping_[(uint8_t)json_type::double_value]; + type_mapping_[(uint8_t)json_type::uint64_value] = type_mapping_[(uint8_t)json_type::double_value]; + } + } + }; + +} // namespace jsonschema +} // namespace jsoncons + +#endif // JSONCONS_JSONSCHEMA_VALUE_RULES_HPP diff --git a/include/jsoncons_ext/jsonschema/keyword_validator_factory.hpp b/include/jsoncons_ext/jsonschema/keyword_validator_factory.hpp new file mode 100644 index 0000000..f538105 --- /dev/null +++ b/include/jsoncons_ext/jsonschema/keyword_validator_factory.hpp @@ -0,0 +1,556 @@ +// Copyright 2020 Daniel Parker +// Distributed under the Boost license, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// See https://github.com/danielaparker/jsoncons for latest version + +#ifndef JSONCONS_JSONSCHEMA_KEYWORD_VALIDATOR_FACTORY_HPP +#define JSONCONS_JSONSCHEMA_KEYWORD_VALIDATOR_FACTORY_HPP + +#include <jsoncons/config/jsoncons_config.hpp> +#include <jsoncons/uri.hpp> +#include <jsoncons/json.hpp> +#include <jsoncons_ext/jsonpointer/jsonpointer.hpp> +#include <jsoncons_ext/jsonschema/subschema.hpp> +#include <jsoncons_ext/jsonschema/keyword_validator.hpp> +#include <jsoncons_ext/jsonschema/schema_draft7.hpp> +#include <jsoncons_ext/jsonschema/schema_version.hpp> +#include <cassert> +#include <set> +#include <sstream> +#include <iostream> +#include <cassert> +#if defined(JSONCONS_HAS_STD_REGEX) +#include <regex> +#endif + +namespace jsoncons { +namespace jsonschema { + + template <class Json> + using uri_resolver = std::function<Json(const jsoncons::uri & /*id*/)>; + + template <class Json> + class reference_schema : public keyword_validator<Json> + { + using validator_pointer = typename keyword_validator<Json>::self_pointer; + + validator_pointer referred_schema_; + + public: + reference_schema(const std::string& id) + : keyword_validator<Json>(id), referred_schema_(nullptr) {} + + void set_referred_schema(validator_pointer target) { referred_schema_ = target; } + + private: + + void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json& patch) const override + { + if (!referred_schema_) + { + reporter.error(validation_output("", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "Unresolved schema reference " + this->absolute_keyword_location())); + return; + } + + referred_schema_->validate(instance, instance_location, reporter, patch); + } + + jsoncons::optional<Json> get_default_value(const jsonpointer::json_pointer& instance_location, + const Json& instance, + error_reporter& reporter) const override + { + if (!referred_schema_) + { + reporter.error(validation_output("", + this->absolute_keyword_location(), + instance_location.to_uri_fragment(), + "Unresolved schema reference " + this->absolute_keyword_location())); + return jsoncons::optional<Json>(); + } + + return referred_schema_->get_default_value(instance_location, instance, reporter); + } + }; + + template <class Json> + class keyword_validator_factory; + + template <class Json> + class json_schema + { + using validator_pointer = typename keyword_validator<Json>::self_pointer; + + friend class keyword_validator_factory<Json>; + + std::vector<std::unique_ptr<keyword_validator<Json>>> subschemas_; + validator_pointer root_; + public: + json_schema(std::vector<std::unique_ptr<keyword_validator<Json>>>&& subschemas, + validator_pointer root) + : subschemas_(std::move(subschemas)), root_(root) + { + if (root_ == nullptr) + JSONCONS_THROW(schema_error("There is no root schema to validate an instance against")); + } + + json_schema(const json_schema&) = delete; + json_schema(json_schema&&) = default; + json_schema& operator=(const json_schema&) = delete; + json_schema& operator=(json_schema&&) = default; + + void validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json& patch) const + { + JSONCONS_ASSERT(root_ != nullptr); + root_->validate(instance, instance_location, reporter, patch); + } + }; + + template <class Json> + struct default_uri_resolver + { + Json operator()(const jsoncons::uri& uri) + { + if (uri.path() == "/draft-07/schema") + { + return jsoncons::jsonschema::schema_draft7<Json>::get_schema(); + } + + JSONCONS_THROW(jsonschema::schema_error("Don't know how to load JSON Schema " + std::string(uri.base()))); + } + }; + + template <class Json> + class keyword_validator_factory : public abstract_keyword_validator_factory<Json> + { + using validator_pointer = typename keyword_validator<Json>::self_pointer; + + struct subschema_registry + { + std::map<std::string, validator_pointer> schemas; // schemas + std::map<std::string, reference_schema<Json>*> unresolved; // unresolved references + std::map<std::string, Json> unprocessed_keywords; + }; + + uri_resolver<Json> resolver_; + validator_pointer root_; + + // Owns all schemas + std::vector<std::unique_ptr<keyword_validator<Json>>> subschemas_; + + // Map location to subschema_registry + std::map<std::string, subschema_registry> subschema_registries_; + + public: + keyword_validator_factory(uri_resolver<Json>&& resolver) noexcept + + : resolver_(std::move(resolver)) + { + } + + keyword_validator_factory(const keyword_validator_factory&) = delete; + keyword_validator_factory& operator=(const keyword_validator_factory&) = delete; + keyword_validator_factory(keyword_validator_factory&&) = default; + keyword_validator_factory& operator=(keyword_validator_factory&&) = default; + + std::shared_ptr<json_schema<Json>> get_schema() + { + return std::make_shared<json_schema<Json>>(std::move(subschemas_), root_); + } + + validator_pointer make_required_validator(const std::vector<schema_location>& uris, + const std::vector<std::string>& r) override + { + auto sch_orig = jsoncons::make_unique<required_validator<Json>>(uris, r); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_null_validator(const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<null_validator<Json>>(uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_true_validator(const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<true_validator<Json>>(uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_false_validator(const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<false_validator<Json>>(uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_object_validator(const Json& schema, + const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<object_validator<Json>>(this, schema, uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_array_validator(const Json& schema, + const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<array_validator<Json>>(this, schema, uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_string_validator(const Json& schema, + const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<string_validator<Json>>(schema, uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_boolean_validator(const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<boolean_validator<Json>>(uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_integer_validator(const Json& schema, + const std::vector<schema_location>& uris, + std::set<std::string>& keywords) override + { + auto sch_orig = jsoncons::make_unique<integer_validator<Json>>(schema, uris, keywords); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_number_validator(const Json& schema, + const std::vector<schema_location>& uris, + std::set<std::string>& keywords) override + { + auto sch_orig = jsoncons::make_unique<number_validator<Json>>(schema, uris, keywords); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_not_validator(const Json& schema, + const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<not_validator<Json>>(this, schema, uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_all_of_validator(const Json& schema, + const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<combining_validator<Json,all_of_criterion<Json>>>(this, schema, uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_any_of_validator(const Json& schema, + const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<combining_validator<Json,any_of_criterion<Json>>>(this, schema, uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_one_of_validator(const Json& schema, + const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<combining_validator<Json,one_of_criterion<Json>>>(this, schema, uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_type_validator(const Json& schema, + const std::vector<schema_location>& uris) override + { + auto sch_orig = jsoncons::make_unique<type_validator<Json>>(this, schema, uris); + auto sch = sch_orig.get(); + subschemas_.emplace_back(std::move(sch_orig)); + return sch; + } + + validator_pointer make_keyword_validator(const Json& schema, + const std::vector<schema_location>& uris, + const std::vector<std::string>& keys) override + { + std::vector<schema_location> new_uris = update_uris(schema, uris, keys); + + validator_pointer sch = nullptr; + + switch (schema.type()) + { + case json_type::bool_value: + if (schema.template as<bool>()) + { + sch = make_true_validator(new_uris); + } + else + { + sch = make_false_validator(new_uris); + } + break; + case json_type::object_value: + { + auto it = schema.find("definitions"); + if (it != schema.object_range().end()) + { + for (const auto& def : it->value().object_range()) + make_keyword_validator(def.value(), new_uris, {"definitions", def.key()}); + } + + it = schema.find("$ref"); + if (it != schema.object_range().end()) // this schema is a reference + { + schema_location relative(it->value().template as<std::string>()); + schema_location id = relative.resolve(new_uris.back()); + sch = get_or_create_reference(id); + } + else + { + sch = make_type_validator(schema, new_uris); + } + break; + } + default: + JSONCONS_THROW(schema_error("invalid JSON-type for a schema for " + new_uris[0].string() + ", expected: boolean or object")); + break; + } + + for (const auto& uri : new_uris) + { + insert(uri, sch); + + if (schema.type() == json_type::object_value) + { + for (const auto& item : schema.object_range()) + insert_unknown_keyword(uri, item.key(), item.value()); // save unknown keywords for later reference + } + } + return sch; + } + + void load_root(const Json& sch) + { + if (sch.is_object()) + { + auto it = sch.find("$schema"); + if (it != sch.object_range().end()) + { + auto sv = it->value().as_string_view(); + if (!schema_version::contains(sv)) + { + std::string message("Unsupported schema version "); + message.append(sv.data(), sv.size()); + JSONCONS_THROW(schema_error(message)); + } + } + } + load(sch); + } + + void load(const Json& sch) + { + subschema_registries_.clear(); + root_ = make_keyword_validator(sch, {{"#"}}, {}); + + // load all external schemas that have not already been loaded + + std::size_t loaded_count = 0; + do + { + loaded_count = 0; + + std::vector<std::string> locations; + for (const auto& item : subschema_registries_) + locations.push_back(item.first); + + for (const auto& loc : locations) + { + if (subschema_registries_[loc].schemas.empty()) // registry for this file is empty + { + if (resolver_) + { + Json external_schema = resolver_(loc); + make_keyword_validator(external_schema, {{loc}}, {}); + ++loaded_count; + } + else + { + JSONCONS_THROW(schema_error("External schema reference '" + loc + "' needs to be loaded, but no resolver provided")); + } + } + } + } + while (loaded_count > 0); + + for (const auto &file : subschema_registries_) + { + if (!file.second.unresolved.empty()) + { + JSONCONS_THROW(schema_error("after all files have been parsed, '" + + (file.first == "" ? "<root>" : file.first) + + "' has still undefined references.")); + } + } + } + + private: + + void insert(const schema_location& uri, validator_pointer s) + { + auto& file = get_or_create_file(std::string(uri.base())); + auto schemas_it = file.schemas.find(std::string(uri.fragment())); + if (schemas_it != file.schemas.end()) + { + JSONCONS_THROW(schema_error("schema with " + uri.string() + " already inserted")); + return; + } + + file.schemas.insert({std::string(uri.fragment()), s}); + + // is there an unresolved reference to this newly inserted schema? + auto unresolved_it = file.unresolved.find(std::string(uri.fragment())); + if (unresolved_it != file.unresolved.end()) + { + unresolved_it->second->set_referred_schema(s); + file.unresolved.erase(unresolved_it); + + } + } + + void insert_unknown_keyword(const schema_location& uri, + const std::string& key, + const Json& value) + { + auto &file = get_or_create_file(std::string(uri.base())); + auto new_u = uri.append(key); + schema_location new_uri(new_u); + + if (new_uri.has_fragment() && !new_uri.has_identifier()) + { + auto fragment = std::string(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(fragment); + if (unresolved != file.unresolved.end()) + make_keyword_validator(value, {{new_uri}}, {}); + else // no, nothing ref'd it, keep for later + file.unprocessed_keywords[fragment] = value; + + // recursively add possible subschemas of unknown keywords + if (value.type() == json_type::object_value) + for (const auto& subsch : value.object_range()) + { + insert_unknown_keyword(new_uri, subsch.key(), subsch.value()); + } + } + } + + validator_pointer get_or_create_reference(const schema_location& uri) + { + auto &file = get_or_create_file(std::string(uri.base())); + + // a schema already exists + auto sch = file.schemas.find(std::string(uri.fragment())); + if (sch != file.schemas.end()) + return sch->second; + + // referencing an unknown keyword, turn it into schema + // + // an unknown keyword can only be referenced by a JSONPointer, + // not by a plain name identifier + if (uri.has_fragment() && !uri.has_identifier()) + { + std::string fragment = std::string(uri.fragment()); + auto unprocessed_keywords_it = file.unprocessed_keywords.find(fragment); + if (unprocessed_keywords_it != file.unprocessed_keywords.end()) + { + auto &subsch = unprocessed_keywords_it->second; + auto s = make_keyword_validator(subsch, {{uri}}, {}); // A JSON Schema MUST be an object or a boolean. + file.unprocessed_keywords.erase(unprocessed_keywords_it); + return s; + } + } + + // get or create a reference_schema + auto ref = file.unresolved.find(std::string(uri.fragment())); + if (ref != file.unresolved.end()) + { + return ref->second; // unresolved, use existing reference + } + else + { + auto orig = jsoncons::make_unique<reference_schema<Json>>(uri.string()); + auto p = file.unresolved.insert(ref, + {std::string(uri.fragment()), orig.get()}) + ->second; // unresolved, create new reference + + subschemas_.emplace_back(std::move(orig)); + return p; + } + } + + subschema_registry& get_or_create_file(const std::string& loc) + { + auto file = subschema_registries_.find(loc); + if (file != subschema_registries_.end()) + return file->second; + else + return subschema_registries_.insert(file, {loc, {}})->second; + } + + }; + + template <class Json> + std::shared_ptr<json_schema<Json>> make_schema(const Json& schema) + { + keyword_validator_factory<Json> loader{default_uri_resolver<Json>()}; + loader.load_root(schema); + + return loader.get_schema(); + } + + template <class Json,class URIResolver> + typename std::enable_if<type_traits::is_unary_function_object_exact<URIResolver,Json,std::string>::value,std::shared_ptr<json_schema<Json>>>::type + make_schema(const Json& schema, const URIResolver& resolver) + { + keyword_validator_factory<Json> loader(resolver); + loader.load_root(schema); + + return loader.get_schema(); + } + +} // namespace jsonschema +} // namespace jsoncons + +#endif // JSONCONS_JSONSCHEMA_SCHEMA_LOADER_HPP diff --git a/include/jsoncons_ext/jsonschema/schema_draft7.hpp b/include/jsoncons_ext/jsonschema/schema_draft7.hpp new file mode 100644 index 0000000..c6f6fbc --- /dev/null +++ b/include/jsoncons_ext/jsonschema/schema_draft7.hpp @@ -0,0 +1,198 @@ +// Copyright 2020 Daniel Parker +// Distributed under the Boost license, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// See https://github.com/danielaparker/jsoncons for latest version + +#ifndef JSONCONS_JSONSCHEMA_SCHEMA_DRAFT7_HPP +#define JSONCONS_JSONSCHEMA_SCHEMA_DRAFT7_HPP + +#include <jsoncons/json.hpp> + +namespace jsoncons { +namespace jsonschema { + + template <class Json> + struct schema_draft7 + { + static Json get_schema() + { + static Json schema = Json::parse(R"( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": true + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": { "$ref": "#" }, + "then": { "$ref": "#" }, + "else": { "$ref": "#" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true + } + )"); + + return schema; + } + }; + +} // namespace jsonschema +} // namespace jsoncons + +#endif // JSONCONS_JSONSCHEMA_SCHEMA_DRAFT7_HPP diff --git a/include/jsoncons_ext/jsonschema/schema_location.hpp b/include/jsoncons_ext/jsonschema/schema_location.hpp new file mode 100644 index 0000000..fd1a743 --- /dev/null +++ b/include/jsoncons_ext/jsonschema/schema_location.hpp @@ -0,0 +1,200 @@ +// Copyright 2020 Daniel Parker +// Distributed under the Boost license, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// See https://github.com/danielaparker/jsoncons for latest version + +#ifndef JSONCONS_JSONSCHEMA_SCHEMA_LOCATION_HPP +#define JSONCONS_JSONSCHEMA_SCHEMA_LOCATION_HPP + +#include <jsoncons/config/jsoncons_config.hpp> +#include <jsoncons/uri.hpp> +#include <jsoncons/json.hpp> +#include <jsoncons_ext/jsonpointer/jsonpointer.hpp> +#include <jsoncons_ext/jsonschema/jsonschema_error.hpp> + +namespace jsoncons { +namespace jsonschema { + + class schema_location + { + jsoncons::uri uri_; + std::string identifier_; + public: + schema_location() + { + } + + schema_location(const std::string& uri) + { + auto pos = uri.find('#'); + if (pos != std::string::npos) + { + identifier_ = uri.substr(pos + 1); + unescape_percent(identifier_); + } + uri_ = jsoncons::uri(uri); + } + + jsoncons::uri uri() const + { + return uri_; + } + + bool has_fragment() const + { + return !identifier_.empty(); + } + + bool has_identifier() const + { + return !identifier_.empty() && identifier_.front() != '/'; + } + + jsoncons::string_view base() const + { + return uri_.base(); + } + + jsoncons::string_view path() const + { + return uri_.path(); + } + + bool is_absolute() const + { + return uri_.is_absolute(); + } + + std::string identifier() const + { + return identifier_; + } + + std::string fragment() const + { + return identifier_; + } + + schema_location resolve(const schema_location& uri) const + { + schema_location new_uri; + new_uri.identifier_ = identifier_; + new_uri.uri_ = uri_.resolve(uri.uri_); + return new_uri; + } + + int compare(const schema_location& other) const + { + int result = uri_.compare(other.uri_); + if (result != 0) + { + return result; + } + return result; + } + + schema_location append(const std::string& field) const + { + if (has_identifier()) + return *this; + + jsoncons::jsonpointer::json_pointer pointer(std::string(uri_.fragment())); + pointer /= field; + + jsoncons::uri new_uri(uri_.scheme(), + uri_.userinfo(), + uri_.host(), + uri_.port(), + uri_.path(), + uri_.query(), + pointer.to_string()); + + schema_location wrapper; + wrapper.uri_ = new_uri; + wrapper.identifier_ = pointer.to_string(); + + return wrapper; + } + + schema_location append(std::size_t index) const + { + if (has_identifier()) + return *this; + + jsoncons::jsonpointer::json_pointer pointer(std::string(uri_.fragment())); + pointer /= index; + + jsoncons::uri new_uri(uri_.scheme(), + uri_.userinfo(), + uri_.host(), + uri_.port(), + uri_.path(), + uri_.query(), + pointer.to_string()); + + schema_location wrapper; + wrapper.uri_ = new_uri; + wrapper.identifier_ = pointer.to_string(); + + return wrapper; + } + + std::string string() const + { + std::string s = uri_.string(); + return s; + } + + friend bool operator==(const schema_location& lhs, const schema_location& rhs) + { + return lhs.compare(rhs) == 0; + } + + friend bool operator!=(const schema_location& lhs, const schema_location& rhs) + { + return lhs.compare(rhs) != 0; + } + + friend bool operator<(const schema_location& lhs, const schema_location& rhs) + { + return lhs.compare(rhs) < 0; + } + + friend bool operator<=(const schema_location& lhs, const schema_location& rhs) + { + return lhs.compare(rhs) <= 0; + } + + friend bool operator>(const schema_location& lhs, const schema_location& rhs) + { + return lhs.compare(rhs) > 0; + } + + friend bool operator>=(const schema_location& lhs, const schema_location& rhs) + { + return lhs.compare(rhs) >= 0; + } + private: + static void unescape_percent(std::string& s) + { + if (s.size() >= 3) + { + std::size_t pos = s.size() - 2; + while (pos-- >= 1) + { + if (s[pos] == '%') + { + std::string hex = s.substr(pos + 1, 2); + char ch = (char) std::strtoul(hex.c_str(), nullptr, 16); + s.replace(pos, 3, 1, ch); + } + } + } + } + }; + +} // namespace jsonschema +} // namespace jsoncons + +#endif // JSONCONS_JSONSCHEMA_RULE_HPP diff --git a/include/jsoncons_ext/jsonschema/schema_version.hpp b/include/jsoncons_ext/jsonschema/schema_version.hpp new file mode 100644 index 0000000..b804712 --- /dev/null +++ b/include/jsoncons_ext/jsonschema/schema_version.hpp @@ -0,0 +1,35 @@ +// Copyright 2021 Daniel Parker +// Distributed under the Boost license, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// See https://github.com/danielaparker/jsoncons for latest version + +#ifndef JSONCONS_JSONSCHEMA_SCHEMA_VERSION_HPP +#define JSONCONS_JSONSCHEMA_SCHEMA_VERSION_HPP + +#include <jsoncons/json.hpp> + +namespace jsoncons { +namespace jsonschema { + + class schema_version + { + public: + static bool contains(const string_view& url) + { + if (url.find("json-schema.org/draft-07/schema#") != string_view::npos) + { + return true; + } + else + { + return false; + } + } + }; + + +} // namespace jsonschema +} // namespace jsoncons + +#endif // JSONCONS_JSONSCHEMA_JSONSCHEMA_VERSION_HPP diff --git a/include/jsoncons_ext/jsonschema/subschema.hpp b/include/jsoncons_ext/jsonschema/subschema.hpp new file mode 100644 index 0000000..cbe0af4 --- /dev/null +++ b/include/jsoncons_ext/jsonschema/subschema.hpp @@ -0,0 +1,144 @@ +// Copyright 2020 Daniel Parker +// Distributed under the Boost license, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// See https://github.com/danielaparker/jsoncons for latest version + +#ifndef JSONCONS_JSONSCHEMA_SUBSCHEMA_HPP +#define JSONCONS_JSONSCHEMA_SUBSCHEMA_HPP + +#include <jsoncons/config/jsoncons_config.hpp> +#include <jsoncons/uri.hpp> +#include <jsoncons/json.hpp> +#include <jsoncons_ext/jsonpointer/jsonpointer.hpp> +#include <jsoncons_ext/jsonschema/jsonschema_error.hpp> +#include <jsoncons_ext/jsonschema/schema_location.hpp> + +namespace jsoncons { +namespace jsonschema { + + // Interface for validation error handlers + class error_reporter + { + bool fail_early_; + std::size_t error_count_; + public: + error_reporter(bool fail_early = false) + : fail_early_(fail_early), error_count_(0) + { + } + + virtual ~error_reporter() = default; + + void error(const validation_output& o) + { + ++error_count_; + do_error(o); + } + + std::size_t error_count() const + { + return error_count_; + } + + bool fail_early() const + { + return fail_early_; + } + + private: + virtual void do_error(const validation_output& /* e */) = 0; + }; + + template <class Json> + class keyword_validator + { + std::string absolute_keyword_location_; + public: + using self_pointer = keyword_validator<Json>*; + + keyword_validator(const std::string& absolute_keyword_location) + : absolute_keyword_location_(absolute_keyword_location) + { + } + + keyword_validator(const keyword_validator&) = delete; + keyword_validator(keyword_validator&&) = default; + keyword_validator& operator=(const keyword_validator&) = delete; + keyword_validator& operator=(keyword_validator&&) = default; + + virtual ~keyword_validator() = default; + + const std::string& absolute_keyword_location() const + { + return absolute_keyword_location_; + } + + void validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json& patch) const + { + do_validate(instance, + instance_location, + reporter, + patch); + } + + virtual jsoncons::optional<Json> get_default_value(const jsonpointer::json_pointer&, const Json&, error_reporter&) const + { + return jsoncons::optional<Json>(); + } + + private: + virtual void do_validate(const Json& instance, + const jsonpointer::json_pointer& instance_location, + error_reporter& reporter, + Json& patch) const = 0; + }; + + template <class Json> + std::vector<schema_location> update_uris(const Json& schema, + const std::vector<schema_location>& uris, + const std::vector<std::string>& keys) + { + // Exclude uri's that are not plain name identifiers + std::vector<schema_location> new_uris; + for (const auto& uri : uris) + { + if (!uri.has_identifier()) + new_uris.push_back(uri); + } + + // Append the keys for this sub-schema to the uri's + for (const auto& key : keys) + { + for (auto& uri : new_uris) + { + auto new_u = uri.append(key); + uri = schema_location(new_u); + } + } + if (schema.type() == json_type::object_value) + { + auto it = schema.find("$id"); // If $id is found, this schema can be referenced by the id + if (it != schema.object_range().end()) + { + std::string id = it->value().template as<std::string>(); + // Add it to the list if it is not already there + if (std::find(new_uris.begin(), new_uris.end(), id) == new_uris.end()) + { + schema_location relative(id); + schema_location new_uri = relative.resolve(new_uris.back()); + new_uris.emplace_back(new_uri); + } + } + } + + return new_uris; + } + +} // namespace jsonschema +} // namespace jsoncons + +#endif // JSONCONS_JSONSCHEMA_RULE_HPP |