diff --git a/src/json-schema.hpp b/src/json-schema.hpp index 0211fbb..0d68a06 100644 --- a/src/json-schema.hpp +++ b/src/json-schema.hpp @@ -164,21 +164,9 @@ class json_validator std::map schema_refs_; - void not_yet_implemented(const json &schema, const std::string &field, const std::string &type); - - void validate_type(const json &schema, const std::string &expected_type, const std::string &name); - void validate_enum(json &instance, const json &schema, const std::string &name); - void validate_numeric(json &instance, const json &schema, const std::string &name); - void validate(json &instance, const json &schema, const std::string &name); - - void validate_string(json &instance, const json &schema, const std::string &name); - void validate_boolean(json &instance, const json &schema, const std::string &name); - void validate_integer(json &instance, const json &schema, const std::string &name); - void validate_unsigned(json &instance, const json &schema, const std::string &name); - void validate_float(json &instance, const json &schema, const std::string &name); - void validate_null(json &instance, const json &schema, const std::string &name); - void validate_array(json &instance, const json &schema, const std::string &name); - void validate_object(json &instance, const json &schema, const std::string &name); + void validate(json &instance, const json &schema_, const std::string &name); + void validate_array(json &instance, const json &schema_, const std::string &name); + void validate_object(json &instance, const json &schema_, const std::string &name); public: std::set insert_schema(const json &input, json_uri id); diff --git a/src/json-validator.cpp b/src/json-validator.cpp index 877afde..1b2d05f 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -118,6 +118,152 @@ public: } }; +void not_yet_implemented(const json &schema, const std::string &field, const std::string &type) +{ + if (schema.find(field) != schema.end()) + throw std::logic_error(field + " for " + type + " is not yet implemented"); +} + +void validate_type(const json &schema, const std::string &expected_type, const std::string &name) +{ + const auto &type_it = schema.find("type"); + if (type_it == schema.end()) + /* TODO guess type for more safety, + * TODO use definitions + * TODO valid by not being defined? FIXME not clear - there are + * schema-test case which are not specifying a type */ + return; + + const auto &type_instance = type_it.value(); + + // any of the types in this array + if (type_instance.type() == json::value_t::array) { + if (std::find(type_instance.begin(), + type_instance.end(), + expected_type) != type_instance.end()) + return; + + std::ostringstream s; + s << expected_type << " is not any of " << type_instance << " for " << name; + throw std::invalid_argument(s.str()); + + } else { // type_instance is a string + if (type_instance == expected_type) + return; + + throw std::invalid_argument(type_instance.get() + " is not a " + expected_type + " for " + name); + } +} + +void validate_enum(json &instance, const json &schema, const std::string &name) +{ + const auto &enum_value = schema.find("enum"); + if (enum_value == schema.end()) + return; + + if (std::find(enum_value.value().begin(), enum_value.value().end(), instance) != enum_value.value().end()) + return; + + std::ostringstream s; + s << "invalid enum-value '" << instance << "' " + << "for instance '" << name << "'. Candidates are " << enum_value.value() << "."; + + throw std::invalid_argument(s.str()); +} + +void validate_string(json &instance, const json &schema, const std::string &name) +{ + // possibile but unhanled keywords + not_yet_implemented(schema, "format", "string"); + not_yet_implemented(schema, "pattern", "string"); + + validate_type(schema, "string", name); + + // minLength + auto attr = schema.find("minLength"); + if (attr != schema.end()) + if (instance.get().size() < attr.value()) { + std::ostringstream s; + s << "'" << name << "' of value '" << instance << "' is too short as per minLength (" + << attr.value() << ")"; + throw std::out_of_range(s.str()); + } + + // maxLength + attr = schema.find("maxLength"); + if (attr != schema.end()) + if (instance.get().size() > attr.value()) { + std::ostringstream s; + s << "'" << name << "' of value '" << instance << "' is too long as per maxLength (" + << attr.value() << ")"; + throw std::out_of_range(s.str()); + } +} + +void validate_boolean(json & /*instance*/, const json &schema, const std::string &name) +{ + validate_type(schema, "boolean", name); +} + +void validate_numeric(const json &schema, const std::string &name, double value) +{ + const auto &multipleOf = schema.find("multipleOf"); + if (multipleOf != schema.end()) { + double rem = fmod(value, multipleOf.value()); + if (rem != 0.0) + throw std::out_of_range(name + " is not a multiple ..."); + } + + const auto &maximum = schema.find("maximum"); + if (maximum != schema.end()) { + double maxi = maximum.value(); + auto ex = std::out_of_range(name + " exceeds maximum of ..."); + if (schema.find("exclusiveMaximum") != schema.end()) { + if (value >= maxi) + throw ex; + } else { + if (value > maxi) + throw ex; + } + } + + const auto &minimum = schema.find("minimum"); + if (minimum != schema.end()) { + double mini = minimum.value(); + auto ex = std::out_of_range(name + " exceeds minimum of ..."); + if (schema.find("exclusiveMinimum") != schema.end()) { + if (value <= mini) + throw ex; + } else { + if (value < mini) + throw ex; + } + } +} + +void validate_integer(json &instance, const json &schema, const std::string &name) +{ + validate_type(schema, "integer", name); + validate_numeric(schema, name, instance.get()); +} + +void validate_unsigned(json &instance, const json &schema, const std::string &name) +{ + validate_type(schema, "integer", name); + validate_numeric(schema, name, instance.get()); +} + +void validate_float(json &instance, const json &schema, const std::string &name) +{ + validate_type(schema, "number", name); + validate_numeric(schema, name, instance.get()); +} + +void validate_null(json & /*instance*/, const json &schema, const std::string &name) +{ + validate_type(schema, "null", name); +} + } // anonymous namespace namespace nlohmann @@ -164,378 +310,12 @@ std::set json_validator::insert_schema(const json &input, json_uri id) return undefined; } -void json_validator::not_yet_implemented(const json &schema, const std::string &field, const std::string &type) +void json_validator::validate(json &instance) { - if (schema.find(field) != schema.end()) - throw std::logic_error(field + " for " + type + " is not yet implemented"); -} + if (root_schema_ == nullptr) + throw std::invalid_argument("no root-schema has been inserted. Cannot validate an instance without it."); -void json_validator::validate_type(const json &schema, const std::string &expected_type, const std::string &name) -{ - const auto &type_it = schema.find("type"); - if (type_it == schema.end()) - /* TODO guess type for more safety, - * TODO use definitions - * TODO valid by not being defined? FIXME not clear - there are - * schema-test case which are not specifying a type */ - return; - - const auto &type_instance = type_it.value(); - - // any of the types in this array - if (type_instance.type() == json::value_t::array) { - if (std::find(type_instance.begin(), - type_instance.end(), - expected_type) != type_instance.end()) - return; - - std::ostringstream s; - s << expected_type << " is not any of " << type_instance << " for " << name; - throw std::invalid_argument(s.str()); - - } else { // type_instance is a string - if (type_instance == expected_type) - return; - - throw std::invalid_argument(type_instance.get() + " is not a " + expected_type + " for " + name); - } -} - -void json_validator::validate_enum(json &instance, const json &schema, const std::string &name) -{ - const auto &enum_value = schema.find("enum"); - if (enum_value == schema.end()) - return; - - if (std::find(enum_value.value().begin(), enum_value.value().end(), instance) != enum_value.value().end()) - return; - - std::ostringstream s; - s << "invalid enum-value '" << instance << "' " - << "for instance '" << name << "'. Candidates are " << enum_value.value() << "."; - - throw std::invalid_argument(s.str()); -} - -void json_validator::validate_string(json &instance, const json &schema, const std::string &name) -{ - // possibile but unhanled keywords - not_yet_implemented(schema, "format", "string"); - not_yet_implemented(schema, "pattern", "string"); - - validate_type(schema, "string", name); - - // minLength - auto attr = schema.find("minLength"); - if (attr != schema.end()) - if (instance.get().size() < attr.value()) { - std::ostringstream s; - s << "'" << name << "' of value '" << instance << "' is too short as per minLength (" - << attr.value() << ")"; - throw std::out_of_range(s.str()); - } - - // maxLength - attr = schema.find("maxLength"); - if (attr != schema.end()) - if (instance.get().size() > attr.value()) { - std::ostringstream s; - s << "'" << name << "' of value '" << instance << "' is too long as per maxLength (" - << attr.value() << ")"; - throw std::out_of_range(s.str()); - } -} - -void json_validator::validate_boolean(json & /*instance*/, const json &schema, const std::string &name) -{ - validate_type(schema, "boolean", name); -} - -void json_validator::validate_numeric(json &instance, const json &schema, const std::string &name) -{ - double value = instance; - - const auto &multipleOf = schema.find("multipleOf"); - if (multipleOf != schema.end()) { - double rem = fmod(value, multipleOf.value()); - if (rem != 0.0) - throw std::out_of_range(name + " is not a multiple ..."); - } - - const auto &maximum = schema.find("maximum"); - if (maximum != schema.end()) { - double maxi = maximum.value(); - auto ex = std::out_of_range(name + " exceeds maximum of ..."); - if (schema.find("exclusiveMaximum") != schema.end()) { - if (value >= maxi) - throw ex; - } else { - if (value > maxi) - throw ex; - } - } - - const auto &minimum = schema.find("minimum"); - if (minimum != schema.end()) { - double mini = minimum.value(); - auto ex = std::out_of_range(name + " exceeds minimum of ..."); - if (schema.find("exclusiveMinimum") != schema.end()) { - if (value <= mini) - throw ex; - } else { - if (value < mini) - throw ex; - } - } -} - -void json_validator::validate_integer(json &instance, const json &schema, const std::string &name) -{ - validate_type(schema, "integer", name); - validate_numeric(instance, schema, name); -} - -void json_validator::validate_unsigned(json &instance, const json &schema, const std::string &name) -{ - validate_type(schema, "integer", name); - validate_numeric(instance, schema, name); -} - -void json_validator::validate_float(json &instance, const json &schema, const std::string &name) -{ - validate_type(schema, "number", name); - validate_numeric(instance, schema, name); -} - -void json_validator::validate_null(json & /*instance*/, const json &schema, const std::string &name) -{ - validate_type(schema, "null", name); -} - -void json_validator::validate_array(json &instance, const json &schema, const std::string &name) -{ - validate_type(schema, "array", name); - - // maxItems - const auto &maxItems = schema.find("maxItems"); - if (maxItems != schema.end()) - if (instance.size() > maxItems.value()) - throw std::out_of_range(name + " has too many items."); - - // minItems - const auto &minItems = schema.find("minItems"); - if (minItems != schema.end()) - if (instance.size() < minItems.value()) - throw std::out_of_range(name + " has too many items."); - - // uniqueItems - const auto &uniqueItems = schema.find("uniqueItems"); - if (uniqueItems != schema.end()) - if (uniqueItems.value() == true) { - std::set array_to_set; - for (auto v : instance) { - auto ret = array_to_set.insert(v); - if (ret.second == false) - throw std::out_of_range(name + " should have only unique items."); - } - } - - // items and additionalItems - // default to empty schemas - auto items_iter = schema.find("items"); - json items = {}; - if (items_iter != schema.end()) - items = items_iter.value(); - - auto additionalItems_iter = schema.find("additionalItems"); - json additionalItems = {}; - if (additionalItems_iter != schema.end()) - additionalItems = additionalItems_iter.value(); - - size_t i = 0; - bool validation_done = false; - - for (auto &value : instance) { - std::string sub_name = name + "[" + std::to_string(i) + "]"; - - switch (items.type()) { - - case json::value_t::array: - - if (i < items.size()) - validate(value, items[i], sub_name); - else { - switch (additionalItems.type()) { // items is an array - // we need to take into consideration additionalItems - case json::value_t::object: - validate(value, additionalItems, sub_name); - break; - - case json::value_t::boolean: - if (additionalItems == false) - throw std::out_of_range("additional values in array are not allowed for " + sub_name); - else - validation_done = true; - break; - - default: - break; - } - } - - break; - - case json::value_t::object: // items is a schema - validate(value, items, sub_name); - break; - - default: - break; - } - if (validation_done) - break; - - i++; - } -} - -void json_validator::validate_object(json &instance, const json &schema, const std::string &name) -{ - validate_type(schema, "object", name); - - json properties = {}; - if (schema.find("properties") != schema.end()) - properties = schema["properties"]; - - // check for default values of properties - // and insert them into this object, if they don't exists - // works only for object properties for the moment - if (default_value_insertion) - for (auto it = properties.begin(); it != properties.end(); ++it) { - - const auto &default_value = it.value().find("default"); - if (default_value == it.value().end()) - continue; /* no default value -> continue */ - - if (instance.find(it.key()) != instance.end()) - continue; /* value is present */ - - /* create element from default value */ - instance[it.key()] = default_value.value(); - } - - // maxProperties - const auto &maxProperties = schema.find("maxProperties"); - if (maxProperties != schema.end()) - if (instance.size() > maxProperties.value()) - throw std::out_of_range(name + " has too many properties."); - - // minProperties - const auto &minProperties = schema.find("minProperties"); - if (minProperties != schema.end()) - if (instance.size() < minProperties.value()) - throw std::out_of_range(name + " has too few properties."); - - // additionalProperties - enum { - True, - False, - Object - } additionalProperties = True; - - const auto &additionalPropertiesVal = schema.find("additionalProperties"); - if (additionalPropertiesVal != schema.end()) { - if (additionalPropertiesVal.value().type() == json::value_t::boolean) - additionalProperties = additionalPropertiesVal.value() == true ? True : False; - else - additionalProperties = Object; - } - - // patternProperties - json patternProperties = {}; - if (schema.find("patternProperties") != schema.end()) - patternProperties = schema["patternProperties"]; - - // check all elements in object - for (auto child = instance.begin(); child != instance.end(); ++child) { - std::string child_name = name + "." + child.key(); - - // is this a property which is described in the schema - const auto &object_prop = properties.find(child.key()); - if (object_prop != properties.end()) { - // validate the element with its schema - validate(child.value(), object_prop.value(), child_name); - continue; - } - - bool patternProperties_has_matched = false; - for (auto pp = patternProperties.begin(); - pp != patternProperties.end(); ++pp) { - std::regex re(pp.key(), std::regex::ECMAScript); - - if (std::regex_search(child.key(), re)) { - validate(child.value(), pp.value(), child_name); - patternProperties_has_matched = true; - } - } - if (patternProperties_has_matched) - continue; - - switch (additionalProperties) { - case True: - break; - - case Object: - validate(child.value(), additionalPropertiesVal.value(), child_name); - break; - - case False: - throw std::invalid_argument("unknown property '" + child.key() + "' in object '" + name + "'"); - break; - }; - } - - // required - const auto &required = schema.find("required"); - if (required != schema.end()) - for (const auto &element : required.value()) { - if (instance.find(element) == instance.end()) { - throw std::invalid_argument("required element '" + element.get() + - "' not found in object '" + name + "'"); - } - } - - // dependencies - const auto &dependencies = schema.find("dependencies"); - if (dependencies == schema.end()) - return; - - for (auto dep = dependencies.value().cbegin(); - dep != dependencies.value().cend(); - ++dep) { - - // property not present in this instance - next - if (instance.find(dep.key()) == instance.end()) - continue; - - std::string sub_name = name + ".dependency-of-" + dep.key(); - - switch (dep.value().type()) { - - case json::value_t::object: - validate(instance, dep.value(), sub_name); - break; - - case json::value_t::array: - for (const auto &prop : dep.value()) - if (instance.find(prop) == instance.end()) - throw std::invalid_argument("failed dependency for " + sub_name + ". Need property " + prop.get()); - break; - - default: - break; - } - } + validate(instance, *root_schema_, "root"); } void json_validator::validate(json &instance, const json &schema_, const std::string &name) @@ -661,12 +441,231 @@ void json_validator::validate(json &instance, const json &schema_, const std::st } } -void json_validator::validate(json &instance) +void json_validator::validate_array(json &instance, const json &schema, const std::string &name) { - if (root_schema_ == nullptr) - throw std::invalid_argument("no root-schema has been inserted. Cannot validate an instance without it."); + validate_type(schema, "array", name); - validate(instance, *root_schema_, "root"); + // maxItems + const auto &maxItems = schema.find("maxItems"); + if (maxItems != schema.end()) + if (instance.size() > maxItems.value()) + throw std::out_of_range(name + " has too many items."); + + // minItems + const auto &minItems = schema.find("minItems"); + if (minItems != schema.end()) + if (instance.size() < minItems.value()) + throw std::out_of_range(name + " has too many items."); + + // uniqueItems + const auto &uniqueItems = schema.find("uniqueItems"); + if (uniqueItems != schema.end()) + if (uniqueItems.value() == true) { + std::set array_to_set; + for (auto v : instance) { + auto ret = array_to_set.insert(v); + if (ret.second == false) + throw std::out_of_range(name + " should have only unique items."); + } + } + + // items and additionalItems + // default to empty schemas + auto items_iter = schema.find("items"); + json items = {}; + if (items_iter != schema.end()) + items = items_iter.value(); + + auto additionalItems_iter = schema.find("additionalItems"); + json additionalItems = {}; + if (additionalItems_iter != schema.end()) + additionalItems = additionalItems_iter.value(); + + size_t i = 0; + bool validation_done = false; + + for (auto &value : instance) { + std::string sub_name = name + "[" + std::to_string(i) + "]"; + + switch (items.type()) { + + case json::value_t::array: + + if (i < items.size()) + validate(value, items[i], sub_name); + else { + switch (additionalItems.type()) { // items is an array + // we need to take into consideration additionalItems + case json::value_t::object: + validate(value, additionalItems, sub_name); + break; + + case json::value_t::boolean: + if (additionalItems == false) + throw std::out_of_range("additional values in array are not allowed for " + sub_name); + else + validation_done = true; + break; + + default: + break; + } + } + + break; + + case json::value_t::object: // items is a schema + validate(value, items, sub_name); + break; + + default: + break; + } + if (validation_done) + break; + + i++; + } +} + +void json_validator::validate_object(json &instance, const json &schema, const std::string &name) +{ + validate_type(schema, "object", name); + + json properties = {}; + if (schema.find("properties") != schema.end()) + properties = schema["properties"]; + +#if 0 + // check for default values of properties + // and insert them into this object, if they don't exists + // works only for object properties for the moment + if (default_value_insertion) + for (auto it = properties.begin(); it != properties.end(); ++it) { + + const auto &default_value = it.value().find("default"); + if (default_value == it.value().end()) + continue; /* no default value -> continue */ + + if (instance.find(it.key()) != instance.end()) + continue; /* value is present */ + + /* create element from default value */ + instance[it.key()] = default_value.value(); + } +#endif + // maxProperties + const auto &maxProperties = schema.find("maxProperties"); + if (maxProperties != schema.end()) + if (instance.size() > maxProperties.value()) + throw std::out_of_range(name + " has too many properties."); + + // minProperties + const auto &minProperties = schema.find("minProperties"); + if (minProperties != schema.end()) + if (instance.size() < minProperties.value()) + throw std::out_of_range(name + " has too few properties."); + + // additionalProperties + enum { + True, + False, + Object + } additionalProperties = True; + + const auto &additionalPropertiesVal = schema.find("additionalProperties"); + if (additionalPropertiesVal != schema.end()) { + if (additionalPropertiesVal.value().type() == json::value_t::boolean) + additionalProperties = additionalPropertiesVal.value() == true ? True : False; + else + additionalProperties = Object; + } + + // patternProperties + json patternProperties = {}; + if (schema.find("patternProperties") != schema.end()) + patternProperties = schema["patternProperties"]; + + // check all elements in object + for (auto child = instance.begin(); child != instance.end(); ++child) { + std::string child_name = name + "." + child.key(); + + // is this a property which is described in the schema + const auto &object_prop = properties.find(child.key()); + if (object_prop != properties.end()) { + // validate the element with its schema + validate(child.value(), object_prop.value(), child_name); + continue; + } + + bool patternProperties_has_matched = false; + for (auto pp = patternProperties.begin(); + pp != patternProperties.end(); ++pp) { + std::regex re(pp.key(), std::regex::ECMAScript); + + if (std::regex_search(child.key(), re)) { + validate(child.value(), pp.value(), child_name); + patternProperties_has_matched = true; + } + } + if (patternProperties_has_matched) + continue; + + switch (additionalProperties) { + case True: + break; + + case Object: + validate(child.value(), additionalPropertiesVal.value(), child_name); + break; + + case False: + throw std::invalid_argument("unknown property '" + child.key() + "' in object '" + name + "'"); + break; + }; + } + + // required + const auto &required = schema.find("required"); + if (required != schema.end()) + for (const auto &element : required.value()) { + if (instance.find(element) == instance.end()) { + throw std::invalid_argument("required element '" + element.get() + + "' not found in object '" + name + "'"); + } + } + + // dependencies + const auto &dependencies = schema.find("dependencies"); + if (dependencies == schema.end()) + return; + + for (auto dep = dependencies.value().cbegin(); + dep != dependencies.value().cend(); + ++dep) { + + // property not present in this instance - next + if (instance.find(dep.key()) == instance.end()) + continue; + + std::string sub_name = name + ".dependency-of-" + dep.key(); + + switch (dep.value().type()) { + + case json::value_t::object: + validate(instance, dep.value(), sub_name); + break; + + case json::value_t::array: + for (const auto &prop : dep.value()) + if (instance.find(prop) == instance.end()) + throw std::invalid_argument("failed dependency for " + sub_name + ". Need property " + prop.get()); + break; + + default: + break; + } + } } } }