diff --git a/examples/custom_schema.cpp b/examples/custom_schema.cpp index 829835a..158d456 100644 --- a/examples/custom_schema.cpp +++ b/examples/custom_schema.cpp @@ -90,7 +90,7 @@ using valijson::constraints::TypeConstraint; void addPropertiesConstraint(Schema &schema) { - PropertiesConstraint::PropertySchemaMap propertySchemaMap; + PropertiesConstraint propertiesConstraint; { // Prepare an enum constraint requires a document to be equal to at @@ -106,7 +106,7 @@ void addPropertiesConstraint(Schema &schema) schema.addConstraintToSubschema(constraint, subschema); // Include subschema in properties constraint - propertySchemaMap["category"] = subschema; + propertiesConstraint.addPropertySubschema("category", subschema); } { @@ -118,7 +118,7 @@ void addPropertiesConstraint(Schema &schema) schema.addConstraintToSubschema(typeConstraint, subschema); // Include subschema in properties constraint - propertySchemaMap["description"] = subschema; + propertiesConstraint.addPropertySubschema("description", subschema); } { @@ -131,7 +131,7 @@ void addPropertiesConstraint(Schema &schema) schema.addConstraintToSubschema(typeConstraint, subschema); // Include subschema in properties constraint - propertySchemaMap["price"] = subschema; + propertiesConstraint.addPropertySubschema("price", subschema); } { @@ -145,12 +145,11 @@ void addPropertiesConstraint(Schema &schema) schema.addConstraintToSubschema(typeConstraint, subschema); // Include subschema in properties constraint - propertySchemaMap["title"] = subschema; + propertiesConstraint.addPropertySubschema("title", subschema); } - // Add a PropertiesConstraint to the schema, with the properties defined - // above, no pattern properties or additional property schemas - schema.addConstraint(PropertiesConstraint(propertySchemaMap)); + // Add a PropertiesConstraint to the root schema + schema.addConstraint(propertiesConstraint); } void addRequiredConstraint(Schema &schema) diff --git a/include/valijson/constraints/concrete_constraints.hpp b/include/valijson/constraints/concrete_constraints.hpp index 6984f01..62bde1c 100644 --- a/include/valijson/constraints/concrete_constraints.hpp +++ b/include/valijson/constraints/concrete_constraints.hpp @@ -558,43 +558,97 @@ private: }; /** - * @brief Represents a tuple of 'properties', 'patternProperties' and - * 'additionalProperties' constraints. + * @brief Represents a combination of 'properties', 'patternProperties' and + * 'additionalProperties' constraints */ -struct PropertiesConstraint: BasicConstraint { - - typedef std::map PropertySchemaMap; - - PropertiesConstraint(const PropertySchemaMap &properties) - : properties(properties), +class PropertiesConstraint: public BasicConstraint +{ +public: + PropertiesConstraint() + : properties(std::less(), allocator), + patternProperties(std::less(), allocator), additionalProperties(NULL) { } - PropertiesConstraint(const PropertySchemaMap &properties, - const PropertySchemaMap &patternProperties) - : properties(properties), - patternProperties(patternProperties), + PropertiesConstraint(CustomAlloc allocFn, CustomFree freeFn) + : BasicConstraint(allocFn, freeFn), + properties(std::less(), allocator), + patternProperties(std::less(), allocator), additionalProperties(NULL) { } - PropertiesConstraint(const PropertySchemaMap &properties, - const PropertySchemaMap &patternProperties, - const Subschema *additionalProperties) - : properties(properties), - patternProperties(patternProperties), - additionalProperties(additionalProperties) { } + bool addPatternPropertySubschema(const char *patternProperty, + const Subschema *subschema) + { + return patternProperties.insert(PropertySchemaMap::value_type( + String(patternProperty, allocator), subschema)).second; + } - PropertiesConstraint(const PropertiesConstraint &other) - : properties(other.properties), - patternProperties(other.patternProperties), - additionalProperties(other.additionalProperties) {} + template + bool addPatternPropertySubschema(const std::basic_string, AllocatorType> &patternProperty, + const Subschema *subschema) + { + return addPatternPropertySubschema(patternProperty.c_str(), subschema); + } + + bool addPropertySubschema(const char *propertyName, + const Subschema *subschema) + { + return properties.insert(PropertySchemaMap::value_type( + String(propertyName, allocator), subschema)).second; + } + + template + bool addPropertySubschema(const std::basic_string, AllocatorType> &propertyName, + const Subschema *subschema) + { + return addPropertySubschema(propertyName.c_str(), subschema); + } + + template + void applyToPatternProperties(const FunctorType &fn) const + { + typedef typename PropertySchemaMap::value_type ValueType; + BOOST_FOREACH( const ValueType &value, patternProperties ) { + if (!fn(value.first, value.second)) { + return; + } + } + } + + template + void applyToProperties(const FunctorType &fn) const + { + typedef typename PropertySchemaMap::value_type ValueType; + BOOST_FOREACH( const ValueType &value, properties ) { + if (!fn(value.first, value.second)) { + return; + } + } + } + + const Subschema * getAdditionalPropertiesSubschema() const + { + return additionalProperties; + } + + void setAdditionalPropertiesSubschema(const Subschema *subschema) + { + additionalProperties = subschema; + } + +private: + typedef std::map, Allocator> + PropertySchemaMap; + + PropertySchemaMap properties; + PropertySchemaMap patternProperties; - const PropertySchemaMap properties; - const PropertySchemaMap patternProperties; const Subschema *additionalProperties; - }; /** - * @brief Represents a 'required' constraint. + * @brief Represents a 'required' constraint */ class RequiredConstraint: public BasicConstraint { diff --git a/include/valijson/constraints/constraint_visitor.hpp b/include/valijson/constraints/constraint_visitor.hpp index 0958fd9..03e35fe 100644 --- a/include/valijson/constraints/constraint_visitor.hpp +++ b/include/valijson/constraints/constraint_visitor.hpp @@ -17,7 +17,6 @@ struct MinLengthConstraint; struct MinPropertiesConstraint; struct MultipleOfConstraint; struct PatternConstraint; -struct PropertiesConstraint; class AllOfConstraint; class AnyOfConstraint; @@ -26,6 +25,7 @@ class EnumConstraint; class LinearItemsConstraint; class NotConstraint; class OneOfConstraint; +class PropertiesConstraint; class RequiredConstraint; class SingularItemsConstraint; class TypeConstraint; diff --git a/include/valijson/schema_parser.hpp b/include/valijson/schema_parser.hpp index c3c6606..eca5c7e 100644 --- a/include/valijson/schema_parser.hpp +++ b/include/valijson/schema_parser.hpp @@ -1273,42 +1273,37 @@ private: const Subschema *parentSubschema) { typedef typename AdapterType::ObjectMember Member; - typedef constraints::PropertiesConstraint::PropertySchemaMap PSM; - // Populate a PropertySchemaMap for each of the properties defined by - // the 'properties' keyword. - PSM propertySchemas; + constraints::PropertiesConstraint constraint; + + // Create subschemas for 'properties' constraint if (properties) { BOOST_FOREACH( const Member m, properties->getObject() ) { - const std::string &propertyName = m.first; - const std::string childPath = propertiesPath + "/" + - propertyName; - const Subschema *childSubschema = rootSchema.createSubschema(); - propertySchemas[propertyName] = childSubschema; + const std::string &property = m.first; + const std::string childPath = propertiesPath + "/" + property; + const Subschema *subschema = rootSchema.createSubschema(); + constraint.addPropertySubschema(property, subschema); populateSchema(rootSchema, rootNode, m.second, - *childSubschema, currentScope, childPath, fetchDoc, - parentSubschema, &propertyName); + *subschema, currentScope, childPath, fetchDoc, + parentSubschema, &property); } } - // Populate a PropertySchemaMap for each of the properties defined by - // the 'patternProperties' keyword - PSM patternPropertySchemas; + // Create subschemas for 'patternProperties' constraint if (patternProperties) { BOOST_FOREACH( const Member m, patternProperties->getObject() ) { - const std::string &propertyName = m.first; + const std::string &pattern = m.first; const std::string childPath = patternPropertiesPath + "/" + - propertyName; - const Subschema *childSubschema = rootSchema.createSubschema(); - patternPropertySchemas[propertyName] = childSubschema; + pattern; + const Subschema *subschema = rootSchema.createSubschema(); + constraint.addPatternPropertySubschema(pattern, subschema); populateSchema(rootSchema, rootNode, m.second, - *childSubschema, currentScope, childPath, fetchDoc, - parentSubschema, &propertyName); + *subschema, currentScope, childPath, fetchDoc, + parentSubschema, &pattern); } } - // Populate an additionalItems schema if required - const Subschema *additionalPropertiesSchema = NULL; + // Create an additionalItems subschema if required if (additionalProperties) { // If additionalProperties has been set, check for a boolean value. // Setting 'additionalProperties' to true allows the values of @@ -1322,15 +1317,17 @@ private: // If it has a boolean value that is 'true', then an empty // schema should be used. if (additionalProperties->asBool()) { - additionalPropertiesSchema = rootSchema.createSubschema(); + constraint.setAdditionalPropertiesSubschema( + rootSchema.emptySubschema()); } } else if (additionalProperties->isObject()) { // If additionalProperties is an object, it should be used as // a child schema. - additionalPropertiesSchema = rootSchema.createSubschema(); + const Subschema *subschema = rootSchema.createSubschema(); + constraint.setAdditionalPropertiesSubschema(subschema); populateSchema(rootSchema, rootNode, - *additionalProperties, *additionalPropertiesSchema, - currentScope, additionalPropertiesPath, fetchDoc); + *additionalProperties, *subschema, currentScope, + additionalPropertiesPath, fetchDoc); } else { // All other types are invalid throw std::runtime_error( @@ -1339,18 +1336,11 @@ private: } else { // If an additionalProperties constraint is not provided, then the // default value is an empty schema. - additionalPropertiesSchema = rootSchema.emptySubschema(); + constraint.setAdditionalPropertiesSubschema( + rootSchema.emptySubschema()); } - if (additionalPropertiesSchema) { - // If an additionalProperties schema has been created, construct a - // new PropertiesConstraint object using that schema. - return constraints::PropertiesConstraint(propertySchemas, - patternPropertySchemas, additionalPropertiesSchema); - } - - return constraints::PropertiesConstraint(propertySchemas, - patternPropertySchemas); + return constraint; } /** diff --git a/include/valijson/validation_visitor.hpp b/include/valijson/validation_visitor.hpp index cb25c9c..5e8a746 100644 --- a/include/valijson/validation_visitor.hpp +++ b/include/valijson/validation_visitor.hpp @@ -785,13 +785,28 @@ public: } /** - * @brief Validate against the properties, patternProperties, and - * additionalProperties constraints represented by a - * PatternConstraint object. + * @brief Validate a value against a PropertiesConstraint * - * @param constraint Constraint that the target must validate against. + * Validation of an object against a PropertiesConstraint proceeds in three + * stages. The first stage finds all properties in the object that have a + * corresponding subschema in the constraint, and validates those properties + * recursively. * - * @return true if the constraint is satisfied, false otherwise. + * Next, the object's properties will be validated against the subschemas + * for any 'patternProperties' that match a given property name. A property + * is required to validate against the sub-schema for all patterns that it + * matches. + * + * Finally, any properties that have not yet been validated against at least + * one subschema will be validated against the 'additionalItems' subschema. + * If this subschema is not present, then all properties must have been + * validated at least once. + * + * Non-object values are always considered valid. + * + * @param constraint Constraint that the target must validate against + * + * @return \c true if the constraint is satisfied; \c false otherwise */ virtual bool visit(const PropertiesConstraint &constraint) { @@ -801,83 +816,53 @@ public: bool validated = true; - const typename AdapterType::Object obj = target.asObject(); + // Track which properties have already been validated + std::set propertiesMatched; - // Validate each property in the target object - BOOST_FOREACH( const typename AdapterType::ObjectMember m, obj ) { + // Validate properties against subschemas for matching 'properties' + // constraints + const typename AdapterType::Object object = target.asObject(); + constraint.applyToProperties(ValidatePropertySubschemas(object, context, + true, false, true, strictTypes, results, &propertiesMatched, + &validated)); - const std::string propertyName = m.first; - bool propertyNameMatched = false; + // Exit early if validation failed, and we're not collecting exhaustive + // validation results + if (!validated && !results) { + return false; + } - std::vector newContext = context; - newContext.push_back("[\"" + m.first + "\"]"); + // Validate properties against subschemas for matching patternProperties + // constraints + constraint.applyToPatternProperties(ValidatePatternPropertySubschemas( + object, context, true, false, true, strictTypes, results, + &propertiesMatched, &validated)); - ValidationVisitor v(m.second, - newContext, strictTypes, results); + // Validate against additionalProperties subschema for any properties + // that have not yet been matched + const Subschema *additionalPropertiesSubschema = + constraint.getAdditionalPropertiesSubschema(); + if (!additionalPropertiesSubschema) { + return propertiesMatched.size() == target.getObjectSize(); + } - // Search for matching property name - PropertiesConstraint::PropertySchemaMap::const_iterator itr = - constraint.properties.find(propertyName); - if (itr != constraint.properties.end()) { - propertyNameMatched = true; - if (!v.validateSchema(*itr->second)) { + BOOST_FOREACH( const typename AdapterType::ObjectMember m, object ) { + if (propertiesMatched.find(m.first) == propertiesMatched.end()) { + // Update context + std::vector newContext = context; + newContext.push_back("[" + m.first + "]"); + + // Create a validator to validate the property's value + ValidationVisitor validator(m.second, newContext, strictTypes, + results); + if (!validator.validateSchema(*additionalPropertiesSubschema)) { if (results) { - results->pushError(context, - "Failed to validate against schema associated with property name '" + - propertyName + "' in properties constraint."); - validated = false; - } else { - return false; + results->pushError(context, "Failed to validate " + "against additional properties schema"); } - } - } - // Search for a regex that matches the property name - for (itr = constraint.patternProperties.begin(); itr != constraint.patternProperties.end(); ++itr) { - const boost::regex r(itr->first, boost::regex::perl); - if (boost::regex_search(propertyName, r)) { - propertyNameMatched = true; - // Check schema - if (!v.validateSchema(*itr->second)) { - if (results) { - results->pushError(context, - "Failed to validate against schema associated with regex '" + - itr->first + "' in patternProperties constraint."); - validated = false; - } else { - return false; - } - } - } - } - - // If the property name has been matched by a name in 'properties' - // or a regex in 'patternProperties', then it should not be - // validated against the 'additionalPatterns' schema. - if (propertyNameMatched) { - continue; - } - - // If an additionalProperties schema has been provided, the values - // associated with unmatched property names should be validated - // against that schema. - if (constraint.additionalProperties) { - if (v.validateSchema(*constraint.additionalProperties)) { - continue; - } else if (results) { - results->pushError(context, "Failed to validate property '" + - propertyName + "' against schema in additionalProperties constraint."); validated = false; - } else { - return false; } - } else if (results) { - results->pushError(context, "Failed to match property name '" + - propertyName + "' to any names in 'properties' or " - "regexes in 'patternProperties'"); - validated = false; - } else { - return false; } } @@ -1351,6 +1336,179 @@ private: bool * const validated; }; + /** + * @brief Functor to validate object properties against sub-schemas + * defined by a 'patternProperties' constraint + */ + struct ValidatePatternPropertySubschemas + { + ValidatePatternPropertySubschemas( + const typename AdapterType::Object &object, + const std::vector &context, + bool continueOnSuccess, + bool continueOnFailure, + bool continueIfUnmatched, + bool strictTypes, + ValidationResults *results, + std::set *propertiesMatched, + bool *validated) + : object(object), + context(context), + continueOnSuccess(continueOnSuccess), + continueOnFailure(continueOnFailure), + continueIfUnmatched(continueIfUnmatched), + strictTypes(strictTypes), + results(results), + propertiesMatched(propertiesMatched), + validated(validated) { } + + template + bool operator()(const StringType &patternProperty, + const Subschema *subschema) const + { + const std::string patternPropertyStr(patternProperty.c_str()); + + // It would be nice to store pre-allocated regex objects in the + // PropertiesConstraint, but boost::regex does not currently support + // custom allocators. This isn't an issue here, because Valijson's + // JSON Scheme validator does not yet support custom allocators. + const boost::regex r(patternPropertyStr, boost::regex::perl); + + bool matchFound = false; + + // Recursively validate all matching properties + typedef const typename AdapterType::ObjectMember ObjectMember; + BOOST_FOREACH( const ObjectMember m, object ) { + if (boost::regex_search(m.first, r)) { + matchFound = true; + if (propertiesMatched) { + propertiesMatched->insert(m.first); + } + + // Update context + std::vector newContext = context; + newContext.push_back("[" + m.first + "]"); + + // Recursively validate property's value + ValidationVisitor validator(m.second, newContext, + strictTypes, results); + if (validator.validateSchema(*subschema)) { + continue; + } + + if (results) { + results->pushError(context, "Failed to validate " + "against schema associated with pattern '" + + patternPropertyStr + "'."); + } + + if (validated) { + *validated = false; + } + + if (!continueOnFailure) { + return false; + } + } + } + + // Allow iteration to terminate if there was not at least one match + if (!matchFound && !continueIfUnmatched) { + return false; + } + + return continueOnSuccess; + } + + private: + const typename AdapterType::Object &object; + const std::vector &context; + const bool continueOnSuccess; + const bool continueOnFailure; + const bool continueIfUnmatched; + const bool strictTypes; + ValidationResults * const results; + std::set * const propertiesMatched; + bool * const validated; + }; + + /** + * @brief Functor to validate object properties against sub-schemas defined + * by a 'properties' constraint + */ + struct ValidatePropertySubschemas + { + ValidatePropertySubschemas( + const typename AdapterType::Object &object, + const std::vector &context, + bool continueOnSuccess, + bool continueOnFailure, + bool continueIfUnmatched, + bool strictTypes, + ValidationResults *results, + std::set *propertiesMatched, + bool *validated) + : object(object), + context(context), + continueOnSuccess(continueOnSuccess), + continueOnFailure(continueOnFailure), + continueIfUnmatched(continueIfUnmatched), + strictTypes(strictTypes), + results(results), + propertiesMatched(propertiesMatched), + validated(validated) { } + + template + bool operator()(const StringType &propertyName, + const Subschema *subschema) const + { + const std::string propertyNameKey(propertyName.c_str()); + const typename AdapterType::Object::const_iterator itr = + object.find(propertyNameKey); + if (itr == object.end()) { + return continueIfUnmatched; + } + + if (propertiesMatched) { + propertiesMatched->insert(propertyNameKey); + } + + // Update context + std::vector newContext = context; + newContext.push_back("[" + propertyNameKey + "]"); + + // Recursively validate property's value + ValidationVisitor validator(itr->second, newContext, strictTypes, + results); + if (validator.validateSchema(*subschema)) { + return continueOnSuccess; + } + + if (results) { + results->pushError(context, "Failed to validate against " + "schema associated with property name '" + + propertyNameKey + "'."); + } + + if (validated) { + *validated = false; + } + + return continueOnFailure; + } + + private: + const typename AdapterType::Object &object; + const std::vector &context; + const bool continueOnSuccess; + const bool continueOnFailure; + const bool continueIfUnmatched; + const bool strictTypes; + ValidationResults * const results; + std::set * const propertiesMatched; + bool * const validated; + }; + /** * @brief Functor to validate schema-based dependencies */