diff --git a/include/valijson/constraints/concrete_constraints.hpp b/include/valijson/constraints/concrete_constraints.hpp index 9f35967..990969f 100644 --- a/include/valijson/constraints/concrete_constraints.hpp +++ b/include/valijson/constraints/concrete_constraints.hpp @@ -165,6 +165,36 @@ private: const Subschema *elseSubschema; }; +/** + * @brief Represents a 'contains' constraint + * + * An contains constraint provides a collection of values that must be present + * in an array. + */ +class ContainsConstraint: public BasicConstraint +{ +public: + ContainsConstraint() + : subschema(nullptr) { } + + ContainsConstraint(CustomAlloc allocFn, CustomFree freeFn) + : BasicConstraint(allocFn, freeFn), + subschema(nullptr) { } + + const Subschema * getSubschema() const + { + return subschema; + } + + void setSubschema(const Subschema *subschema) + { + this->subschema = subschema; + } + +private: + const Subschema *subschema; +}; + /** * @brief Represents a 'dependencies' constraint. * @@ -307,7 +337,6 @@ public: throw; } } - } catch (...) { // Delete values already added to constraint for (const EnumValue *value : enumValues) { diff --git a/include/valijson/constraints/constraint_visitor.hpp b/include/valijson/constraints/constraint_visitor.hpp index 947d0b3..ebd5ffb 100644 --- a/include/valijson/constraints/constraint_visitor.hpp +++ b/include/valijson/constraints/constraint_visitor.hpp @@ -6,6 +6,7 @@ namespace constraints { class AllOfConstraint; class AnyOfConstraint; class ConditionalConstraint; +class ContainsConstraint; class DependenciesConstraint; class EnumConstraint; class LinearItemsConstraint; @@ -39,6 +40,7 @@ protected: typedef constraints::AllOfConstraint AllOfConstraint; typedef constraints::AnyOfConstraint AnyOfConstraint; typedef constraints::ConditionalConstraint ConditionalConstraint; + typedef constraints::ContainsConstraint ContainsConstraint; typedef constraints::DependenciesConstraint DependenciesConstraint; typedef constraints::EnumConstraint EnumConstraint; typedef constraints::LinearItemsConstraint LinearItemsConstraint; @@ -67,6 +69,7 @@ public: virtual bool visit(const AllOfConstraint &) = 0; virtual bool visit(const AnyOfConstraint &) = 0; virtual bool visit(const ConditionalConstraint &) = 0; + virtual bool visit(const ContainsConstraint &) = 0; virtual bool visit(const DependenciesConstraint &) = 0; virtual bool visit(const EnumConstraint &) = 0; virtual bool visit(const LinearItemsConstraint &) = 0; diff --git a/include/valijson/schema_parser.hpp b/include/valijson/schema_parser.hpp index 5319eda..c3df8c5 100644 --- a/include/valijson/schema_parser.hpp +++ b/include/valijson/schema_parser.hpp @@ -649,6 +649,13 @@ private: &subschema); } + if ((itr = object.find("contains")) != object.end()) { + rootSchema.addConstraintToSubschema( + makeContainsConstraint(rootSchema, rootNode, itr->second, + updatedScope, nodePath + "/contains", fetchDoc, + docCache, schemaCache), &subschema); + } + if ((itr = object.find("dependencies")) != object.end()) { rootSchema.addConstraintToSubschema( makeDependenciesConstraint(rootSchema, rootNode, @@ -689,8 +696,7 @@ private: } if ((itr = object.find("enum")) != object.end()) { - rootSchema.addConstraintToSubschema(makeEnumConstraint(itr->second), - &subschema); + rootSchema.addConstraintToSubschema(makeEnumConstraint(itr->second), &subschema); } { @@ -1177,6 +1183,35 @@ private: return constraint; } + /** + * @brief Make a new ConditionalConstraint object. + * + * @param rootSchema The Schema instance, and root subschema, + * through which other subschemas can be + * created and modified + * @param rootNode Reference to the node from which JSON + * References will be resolved when they refer + * to the current document; used for recursive + * parsing of schemas + * @param ifNode Schema that will be used to evaluate the + * conditional. + * @param thenNode Optional pointer to a JSON node containing + * a schema that will be used when the conditional + * evaluates to true. + * @param elseNode Optional pointer to a JSON node containing + * a schema that will be used when the conditional + * evaluates to false. + * @param currentScope URI for current resolution scope + * @param containsPath JSON Pointer representing the path to + * the 'contains' node + * @param fetchDoc Function to fetch remote JSON documents + * (optional) + * @param docCache Cache of resolved and fetched remote + * documents + * @param schemaCache Cache of populated schemas + * + * @return pointer to a new ContainsConstraint that belongs to the caller + */ template constraints::ConditionalConstraint makeConditionalConstraint( Schema &rootSchema, @@ -1215,6 +1250,63 @@ private: return constraint; } + /** + * @brief Make a new ContainsConstraint object. + * + * @param rootSchema The Schema instance, and root subschema, + * through which other subschemas can be + * created and modified + * @param rootNode Reference to the node from which JSON + * References will be resolved when they refer + * to the current document; used for recursive + * parsing of schemas + * @param contains Optional pointer to a JSON node containing + * an object mapping property names to + * schemas. + * @param currentScope URI for current resolution scope + * @param containsPath JSON Pointer representing the path to + * the 'contains' node + * @param fetchDoc Function to fetch remote JSON documents + * (optional) + * @param docCache Cache of resolved and fetched remote + * documents + * @param schemaCache Cache of populated schemas + * + * @return pointer to a new ContainsConstraint that belongs to the caller + */ + template + constraints::ContainsConstraint makeContainsConstraint( + Schema &rootSchema, + const AdapterType &rootNode, + const AdapterType &contains, + const opt::optional currentScope, + const std::string &containsPath, + const typename FunctionPtrs::FetchDoc fetchDoc, + typename DocumentCache::Type &docCache, + SchemaCache &schemaCache) + { + constraints::ContainsConstraint constraint; + + if (contains.isObject() || (version == kDraft7 && contains.maybeBool())) { + const Subschema *subschema = makeOrReuseSchema( + rootSchema, rootNode, contains, currentScope, containsPath, + fetchDoc, NULL, NULL, docCache, schemaCache); + constraint.setSubschema(subschema); + + } else if (contains.maybeObject()) { + // If a loosely-typed Adapter type is being used, then we'll + // assume that an empty schema has been provided. + constraint.setSubschema(rootSchema.emptySubschema()); + + } else { + // All other formats will result in an exception being thrown. + throw std::runtime_error( + "Expected valid schema for 'contains' constraint."); + } + + return constraint; + } + /** * @brief Make a new DependenciesConstraint object * diff --git a/include/valijson/validation_visitor.hpp b/include/valijson/validation_visitor.hpp index d7e4a25..476ef50 100644 --- a/include/valijson/validation_visitor.hpp +++ b/include/valijson/validation_visitor.hpp @@ -189,6 +189,46 @@ public: } } + /** + * @brief Validate current node using a 'contains' constraint + * + * A contains constraint is satisfied if the target is not an array, or if it is an array, + * only if it contains at least one value that matches the specified schema. + * + * @param constraint ContainsConstraint that the current node must validate against + * + * @return \c true if validation passes; \c false otherwise + */ + virtual bool visit(const ContainsConstraint &constraint) + { + if ((strictTypes && !target.isArray()) || !target.maybeArray()) { + return true; + } + + const Subschema *subschema = constraint.getSubschema(); + const typename AdapterType::Array arr = target.asArray(); + + bool validated = false; + for (auto itr = arr.begin(); itr != arr.end(); ++itr) { + ValidationVisitor containsValidator(*itr, context, strictTypes, nullptr); + if (containsValidator.validateSchema(*subschema)) { + validated = true; + break; + } + } + + if (!validated) { + if (results) { + results->pushError(context, + "Failed to any values against subschema in 'contains' constraint."); + } + + return false; + } + + return validated; + } + /** * @brief Validate current node against a 'dependencies' constraint * diff --git a/tests/test_validator.cpp b/tests/test_validator.cpp index e700388..6d65345 100644 --- a/tests/test_validator.cpp +++ b/tests/test_validator.cpp @@ -453,7 +453,12 @@ TEST_F(TestValidator, Draft7_BooleanSchema) // TODO: untested const -// TODO: untested contains +TEST_F(TestValidator, Draft7_Contains) +{ + // TODO: currently failing due to missing support for const + + processDraft7TestFile(TEST_SUITE_DIR "draft7/contains.json"); +} // TOOD: untested default