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 |