diff --git a/CMakeLists.txt b/CMakeLists.txt index c4ea69a0..db0278b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -145,6 +145,7 @@ if(valijson_BUILD_TESTS) tests/test_jsoncpp_adapter.cpp tests/test_nlohmann_json_adapter.cpp tests/test_rapidjson_adapter.cpp + tests/test_schema_parser_dialects.cpp tests/test_picojson_adapter.cpp tests/test_poly_constraint.cpp tests/test_uri.cpp diff --git a/PLAN.md b/PLAN.md index 0f33e09f..05333735 100644 --- a/PLAN.md +++ b/PLAN.md @@ -5,22 +5,26 @@ The road to full draft 2020-12 support. ## Prerequisites * ~~Update to latest version of JSON-Schema-Test-Suite~~ -* Add `kDraft202012` parser mode +* ~~Add `kDraft202012` parser mode~~ ## Non-annotation keywords -* `$id` parsing (**in progress**) -* `$defs` alias/support +* ~~`$id` parsing~~ +* ~~`$defs` alias/support~~ +* `items` alternative behaviour for 2020 (**in progress**) * `dependentRequired` * `dependentSchemas` * `prefixItems` -* `items` alternative behaviour for 2020 * `minContains`/`maxContains`, may be achievable before full annotation support +## General + +* common `format` behaviours +* schema registry and canonical URI handling + ## References * `$ref` as applicator with siblings -* schema registry and canonical URI handling * `$anchor` support * compound schema documents * remote reference cache improvements @@ -38,7 +42,3 @@ The road to full draft 2020-12 support. * `$dynamicAnchor` * `$dynamicRef` * dynamic-scope evaluation - -## General - -* `format` behaviours diff --git a/README.md b/README.md index 774ec340..be586f5c 100644 --- a/README.md +++ b/README.md @@ -371,12 +371,20 @@ Documents: 2, Iterations: 1000000 (301487 per second) ## JSON Schema Support -Valijson supports most of the constraints defined in [Draft 7](https://json-schema.org/draft-07/json-schema-release-notes.html) +Valijson supports most of the constraints defined in [Draft 7](https://json-schema.org/draft-07/json-schema-release-notes.html). The main exceptions are - default - format +### Draft 2020-12 + +An experimental `SchemaParser::kDraft202012` mode is available as a starting point for JSON Schema Draft 2020-12 support. It currently enables the dialect selection path, boolean schemas, `$id` scope handling, and `$defs` aliases for legacy `definitions` references. Basic Draft 2020-12 array item assertions (`prefixItems`, new `items`, and `unevaluatedItems`) have also been implemented. + +Full Draft 2020-12 support still requires vocabulary handling, complete annotation propagation, `unevaluatedProperties`, and `$dynamicRef`/`$dynamicAnchor`. + +### JSON References + Support for JSON References is in development. It is mostly working, however some of the test cases added to [JSON Schema Test Suite](https://github.com/json-schema/JSON-Schema-Test-Suite) for v6/v7 are still failing. ## JSON Inspector diff --git a/doc/design/schema_parser.md b/doc/design/schema_parser.md index 099b880d..56d2e69c 100644 --- a/doc/design/schema_parser.md +++ b/doc/design/schema_parser.md @@ -21,6 +21,17 @@ This version of `populateSchema()` is typically invoked using default values (`n The initial call to `populateSchema()` doesn't do much, as it is primarily an entry point for a recursive parsing process. The initial call sets up a document cache and schema registry, which are used to minimise unnecessary work and to help resolve cycles. Then it calls `resolveThenPopulateSchema()`, which is where the real work begins. +## Dialects + +`SchemaParser` is constructed with a `Version` value. The default is `kDraft7`, and the parser also supports `kDraft3`, `kDraft4`, and the experimental `kDraft202012` mode. + +The dialect changes how some keywords are parsed: + +* Draft 3 treats property-level `required` and `extends` as legacy syntax. +* Draft 7 and Draft 2020-12 accept boolean schemas and Draft 7-era keywords such as `contains`, `const`, `if`, `then`, and `else`. +* Draft 2020-12 reads `$id`, treats `$defs` as the replacement for `definitions`, and aliases legacy `#/definitions/...` references to `#/$defs/...` while this mode is active. +* Draft 2020-12 gives array applicators their newer meaning: `prefixItems` is used for tuple validation, `items` applies to elements after the tuple prefix, and `additionalItems` is ignored. + ## Resolve Then Populate This step is a little more complicated. This occurs in `resolveThenPopulateSchema()`, which is declared as: @@ -54,6 +65,22 @@ If a JSON Reference is found, we must first resolve it. This may involve using t Resolving a JSON Reference may require a recursive call to `resolveThenPopulateSchema()`. +## Document Cache + +The document cache is a map from resolved document URI to the external document returned by the `fetchDoc` callback. It is only used when remote document fetching is enabled. Fetching must be enabled by providing both `fetchDoc` and `freeDoc`; providing only one of them is rejected before parsing begins. + +When the parser resolves a `$ref` to a document URI outside the current scope, it first checks this cache. A cache miss calls `fetchDoc`, stores the returned document pointer, and continues parsing through that document's adapter type. After parsing succeeds or throws, the cache is released with `freeDoc`. + +The document cache owns external documents for the lifetime of one top-level `populateSchema()` call. It does not cache local subschemas; local reuse is handled by the schema registry. + +## Schema Registry + +The schema registry maps canonical lookup keys to populated `Subschema` instances. Keys are built from the current resolution scope and JSON Pointer path, or from resolved `$ref` targets. The registry prevents duplicate parsing and breaks reference cycles by allowing later lookups to reuse an existing `Subschema`. + +`makeOrReuseSchema()` records registry keys encountered while chasing `$ref` chains. Once it reaches a concrete schema node, all pending keys are registered against the concrete `Subschema`. If the concrete node was already registered, the existing `Subschema` is reused instead of creating and populating a duplicate. + +The registry is intentionally stricter than a plain `std::map` lookup. `querySchemaRegistry()` never creates missing entries, and `updateSchemaRegistry()` throws if a key is registered twice. Duplicate registration indicates parser bookkeeping has gone wrong rather than malformed user input. + ## Populate Schema The next step in parsing a schema is a recursive call to `populateSchema()`. The recursive version of this function is declared as: @@ -76,6 +103,8 @@ void populateSchema( This is a huge function that searches for all of the supported JSON Schema rules (referred to in Valijson as 'constraints'). When a supported rule is found, it is parsed and instantiated as subclass of the `Constraint` class. +In Draft 2020-12 mode, `populateSchema()` also switches the array-keyword path. Instead of treating an array-valued `items` as tuple validation, it builds a `LinearItemsConstraint` from `prefixItems`, uses `items` as the schema for remaining elements, and falls back to `unevaluatedItems` when no `items` schema is present. `additionalItems` is not consulted in this mode. + ## Constraints Constraints are Valijson's internal representation of JSON Schema validation keywords, which can later be applied when validating a document. An example would be the `required` keyword. From JSON Schema Draft 4 onwards the value associated with this keyword is an array of property names that must be present on an object being validated. @@ -113,4 +142,8 @@ const Subschema * makeOrReuseSchema( SchemaRegistry &schemaRegistry) ``` -The return value is a `Subschema *`, which may be retrieved from the schema registry (described below). +The return value is a `Subschema *`, which may be retrieved from the schema registry. + +For Draft 2020-12 array keywords, `makeDraft202012ItemsConstraint()` reuses `LinearItemsConstraint` with a mode flag that records whether the schema for remaining array elements came from `items` or `unevaluatedItems`. This keeps validation compatible with the existing linear tuple machinery while preserving enough information for error messages and basic evaluated-item tracking. + +The current `unevaluatedItems` implementation is intentionally limited: it tracks the highest contiguous array index count evaluated by item applicators. That supports direct `prefixItems` cases and simple annotation flow such as `allOf`, but it does not model the full Draft 2020-12 annotation semantics for all combiners, `$ref`, `$dynamicRef`, or `contains` interactions. diff --git a/examples/valijson_nlohmann_bundled.hpp b/examples/valijson_nlohmann_bundled.hpp index 60814067..21297196 100644 --- a/examples/valijson_nlohmann_bundled.hpp +++ b/examples/valijson_nlohmann_bundled.hpp @@ -3749,14 +3749,22 @@ class FormatConstraint: public BasicConstraint class LinearItemsConstraint: public BasicConstraint { public: + enum RemainingItemsMode { + kAdditionalItems, + kItems, + kUnevaluatedItems + }; + LinearItemsConstraint() : m_itemSubschemas(Allocator::rebind::other(m_allocator)), - m_additionalItemsSubschema(nullptr) { } + m_additionalItemsSubschema(nullptr), + m_remainingItemsMode(kAdditionalItems) { } LinearItemsConstraint(CustomAlloc allocFn, CustomFree freeFn) : BasicConstraint(allocFn, freeFn), m_itemSubschemas(Allocator::rebind::other(m_allocator)), - m_additionalItemsSubschema(nullptr) { } + m_additionalItemsSubschema(nullptr), + m_remainingItemsMode(kAdditionalItems) { } void addItemSubschema(const Subschema *subschema) { @@ -3786,17 +3794,28 @@ class LinearItemsConstraint: public BasicConstraint return m_itemSubschemas.size(); } + RemainingItemsMode getRemainingItemsMode() const + { + return m_remainingItemsMode; + } + void setAdditionalItemsSubschema(const Subschema *subschema) { m_additionalItemsSubschema = subschema; } + void setRemainingItemsMode(RemainingItemsMode mode) + { + m_remainingItemsMode = mode; + } + private: typedef std::vector> Subschemas; Subschemas m_itemSubschemas; const Subschema* m_additionalItemsSubschema; + RemainingItemsMode m_remainingItemsMode; }; /** @@ -4589,8 +4608,9 @@ namespace valijson { /** * @brief Parser for populating a Schema based on a JSON Schema document. * - * The SchemaParser class supports Drafts 3 and 4 of JSON Schema, however - * Draft 3 support should be considered deprecated. + * The SchemaParser class supports Drafts 3, 4, and 7 of JSON Schema, plus an + * experimental Draft 2020-12 dialect mode. Draft 3 support should be + * considered deprecated. * * The functions provided by this class have been templated so that they can * be used with different Adapter types. @@ -4602,7 +4622,8 @@ class SchemaParser enum Version { kDraft3, ///< @deprecated JSON Schema v3 has been superseded by v4 kDraft4, - kDraft7 + kDraft7, + kDraft202012 ///< Experimental JSON Schema Draft 2020-12 dialect mode }; /** @@ -4708,6 +4729,34 @@ class SchemaParser ConstraintBuilders constraintBuilders; + bool supportsBooleanSchemas() const + { + return m_version == kDraft7 || m_version == kDraft202012; + } + + bool supportsDraft7Keywords() const + { + return m_version == kDraft7 || m_version == kDraft202012; + } + + /** + * @brief Resolve legacy definitions references against Draft 2020-12 $defs. + */ + std::string applyDefinitionsAlias(const std::string &jsonPointer) const + { + if (m_version != kDraft202012) { + return jsonPointer; + } + + static const std::string definitionsToken = "/definitions"; + if (jsonPointer == definitionsToken || + jsonPointer.find(definitionsToken + "/") == 0) { + return "/$defs" + jsonPointer.substr(definitionsToken.size()); + } + + return jsonPointer; + } + template struct DocumentCache { @@ -4861,7 +4910,7 @@ class SchemaParser const char *idKeyword() const { - return m_version == kDraft7 ? "$id" : "id"; + return supportsDraft7Keywords() ? "$id" : "id"; } std::optional resolveId( @@ -5105,8 +5154,8 @@ class SchemaParser // Extract JSON Pointer from JSON Reference, with any trailing // slashes removed so that keys in the schema registry end // consistently - const std::string actualJsonPointer = sanitiseJsonPointer( - internal::json_reference::getJsonReferencePointer(jsonRef)); + const std::string actualJsonPointer = applyDefinitionsAlias(sanitiseJsonPointer( + internal::json_reference::getJsonReferencePointer(jsonRef))); // Determine the actual document URI based on the resolution // scope. An absolute document URI will take precedence when @@ -5351,7 +5400,7 @@ class SchemaParser "appropriate Adapter implementation"); if (!node.isObject()) { - if (m_version == kDraft7 && node.maybeBool()) { + if (supportsBooleanSchemas() && node.maybeBool()) { // Boolean schema if (!node.asBool()) { rootSchema.setAlwaysInvalid(&subschema, true); @@ -5361,7 +5410,7 @@ class SchemaParser std::string s; s += "Expected node at "; s += nodePath; - if (m_version == kDraft7) { + if (supportsDraft7Keywords()) { s += " to contain schema object or boolean value; actual node type is: "; } else { s += " to contain schema object; actual node type is: "; @@ -5376,9 +5425,10 @@ class SchemaParser // Check for schema identifier attribute and update current scope. std::optional updatedScope; - bool foundId = false; - if ((itr = object.find(idKeyword())) != object.end() && itr->second.maybeString()) { - foundId = true; + const bool foundId = + (itr = object.find(idKeyword())) != object.end() && + itr->second.maybeString(); + if (foundId) { const std::string id = itr->second.asString(); rootSchema.setSubschemaId(&subschema, itr->second.asString()); updatedScope = resolveId(currentScope, id); @@ -5486,7 +5536,25 @@ class SchemaParser const typename AdapterType::Object::const_iterator itemsItr = object.find("items"); - if (object.end() != itemsItr) { + if (m_version == kDraft202012) { + const typename AdapterType::Object::const_iterator + prefixItemsItr = object.find("prefixItems"), + unevaluatedItemsItr = object.find("unevaluatedItems"); + + if (object.end() != prefixItemsItr || object.end() != itemsItr || + object.end() != unevaluatedItemsItr) { + rootSchema.addConstraintToSubschema( + makeDraft202012ItemsConstraint(rootSchema, rootNode, + prefixItemsItr != object.end() ? &prefixItemsItr->second : nullptr, + itemsItr != object.end() ? &itemsItr->second : nullptr, + unevaluatedItemsItr != object.end() ? &unevaluatedItemsItr->second : nullptr, + updatedScope, nodePath + "/prefixItems", + nodePath + "/items", + nodePath + "/unevaluatedItems", fetchDoc, + docCache, schemaRegistry), + &subschema); + } + } else if (object.end() != itemsItr) { if (!itemsItr->second.isArray()) { rootSchema.addConstraintToSubschema( makeSingularItemsConstraint(rootSchema, rootNode, @@ -5516,7 +5584,7 @@ class SchemaParser const typename AdapterType::Object::const_iterator elseItr = object.find("else"); if (object.end() != ifItr) { - if (m_version == kDraft7) { + if (supportsDraft7Keywords()) { rootSchema.addConstraintToSubschema( makeConditionalConstraint(rootSchema, rootNode, ifItr->second, @@ -5530,7 +5598,7 @@ class SchemaParser } } - if (m_version == kDraft7) { + if (supportsDraft7Keywords()) { if ((itr = object.find("exclusiveMaximum")) != object.end()) { rootSchema.addConstraintToSubschema( makeMaximumConstraintExclusive(itr->second), @@ -5574,7 +5642,7 @@ class SchemaParser makeMaxPropertiesConstraint(itr->second), &subschema); } - if (m_version == kDraft7) { + if (supportsDraft7Keywords()) { if ((itr = object.find("exclusiveMinimum")) != object.end()) { rootSchema.addConstraintToSubschema( makeMinimumConstraintExclusive(itr->second), &subschema); @@ -5675,7 +5743,7 @@ class SchemaParser } if ((itr = object.find("propertyNames")) != object.end()) { - if (m_version == kDraft7) { + if (supportsDraft7Keywords()) { rootSchema.addConstraintToSubschema( makePropertyNamesConstraint(rootSchema, rootNode, itr->second, updatedScope, nodePath, fetchDoc, docCache, schemaRegistry), @@ -5786,8 +5854,8 @@ class SchemaParser const std::optional documentUri = internal::json_reference::getJsonReferenceUri(jsonRef); // Extract JSON Pointer from JSON Reference - const std::string actualJsonPointer = sanitiseJsonPointer( - internal::json_reference::getJsonReferencePointer(jsonRef)); + const std::string actualJsonPointer = applyDefinitionsAlias(sanitiseJsonPointer( + internal::json_reference::getJsonReferencePointer(jsonRef))); const std::optional actualDocumentUri = resolveDocumentUri(currentScope, documentUri); @@ -5912,7 +5980,7 @@ class SchemaParser int index = 0; for (const AdapterType schemaNode : node.asArray()) { - if (schemaNode.maybeObject() || (m_version == kDraft7 && schemaNode.isBool())) { + if (schemaNode.maybeObject() || (supportsBooleanSchemas() && schemaNode.isBool())) { const std::string childPath = nodePath + "/" + std::to_string(index); const Subschema *subschema = makeOrReuseSchema( rootSchema, rootNode, schemaNode, currentScope, @@ -6013,7 +6081,7 @@ class SchemaParser int index = 0; for (const AdapterType schemaNode : node.asArray()) { - if (schemaNode.maybeObject() || (m_version == kDraft7 && schemaNode.isBool())) { + if (schemaNode.maybeObject() || (supportsBooleanSchemas() && schemaNode.isBool())) { const std::string childPath = nodePath + "/" + std::to_string(index); const Subschema *subschema = makeOrReuseSchema( rootSchema, rootNode, schemaNode, currentScope, @@ -6147,7 +6215,7 @@ class SchemaParser { constraints::ContainsConstraint constraint; - if (contains.isObject() || (m_version == kDraft7 && contains.maybeBool())) { + if (contains.isObject() || (supportsBooleanSchemas() && contains.maybeBool())) { const Subschema *subschema = makeOrReuseSchema( rootSchema, rootNode, contains, currentScope, containsPath, fetchDoc, nullptr, nullptr, docCache, schemaRegistry); @@ -6250,7 +6318,7 @@ class SchemaParser // exercised the flexibility by loosely-typed Adapter types. If the // value of the dependency mapping is an object, then we'll try to // process it as a dependent schema. - } else if (member.second.isObject() || (m_version == kDraft7 && member.second.maybeBool())) { + } else if (member.second.isObject() || (supportsBooleanSchemas() && member.second.maybeBool())) { // Parse dependent subschema const std::string childPath = nodePath + "/" + escapeJsonPointerToken(member.first); @@ -6428,6 +6496,72 @@ class SchemaParser return constraint; } + /** + * @brief Make a Draft 2020-12 items constraint. + */ + template + constraints::LinearItemsConstraint makeDraft202012ItemsConstraint( + Schema &rootSchema, + const AdapterType &rootNode, + const AdapterType *prefixItems, + const AdapterType *items, + const AdapterType *unevaluatedItems, + const std::optional currentScope, + const std::string &prefixItemsPath, + const std::string &itemsPath, + const std::string &unevaluatedItemsPath, + const typename FunctionPtrs::FetchDoc fetchDoc, + typename DocumentCache::Type &docCache, + SchemaRegistry &schemaRegistry) + { + constraints::LinearItemsConstraint constraint; + + if (prefixItems) { + if (!prefixItems->maybeArray()) { + throwRuntimeError("Expected array value for 'prefixItems' constraint."); + } + + int index = 0; + for (const AdapterType v : prefixItems->asArray()) { + if (!v.maybeObject() && !v.maybeBool()) { + throwRuntimeError("Expected array element to be a valid schema in 'prefixItems' constraint."); + } + + const std::string childPath = prefixItemsPath + "/" + + std::to_string(index); + const Subschema *subschema = makeOrReuseSchema( + rootSchema, rootNode, v, currentScope, childPath, + fetchDoc, nullptr, nullptr, docCache, schemaRegistry); + constraint.addItemSubschema(subschema); + index++; + } + } + + const AdapterType *additionalSchema = items ? items : unevaluatedItems; + const std::string &additionalSchemaPath = items ? itemsPath : unevaluatedItemsPath; + if (items) { + constraint.setRemainingItemsMode(constraints::LinearItemsConstraint::kItems); + } else if (unevaluatedItems) { + constraint.setRemainingItemsMode(constraints::LinearItemsConstraint::kUnevaluatedItems); + } + + if (additionalSchema) { + if (additionalSchema->maybeObject() || additionalSchema->maybeBool()) { + const Subschema *subschema = makeOrReuseSchema( + rootSchema, rootNode, *additionalSchema, currentScope, + additionalSchemaPath, fetchDoc, nullptr, nullptr, + docCache, schemaRegistry); + constraint.setAdditionalItemsSubschema(subschema); + } else { + throwRuntimeError("Expected valid schema for Draft 2020-12 'items' or 'unevaluatedItems' constraint."); + } + } else { + constraint.setAdditionalItemsSubschema(rootSchema.emptySubschema()); + } + + return constraint; + } + /** * @brief Make a new ItemsConstraint object. * @@ -6469,7 +6603,7 @@ class SchemaParser // array is provided, or a single Schema object, in an object value is // provided. If the items constraint is not provided, then array items // will be validated against the additionalItems schema. - if (items.isObject() || (m_version == kDraft7 && items.maybeBool())) { + if (items.isObject() || (supportsBooleanSchemas() && items.maybeBool())) { // If the items constraint contains an object value, then it // should contain a Schema that will be used to validate all // items in a target array. Any schema defined by the @@ -6806,7 +6940,7 @@ class SchemaParser typename DocumentCache::Type &docCache, SchemaRegistry &schemaRegistry) { - if (node.maybeObject() || (m_version == kDraft7 && node.maybeBool())) { + if (node.maybeObject() || (supportsBooleanSchemas() && node.maybeBool())) { const Subschema *subschema = makeOrReuseSchema( rootSchema, rootNode, node, currentScope, nodePath, fetchDoc, nullptr, nullptr, docCache, schemaRegistry); @@ -7752,7 +7886,8 @@ class ValidationVisitor: public constraints::ConstraintVisitor m_results(results), m_strictTypes(strictTypes), m_strictDateTime(strictDateTime), - m_regexesCache(regexesCache) { } + m_regexesCache(regexesCache), + m_evaluatedArrayItemCount(0) { } /** * @brief Validate the target against a schema. @@ -8191,6 +8326,12 @@ class ValidationVisitor: public constraints::ConstraintVisitor // 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(); + const typename LinearItemsConstraint::RemainingItemsMode remainingItemsMode = + constraint.getRemainingItemsMode(); + const char *remainingItemsDescription = + remainingItemsMode == LinearItemsConstraint::kItems ? "items" : + remainingItemsMode == LinearItemsConstraint::kUnevaluatedItems ? "unevaluated items" : + "additional items"; // Track how many items are validated using 'items' constraint unsigned int numValidated = 0; @@ -8220,11 +8361,20 @@ class ValidationVisitor: public constraints::ConstraintVisitor m_strictTypes, m_strictDateTime, m_results, &numValidated, &validated, m_regexesCache)); + if (numValidated > m_evaluatedArrayItemCount) { + m_evaluatedArrayItemCount = numValidated; + } + if (!m_results && !validated) { return false; } } + if (remainingItemsMode == LinearItemsConstraint::kUnevaluatedItems && + m_evaluatedArrayItemCount > numValidated) { + numValidated = m_evaluatedArrayItemCount; + } + // Validate remaining items using 'additionalItems' sub-schema if (numValidated < arrSize) { if (additionalItemsSubschema) { @@ -8250,7 +8400,7 @@ class ValidationVisitor: public constraints::ConstraintVisitor if (!validator.validateSchema(*additionalItemsSubschema)) { if (m_results) { m_results->pushError(m_path, "Failed to validate item #" + std::to_string(index) + - " against additional items schema."); + std::string(" against ") + remainingItemsDescription + " schema."); validated = false; } else { return false; @@ -8260,6 +8410,11 @@ class ValidationVisitor: public constraints::ConstraintVisitor index++; } + if (validated && (remainingItemsMode == LinearItemsConstraint::kItems || + remainingItemsMode == LinearItemsConstraint::kUnevaluatedItems)) { + m_evaluatedArrayItemCount = arrSize; + } + } else if (m_results) { m_results->pushError(m_path, "Cannot validate item #" + std::to_string(numValidated) + " or greater using 'items' constraint or 'additionalItems' constraint."); @@ -9749,6 +9904,9 @@ class ValidationVisitor: public constraints::ConstraintVisitor /// Cached regex objects for pattern constraint std::unordered_map& m_regexesCache; + + /// Number of array items evaluated by item applicators on this target. + size_t m_evaluatedArrayItemCount; }; } // namespace valijson diff --git a/include/valijson/constraints/concrete_constraints.hpp b/include/valijson/constraints/concrete_constraints.hpp index ba13343d..26d27614 100644 --- a/include/valijson/constraints/concrete_constraints.hpp +++ b/include/valijson/constraints/concrete_constraints.hpp @@ -472,14 +472,22 @@ class FormatConstraint: public BasicConstraint class LinearItemsConstraint: public BasicConstraint { public: + enum RemainingItemsMode { + kAdditionalItems, + kItems, + kUnevaluatedItems + }; + LinearItemsConstraint() : m_itemSubschemas(Allocator::rebind::other(m_allocator)), - m_additionalItemsSubschema(nullptr) { } + m_additionalItemsSubschema(nullptr), + m_remainingItemsMode(kAdditionalItems) { } LinearItemsConstraint(CustomAlloc allocFn, CustomFree freeFn) : BasicConstraint(allocFn, freeFn), m_itemSubschemas(Allocator::rebind::other(m_allocator)), - m_additionalItemsSubschema(nullptr) { } + m_additionalItemsSubschema(nullptr), + m_remainingItemsMode(kAdditionalItems) { } void addItemSubschema(const Subschema *subschema) { @@ -509,17 +517,28 @@ class LinearItemsConstraint: public BasicConstraint return m_itemSubschemas.size(); } + RemainingItemsMode getRemainingItemsMode() const + { + return m_remainingItemsMode; + } + void setAdditionalItemsSubschema(const Subschema *subschema) { m_additionalItemsSubschema = subschema; } + void setRemainingItemsMode(RemainingItemsMode mode) + { + m_remainingItemsMode = mode; + } + private: typedef std::vector> Subschemas; Subschemas m_itemSubschemas; const Subschema* m_additionalItemsSubschema; + RemainingItemsMode m_remainingItemsMode; }; /** diff --git a/include/valijson/schema_parser.hpp b/include/valijson/schema_parser.hpp index 065730ed..e2066f4b 100644 --- a/include/valijson/schema_parser.hpp +++ b/include/valijson/schema_parser.hpp @@ -21,8 +21,9 @@ namespace valijson { /** * @brief Parser for populating a Schema based on a JSON Schema document. * - * The SchemaParser class supports Drafts 3 and 4 of JSON Schema, however - * Draft 3 support should be considered deprecated. + * The SchemaParser class supports Drafts 3, 4, and 7 of JSON Schema, plus an + * experimental Draft 2020-12 dialect mode. Draft 3 support should be + * considered deprecated. * * The functions provided by this class have been templated so that they can * be used with different Adapter types. @@ -34,7 +35,8 @@ class SchemaParser enum Version { kDraft3, ///< @deprecated JSON Schema v3 has been superseded by v4 kDraft4, - kDraft7 + kDraft7, + kDraft202012 ///< Experimental JSON Schema Draft 2020-12 dialect mode }; /** @@ -140,6 +142,34 @@ class SchemaParser ConstraintBuilders constraintBuilders; + bool supportsBooleanSchemas() const + { + return m_version == kDraft7 || m_version == kDraft202012; + } + + bool supportsDraft7Keywords() const + { + return m_version == kDraft7 || m_version == kDraft202012; + } + + /** + * @brief Resolve legacy definitions references against Draft 2020-12 $defs. + */ + std::string applyDefinitionsAlias(const std::string &jsonPointer) const + { + if (m_version != kDraft202012) { + return jsonPointer; + } + + static const std::string definitionsToken = "/definitions"; + if (jsonPointer == definitionsToken || + jsonPointer.find(definitionsToken + "/") == 0) { + return "/$defs" + jsonPointer.substr(definitionsToken.size()); + } + + return jsonPointer; + } + template struct DocumentCache { @@ -293,7 +323,7 @@ class SchemaParser const char *idKeyword() const { - return m_version == kDraft7 ? "$id" : "id"; + return supportsDraft7Keywords() ? "$id" : "id"; } std::optional resolveId( @@ -537,8 +567,8 @@ class SchemaParser // Extract JSON Pointer from JSON Reference, with any trailing // slashes removed so that keys in the schema registry end // consistently - const std::string actualJsonPointer = sanitiseJsonPointer( - internal::json_reference::getJsonReferencePointer(jsonRef)); + const std::string actualJsonPointer = applyDefinitionsAlias(sanitiseJsonPointer( + internal::json_reference::getJsonReferencePointer(jsonRef))); // Determine the actual document URI based on the resolution // scope. An absolute document URI will take precedence when @@ -783,7 +813,7 @@ class SchemaParser "appropriate Adapter implementation"); if (!node.isObject()) { - if (m_version == kDraft7 && node.maybeBool()) { + if (supportsBooleanSchemas() && node.maybeBool()) { // Boolean schema if (!node.asBool()) { rootSchema.setAlwaysInvalid(&subschema, true); @@ -793,7 +823,7 @@ class SchemaParser std::string s; s += "Expected node at "; s += nodePath; - if (m_version == kDraft7) { + if (supportsDraft7Keywords()) { s += " to contain schema object or boolean value; actual node type is: "; } else { s += " to contain schema object; actual node type is: "; @@ -808,9 +838,10 @@ class SchemaParser // Check for schema identifier attribute and update current scope. std::optional updatedScope; - bool foundId = false; - if ((itr = object.find(idKeyword())) != object.end() && itr->second.maybeString()) { - foundId = true; + const bool foundId = + (itr = object.find(idKeyword())) != object.end() && + itr->second.maybeString(); + if (foundId) { const std::string id = itr->second.asString(); rootSchema.setSubschemaId(&subschema, itr->second.asString()); updatedScope = resolveId(currentScope, id); @@ -918,7 +949,25 @@ class SchemaParser const typename AdapterType::Object::const_iterator itemsItr = object.find("items"); - if (object.end() != itemsItr) { + if (m_version == kDraft202012) { + const typename AdapterType::Object::const_iterator + prefixItemsItr = object.find("prefixItems"), + unevaluatedItemsItr = object.find("unevaluatedItems"); + + if (object.end() != prefixItemsItr || object.end() != itemsItr || + object.end() != unevaluatedItemsItr) { + rootSchema.addConstraintToSubschema( + makeDraft202012ItemsConstraint(rootSchema, rootNode, + prefixItemsItr != object.end() ? &prefixItemsItr->second : nullptr, + itemsItr != object.end() ? &itemsItr->second : nullptr, + unevaluatedItemsItr != object.end() ? &unevaluatedItemsItr->second : nullptr, + updatedScope, nodePath + "/prefixItems", + nodePath + "/items", + nodePath + "/unevaluatedItems", fetchDoc, + docCache, schemaRegistry), + &subschema); + } + } else if (object.end() != itemsItr) { if (!itemsItr->second.isArray()) { rootSchema.addConstraintToSubschema( makeSingularItemsConstraint(rootSchema, rootNode, @@ -948,7 +997,7 @@ class SchemaParser const typename AdapterType::Object::const_iterator elseItr = object.find("else"); if (object.end() != ifItr) { - if (m_version == kDraft7) { + if (supportsDraft7Keywords()) { rootSchema.addConstraintToSubschema( makeConditionalConstraint(rootSchema, rootNode, ifItr->second, @@ -962,7 +1011,7 @@ class SchemaParser } } - if (m_version == kDraft7) { + if (supportsDraft7Keywords()) { if ((itr = object.find("exclusiveMaximum")) != object.end()) { rootSchema.addConstraintToSubschema( makeMaximumConstraintExclusive(itr->second), @@ -1006,7 +1055,7 @@ class SchemaParser makeMaxPropertiesConstraint(itr->second), &subschema); } - if (m_version == kDraft7) { + if (supportsDraft7Keywords()) { if ((itr = object.find("exclusiveMinimum")) != object.end()) { rootSchema.addConstraintToSubschema( makeMinimumConstraintExclusive(itr->second), &subschema); @@ -1107,7 +1156,7 @@ class SchemaParser } if ((itr = object.find("propertyNames")) != object.end()) { - if (m_version == kDraft7) { + if (supportsDraft7Keywords()) { rootSchema.addConstraintToSubschema( makePropertyNamesConstraint(rootSchema, rootNode, itr->second, updatedScope, nodePath, fetchDoc, docCache, schemaRegistry), @@ -1218,8 +1267,8 @@ class SchemaParser const std::optional documentUri = internal::json_reference::getJsonReferenceUri(jsonRef); // Extract JSON Pointer from JSON Reference - const std::string actualJsonPointer = sanitiseJsonPointer( - internal::json_reference::getJsonReferencePointer(jsonRef)); + const std::string actualJsonPointer = applyDefinitionsAlias(sanitiseJsonPointer( + internal::json_reference::getJsonReferencePointer(jsonRef))); const std::optional actualDocumentUri = resolveDocumentUri(currentScope, documentUri); @@ -1344,7 +1393,7 @@ class SchemaParser int index = 0; for (const AdapterType schemaNode : node.asArray()) { - if (schemaNode.maybeObject() || (m_version == kDraft7 && schemaNode.isBool())) { + if (schemaNode.maybeObject() || (supportsBooleanSchemas() && schemaNode.isBool())) { const std::string childPath = nodePath + "/" + std::to_string(index); const Subschema *subschema = makeOrReuseSchema( rootSchema, rootNode, schemaNode, currentScope, @@ -1445,7 +1494,7 @@ class SchemaParser int index = 0; for (const AdapterType schemaNode : node.asArray()) { - if (schemaNode.maybeObject() || (m_version == kDraft7 && schemaNode.isBool())) { + if (schemaNode.maybeObject() || (supportsBooleanSchemas() && schemaNode.isBool())) { const std::string childPath = nodePath + "/" + std::to_string(index); const Subschema *subschema = makeOrReuseSchema( rootSchema, rootNode, schemaNode, currentScope, @@ -1579,7 +1628,7 @@ class SchemaParser { constraints::ContainsConstraint constraint; - if (contains.isObject() || (m_version == kDraft7 && contains.maybeBool())) { + if (contains.isObject() || (supportsBooleanSchemas() && contains.maybeBool())) { const Subschema *subschema = makeOrReuseSchema( rootSchema, rootNode, contains, currentScope, containsPath, fetchDoc, nullptr, nullptr, docCache, schemaRegistry); @@ -1682,7 +1731,7 @@ class SchemaParser // exercised the flexibility by loosely-typed Adapter types. If the // value of the dependency mapping is an object, then we'll try to // process it as a dependent schema. - } else if (member.second.isObject() || (m_version == kDraft7 && member.second.maybeBool())) { + } else if (member.second.isObject() || (supportsBooleanSchemas() && member.second.maybeBool())) { // Parse dependent subschema const std::string childPath = nodePath + "/" + escapeJsonPointerToken(member.first); @@ -1860,6 +1909,72 @@ class SchemaParser return constraint; } + /** + * @brief Make a Draft 2020-12 items constraint. + */ + template + constraints::LinearItemsConstraint makeDraft202012ItemsConstraint( + Schema &rootSchema, + const AdapterType &rootNode, + const AdapterType *prefixItems, + const AdapterType *items, + const AdapterType *unevaluatedItems, + const std::optional currentScope, + const std::string &prefixItemsPath, + const std::string &itemsPath, + const std::string &unevaluatedItemsPath, + const typename FunctionPtrs::FetchDoc fetchDoc, + typename DocumentCache::Type &docCache, + SchemaRegistry &schemaRegistry) + { + constraints::LinearItemsConstraint constraint; + + if (prefixItems) { + if (!prefixItems->maybeArray()) { + throwRuntimeError("Expected array value for 'prefixItems' constraint."); + } + + int index = 0; + for (const AdapterType v : prefixItems->asArray()) { + if (!v.maybeObject() && !v.maybeBool()) { + throwRuntimeError("Expected array element to be a valid schema in 'prefixItems' constraint."); + } + + const std::string childPath = prefixItemsPath + "/" + + std::to_string(index); + const Subschema *subschema = makeOrReuseSchema( + rootSchema, rootNode, v, currentScope, childPath, + fetchDoc, nullptr, nullptr, docCache, schemaRegistry); + constraint.addItemSubschema(subschema); + index++; + } + } + + const AdapterType *additionalSchema = items ? items : unevaluatedItems; + const std::string &additionalSchemaPath = items ? itemsPath : unevaluatedItemsPath; + if (items) { + constraint.setRemainingItemsMode(constraints::LinearItemsConstraint::kItems); + } else if (unevaluatedItems) { + constraint.setRemainingItemsMode(constraints::LinearItemsConstraint::kUnevaluatedItems); + } + + if (additionalSchema) { + if (additionalSchema->maybeObject() || additionalSchema->maybeBool()) { + const Subschema *subschema = makeOrReuseSchema( + rootSchema, rootNode, *additionalSchema, currentScope, + additionalSchemaPath, fetchDoc, nullptr, nullptr, + docCache, schemaRegistry); + constraint.setAdditionalItemsSubschema(subschema); + } else { + throwRuntimeError("Expected valid schema for Draft 2020-12 'items' or 'unevaluatedItems' constraint."); + } + } else { + constraint.setAdditionalItemsSubschema(rootSchema.emptySubschema()); + } + + return constraint; + } + /** * @brief Make a new ItemsConstraint object. * @@ -1901,7 +2016,7 @@ class SchemaParser // array is provided, or a single Schema object, in an object value is // provided. If the items constraint is not provided, then array items // will be validated against the additionalItems schema. - if (items.isObject() || (m_version == kDraft7 && items.maybeBool())) { + if (items.isObject() || (supportsBooleanSchemas() && items.maybeBool())) { // If the items constraint contains an object value, then it // should contain a Schema that will be used to validate all // items in a target array. Any schema defined by the @@ -2238,7 +2353,7 @@ class SchemaParser typename DocumentCache::Type &docCache, SchemaRegistry &schemaRegistry) { - if (node.maybeObject() || (m_version == kDraft7 && node.maybeBool())) { + if (node.maybeObject() || (supportsBooleanSchemas() && node.maybeBool())) { const Subschema *subschema = makeOrReuseSchema( rootSchema, rootNode, node, currentScope, nodePath, fetchDoc, nullptr, nullptr, docCache, schemaRegistry); diff --git a/include/valijson/validation_visitor.hpp b/include/valijson/validation_visitor.hpp index c5e5dd23..86e6078a 100644 --- a/include/valijson/validation_visitor.hpp +++ b/include/valijson/validation_visitor.hpp @@ -85,7 +85,8 @@ class ValidationVisitor: public constraints::ConstraintVisitor m_results(results), m_strictTypes(strictTypes), m_strictDateTime(strictDateTime), - m_regexesCache(regexesCache) { } + m_regexesCache(regexesCache), + m_evaluatedArrayItemCount(0) { } /** * @brief Validate the target against a schema. @@ -524,6 +525,12 @@ class ValidationVisitor: public constraints::ConstraintVisitor // 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(); + const typename LinearItemsConstraint::RemainingItemsMode remainingItemsMode = + constraint.getRemainingItemsMode(); + const char *remainingItemsDescription = + remainingItemsMode == LinearItemsConstraint::kItems ? "items" : + remainingItemsMode == LinearItemsConstraint::kUnevaluatedItems ? "unevaluated items" : + "additional items"; // Track how many items are validated using 'items' constraint unsigned int numValidated = 0; @@ -553,11 +560,20 @@ class ValidationVisitor: public constraints::ConstraintVisitor m_strictTypes, m_strictDateTime, m_results, &numValidated, &validated, m_regexesCache)); + if (numValidated > m_evaluatedArrayItemCount) { + m_evaluatedArrayItemCount = numValidated; + } + if (!m_results && !validated) { return false; } } + if (remainingItemsMode == LinearItemsConstraint::kUnevaluatedItems && + m_evaluatedArrayItemCount > numValidated) { + numValidated = m_evaluatedArrayItemCount; + } + // Validate remaining items using 'additionalItems' sub-schema if (numValidated < arrSize) { if (additionalItemsSubschema) { @@ -583,7 +599,7 @@ class ValidationVisitor: public constraints::ConstraintVisitor if (!validator.validateSchema(*additionalItemsSubschema)) { if (m_results) { m_results->pushError(m_path, "Failed to validate item #" + std::to_string(index) + - " against additional items schema."); + std::string(" against ") + remainingItemsDescription + " schema."); validated = false; } else { return false; @@ -593,6 +609,11 @@ class ValidationVisitor: public constraints::ConstraintVisitor index++; } + if (validated && (remainingItemsMode == LinearItemsConstraint::kItems || + remainingItemsMode == LinearItemsConstraint::kUnevaluatedItems)) { + m_evaluatedArrayItemCount = arrSize; + } + } else if (m_results) { m_results->pushError(m_path, "Cannot validate item #" + std::to_string(numValidated) + " or greater using 'items' constraint or 'additionalItems' constraint."); @@ -2082,6 +2103,9 @@ class ValidationVisitor: public constraints::ConstraintVisitor /// Cached regex objects for pattern constraint std::unordered_map& m_regexesCache; + + /// Number of array items evaluated by item applicators on this target. + size_t m_evaluatedArrayItemCount; }; } // namespace valijson diff --git a/tests/test_schema_parser_dialects.cpp b/tests/test_schema_parser_dialects.cpp new file mode 100644 index 00000000..c424f813 --- /dev/null +++ b/tests/test_schema_parser_dialects.cpp @@ -0,0 +1,214 @@ +#include + +#include +#include +#include +#include + +using valijson::Schema; +using valijson::SchemaParser; +using valijson::Validator; +using valijson::adapters::RapidJsonAdapter; + +class TestSchemaParserDialects : public ::testing::Test +{ + +}; + +TEST_F(TestSchemaParserDialects, Draft202012AcceptsBooleanSchema) +{ + rapidjson::Document schemaDocument; + schemaDocument.SetBool(false); + + Schema schema; + SchemaParser schemaParser(SchemaParser::kDraft202012); + schemaParser.populateSchema(RapidJsonAdapter(schemaDocument), schema); + + rapidjson::Document targetDocument; + targetDocument.SetObject(); + + Validator validator; + EXPECT_FALSE(validator.validate(schema, RapidJsonAdapter(targetDocument), nullptr)); +} + +TEST_F(TestSchemaParserDialects, Draft202012ReadsDollarId) +{ + rapidjson::Document schemaDocument; + schemaDocument.SetObject(); + schemaDocument.AddMember("$id", "https://example.com/schema", schemaDocument.GetAllocator()); + + Schema schema; + SchemaParser schemaParser(SchemaParser::kDraft202012); + schemaParser.populateSchema(RapidJsonAdapter(schemaDocument), schema); + + ASSERT_TRUE(schema.hasId()); + EXPECT_EQ("https://example.com/schema", schema.getId()); +} + +TEST_F(TestSchemaParserDialects, Draft202012AliasesDollarDefsToDefinitions) +{ + rapidjson::Document schemaDocument; + schemaDocument.Parse(R"({ + "$defs": { + "positiveInteger": { + "type": "integer", + "minimum": 1 + } + }, + "$ref": "#/definitions/positiveInteger" + })"); + ASSERT_FALSE(schemaDocument.HasParseError()); + + Schema schema; + SchemaParser schemaParser(SchemaParser::kDraft202012); + schemaParser.populateSchema(RapidJsonAdapter(schemaDocument), schema); + + rapidjson::Document validDocument; + validDocument.SetInt(2); + + rapidjson::Document invalidDocument; + invalidDocument.SetInt(0); + + Validator validator; + EXPECT_TRUE(validator.validate(schema, RapidJsonAdapter(validDocument), nullptr)); + EXPECT_FALSE(validator.validate(schema, RapidJsonAdapter(invalidDocument), nullptr)); +} + +TEST_F(TestSchemaParserDialects, Draft202012PrefixItemsValidatesTupleElements) +{ + rapidjson::Document schemaDocument; + schemaDocument.Parse(R"({ + "prefixItems": [ + { "type": "string" }, + { "type": "number" } + ] + })"); + ASSERT_FALSE(schemaDocument.HasParseError()); + + Schema schema; + SchemaParser schemaParser(SchemaParser::kDraft202012); + schemaParser.populateSchema(RapidJsonAdapter(schemaDocument), schema); + + rapidjson::Document validDocument; + validDocument.Parse(R"(["first", 2, false])"); + ASSERT_FALSE(validDocument.HasParseError()); + + rapidjson::Document invalidDocument; + invalidDocument.Parse(R"([1, 2])"); + ASSERT_FALSE(invalidDocument.HasParseError()); + + Validator validator; + EXPECT_TRUE(validator.validate(schema, RapidJsonAdapter(validDocument), nullptr)); + EXPECT_FALSE(validator.validate(schema, RapidJsonAdapter(invalidDocument), nullptr)); +} + +TEST_F(TestSchemaParserDialects, Draft202012ItemsAppliesAfterPrefixItems) +{ + rapidjson::Document schemaDocument; + schemaDocument.Parse(R"({ + "prefixItems": [ + { "type": "string" } + ], + "items": { "type": "number" } + })"); + ASSERT_FALSE(schemaDocument.HasParseError()); + + Schema schema; + SchemaParser schemaParser(SchemaParser::kDraft202012); + schemaParser.populateSchema(RapidJsonAdapter(schemaDocument), schema); + + rapidjson::Document validDocument; + validDocument.Parse(R"(["first", 2, 3])"); + ASSERT_FALSE(validDocument.HasParseError()); + + rapidjson::Document invalidDocument; + invalidDocument.Parse(R"(["first", 2, "third"])"); + ASSERT_FALSE(invalidDocument.HasParseError()); + + Validator validator; + EXPECT_TRUE(validator.validate(schema, RapidJsonAdapter(validDocument), nullptr)); + EXPECT_FALSE(validator.validate(schema, RapidJsonAdapter(invalidDocument), nullptr)); +} + +TEST_F(TestSchemaParserDialects, Draft202012UnevaluatedItemsAppliesAfterPrefixItems) +{ + rapidjson::Document schemaDocument; + schemaDocument.Parse(R"({ + "prefixItems": [ + { "type": "string" } + ], + "unevaluatedItems": false + })"); + ASSERT_FALSE(schemaDocument.HasParseError()); + + Schema schema; + SchemaParser schemaParser(SchemaParser::kDraft202012); + schemaParser.populateSchema(RapidJsonAdapter(schemaDocument), schema); + + rapidjson::Document validDocument; + validDocument.Parse(R"(["first"])"); + ASSERT_FALSE(validDocument.HasParseError()); + + rapidjson::Document invalidDocument; + invalidDocument.Parse(R"(["first", 2])"); + ASSERT_FALSE(invalidDocument.HasParseError()); + + Validator validator; + EXPECT_TRUE(validator.validate(schema, RapidJsonAdapter(validDocument), nullptr)); + EXPECT_FALSE(validator.validate(schema, RapidJsonAdapter(invalidDocument), nullptr)); +} + +TEST_F(TestSchemaParserDialects, Draft202012UnevaluatedItemsUsesAllOfPrefixItemsAnnotations) +{ + rapidjson::Document schemaDocument; + schemaDocument.Parse(R"({ + "allOf": [ + { + "prefixItems": [ + { "type": "string" } + ] + } + ], + "unevaluatedItems": false + })"); + ASSERT_FALSE(schemaDocument.HasParseError()); + + Schema schema; + SchemaParser schemaParser(SchemaParser::kDraft202012); + schemaParser.populateSchema(RapidJsonAdapter(schemaDocument), schema); + + rapidjson::Document validDocument; + validDocument.Parse(R"(["first"])"); + ASSERT_FALSE(validDocument.HasParseError()); + + rapidjson::Document invalidDocument; + invalidDocument.Parse(R"(["first", 2])"); + ASSERT_FALSE(invalidDocument.HasParseError()); + + Validator validator; + EXPECT_TRUE(validator.validate(schema, RapidJsonAdapter(validDocument), nullptr)); + EXPECT_FALSE(validator.validate(schema, RapidJsonAdapter(invalidDocument), nullptr)); +} + +TEST_F(TestSchemaParserDialects, Draft202012IgnoresAdditionalItems) +{ + rapidjson::Document schemaDocument; + schemaDocument.Parse(R"({ + "prefixItems": [ + { "type": "string" } + ], + "additionalItems": false + })"); + ASSERT_FALSE(schemaDocument.HasParseError()); + + Schema schema; + SchemaParser schemaParser(SchemaParser::kDraft202012); + schemaParser.populateSchema(RapidJsonAdapter(schemaDocument), schema); + + rapidjson::Document document; + document.Parse(R"(["first", 2])"); + ASSERT_FALSE(document.HasParseError()); + + Validator validator; + EXPECT_TRUE(validator.validate(schema, RapidJsonAdapter(document), nullptr)); +} diff --git a/tests/test_validator.cpp b/tests/test_validator.cpp index 2a509e44..9a34d4af 100644 --- a/tests/test_validator.cpp +++ b/tests/test_validator.cpp @@ -214,6 +214,13 @@ class TestValidator : public ::testing::TestWithParam { return processTestFile(testFile, SchemaParser::kDraft7); } + + void processDraft202012TestFile(const std::string &testFile, + const std::set &skipCases = + std::set()) + { + return processTestFile(testFile, SchemaParser::kDraft202012, skipCases); + } }; // @@ -657,3 +664,192 @@ TEST_F(TestValidator, Draft7_OptionalFormatDateTime) { processDraft7TestFile(TEST_SUITE_DIR "draft7/optional/format/date-time.json"); } + +// +// draft 2020-12 +// ------------------------------------------------------------------------------------------------ +// + +TEST_F(TestValidator, Draft202012_AdditionalProperties) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/additionalProperties.json"); +} + +TEST_F(TestValidator, Draft202012_AllOf) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/allOf.json"); +} + +TEST_F(TestValidator, Draft202012_AnyOf) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/anyOf.json"); +} + +TEST_F(TestValidator, Draft202012_BooleanSchema) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/boolean_schema.json"); +} + +TEST_F(TestValidator, Draft202012_Const) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/const.json"); +} + +TEST_F(TestValidator, Draft202012_Contains) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/contains.json"); +} + +TEST_F(TestValidator, Draft202012_Default) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/default.json"); +} + +TEST_F(TestValidator, Draft202012_Enum) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/enum.json"); +} + +TEST_F(TestValidator, Draft202012_ExclusiveMaximum) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/exclusiveMaximum.json"); +} + +TEST_F(TestValidator, Draft202012_ExclusiveMinimum) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/exclusiveMinimum.json"); +} + +TEST_F(TestValidator, Draft202012_IfThenElse) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/if-then-else.json"); +} + +TEST_F(TestValidator, Draft202012_Items) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/items.json"); +} + +TEST_F(TestValidator, Draft202012_Maximum) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/maximum.json"); +} + +TEST_F(TestValidator, Draft202012_MaxItems) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/maxItems.json"); +} + +TEST_F(TestValidator, Draft202012_MaxLength) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/maxLength.json"); +} + +TEST_F(TestValidator, Draft202012_MaxProperties) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/maxProperties.json"); +} + +TEST_F(TestValidator, Draft202012_Minimum) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/minimum.json"); +} + +TEST_F(TestValidator, Draft202012_MinItems) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/minItems.json"); +} + +TEST_F(TestValidator, Draft202012_MinLength) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/minLength.json"); +} + +TEST_F(TestValidator, Draft202012_MinProperties) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/minProperties.json"); +} + +TEST_F(TestValidator, Draft202012_MultipleOf) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/multipleOf.json"); +} + +TEST_F(TestValidator, Draft202012_Not) +{ + const std::set skipCases = { + "collect annotations inside a 'not', even if collection is disabled" + }; + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/not.json", skipCases); +} + +TEST_F(TestValidator, Draft202012_OneOf) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/oneOf.json"); +} + +TEST_F(TestValidator, Draft202012_Pattern) +{ + const std::set skipCases = { + "pattern with Unicode property escape requires unicode mode" + }; + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/pattern.json", skipCases); +} + +TEST_F(TestValidator, Draft202012_PatternProperties) +{ + const std::set skipCases = { + "patternProperties with Unicode property escape" + }; + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/patternProperties.json", skipCases); +} + +TEST_F(TestValidator, Draft202012_PrefixItems) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/prefixItems.json"); +} + +TEST_F(TestValidator, Draft202012_Properties) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/properties.json"); +} + +TEST_F(TestValidator, Draft202012_PropertyNames) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/propertyNames.json"); +} + +TEST_F(TestValidator, Draft202012_Required) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/required.json"); +} + +TEST_F(TestValidator, Draft202012_Type) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/type.json"); +} + +TEST_F(TestValidator, Draft202012_UnevaluatedItems) +{ + const std::set skipCases = { + "unevaluatedItems with nested items", + "unevaluatedItems with anyOf", + "unevaluatedItems with oneOf", + "unevaluatedItems with if/then/else", + "unevaluatedItems with $ref", + "unevaluatedItems before $ref", + "unevaluatedItems with $dynamicRef", + "unevaluatedItems can't see inside cousins", + "unevaluatedItems depends on adjacent contains", + "unevaluatedItems depends on multiple nested contains", + "unevaluatedItems and contains interact to control item dependency relationship", + "unevaluatedItems with minContains = 0", + "unevaluatedItems can see annotations from if without then and else" + }; + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/unevaluatedItems.json", skipCases); +} + +TEST_F(TestValidator, Draft202012_UniqueItems) +{ + processDraft202012TestFile(TEST_SUITE_DIR "draft2020-12/uniqueItems.json"); +}