Files
Shiip-of-Hakinian-Espanol/subprojects/valijson/include/valijson/validation_visitor.hpp
2025-05-06 13:17:14 +02:00

1793 lines
63 KiB
C++

#pragma once
#include <cmath>
#include <string>
#include <regex>
#include <unordered_map>
#include <valijson/adapters/std_string_adapter.hpp>
#include <valijson/constraints/concrete_constraints.hpp>
#include <valijson/constraints/constraint_visitor.hpp>
#include <utility>
#include <valijson/validation_results.hpp>
#include <valijson/utils/utf8_utils.hpp>
#ifdef _MSC_VER
#pragma warning( push )
#pragma warning( disable : 4702 )
#endif
namespace valijson {
class ValidationResults;
/**
* @brief Implementation of the ConstraintVisitor interface that validates a
* target document
*
* @tparam AdapterType Adapter type for the target document.
*/
template<typename AdapterType>
class ValidationVisitor: public constraints::ConstraintVisitor
{
public:
/**
* @brief Construct a new validator for a given target value and context.
*
* @param target Target value to be validated
* @param context Current context for validation error descriptions,
* only used if results is set.
* @param strictTypes Use strict type comparison
* @param results Optional pointer to ValidationResults object, for
* recording error descriptions. If this pointer is set
* to nullptr, validation errors will caused validation to
* stop immediately.
* @param regexesCache Cache of already created std::regex objects for pattern
* constraints.
*/
ValidationVisitor(const AdapterType &target,
std::vector<std::string> context,
const bool strictTypes,
ValidationResults *results,
std::unordered_map<std::string, std::regex>& regexesCache)
: m_target(target),
m_context(std::move(context)),
m_results(results),
m_strictTypes(strictTypes),
m_regexesCache(regexesCache) { }
/**
* @brief Validate the target against a schema.
*
* When a ValidationResults object has been set via the 'results' member
* variable, validation will proceed as long as no fatal errors occur,
* with error descriptions added to the ValidationResults object.
*
* If a pointer to a ValidationResults instance is not provided, validation
* will only continue for as long as the constraints are validated
* successfully.
*
* @param subschema Sub-schema that the target must validate against
*
* @return \c true if validation passes; \c false otherwise
*/
bool validateSchema(const Subschema &subschema)
{
if (subschema.getAlwaysInvalid()) {
return false;
}
// Wrap the validationCallback() function below so that it will be
// passed a reference to a constraint (_1), and a reference to the
// visitor (*this).
Subschema::ApplyFunction fn(std::bind(validationCallback, std::placeholders::_1, std::ref(*this)));
// Perform validation against each constraint defined in the schema
if (m_results == nullptr) {
// The applyStrict() function will return immediately if the
// callback function returns false
if (!subschema.applyStrict(fn)) {
return false;
}
} else {
// The apply() function will iterate over all constraints in the
// schema, even if the callback function returns false. Once
// iteration is complete, the apply() function will return true
// only if all invokations of the callback function returned true.
if (!subschema.apply(fn)) {
return false;
}
}
return true;
}
/**
* @brief Validate a value against an AllOfConstraint
*
* An allOf constraint provides a set of child schemas against which the
* target must be validated in order for the constraint to the satifisfied.
*
* When a ValidationResults object has been set via the 'results' member
* variable, validation will proceed as long as no fatal errors occur,
* with error descriptions added to the ValidationResults object.
*
* If a pointer to a ValidationResults instance is not provided, validation
* will only continue for as long as the child schemas are validated
* successfully.
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if validation passes; \c false otherwise
*/
bool visit(const AllOfConstraint &constraint) override
{
bool validated = true;
constraint.applyToSubschemas(
ValidateSubschemas(m_target, m_context, true, false, *this, m_results, nullptr, &validated));
return validated;
}
/**
* @brief Validate a value against an AnyOfConstraint
*
* An anyOf constraint provides a set of child schemas, any of which the
* target may be validated against in order for the constraint to the
* satifisfied.
*
* Because an anyOf constraint does not require the target to validate
* against all child schemas, if validation against a single schema fails,
* the results will not be added to a ValidationResults object. Only if
* validation fails for all child schemas will an error be added to the
* ValidationResults object.
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if validation passes; \c false otherwise
*/
bool visit(const AnyOfConstraint &constraint) override
{
unsigned int numValidated = 0;
ValidationResults newResults;
ValidationResults *childResults = (m_results) ? &newResults : nullptr;
ValidationVisitor<AdapterType> v(m_target, m_context, m_strictTypes, childResults, m_regexesCache);
constraint.applyToSubschemas(
ValidateSubschemas(m_target, m_context, false, true, v, childResults, &numValidated, nullptr));
if (numValidated == 0 && m_results) {
ValidationResults::Error childError;
while (childResults->popError(childError)) {
m_results->pushError( childError.context, childError.description);
}
m_results->pushError(m_context, "Failed to validate against any schemas allowed by anyOf constraint.");
}
return numValidated > 0;
}
/**
* @brief Validate current node using a set of 'if', 'then' and 'else' subschemas
*
* A conditional constraint allows a document to be validated against one of two additional
* subschemas (specified via 'then' or 'else' properties) depending on whether the document
* satifies an optional subschema (specified via the 'if' property).
*
* @param constraint ConditionalConstraint that the current node must validate against
*
* @return \c true if validation passes; \c false otherwise
*/
bool visit(const ConditionalConstraint &constraint) override
{
ValidationResults newResults;
ValidationResults* conditionalResults = (m_results) ? &newResults : nullptr;
// Create a validator to evaluate the conditional
ValidationVisitor ifValidator(m_target, m_context, m_strictTypes, nullptr, m_regexesCache);
ValidationVisitor thenElseValidator(m_target, m_context, m_strictTypes, conditionalResults, m_regexesCache);
bool validated = false;
if (ifValidator.validateSchema(*constraint.getIfSubschema())) {
const Subschema *thenSubschema = constraint.getThenSubschema();
validated = thenSubschema == nullptr || thenElseValidator.validateSchema(*thenSubschema);
} else {
const Subschema *elseSubschema = constraint.getElseSubschema();
validated = elseSubschema == nullptr || thenElseValidator.validateSchema(*elseSubschema);
}
if (!validated && m_results) {
ValidationResults::Error conditionalError;
while (conditionalResults->popError(conditionalError)) {
m_results->pushError(conditionalError.context, conditionalError.description);
}
m_results->pushError(m_context, "Failed to validate against a conditional schema set by if-then-else constraints.");
}
return validated;
}
/**
* @brief Validate current node using a 'const' constraint
*
* A const constraint allows a document to be validated against a specific value.
*
* @param constraint ConstConstraint that the current node must validate against
*
* @return \c true if validation passes; \f false otherwise
*/
bool visit(const ConstConstraint &constraint) override
{
if (!constraint.getValue()->equalTo(m_target, m_strictTypes)) {
if (m_results) {
m_results->pushError(m_context, "Failed to match expected value set by 'const' constraint.");
}
return false;
}
return true;
}
/**
* @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
*/
bool visit(const ContainsConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isArray()) || !m_target.maybeArray()) {
return true;
}
const Subschema *subschema = constraint.getSubschema();
const typename AdapterType::Array arr = m_target.asArray();
bool validated = false;
for (const auto &el : arr) {
ValidationVisitor containsValidator(el, m_context, m_strictTypes, nullptr, m_regexesCache);
if (containsValidator.validateSchema(*subschema)) {
validated = true;
break;
}
}
if (!validated) {
if (m_results) {
m_results->pushError(m_context, "Failed to any values against subschema in 'contains' constraint.");
}
return false;
}
return validated;
}
/**
* @brief Validate current node against a 'dependencies' constraint
*
* A 'dependencies' constraint can be used to specify property-based or
* schema-based dependencies that must be fulfilled when a particular
* property is present in an object.
*
* Property-based dependencies define a set of properties that must be
* present in addition to a particular property, whereas a schema-based
* dependency defines an additional schema that the current document must
* validate against.
*
* @param constraint DependenciesConstraint that the current node
* must validate against
*
* @return \c true if validation passes; \c false otherwise
*/
bool visit(const DependenciesConstraint &constraint) override
{
// Ignore non-objects
if ((m_strictTypes && !m_target.isObject()) || (!m_target.maybeObject())) {
return true;
}
// Object to be validated
const typename AdapterType::Object object = m_target.asObject();
// Cleared if validation fails
bool validated = true;
// Iterate over all dependent properties defined by this constraint,
// invoking the DependentPropertyValidator functor once for each
// set of dependent properties
constraint.applyToPropertyDependencies(ValidatePropertyDependencies(object, m_context, m_results, &validated));
if (!m_results && !validated) {
return false;
}
// Iterate over all dependent schemas defined by this constraint,
// invoking the DependentSchemaValidator function once for each schema
// that must be validated if a given property is present
constraint.applyToSchemaDependencies(ValidateSchemaDependencies(
object, m_context, *this, m_results, &validated));
if (!m_results && !validated) {
return false;
}
return validated;
}
/**
* @brief Validate current node against an EnumConstraint
*
* Validation succeeds if the target is equal to one of the values provided
* by the EnumConstraint.
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if validation succeeds; \c false otherwise
*/
bool visit(const EnumConstraint &constraint) override
{
unsigned int numValidated = 0;
constraint.applyToValues(
ValidateEquality(m_target, m_context, false, true, m_strictTypes, nullptr, &numValidated));
if (numValidated == 0) {
if (m_results) {
m_results->pushError(m_context, "Failed to match against any enum values.");
}
return false;
}
return numValidated > 0;
}
/**
* @brief Validate a value against a LinearItemsConstraint
*
* A LinearItemsConstraint represents an 'items' constraint that specifies,
* for each item in array, an individual sub-schema that the item must
* validate against. The LinearItemsConstraint class also captures the
* presence of an 'additionalItems' constraint, which specifies a default
* sub-schema that should be used if an array contains more items than
* there are sub-schemas in the 'items' constraint.
*
* If the current value is not an array, validation always succeeds.
*
* @param constraint SingularItemsConstraint to validate against
*
* @returns \c true if validation is successful; \c false otherwise
*/
bool visit(const LinearItemsConstraint &constraint) override
{
// Ignore values that are not arrays
if ((m_strictTypes && !m_target.isArray()) || (!m_target.maybeArray())) {
return true;
}
// Sub-schema to validate against when number of items in array exceeds
// the number of sub-schemas provided by the 'items' constraint
const Subschema * const additionalItemsSubschema = constraint.getAdditionalItemsSubschema();
// Track how many items are validated using 'items' constraint
unsigned int numValidated = 0;
// Array to validate
const typename AdapterType::Array arr = m_target.asArray();
const size_t arrSize = arr.size();
// Track validation status
bool validated = true;
// Validate as many items as possible using 'items' sub-schemas
const size_t itemSubschemaCount = constraint.getItemSubschemaCount();
if (itemSubschemaCount > 0) {
if (!additionalItemsSubschema) {
if (arrSize > itemSubschemaCount) {
if (!m_results) {
return false;
}
m_results->pushError(m_context, "Array contains more items than allowed by items constraint.");
validated = false;
}
}
constraint.applyToItemSubschemas(
ValidateItems(arr, m_context, true, m_results != nullptr, m_strictTypes, m_results, &numValidated,
&validated, m_regexesCache));
if (!m_results && !validated) {
return false;
}
}
// Validate remaining items using 'additionalItems' sub-schema
if (numValidated < arrSize) {
if (additionalItemsSubschema) {
// Begin validation from the first item not validated against
// an sub-schema provided by the 'items' constraint
unsigned int index = numValidated;
typename AdapterType::Array::const_iterator begin = arr.begin();
begin.advance(numValidated);
for (typename AdapterType::Array::const_iterator itr = begin;
itr != arr.end(); ++itr) {
// Update context for current array item
std::vector<std::string> newContext = m_context;
newContext.push_back("[" + std::to_string(index) + "]");
ValidationVisitor<AdapterType> validator(*itr, newContext, m_strictTypes, m_results, m_regexesCache);
if (!validator.validateSchema(*additionalItemsSubschema)) {
if (m_results) {
m_results->pushError(m_context, "Failed to validate item #" + std::to_string(index) +
" against additional items schema.");
validated = false;
} else {
return false;
}
}
index++;
}
} else if (m_results) {
m_results->pushError(m_context, "Cannot validate item #" + std::to_string(numValidated) +
" or greater using 'items' constraint or 'additionalItems' constraint.");
validated = false;
} else {
return false;
}
}
return validated;
}
/**
* @brief Validate a value against a MaximumConstraint object
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if constraints are satisfied; \c false otherwise
*/
bool visit(const MaximumConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isNumber()) || !m_target.maybeDouble()) {
// Ignore values that are not numbers
return true;
}
const double maximum = constraint.getMaximum();
if (constraint.getExclusiveMaximum()) {
if (m_target.asDouble() >= maximum) {
if (m_results) {
m_results->pushError(m_context, "Expected number less than " + std::to_string(maximum));
}
return false;
}
} else if (m_target.asDouble() > maximum) {
if (m_results) {
m_results->pushError(m_context, "Expected number less than or equal to " + std::to_string(maximum));
}
return false;
}
return true;
}
/**
* @brief Validate a value against a MaxItemsConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if constraint is satisfied; \c false otherwise
*/
bool visit(const MaxItemsConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isArray()) || !m_target.maybeArray()) {
return true;
}
const uint64_t maxItems = constraint.getMaxItems();
if (m_target.asArray().size() <= maxItems) {
return true;
}
if (m_results) {
m_results->pushError(m_context, "Array should contain no more than " + std::to_string(maxItems) +
" elements.");
}
return false;
}
/**
* @brief Validate a value against a MaxLengthConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if constraint is satisfied; \c false otherwise
*/
bool visit(const MaxLengthConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isString()) || !m_target.maybeString()) {
return true;
}
const std::string s = m_target.asString();
const uint64_t len = utils::u8_strlen(s.c_str());
const uint64_t maxLength = constraint.getMaxLength();
if (len <= maxLength) {
return true;
}
if (m_results) {
m_results->pushError(m_context, "String should be no more than " + std::to_string(maxLength) +
" characters in length.");
}
return false;
}
/**
* @brief Validate a value against a MaxPropertiesConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if the constraint is satisfied; \c false otherwise
*/
bool visit(const MaxPropertiesConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isObject()) || !m_target.maybeObject()) {
return true;
}
const uint64_t maxProperties = constraint.getMaxProperties();
if (m_target.asObject().size() <= maxProperties) {
return true;
}
if (m_results) {
m_results->pushError(m_context, "Object should have no more than " + std::to_string(maxProperties) +
" properties.");
}
return false;
}
/**
* @brief Validate a value against a MinimumConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if the constraint is satisfied; \c false otherwise
*/
bool visit(const MinimumConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isNumber()) || !m_target.maybeDouble()) {
// Ignore values that are not numbers
return true;
}
const double minimum = constraint.getMinimum();
if (constraint.getExclusiveMinimum()) {
if (m_target.asDouble() <= minimum) {
if (m_results) {
m_results->pushError(m_context, "Expected number greater than " + std::to_string(minimum));
}
return false;
}
} else if (m_target.asDouble() < minimum) {
if (m_results) {
m_results->pushError(m_context, "Expected number greater than or equal to " + std::to_string(minimum));
}
return false;
}
return true;
}
/**
* @brief Validate a value against a MinItemsConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if the constraint is satisfied; \c false otherwise
*/
bool visit(const MinItemsConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isArray()) || !m_target.maybeArray()) {
return true;
}
const uint64_t minItems = constraint.getMinItems();
if (m_target.asArray().size() >= minItems) {
return true;
}
if (m_results) {
m_results->pushError(m_context, "Array should contain no fewer than " + std::to_string(minItems) +
" elements.");
}
return false;
}
/**
* @brief Validate a value against a MinLengthConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if the constraint is satisfied; \c false otherwise
*/
bool visit(const MinLengthConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isString()) || !m_target.maybeString()) {
return true;
}
const std::string s = m_target.asString();
const uint64_t len = utils::u8_strlen(s.c_str());
const uint64_t minLength = constraint.getMinLength();
if (len >= minLength) {
return true;
}
if (m_results) {
m_results->pushError(m_context, "String should be no fewer than " + std::to_string(minLength) +
" characters in length.");
}
return false;
}
/**
* @brief Validate a value against a MinPropertiesConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if the constraint is satisfied; \c false otherwise
*/
bool visit(const MinPropertiesConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isObject()) || !m_target.maybeObject()) {
return true;
}
const uint64_t minProperties = constraint.getMinProperties();
if (m_target.asObject().size() >= minProperties) {
return true;
}
if (m_results) {
m_results->pushError(m_context, "Object should have no fewer than " + std::to_string(minProperties) +
" properties.");
}
return false;
}
/**
* @brief Validate a value against a MultipleOfDoubleConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if the constraint is satisfied; \c false otherwise
*/
bool visit(const MultipleOfDoubleConstraint &constraint) override
{
const double divisor = constraint.getDivisor();
double d = 0.;
if (m_target.maybeDouble()) {
if (!m_target.asDouble(d)) {
if (m_results) {
m_results->pushError(m_context, "Value could not be converted "
"to a number to check if it is a multiple of " + std::to_string(divisor));
}
return false;
}
} else if (m_target.maybeInteger()) {
int64_t i = 0;
if (!m_target.asInteger(i)) {
if (m_results) {
m_results->pushError(m_context, "Value could not be converted "
"to a number to check if it is a multiple of " + std::to_string(divisor));
}
return false;
}
d = static_cast<double>(i);
} else {
return true;
}
if (d == 0) {
return true;
}
const double r = remainder(d, divisor);
if (fabs(r) > std::numeric_limits<double>::epsilon()) {
if (m_results) {
m_results->pushError(m_context, "Value should be a multiple of " + std::to_string(divisor));
}
return false;
}
return true;
}
/**
* @brief Validate a value against a MultipleOfIntConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if the constraint is satisfied; \c false otherwise
*/
bool visit(const MultipleOfIntConstraint &constraint) override
{
const int64_t divisor = constraint.getDivisor();
int64_t i = 0;
if (m_target.maybeInteger()) {
if (!m_target.asInteger(i)) {
if (m_results) {
m_results->pushError(m_context, "Value could not be converted to an integer for multipleOf check");
}
return false;
}
} else if (m_target.maybeDouble()) {
double d;
if (!m_target.asDouble(d)) {
if (m_results) {
m_results->pushError(m_context, "Value could not be converted to a double for multipleOf check");
}
return false;
}
i = static_cast<int64_t>(d);
} else {
return true;
}
if (i == 0) {
return true;
}
if (i % divisor != 0) {
if (m_results) {
m_results->pushError(m_context, "Value should be a multiple of " + std::to_string(divisor));
}
return false;
}
return true;
}
/**
* @brief Validate a value against a NotConstraint
*
* If the subschema NotConstraint currently holds a nullptr, the
* schema will be treated like the empty schema. Therefore validation
* will always fail.
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if the constraint is satisfied; \c false otherwise
*/
bool visit(const NotConstraint &constraint) override
{
const Subschema *subschema = constraint.getSubschema();
if (!subschema) {
// Treat nullptr like empty schema
return false;
}
ValidationVisitor<AdapterType> v(m_target, m_context, m_strictTypes, nullptr, m_regexesCache);
if (v.validateSchema(*subschema)) {
if (m_results) {
m_results->pushError(m_context,
"Target should not validate against schema specified in 'not' constraint.");
}
return false;
}
return true;
}
/**
* @brief Validate a value against a OneOfConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if the constraint is satisfied; \c false otherwise
*/
bool visit(const OneOfConstraint &constraint) override
{
unsigned int numValidated = 0;
ValidationResults newResults;
ValidationResults *childResults = (m_results) ? &newResults : nullptr;
ValidationVisitor<AdapterType> v(m_target, m_context, m_strictTypes, childResults, m_regexesCache);
constraint.applyToSubschemas(
ValidateSubschemas(m_target, m_context, true, true, v, childResults, &numValidated, nullptr));
if (numValidated == 0) {
if (m_results) {
ValidationResults::Error childError;
while (childResults->popError(childError)) {
m_results->pushError(
childError.context,
childError.description);
}
m_results->pushError(m_context, "Failed to validate against any "
"child schemas allowed by oneOf constraint.");
}
return false;
} else if (numValidated != 1) {
if (m_results) {
m_results->pushError(m_context, "Failed to validate against exactly one child schema.");
}
return false;
}
return true;
}
/**
* @brief Validate a value against a PatternConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if the constraint is satisfied; \c false otherwise
*/
bool visit(const PatternConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isString()) || !m_target.maybeString()) {
return true;
}
std::string pattern(constraint.getPattern<std::string::allocator_type>());
auto it = m_regexesCache.find(pattern);
if (it == m_regexesCache.end()) {
it = m_regexesCache.emplace(pattern, std::regex(pattern)).first;
}
if (!std::regex_search(m_target.asString(), it->second)) {
if (m_results) {
m_results->pushError(m_context, "Failed to match regex specified by 'pattern' constraint.");
}
return false;
}
return true;
}
/**
* @brief Validate a value against a PatternConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if the constraint is satisfied; \c false otherwise
*/
bool visit(const constraints::PolyConstraint &constraint) override
{
return constraint.validate(m_target, m_context, m_results);
}
/**
* @brief Validate a value against a PropertiesConstraint
*
* 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.
*
* 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
*/
bool visit(const PropertiesConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isObject()) || !m_target.maybeObject()) {
return true;
}
bool validated = true;
// Track which properties have already been validated
std::set<std::string> propertiesMatched;
// Validate properties against subschemas for matching 'properties'
// constraints
const typename AdapterType::Object object = m_target.asObject();
constraint.applyToProperties(
ValidatePropertySubschemas(
object, m_context, true, m_results != nullptr, true, m_strictTypes, m_results,
&propertiesMatched, &validated, m_regexesCache));
// Exit early if validation failed, and we're not collecting exhaustive
// validation results
if (!validated && !m_results) {
return false;
}
// Validate properties against subschemas for matching patternProperties
// constraints
constraint.applyToPatternProperties(
ValidatePatternPropertySubschemas(
object, m_context, true, false, true, m_strictTypes, m_results, &propertiesMatched,
&validated, m_regexesCache));
// Validate against additionalProperties subschema for any properties
// that have not yet been matched
const Subschema *additionalPropertiesSubschema =
constraint.getAdditionalPropertiesSubschema();
if (!additionalPropertiesSubschema) {
if (propertiesMatched.size() != m_target.getObjectSize()) {
if (m_results) {
std::string unwanted;
for (const typename AdapterType::ObjectMember m : object) {
if (propertiesMatched.find(m.first) == propertiesMatched.end()) {
unwanted = m.first;
break;
}
}
m_results->pushError(m_context, "Object contains a property "
"that could not be validated using 'properties' "
"or 'additionalProperties' constraints: '" + unwanted + "'.");
}
return false;
}
return validated;
}
for (const typename AdapterType::ObjectMember m : object) {
if (propertiesMatched.find(m.first) == propertiesMatched.end()) {
// Update context
std::vector<std::string> newContext = m_context;
newContext.push_back("[" + m.first + "]");
// Create a validator to validate the property's value
ValidationVisitor validator(m.second, newContext, m_strictTypes, m_results, m_regexesCache);
if (!validator.validateSchema(*additionalPropertiesSubschema)) {
if (m_results) {
m_results->pushError(m_context, "Failed to validate against additional properties schema");
}
validated = false;
}
}
}
return validated;
}
/**
* @brief Validate a value against a PropertyNamesConstraint
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if validation succeeds; \c false otherwise
*/
bool visit(const PropertyNamesConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isObject()) || !m_target.maybeObject()) {
return true;
}
for (const typename AdapterType::ObjectMember m : m_target.asObject()) {
adapters::StdStringAdapter stringAdapter(m.first);
ValidationVisitor<adapters::StdStringAdapter> validator(stringAdapter, m_context, m_strictTypes, nullptr, m_regexesCache);
if (!validator.validateSchema(*constraint.getSubschema())) {
return false;
}
}
return true;
}
/**
* @brief Validate a value against a RequiredConstraint
*
* A required constraint specifies a list of properties that must be present
* in the target.
*
* @param constraint Constraint that the target must validate against
*
* @return \c true if validation succeeds; \c false otherwise
*/
bool visit(const RequiredConstraint &constraint) override
{
if ((m_strictTypes && !m_target.isObject()) || !m_target.maybeObject()) {
return true;
}
bool validated = true;
const typename AdapterType::Object object = m_target.asObject();
constraint.applyToRequiredProperties(
ValidateProperties(object, m_context, true, m_results != nullptr, m_results, &validated));
return validated;
}
/**
* @brief Validate a value against a SingularItemsConstraint
*
* A SingularItemsConstraint represents an 'items' constraint that specifies
* a sub-schema against which all items in an array must validate. If the
* current value is not an array, validation always succeeds.
*
* @param constraint SingularItemsConstraint to validate against
*
* @returns \c true if validation is successful; \c false otherwise
*/
bool visit(const SingularItemsConstraint &constraint) override
{
// Ignore values that are not arrays
if (!m_target.isArray()) {
return true;
}
// Schema against which all items must validate
const Subschema *itemsSubschema = constraint.getItemsSubschema();
// Default items sub-schema accepts all values
if (!itemsSubschema) {
return true;
}
// Track whether validation has failed
bool validated = true;
unsigned int index = 0;
for (const AdapterType &item : m_target.getArray()) {
// Update context for current array item
std::vector<std::string> newContext = m_context;
newContext.push_back("[" + std::to_string(index) + "]");
// Create a validator for the current array item
ValidationVisitor<AdapterType> validationVisitor(item, newContext, m_strictTypes, m_results, m_regexesCache);
// Perform validation
if (!validationVisitor.validateSchema(*itemsSubschema)) {
if (m_results) {
m_results->pushError(m_context, "Failed to validate item #" + std::to_string(index) + " in array.");
validated = false;
} else {
return false;
}
}
index++;
}
return validated;
}
/**
* @brief Validate a value against a TypeConstraint
*
* Checks that the target is one of the valid named types, or matches one
* of a set of valid sub-schemas.
*
* @param constraint TypeConstraint to validate against
*
* @return \c true if validation is successful; \c false otherwise
*/
bool visit(const TypeConstraint &constraint) override
{
// Check named types
{
// ValidateNamedTypes functor assumes target is invalid
bool validated = false;
constraint.applyToNamedTypes(ValidateNamedTypes(m_target, false, true, m_strictTypes, &validated));
if (validated) {
return true;
}
}
// Check schema-based types
{
unsigned int numValidated = 0;
constraint.applyToSchemaTypes(
ValidateSubschemas(m_target, m_context, false, true, *this, nullptr, &numValidated, nullptr));
if (numValidated > 0) {
return true;
} else if (m_results) {
m_results->pushError(m_context, "Value type not permitted by 'type' constraint.");
}
}
return false;
}
/**
* @brief Validate the uniqueItems constraint represented by a
* UniqueItems object.
*
* A uniqueItems constraint requires that each of the values in an array
* are unique. Comparison is performed recursively.
*
* @param constraint Constraint that the target must validate against
*
* @return true if validation succeeds, false otherwise
*/
bool visit(const UniqueItemsConstraint &) override
{
if ((m_strictTypes && !m_target.isArray()) || !m_target.maybeArray()) {
return true;
}
// Empty arrays are always valid
if (m_target.getArraySize() == 0) {
return true;
}
bool validated = true;
const typename AdapterType::Array targetArray = m_target.asArray();
const typename AdapterType::Array::const_iterator end = targetArray.end();
const typename AdapterType::Array::const_iterator secondLast = --targetArray.end();
unsigned int outerIndex = 0;
typename AdapterType::Array::const_iterator outerItr = targetArray.begin();
for (; outerItr != secondLast; ++outerItr) {
unsigned int innerIndex = outerIndex + 1;
typename AdapterType::Array::const_iterator innerItr(outerItr);
for (++innerItr; innerItr != end; ++innerItr) {
if (outerItr->equalTo(*innerItr, true)) {
if (!m_results) {
return false;
}
m_results->pushError(m_context, "Elements at indexes #" + std::to_string(outerIndex)
+ " and #" + std::to_string(innerIndex) + " violate uniqueness constraint.");
validated = false;
}
++innerIndex;
}
++outerIndex;
}
return validated;
}
private:
/**
* @brief Functor to compare a node with a collection of values
*/
struct ValidateEquality
{
ValidateEquality(
const AdapterType &target,
const std::vector<std::string> &context,
bool continueOnSuccess,
bool continueOnFailure,
bool strictTypes,
ValidationResults *results,
unsigned int *numValidated)
: m_target(target),
m_context(context),
m_continueOnSuccess(continueOnSuccess),
m_continueOnFailure(continueOnFailure),
m_strictTypes(strictTypes),
m_results(results),
m_numValidated(numValidated) { }
template<typename OtherValue>
bool operator()(const OtherValue &value) const
{
if (value.equalTo(m_target, m_strictTypes)) {
if (m_numValidated) {
(*m_numValidated)++;
}
return m_continueOnSuccess;
}
if (m_results) {
m_results->pushError(m_context, "Target value and comparison value are not equal");
}
return m_continueOnFailure;
}
private:
const AdapterType &m_target;
const std::vector<std::string> &m_context;
bool m_continueOnSuccess;
bool m_continueOnFailure;
bool m_strictTypes;
ValidationResults * const m_results;
unsigned int * const m_numValidated;
};
/**
* @brief Functor to validate the presence of a set of properties
*/
struct ValidateProperties
{
ValidateProperties(
const typename AdapterType::Object &object,
const std::vector<std::string> &context,
bool continueOnSuccess,
bool continueOnFailure,
ValidationResults *results,
bool *validated)
: m_object(object),
m_context(context),
m_continueOnSuccess(continueOnSuccess),
m_continueOnFailure(continueOnFailure),
m_results(results),
m_validated(validated) { }
template<typename StringType>
bool operator()(const StringType &property) const
{
if (m_object.find(property.c_str()) == m_object.end()) {
if (m_validated) {
*m_validated = false;
}
if (m_results) {
m_results->pushError(m_context, "Missing required property '" +
std::string(property.c_str()) + "'.");
}
return m_continueOnFailure;
}
return m_continueOnSuccess;
}
private:
const typename AdapterType::Object &m_object;
const std::vector<std::string> &m_context;
bool m_continueOnSuccess;
bool m_continueOnFailure;
ValidationResults * const m_results;
bool * const m_validated;
};
/**
* @brief Functor to validate property-based dependencies
*/
struct ValidatePropertyDependencies
{
ValidatePropertyDependencies(
const typename AdapterType::Object &object,
const std::vector<std::string> &context,
ValidationResults *results,
bool *validated)
: m_object(object),
m_context(context),
m_results(results),
m_validated(validated) { }
template<typename StringType, typename ContainerType>
bool operator()(const StringType &propertyName, const ContainerType &dependencyNames) const
{
const std::string propertyNameKey(propertyName.c_str());
if (m_object.find(propertyNameKey) == m_object.end()) {
return true;
}
typedef typename ContainerType::value_type ValueType;
for (const ValueType &dependencyName : dependencyNames) {
const std::string dependencyNameKey(dependencyName.c_str());
if (m_object.find(dependencyNameKey) == m_object.end()) {
if (m_validated) {
*m_validated = false;
}
if (m_results) {
m_results->pushError(m_context, "Missing dependency '" + dependencyNameKey + "'.");
} else {
return false;
}
}
}
return true;
}
private:
const typename AdapterType::Object &m_object;
const std::vector<std::string> &m_context;
ValidationResults * const m_results;
bool * const m_validated;
};
/**
* @brief Functor to validate against sub-schemas in 'items' constraint
*/
struct ValidateItems
{
ValidateItems(
const typename AdapterType::Array &arr,
const std::vector<std::string> &context,
bool continueOnSuccess,
bool continueOnFailure,
bool strictTypes,
ValidationResults *results,
unsigned int *numValidated,
bool *validated,
std::unordered_map<std::string, std::regex>& regexesCache)
: m_arr(arr),
m_context(context),
m_continueOnSuccess(continueOnSuccess),
m_continueOnFailure(continueOnFailure),
m_strictTypes(strictTypes),
m_results(results),
m_numValidated(numValidated),
m_validated(validated),
m_regexesCache(regexesCache) { }
bool operator()(unsigned int index, const Subschema *subschema) const
{
// Check that there are more elements to validate
if (index >= m_arr.size()) {
return false;
}
// Update context
std::vector<std::string> newContext = m_context;
newContext.push_back("[" + std::to_string(index) + "]");
// Find array item
typename AdapterType::Array::const_iterator itr = m_arr.begin();
itr.advance(index);
// Validate current array item
ValidationVisitor validator(*itr, newContext, m_strictTypes, m_results, m_regexesCache);
if (validator.validateSchema(*subschema)) {
if (m_numValidated) {
(*m_numValidated)++;
}
return m_continueOnSuccess;
}
if (m_validated) {
*m_validated = false;
}
if (m_results) {
m_results->pushError(newContext, "Failed to validate item #" + std::to_string(index) +
" against corresponding item schema.");
}
return m_continueOnFailure;
}
private:
const typename AdapterType::Array &m_arr;
const std::vector<std::string> &m_context;
bool m_continueOnSuccess;
bool m_continueOnFailure;
bool m_strictTypes;
ValidationResults * const m_results;
unsigned int * const m_numValidated;
bool * const m_validated;
std::unordered_map<std::string, std::regex>& m_regexesCache;
};
/**
* @brief Functor to validate value against named JSON types
*/
struct ValidateNamedTypes
{
ValidateNamedTypes(
const AdapterType &target,
bool continueOnSuccess,
bool continueOnFailure,
bool strictTypes,
bool *validated)
: m_target(target),
m_continueOnSuccess(continueOnSuccess),
m_continueOnFailure(continueOnFailure),
m_strictTypes(strictTypes),
m_validated(validated) { }
bool operator()(constraints::TypeConstraint::JsonType jsonType) const
{
typedef constraints::TypeConstraint TypeConstraint;
bool valid = false;
switch (jsonType) {
case TypeConstraint::kAny:
valid = true;
break;
case TypeConstraint::kArray:
valid = m_target.isArray();
break;
case TypeConstraint::kBoolean:
valid = m_target.isBool() || (!m_strictTypes && m_target.maybeBool());
break;
case TypeConstraint::kInteger:
valid = m_target.isInteger() || (!m_strictTypes && m_target.maybeInteger());
break;
case TypeConstraint::kNull:
valid = m_target.isNull() || (!m_strictTypes && m_target.maybeNull());
break;
case TypeConstraint::kNumber:
valid = m_target.isNumber() || (!m_strictTypes && m_target.maybeDouble());
break;
case TypeConstraint::kObject:
valid = m_target.isObject();
break;
case TypeConstraint::kString:
valid = m_target.isString();
break;
default:
break;
}
if (valid && m_validated) {
*m_validated = true;
}
return (valid && m_continueOnSuccess) || m_continueOnFailure;
}
private:
const AdapterType m_target;
const bool m_continueOnSuccess;
const bool m_continueOnFailure;
const bool m_strictTypes;
bool * const m_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<std::string> &context,
bool continueOnSuccess,
bool continueOnFailure,
bool continueIfUnmatched,
bool strictTypes,
ValidationResults *results,
std::set<std::string> *propertiesMatched,
bool *validated,
std::unordered_map<std::string, std::regex>& regexesCache)
: m_object(object),
m_context(context),
m_continueOnSuccess(continueOnSuccess),
m_continueOnFailure(continueOnFailure),
m_continueIfUnmatched(continueIfUnmatched),
m_strictTypes(strictTypes),
m_results(results),
m_propertiesMatched(propertiesMatched),
m_validated(validated),
m_regexesCache(regexesCache) { }
template<typename StringType>
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. does std::regex currently support
// custom allocators? Anyway, this isn't an issue here, because Valijson's
// JSON Scheme validator does not yet support custom allocators.
const std::regex r(patternPropertyStr);
bool matchFound = false;
// Recursively validate all matching properties
typedef const typename AdapterType::ObjectMember ObjectMember;
for (const ObjectMember m : m_object) {
if (std::regex_search(m.first, r)) {
matchFound = true;
if (m_propertiesMatched) {
m_propertiesMatched->insert(m.first);
}
// Update context
std::vector<std::string> newContext = m_context;
newContext.push_back("[" + m.first + "]");
// Recursively validate property's value
ValidationVisitor validator(m.second, newContext, m_strictTypes, m_results, m_regexesCache);
if (validator.validateSchema(*subschema)) {
continue;
}
if (m_results) {
m_results->pushError(m_context, "Failed to validate against schema associated with pattern '" +
patternPropertyStr + "'.");
}
if (m_validated) {
*m_validated = false;
}
if (!m_continueOnFailure) {
return false;
}
}
}
// Allow iteration to terminate if there was not at least one match
if (!matchFound && !m_continueIfUnmatched) {
return false;
}
return m_continueOnSuccess;
}
private:
const typename AdapterType::Object &m_object;
const std::vector<std::string> &m_context;
const bool m_continueOnSuccess;
const bool m_continueOnFailure;
const bool m_continueIfUnmatched;
const bool m_strictTypes;
ValidationResults * const m_results;
std::set<std::string> * const m_propertiesMatched;
bool * const m_validated;
std::unordered_map<std::string, std::regex>& m_regexesCache;
};
/**
* @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<std::string> &context,
bool continueOnSuccess,
bool continueOnFailure,
bool continueIfUnmatched,
bool strictTypes,
ValidationResults *results,
std::set<std::string> *propertiesMatched,
bool *validated,
std::unordered_map<std::string, std::regex>& regexesCache)
: m_object(object),
m_context(context),
m_continueOnSuccess(continueOnSuccess),
m_continueOnFailure(continueOnFailure),
m_continueIfUnmatched(continueIfUnmatched),
m_strictTypes(strictTypes),
m_results(results),
m_propertiesMatched(propertiesMatched),
m_validated(validated),
m_regexesCache(regexesCache) { }
template<typename StringType>
bool operator()(const StringType &propertyName, const Subschema *subschema) const
{
const std::string propertyNameKey(propertyName.c_str());
const typename AdapterType::Object::const_iterator itr = m_object.find(propertyNameKey);
if (itr == m_object.end()) {
return m_continueIfUnmatched;
}
if (m_propertiesMatched) {
m_propertiesMatched->insert(propertyNameKey);
}
// Update context
std::vector<std::string> newContext = m_context;
newContext.push_back("[" + propertyNameKey + "]");
// Recursively validate property's value
ValidationVisitor validator(itr->second, newContext, m_strictTypes, m_results, m_regexesCache);
if (validator.validateSchema(*subschema)) {
return m_continueOnSuccess;
}
if (m_results) {
m_results->pushError(m_context, "Failed to validate against schema associated with property name '" +
propertyNameKey + "'.");
}
if (m_validated) {
*m_validated = false;
}
return m_continueOnFailure;
}
private:
const typename AdapterType::Object &m_object;
const std::vector<std::string> &m_context;
const bool m_continueOnSuccess;
const bool m_continueOnFailure;
const bool m_continueIfUnmatched;
const bool m_strictTypes;
ValidationResults * const m_results;
std::set<std::string> * const m_propertiesMatched;
bool * const m_validated;
std::unordered_map<std::string, std::regex>& m_regexesCache;
};
/**
* @brief Functor to validate schema-based dependencies
*/
struct ValidateSchemaDependencies
{
ValidateSchemaDependencies(
const typename AdapterType::Object &object,
const std::vector<std::string> &context,
ValidationVisitor &validationVisitor,
ValidationResults *results,
bool *validated)
: m_object(object),
m_context(context),
m_validationVisitor(validationVisitor),
m_results(results),
m_validated(validated) { }
template<typename StringType>
bool operator()(const StringType &propertyName, const Subschema *schemaDependency) const
{
const std::string propertyNameKey(propertyName.c_str());
if (m_object.find(propertyNameKey) == m_object.end()) {
return true;
}
if (!m_validationVisitor.validateSchema(*schemaDependency)) {
if (m_validated) {
*m_validated = false;
}
if (m_results) {
m_results->pushError(m_context, "Failed to validate against dependent schema.");
} else {
return false;
}
}
return true;
}
private:
const typename AdapterType::Object &m_object;
const std::vector<std::string> &m_context;
ValidationVisitor &m_validationVisitor;
ValidationResults * const m_results;
bool * const m_validated;
};
/**
* @brief Functor that can be used to validate one or more subschemas
*
* This functor is designed to be applied to collections of subschemas
* contained within 'allOf', 'anyOf' and 'oneOf' constraints.
*
* The return value depends on whether a given schema validates, with the
* actual return value for a given case being decided at construction time.
* The return value is used by the 'applyToSubschemas' functions in the
* AllOfConstraint, AnyOfConstraint and OneOfConstrant classes to decide
* whether to terminate early.
*
* The functor uses output parameters (provided at construction) to update
* validation state that may be needed by the caller.
*/
struct ValidateSubschemas
{
ValidateSubschemas(
const AdapterType &adapter,
const std::vector<std::string> &context,
bool continueOnSuccess,
bool continueOnFailure,
ValidationVisitor &validationVisitor,
ValidationResults *results,
unsigned int *numValidated,
bool *validated)
: m_adapter(adapter),
m_context(context),
m_continueOnSuccess(continueOnSuccess),
m_continueOnFailure(continueOnFailure),
m_validationVisitor(validationVisitor),
m_results(results),
m_numValidated(numValidated),
m_validated(validated) { }
bool operator()(unsigned int index, const Subschema *subschema) const
{
if (m_validationVisitor.validateSchema(*subschema)) {
if (m_numValidated) {
(*m_numValidated)++;
}
return m_continueOnSuccess;
}
if (m_validated) {
*m_validated = false;
}
if (m_results) {
m_results->pushError(m_context,
"Failed to validate against child schema #" + std::to_string(index) + ".");
}
return m_continueOnFailure;
}
private:
const AdapterType &m_adapter;
const std::vector<std::string> &m_context;
bool m_continueOnSuccess;
bool m_continueOnFailure;
ValidationVisitor &m_validationVisitor;
ValidationResults * const m_results;
unsigned int * const m_numValidated;
bool * const m_validated;
};
/**
* @brief Callback function that passes a visitor to a constraint.
*
* @param constraint Reference to constraint to be visited
* @param visitor Reference to visitor to be applied
*
* @return true if the visitor returns successfully, false otherwise.
*/
static bool validationCallback(const constraints::Constraint &constraint, ValidationVisitor<AdapterType> &visitor)
{
return constraint.accept(visitor);
}
/// The JSON value being validated
const AdapterType m_target;
/// Vector of strings describing the current object context
const std::vector<std::string> m_context;
/// Optional pointer to a ValidationResults object to be populated
ValidationResults *m_results;
/// Option to use strict type comparison
const bool m_strictTypes;
/// Cached regex objects for pattern constraint
std::unordered_map<std::string, std::regex>& m_regexesCache;
};
} // namespace valijson
#ifdef _MSC_VER
#pragma warning( pop )
#endif