#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace jvalidate { /** * @brief A factory object for the generation of constraints in JSON Schema * Parsing. * * Unless specified, the reference numbers in function documentation * refer to the Draft2020_12 specification, located on the following webpage(s): * https://json-schema.org/draft/2020-12/json-schema-validation OR * https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01 * * The ConstraintFactory supports, by default, all of the vocabulary of every * JSON Schema Draft covered by the Version enum. * * @tparam A The concrete Adapter type being used with this factory. * By providing this as a template parameter to ConstraintFactory - it allows us * to write code that does not need to operate on an abstract interface type * with a lot of annoying pointers and indirections. */ template class ConstraintFactory { public: using pConstraint = std::unique_ptr; using DependentKeyword = vocabulary::DependentKeyword; static constexpr auto Removed = vocabulary::Removed; static constexpr auto Literal = vocabulary::Literal; static constexpr auto Keyword = vocabulary::Keyword; static constexpr auto KeywordMap = vocabulary::KeywordMap; static constexpr auto PostConstraint = vocabulary::PostConstraint; /** * @brief In order to support multiple schema versions in a single instance of * a ConstraintFactory, we need to be able to describe which version a keyword * becomes part of the language vocabulary, and what (if any) version it * leaves the vocabulary after. * * To do this, we store an ordered map of Version enum onto Make (see above), * and then use {@see std::map::lower_bound} to determine which Make object is * the most approriate for the schema version being evaluated. * * For example: * The "additionalProperties" constraint is the same across all versions, and * so can be represented using only a function pointer. * {"additionalProperties", &Self::additionalProperties} * * The "const" constraint was not added until Draft06, so we include the * version when constructing its constraint bindings like so: * {"const", {schema::Version::Draft06, &Self::isConstant}} * * The "divisibleBy" constraint was removed in favor of "multipleOf" in * Draft04, and therefore is represented as: * {"divisibleBy", {{schema::Version::Earliest, &Self::multipleOf}, * {schema::Version::Draft04, Removed}}}, * {"multipleOf", {schema::Version::Draft04, &Self::multipleOf}} * * A small number of rare constraints change their meaning when moving from * one draft version to another in such a significant way that it makes more * sense to use different MakeConstraint functions for them. * {"items", {{schema::Version::Earliest, &Self::itemsTupleOrVector}, * {schema::Version::Draft2020_12, &Self::additionalItems}}} */ struct Versioned { template > Versioned(M make) : data{{schema::Version::Earliest, make}} {} Versioned(schema::Version version, vocabulary::Metadata make) : data{{version, make}} {} Versioned(std::initializer_list>> init) : data(init) {} std::map, std::greater<>> data; }; using Store = std::unordered_map; private: using Self = ConstraintFactory; private: std::unordered_map constraints_{ {"$defs", {schema::Version::Draft2019_09, KeywordMap}}, {"additionalItems", {{schema::Version::Earliest, {&Self::additionalItems, Keyword}}, {schema::Version::Draft2020_12, Removed}}}, {"additionalProperties", {{&Self::additionalProperties, Keyword}}}, {"allOf", {schema::Version::Draft04, {&Self::allOf, Keyword}}}, {"anyOf", {schema::Version::Draft04, {&Self::anyOf, Keyword}}}, {"const", {schema::Version::Draft06, &Self::isConstant}}, {"contains", {schema::Version::Draft06, &Self::contains}}, {"definitions", KeywordMap}, {"dependencies", {{&Self::dependencies, KeywordMap}}}, {"dependentRequired", {schema::Version::Draft2019_09, &Self::dependentRequired}}, {"dependentSchemas", {schema::Version::Draft2019_09, {&Self::dependentSchemas, KeywordMap}}}, {"disallow", {{schema::Version::Earliest, &Self::disallowDraft3}, {schema::Version::Draft04, Removed}}}, {"divisibleBy", {{schema::Version::Earliest, &Self::multipleOf}, {schema::Version::Draft04, Removed}}}, {"else", {{schema::Version::Draft07, DependentKeyword{"if"}}}}, {"enum", &Self::isInEnumuration}, {"exclusiveMaximum", {schema::Version::Draft06, &Self::exclusiveMaximum}}, {"exclusiveMinimum", {schema::Version::Draft06, &Self::exclusiveMinimum}}, {"extends", {{schema::Version::Earliest, &Self::extendsDraft3}, {schema::Version::Draft04, Removed}}}, {"format", &Self::format}, {"if", {schema::Version::Draft07, {&Self::ifThenElse, Keyword}}}, {"items", {{schema::Version::Earliest, {&Self::itemsTupleOrVector, Keyword}}, {schema::Version::Draft2020_12, {&Self::additionalItems, Keyword}}}}, {"maxItems", &Self::maxItems}, {"maxLength", &Self::maxLength}, {"maxProperties", {schema::Version::Draft04, &Self::maxProperties}}, {"maximum", &Self::maximum}, {"minItems", &Self::minItems}, {"minLength", &Self::minLength}, {"minProperties", {schema::Version::Draft04, &Self::minProperties}}, {"minimum", &Self::minimum}, {"multipleOf", {schema::Version::Draft04, &Self::multipleOf}}, {"not", {schema::Version::Draft04, {&Self::isNot, Keyword}}}, {"oneOf", {schema::Version::Draft04, {&Self::oneOf, Keyword}}}, {"pattern", &Self::pattern}, {"patternProperties", {{&Self::patternProperties, KeywordMap}}}, {"prefixItems", {schema::Version::Draft2020_12, {&Self::prefixItems, Keyword}}}, {"properties", {{schema::Version::Earliest, {&Self::propertiesDraft3, KeywordMap}}, {schema::Version::Draft04, {&Self::properties, KeywordMap}}}}, {"propertyNames", {schema::Version::Draft06, &Self::propertyNames}}, {"required", {schema::Version::Draft04, &Self::required}}, {"then", {schema::Version::Draft07, DependentKeyword{"if"}}}, {"type", {{schema::Version::Earliest, &Self::typeDraft3}, {schema::Version::Draft04, &Self::type}}}, {"unevaluatedItems", {schema::Version::Draft2019_09, {&Self::unevaluatedItems, PostConstraint}}}, {"unevaluatedProperties", {schema::Version::Draft2019_09, {&Self::unevaluatedProperties, PostConstraint}}}, {"uniqueItems", &Self::uniqueItems}, }; public: ConstraintFactory() = default; static pConstraint ptr(auto && in) { return std::make_unique(std::move(in)); } /** * @brief Construct a new ConstraintFactory, with a pre-defined list of user * keywords to be injected into the vocabulary. Does not override any keywords * that currently exist. Operates equivalently to calling with_user_keyword * for each element of init. * * @param init A list of keyword => Versioned constraint generators */ ConstraintFactory(std::initializer_list> init) { constraints_.insert(init.begin(), init.end()); } /** * @brief A "with-er" function that adds a user-defined keyword to the * vocabulary. This keyword cannot already exist in the schema (although it is * not asserted). * * Only usable on rval references to prevent injections. * * @param word The keyword being added * @param make The Versioned constraint generators * * @returns This factory */ ConstraintFactory && with_user_keyword(std::string_view word, Versioned make) && { constraints_.insert(word, std::move(make)); return *this; } /** * @brief A "with-er" function that overrides a draft-defined keyword to the * vocabulary. This keyword is expected to already exist (although it is not * asserted). * * Only usable on rval references to prevent injections. * * @param word The keyword being overwritten * @param make The Versioned constraint generators * * @returns This factory */ ConstraintFactory && override_keyword(std::string_view word, Versioned make) && { constraints_[word] = std::move(make); return *this; } detail::Vocabulary keywords(schema::Version version) const { std::unordered_map> rval; for (auto const & [key, versions] : constraints_) { if (auto it = versions.data.lower_bound(version); it != versions.data.end() && it->second) { rval.emplace(key, it->second); } } return detail::Vocabulary(version, std::move(rval)); } // SECTION: Untyped Constraints /** * @brief Parser for the "type" constraint (6.1.1) for JSON Schema Draft04 * and up. This validates that a JSON document instance is of a specified * type or list of types. * * @pre context.schema MUST be either a string, or an array of strings. * @pre each string must be one of the six JSON primitives, or "integer" * * @param context The operating context of the schema parsing. * * @returns If the value at "type" is a string, then we return a constraint * that is true if the type of the instance is of the type represented by the * string. If the value is an array, then the constraint will validate if the * instance is any of the listed types. * * @throws std::runtime_error if precondition #1 is broken * @throws std::out_of_range if precondition #2 is broken */ static auto type(detail::ParserContext const & context) { static std::unordered_map const s_type_names{ {"null", adapter::Type::Null}, {"boolean", adapter::Type::Boolean}, {"integer", adapter::Type::Integer}, {"number", adapter::Type::Number}, {"string", adapter::Type::String}, {"array", adapter::Type::Array}, {"object", adapter::Type::Object}, }; auto to_type = [](std::string_view type) { EXPECT_M(s_type_names.contains(type), "Unknown type " << type); return s_type_names.at(type); }; adapter::Type const type = context.schema.type(); if (type == adapter::Type::String) { return ptr(constraint::TypeConstraint{{to_type(context.schema.as_string())}}); } EXPECT(type == adapter::Type::Array); std::set types; for (auto subschema : context.schema.as_array()) { types.insert(to_type(subschema.as_string())); } return ptr(constraint::TypeConstraint{types}); } /** * @brief Parser for the "type" constraint (5.1) for JSON Schema Draft03 * (https://json-schema.org/draft-03/draft-zyp-json-schema-03.pdf). This * validates that a JSON document instance is of a specified type, or list of * types/subschemas. * * Despite the draft document not indicating so, it is considered legal for a * type constraint to allow subschemas when in array form. * * Additionally, it supports the additional type enumeration "any", which is * equivalent to not having a type constraint at all. * * @pre context.schema MUST be either a string, or an array of * strings/subschemas. * @pre each string MUST be one of the six JSON primitives, "integer", * or "any" * * @param context The operating context of the schema parsing. * * @returns If the value at "type" is a string, then we return a constraint * that is true if the type of the instance is of the type represented by the * string. If the value is an array, then the constraint will validate if the * instance is any of the listed types or validated by the subschema. * * @throws std::runtime_error if precondition #1 is broken * @throws std::out_of_range if precondition #2 is broken */ static pConstraint typeDraft3(detail::ParserContext const & context) { static std::unordered_map const s_type_names{ {"null", adapter::Type::Null}, {"boolean", adapter::Type::Boolean}, {"integer", adapter::Type::Integer}, {"number", adapter::Type::Number}, {"string", adapter::Type::String}, {"array", adapter::Type::Array}, {"object", adapter::Type::Object}, }; auto to_type = [](std::string_view type) { EXPECT_M(s_type_names.contains(type), "Unknown type " << type); return s_type_names.at(type); }; adapter::Type const type = context.schema.type(); if (type == adapter::Type::String) { if (context.schema.as_string() == "any") { return nullptr; // nullptr is a synonym for "always accept" } return ptr(constraint::TypeConstraint{{to_type(context.schema.as_string())}}); } EXPECT(type == adapter::Type::Array); std::vector children; std::set types; for (auto const & [index, subschema] : detail::enumerate(context.schema.as_array())) { if (subschema.type() != adapter::Type::String) { children.push_back(context.child(subschema, index).node()); } else if (subschema.as_string() == "any") { return nullptr; // nullptr is a synonym for "always accept" } else { types.insert(to_type(subschema.as_string())); } } children.push_back(ptr(constraint::TypeConstraint{types})); return ptr(constraint::AnyOfConstraint{std::move(children)}); } /** * @brief Parser for the "disallow" constraint (5.25) for JSON Schema Draft03. * This constraint has the same preconditions and parsing rules as "type" * (Draft03) does, but inverts it. * * @pre context.schema MUST be either a string, or an array of * strings/subschemas. * @pre each string must be one of the six JSON primitives, "integer", * or "any" * * @param context The operating context of the schema parsing. * * @returns not(typeDraft3()) * * @throws {@see ConstraintFactory::typeDraft3} */ static pConstraint disallowDraft3(detail::ParserContext const & context) { return ptr(constraint::NotConstraint{typeDraft3(context)}); } /** * @brief Parser for the "extends" constraint (5.26) for JSON Schema Draft03. * This validates that a JSON document instance meets both the parent schema, * and the child schema(s). * * The draft document shows examples that make sense as "$ref" constraints, * but the actual form of the "extends" is another schema, or an array of * schemas. * In Draft04 - the array/object form is replaced by including an "allOf" * constraint will all of the subschemas. * In Draft2019_09 - the single-object form can be implemented using "$ref", * since the parsing rules of reference handling have been relaxed. * * @pre context.schema MUST be either an object, or an array * @pre each object MUST be valid as a top-level schema * * @param context The operating context of the schema parsing. * * @returns An AllOf constraint matching the one/many requested subschema(s). * * @throws std::runtime_error if precondition #1 is broken * @throws any std::exception if precondition #2 is broken */ static pConstraint extendsDraft3(detail::ParserContext const & context) { std::vector children; switch (context.schema.type()) { case adapter::Type::Object: children.push_back(context.node()); break; case adapter::Type::Array: { for (auto const & [index, subschema] : detail::enumerate(context.schema.as_array())) { children.push_back(context.child(subschema, index).node()); } break; } default: JVALIDATE_THROW(std::runtime_error, "extends must be a schema of array-of-schemas"); } return ptr(constraint::AllOfConstraint{std::move(children)}); } /** * @brief Parser for the "if" constraint (10.2.2.1, 10.2.2.2, 10.2.2.3). This * constraint is divided between three keywords. * If the "if" keyword is present, then we will attempt to validate the "then" * and "else keywords as well. All three are implemented as subschemas. * The evaluation forms an if-then-else, if-then, or if-else block, depending * on which of "then" and "else" are present. * There is no rule preventing an "if" keyword from existing without either * of the "then" or "else" keywords, but doing so only serves the purpose of * annotation gathering. * * @param context The operating context of the schema parsing. * * @returns A ContainsConstraint, with optional minimum and maximum matching */ static pConstraint ifThenElse(detail::ParserContext const & context) { schema::Node const * then_ = context.fixed_schema(true); if (context.parent->contains("then")) { then_ = context.neighbor("then").node(); } schema::Node const * else_ = context.fixed_schema(true); if (context.parent->contains("else")) { else_ = context.neighbor("else").node(); } return ptr(constraint::ConditionalConstraint{context.node(), then_, else_}); } /** * @brief Parser for the "enum" constraint (6.1.2) for JSON Schema Draft04 * and up. This validates that the JSON document instance is equal to one of * the given JSON document samples. * * @param context The operating context of the schema parsing. * * @returns A constraint that checks equality against a set of values. */ static auto isInEnumuration(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Array); std::vector> rval; for (auto subschema : context.schema.as_array()) { rval.push_back(subschema.freeze()); } return ptr(constraint::EnumConstraint{std::move(rval)}); } /** * @brief Parser for the "const" constraint (6.1.3) for JSON Schema Draft04 * and up. This validates that the JSON document instance is equal to the * given JSON document samples. * * @param context The operating context of the schema parsing. * * @returns A constraint that checks equality against a single value. */ static auto isConstant(detail::ParserContext const & context) { constraint::EnumConstraint rval; rval.enumeration.push_back(context.schema.freeze()); return ptr(rval); } /** * @brief Parser for a "allOf" constraint (10.2.1.1). This constraint * validates that all of the underlying schemas validate the instance. * * @pre context.schema is an array * * @param context The operating context of the schema parsing. * * @returns A AllOfConstraint * * @throws std::runtime_error if precondition #1 is broken */ static auto allOf(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Array); std::vector rval; for (auto const & [index, subschema] : detail::enumerate(context.schema.as_array())) { rval.push_back(context.child(subschema, index).node()); } return ptr(constraint::AllOfConstraint{std::move(rval)}); } /** * @brief Parser for a "anyOf" constraint (10.2.1.2). This constraint * validates that any of the underlying schemas validate the instance. * * @pre context.schema is an array * * @param context The operating context of the schema parsing. * * @returns A AnyOfConstraint * * @throws std::runtime_error if precondition #1 is broken */ static auto anyOf(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Array); std::vector rval; for (auto const & [index, subschema] : detail::enumerate(context.schema.as_array())) { rval.push_back(context.child(subschema, index).node()); } return ptr(constraint::AnyOfConstraint{std::move(rval)}); } /** * @brief Parser for a "oneOf" constraint (10.2.1.3). This constraint * validates that exactly one of the underlying schemas validate the instance. * * @pre context.schema is an array * * @param context The operating context of the schema parsing. * * @returns A OneOfConstraint * * @throws std::runtime_error if precondition #1 is broken */ static auto oneOf(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Array); std::vector rval; for (auto const & [index, subschema] : detail::enumerate(context.schema.as_array())) { rval.push_back(context.child(subschema, index).node()); } return ptr(constraint::OneOfConstraint{rval}); } /** * @brief Parser for a "not" constraint (10.2.1.4). This constraint inverts * the acceptance of the underlying schema. * * @param context The operating context of the schema parsing. * * @returns A NotConstraint */ static auto isNot(detail::ParserContext const & context) { return ptr(constraint::NotConstraint{context.node()}); } // SECTION: Numeric Constraints /** * @brief Parser for the "minimum" constraint (6.2.4). This constraint * validates numberic JSON instances where `instance >= context.schema` * Before Draft06, this constraint must test for/evaluate the neighbor keyword * "exclusiveMinimum", which is a boolean. * Starting in Draft06, the "exclusiveMinimum" constraint exists separately, * and this constraint represents "inclusive minimum". * * @param context The operating context of the schema parsing. * * @returns A MinimumConstraint * * @throws If the contained value is not interpretable as a number * @throws std::runtime_error if version < Draft06 AND exclusiveMinimum exists * and is not a boolean. */ static auto minimum(detail::ParserContext const & context) { double value = context.schema.as_number(); if (context.vocab->version() < schema::Version::Draft06 && context.parent->contains("exclusiveMinimum")) { auto exclusive = (*context.parent)["exclusiveMinimum"]; EXPECT(exclusive.type() == adapter::Type::Boolean); return ptr(constraint::MinimumConstraint{value, exclusive.as_boolean()}); } return ptr(constraint::MinimumConstraint{value, false}); } /** * @brief Parser for the "exclusiveMinimum" constraint (6.2.5) for JSON Schema * Draft06 and up. This constraint validates numberic JSON instances where * `instance > context.schema` * * @param context The operating context of the schema parsing. * * @returns A MinimumConstraint * * @throws If the contained value is not interpretable as a number */ static pConstraint exclusiveMinimum(detail::ParserContext const & context) { double value = context.schema.as_number(); return ptr(constraint::MinimumConstraint{value, true}); } /** * @brief Parser for the "maximum" constraint (6.2.2). This constraint * validates numberic JSON instances where `instance <= context.schema` * Before Draft06, this constraint must test for/evaluate the neighbor keyword * "exclusiveMaximum", which is a boolean. * Starting in Draft06, the "exclusiveMaximum" constraint exists separately, * and this constraint represents "inclusive maximum". * * @param context The operating context of the schema parsing. * * @returns A MaximumConstraint * * @throws If the contained value is not interpretable as a number * @throws std::runtime_error if version < Draft06 AND exclusiveMaximum exists * and is not a boolean. */ static auto maximum(detail::ParserContext const & context) { double value = context.schema.as_number(); if (context.vocab->version() < schema::Version::Draft06 && context.parent->contains("exclusiveMaximum")) { auto exclusive = (*context.parent)["exclusiveMaximum"]; EXPECT(exclusive.type() == adapter::Type::Boolean); return ptr(constraint::MaximumConstraint{value, exclusive.as_boolean()}); } return ptr(constraint::MaximumConstraint{value, false}); } /** * @brief Parser for the "exclusiveMaximum" constraint (6.2.3) for JSON Schema * Draft06 and up. This constraint validates numberic JSON instances where * `instance < context.schema` * * @param context The operating context of the schema parsing. * * @returns A MaximumConstraint * * @throws If the contained value is not interpretable as a number */ static pConstraint exclusiveMaximum(detail::ParserContext const & context) { double value = context.schema.as_number(); return ptr(constraint::MaximumConstraint{value, true}); } /** * @brief Parser for the "multipleOf" constraint (6.2.1) for JSON Schema * Draft04 and up. In Draft03 this covers the "divisibleBy" constraint (5.24). * This constraint validates numeric JSON instances where * `instance / context.schema` is a whole number. Because of differences in * handling of numbers between C++, Python, JavaScript, etc. there are some * edge cases which cannot be properly handled. * * @pre context.schema matches { "type": "number" } * @pre context.schema.as_number() > 0 * * @param context The operating context of the schema parsing. * * @returns A MultipleOfConstraint * * @throws If the contained value is not interpretable as a number */ static auto multipleOf(detail::ParserContext const & context) { double value = context.schema.as_number(); return ptr(constraint::MultipleOfConstraint{value}); } // SECTION: String Constraints /** * @brief Parser for the "minLength" constraint (6.3.2). This constraint * validates string JSON instances has a length >= context.schema, as per * RFC 8259. * * @pre context.schema MUST be an integer * @pre context.schema >= 0 * * @param context The operating context of the schema parsing. * * @returns A MinLengthConstraint * * @throws If the contained value is not interpretable as an integer */ static auto minLength(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Integer || context.schema.type() == adapter::Type::Number); return ptr(constraint::MinLengthConstraint{context.schema.as_integer()}); } /** * @brief Parser for the "maxLength" constraint (6.3.1). This constraint * validates string JSON instances have a length <= context.schema, as per * RFC 8259. * * @pre context.schema MUST be an integer * @pre context.schema >= 0 * * @param context The operating context of the schema parsing. * * @returns A MaxLengthConstraint * * @throws If the contained value is not interpretable as an integer */ static auto maxLength(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Integer || context.schema.type() == adapter::Type::Number); return ptr(constraint::MaxLengthConstraint{context.schema.as_integer()}); } /** * @brief Parser for the "pattern" constraint (6.3.3). This constraint * validates string JSON instances match an ECMA-262 compatible regular * expression. * * This function does not attempt to compile the regular expression, * meaning that if the pattern is not a valid regex, it will fail at a later * point. * * @pre context.schema MUST be a string * * @param context The operating context of the schema parsing. * * @returns A PatternConstraint * * @throws If the contained value is not interpretable as a string */ static auto pattern(detail::ParserContext const & context) { return ptr(constraint::PatternConstraint{context.schema.as_string()}); } /** * @brief Parser for the "format" constraint, which validates string JSON * instances against one of several pre-defined formats that either would * be unnecessarily complicated to represent as PatternConstraint, prone to * user-error when done so, or cannot be represented as regular expressions * at all. * * @pre context.schema MUST be a string * * @param context The operating context of the schema parsing. * * @returns A FormatConstraint, if the vocabulary enabled "format assertions", * then this constraint will actually validate the JSON instance. Otherwise, * it simply annotates that the field is expected to match the format. * * @throws If the contained value is not interpretable as a string */ static auto format(detail::ParserContext const & context) { return ptr(constraint::FormatConstraint{context.schema.as_string(), context.vocab->is_format_assertion()}); } // SECTION: Array Constraints /** * @brief Parser for the "contains" constraint (10.3.1.3, 6.4.4, 6.4.5). This * constraint is divided between three different keywords. * The "contains" keyword acts as a subschema, and is required for parsing. * "minContains" and "maxContains" act as boundaries on the number of matches, * such that the number of array elements matching the "contains" schema is at * least "minContains" and at most "maxContains". * * If "minContains" is null, then it is the equivalent of 1. * A "minContains" value of zero is only meaningful in the context of * setting an upper-bound. * If "maxContains" is null, then it is the equivalent of INFINITY * * @pre context.schema MUST be a valid JSON schema * @pre context.parent["maxContains"] is null OR an integer >= 0 * @pre context.parent["minContains"] is null OR an integer >= 0 * * @param context The operating context of the schema parsing. * * @returns A ContainsConstraint, with optional minimum and maximum matching */ static auto contains(detail::ParserContext const & context) { if (context.vocab->version() < schema::Version::Draft2019_09) { return ptr(constraint::ContainsConstraint{context.node()}); } std::optional maximum; std::optional minimum; if (context.parent->contains("maxContains")) { maximum = (*context.parent)["maxContains"].as_integer(); } if (context.parent->contains("minContains")) { minimum = (*context.parent)["minContains"].as_integer(); } return ptr(constraint::ContainsConstraint{context.node(), minimum, maximum}); } /** * @brief Parser for the "minItems" constraint (6.4.2). This constraint * validates array JSON instances have a length <= context.schema. * * @pre context.schema MUST be an integer * @pre context.schema >= 0 * * @param context The operating context of the schema parsing. * * @returns A MinItemsConstraint * * @throws If the contained value is not interpretable as an integer */ static auto minItems(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Integer || context.schema.type() == adapter::Type::Number); return ptr(constraint::MinItemsConstraint{context.schema.as_integer()}); } /** * @brief Parser for the "maxItems" constraint (6.4.1). This constraint * validates array JSON instances have a length <= context.schema. * * @pre context.schema MUST be an integer * @pre context.schema >= 0 * * @param context The operating context of the schema parsing. * * @returns A MaxItemsConstraint * * @throws If the contained value is not interpretable as an integer */ static auto maxItems(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Integer || context.schema.type() == adapter::Type::Number); return ptr(constraint::MaxItemsConstraint{context.schema.as_integer()}); } /** * @brief Parser for the "prefixItems" constraint (10.3.1.1) for JSON Schema * Draft2020_12 and up. This constraint validates the first N elements of an * array JSON instance with its own subschemas. * * @pre context.schema MUST be an array * @pre context.schema MUST have at least 1 element * @pre each element of context.schema MUST be a valid JSON Schema * * @param context The operating context of the schema parsing. * * @returns A TupleConstraint * * @throws std::runtime_error if preconditions #1 or #2 are broken * @throws if precondition #3 is broken */ static auto prefixItems(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Array); std::vector rval; for (auto const & [index, subschema] : detail::enumerate(context.schema.as_array())) { rval.push_back(context.child(subschema, index).node()); } return ptr(constraint::TupleConstraint{rval}); } /** * @brief Parser for the "additionalItems" constraint (9.3.1.1) for JSON * Schema Draft2019_09 and prior, and the "items" constraint (10.3.1.2) for * JSON Schema Draft2020_12 and up. * This constraint validates an array JSON instance starting from the N-th * element, as determined by the "items" schema (<= Draft2019_09), or the * "prefixItems" schema (>= Draft2020_12). * * @pre context.schema MUST be a valid JSON Schema (including boolean schema) * * @param context The operating context of the schema parsing. * * @returns An AdditionalItemsConstraint, unless the special condition * described below is met. * * @throws if the precondition is broken */ static pConstraint additionalItems(detail::ParserContext const & context) { std::string const prefix = context.vocab->version() >= schema::Version::Draft2020_12 ? "prefixItems" : "items"; auto const & parent = *context.parent; // Before Draft 2020-12, the "items" could be either a subschema or a tuple. // When not provided, we therefore treat it as an "accept-all" schema, and // thus will never have additionalItems to process. Similarly - if it is an // Object, then it must act on all items. if (context.vocab->version() < schema::Version::Draft2020_12 && (not parent.contains(prefix) || parent[prefix].type() == adapter::Type::Object)) { return nullptr; } size_t const n = parent[prefix].array_size(); // Prior to Draft06, boolean schemas were not allowed in a general context, // they were instead reserved for the "additionalItems" and // "additionalProperties" keywords. if (context.vocab->version() < schema::Version::Draft06 && context.schema.type() == adapter::Type::Boolean) { return ptr(constraint::AdditionalItemsConstraint{context.always(), n}); } return ptr(constraint::AdditionalItemsConstraint{context.node(), n}); } /** * @brief Parser for the "items" constraint (9.3.1.1) for JSON Schema * Draft2019_09 and prior. * https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02 * This constraint validates either the first N elements of an array JSON * instance with its own subschemas, or all elements of an array JSON instance * with its single subschema. * * @pre context.schema MUST satisfy the preconditions of either * {@see ConstraintFactory::prefixItems} or * {@see ConstraintFactory::additionalItems}. * * @param context The operating context of the schema parsing. * * @returns If the schema is an array, a TupleConstraint. If the schema is an * object, an AdditionalItemsConstraint. * * @throws {@see ConstraintFactory::prefixItems} * @throws {@see ConstraintFactory::additionalItems} */ static pConstraint itemsTupleOrVector(detail::ParserContext const & context) { if (context.schema.type() == adapter::Type::Array) { return prefixItems(context); } return ptr(constraint::AdditionalItemsConstraint{context.node(), 0}); } /** * @brief Parser for the "unevaluatedItems" constraint (11.2). This constraint * validates every element of an array JSON instance that is not handled by * any other subschema of this schema's parent. * In terms of annotation, we flag instance paths as "Accept", "Reject", or * "Noop" - "unevaluatedItems" constraints will be run after all other * constraints, applying to every item that is labeled "Noop" (or was never * visited to get even that tag). * * @param context The operating context of the schema parsing. * * @returns An AdditionalPropertiesConstraint */ static auto unevaluatedItems(detail::ParserContext const & context) { return ptr(constraint::UnevaluatedItemsConstraint{context.node()}); } /** * @brief Parser for the "uniqueItems" constraint (6.4.3). This constraint * validates array JSON instances where no member of the array is repeated. * In other words: `std::set{instance}.size() == instance.size()` * * @pre context.schema MUST be a boolean * * @returns An "accept-all" constraint if the schema is "false", else a * UniqueItemsConstraint. * * @throws std::runtime_error if precondition #1 is broken */ static pConstraint uniqueItems(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Boolean); if (not context.schema.as_boolean()) { return nullptr; } return ptr(constraint::UniqueItemsConstraint{}); } // SECTION: Object Constraints /** * @brief Parser for the "required" constraint (6.5.3) starting in Draft04. * In Draft03, the required keyword is a boolean property of subschemas, and * so must be evaluated by {@see ConstraintFactory::propertiesDraft3}. * This constraint validates object JSON instances MUST contain every property * in the schema array provided. * * @pre context.schema MUST be an array of strings * * @param context The operating context of the schema parsing. * * @returns A RequiredConstraint * * @throws std::runtime_error if precondition #1 is broken */ static auto required(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Array); std::unordered_set rval; for (auto subschema : context.schema.as_array()) { EXPECT(subschema.type() == adapter::Type::String); rval.insert(subschema.as_string()); } return ptr(constraint::RequiredConstraint{rval}); } /** * @brief Parser for the "minProperties" constraint (6.5.2). This constraint * validates object JSON instances have a length <= context.schema. * * @pre context.schema MUST be an integer * @pre context.schema >= 0 * * @param context The operating context of the schema parsing. * * @returns A MinPropertiesConstraint * * @throws If the contained value is not interpretable as an integer */ static auto minProperties(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Integer || context.schema.type() == adapter::Type::Number); return ptr(constraint::MinPropertiesConstraint{context.schema.as_integer()}); } /** * @brief Parser for the "maxProperties" constraint (6.5.1). This constraint * validates object JSON instances have a length <= context.schema. * * @pre context.schema MUST be an integer * @pre context.schema >= 0 * * @param context The operating context of the schema parsing. * * @returns A MaxPropertiesConstraint * * @throws If the contained value is not interpretable as an integer */ static auto maxProperties(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Integer || context.schema.type() == adapter::Type::Number); return ptr(constraint::MaxPropertiesConstraint{context.schema.as_integer()}); } /** * @brief Parser for the "patternProperties" constraint (10.3.2.2). This * constraint validates an object JSON instance where each key in the instance * is checked against each of the ECMA-262 compatible regular expression keys * in this constraint, and validated against the subschema of that pattern if * matched. EVERY subschema whose pattern matches is validated against, so the * order does not strictly matter. * * This function does not attempt to compile the regular expression(s), * meaning that if the pattern is not a valid regex, it will fail at a later * point. * * @pre context.schema is an object * * @param context The operating context of the schema parsing. * * @returns A PatternPropertiesConstraint * * @throws std::runtime_error if precondition #1 is broken */ static auto patternProperties(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Object); std::vector> rval; for (auto [prop, subschema] : context.schema.as_object()) { rval.emplace_back(prop, context.child(subschema, prop).node()); } return ptr(constraint::PatternPropertiesConstraint{rval}); } /** * @brief Parser for the "properties" constraint (10.3.2.1) for JSON Schema * Draft04 and up. This constraint validates an object JSON instance where if * a key in the instance is in this constraint, then the value is validated * against the subschema. * * @pre context.schema is an object * * @param context The operating context of the schema parsing. * * @returns A PropertiesConstraint * * @throws std::runtime_error if precondition #1 is broken */ static auto properties(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Object); std::map rval; for (auto [prop, subschema] : context.schema.as_object()) { rval.emplace(prop, context.child(subschema, prop).node()); } return ptr(constraint::PropertiesConstraint{rval}); } /** * @brief Parser for the "properties" constraint (5.2) for JSON Schema * Draft03. This constraint validates an object JSON instance where if a key * in the instance is in this constraint, then the value is validated against * the subschema. * * The Draft03 version of this method differs from the general version because * of the way that the "required" keyword is handled in Draft03, vs others. * In Draft03, "required" is a boolean field, that may be placed in each * subschema of the properties constraint. * There would be two possible ways to handle this: * 1) Implement a constraint like "MustBeVisitedConstraint", and make it so * the ValidationVisitor will look-forward into properties constraints to * check for its presence. * 2) During Schema parsing, scan the children of the properties constraint * for the required keyword. If present, add a RequiredConstraint in our * output. * * @pre context.schema is an object * * @param context The operating context of the schema parsing. * * @returns A PropertiesConstraint * * @throws std::runtime_error if precondition #1 is broken */ static pConstraint propertiesDraft3(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Object); std::unordered_set required; for (auto [prop, subschema] : context.schema.as_object()) { EXPECT(subschema.type() == adapter::Type::Object); if (auto sub = subschema.as_object(); sub.contains("required") && sub["required"].as_boolean()) { required.insert(prop); } } if (required.empty()) { return properties(context); } std::vector rval; rval.push_back(properties(context)); rval.push_back(ptr(constraint::RequiredConstraint{std::move(required)})); return ptr(constraint::AllOfConstraint{std::move(rval)}); } /** * @brief Parser for the "propertyNames" constraint (10.3.2.4). This * constraint validates the keys of an object JSON instance against a * subschema, the values of the object are ignored. * * @param context The operating context of the schema parsing. * * @returns A PropertyNamesConstraint */ static auto propertyNames(detail::ParserContext const & context) { return ptr(constraint::PropertyNamesConstraint{context.node()}); } /** * @brief Parser for the "unevaluatedProperties" constraint (11.3). This * constraint validates every element of an object JSON instance that is not * handled by any other subschema of this schema's parent. * In terms of annotation, we flag instance paths as "Accept", "Reject", or * "Noop" - "unevaluatedProperties" constraints will be run after all other * constraints, applying to every property that is labeled "Noop" (or was * never visited to get even that tag). * * @param context The operating context of the schema parsing. * * @returns An AdditionalPropertiesConstraint */ static auto unevaluatedProperties(detail::ParserContext const & context) { return ptr(constraint::UnevaluatedPropertiesConstraint{context.node()}); } /** * @brief Parser for the "additionalProperties" constraint (10.3.2.3). This * constraint validates every element of an object JSON instance that is not * handled by a "properties" or "patternProperties" constraint in the same * parent schema. * * Constrast this with the "unevaluatedProperties" of Draft2019_09, which is * able to investigate various complex interactions and nested schemas. * * @param context The operating context of the schema parsing. * * @returns An AdditionalPropertiesConstraint */ static auto additionalProperties(detail::ParserContext const & context) { std::unordered_set properties; std::vector patterns; auto const & parent = *context.parent; if (parent.contains("properties")) { for (auto [key, _] : parent["properties"].as_object()) { properties.insert(key); } } if (parent.contains("patternProperties")) { for (auto [key, _] : parent["patternProperties"].as_object()) { patterns.push_back(key); } } using C = constraint::AdditionalPropertiesConstraint; // Otherwise - the formatting is ugly // Prior to Draft06, boolean schemas were not allowed in a general context, // they were instead reserved for the "additionalItems" and // "additionalProperties" keywords. if (context.vocab->version() < schema::Version::Draft06 && context.schema.type() == adapter::Type::Boolean) { return ptr(C{context.always(), properties, patterns}); } return ptr(C{context.node(), properties, patterns}); } /** * @brief Parser for the "dependencies" constraint (6.5.7) until JSON Schema * Draft2019_09 * (https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01) * This constraint creates an if-then relationship where "if property X exists * in the instance, then we must validate schema Y (object) or properties Y... * must also exist (array)". * It is not required for every key in this schema to be contained within the * instance being validated. * * In Draft03, we additionally permit * In Draft2019_09, this was split into the "dependentSchemas" and * "dependentRequired" keyword. * * @pre context.schema MUST be an object * @pre all object values in context.schema are valid JSON Schemas, or a * lists of strings * * @param context The operating context of the schema parsing. * * @returns A DependenciesConstraint * * @throws std::runtime_error if precondition #1 is broken * @throws if precondition #2 is broken */ static auto dependencies(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Object); std::map schemas; std::map> required; for (auto [prop, subschema] : context.schema.as_object()) { if (subschema.type() == adapter::Type::Array) { // Option 1) dependentRequired for (auto key : subschema.as_array()) { EXPECT(key.type() == adapter::Type::String); required[prop].insert(key.as_string()); } } else if (context.vocab->version() <= schema::Version::Draft03 && subschema.type() == adapter::Type::String) { // Option 2) Special single-element dependentRequired in Draft03 required[prop].insert(subschema.as_string()); } else { // Option 3) dependentSchemas schemas.emplace(prop, context.child(subschema, prop).node()); } } return ptr(constraint::DependenciesConstraint{schemas, required}); } /** * @brief Parser for the "dependentSchemas" constraint (10.2.2.4) for * JSON Schema Draft2019_09 and up. This constraint creates an if-then * relationship where "if property X exists in the instance, then we must * validate schema Y". It is not required for every key in this schema to be * contained within the instance being validated. * * Before Draft2019_09, this was part of the "dependencies" keyword, along * with "dependentRequired". * * @pre context.schema MUST be an object * @pre all object values in context.schema are lists of strings * * @param context The operating context of the schema parsing. * * @returns A DependenciesConstraint * * @throws std::runtime_error if precondition #1 is broken * @throws if precondition #2 is broken */ static auto dependentSchemas(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Object); std::map rval; for (auto [prop, subschema] : context.schema.as_object()) { rval.emplace(prop, context.child(subschema, prop).node()); } return ptr(constraint::DependenciesConstraint{rval}); } /** * @brief Parser for the "dependentRequired" constraint (6.5.4) for * JSON Schema Draft2019_09 and up. This constraint creates an if-then * relationship where "if property X exists in the instance, properties Y... * must also exist". It is not required for every key in this schema to be * contained within the instance being validated. * * Before Draft2019_09, this was part of the "dependencies" keyword, along * with "dependentSchemas". * * @pre context.schema MUST be an object * @pre all object values in context.schema are valid JSON Schemas * * @param context The operating context of the schema parsing. * * @returns A DependenciesConstraint * * @throws std::runtime_error if any preconditions are broken */ static auto dependentRequired(detail::ParserContext const & context) { EXPECT(context.schema.type() == adapter::Type::Object); std::map> rval; for (auto [prop, subschema] : context.schema.as_object()) { EXPECT(subschema.type() == adapter::Type::Array); for (auto key : subschema.as_array()) { EXPECT(key.type() == adapter::Type::String); rval[prop].insert(key.as_string()); } } return ptr(constraint::DependenciesConstraint{{}, rval}); } }; }