From 0c81da0fb40f06cc64602089e157eaf134e1239c Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 16 Oct 2024 16:01:14 -0400 Subject: [PATCH] [WIP] Decouple `unevaluatedProperties` from annotations Signed-off-by: Juan Cruz Viotti --- benchmark/CMakeLists.txt | 1 + benchmark/evaluator_2019_09.cc | 45 +++ src/compiler/compile_describe.cc | 4 + src/compiler/compile_json.cc | 6 + src/compiler/default_compiler_2019_09.h | 50 ++-- src/compiler/default_compiler_2020_12.h | 2 +- src/compiler/default_compiler_draft4.h | 57 ++-- src/evaluator/context.cc | 30 ++ src/evaluator/evaluator.cc | 19 +- .../sourcemeta/blaze/evaluator_context.h | 16 +- .../sourcemeta/blaze/evaluator_template.h | 15 +- .../sourcemeta/blaze/evaluator_value.h | 5 + test/evaluator/evaluator_2019_09_test.cc | 260 +++++++++++++----- 13 files changed, 371 insertions(+), 139 deletions(-) create mode 100644 benchmark/evaluator_2019_09.cc diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index c9bb0cc..b04ff0f 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -8,6 +8,7 @@ if(BLAZE_EVALUATOR) evaluator_draft4.cc evaluator_draft6.cc evaluator_draft7.cc + evaluator_2019_09.cc evaluator_2020_12.cc) endif() diff --git a/benchmark/evaluator_2019_09.cc b/benchmark/evaluator_2019_09.cc new file mode 100644 index 0000000..d306c76 --- /dev/null +++ b/benchmark/evaluator_2019_09.cc @@ -0,0 +1,45 @@ +#include + +#include // assert + +#include +#include + +#include +#include + +static void Evaluator_2019_09_Unevaluated_Properties(benchmark::State &state) { + const sourcemeta::jsontoolkit::JSON schema{ + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "properties": { + "name": true, + "prohibited": false + }, + "unevaluatedProperties": false, + "$ref": "#/$defs/extension", + "$defs": { + "extension": { + "patternProperties": { "^x-": true } + } + } + })JSON")}; + + const auto instance{sourcemeta::jsontoolkit::parse(R"JSON([ + "name": "John Doe", + "x-foo": "bar" + ])JSON")}; + + const auto schema_template{sourcemeta::blaze::compile( + schema, sourcemeta::jsontoolkit::default_schema_walker, + sourcemeta::jsontoolkit::official_resolver, + sourcemeta::blaze::default_schema_compiler)}; + for (auto _ : state) { + auto result{sourcemeta::blaze::evaluate(schema_template, instance)}; + assert(result); + benchmark::DoNotOptimize(result); + } +} + +BENCHMARK(Evaluator_2019_09_Unevaluated_Properties); diff --git a/src/compiler/compile_describe.cc b/src/compiler/compile_describe.cc index 6206063..92b9a95 100644 --- a/src/compiler/compile_describe.cc +++ b/src/compiler/compile_describe.cc @@ -258,6 +258,10 @@ struct DescribeVisitor { return describe_reference(this->target); } + auto operator()(const ControlEvaluate &) const -> std::string { + return unknown(); + } + auto operator()(const ControlJump &) const -> std::string { return describe_reference(this->target); } diff --git a/src/compiler/compile_json.cc b/src/compiler/compile_json.cc index 0bb5fac..080adb7 100644 --- a/src/compiler/compile_json.cc +++ b/src/compiler/compile_json.cc @@ -182,6 +182,11 @@ auto value_to_json(const T &value) -> sourcemeta::jsontoolkit::JSON { data.push_back(sourcemeta::jsontoolkit::JSON{value.second}); result.assign("value", std::move(data)); return result; + } else if constexpr (std::is_same_v) { + result.assign("type", sourcemeta::jsontoolkit::JSON{"pointer"}); + result.assign("value", sourcemeta::jsontoolkit::JSON{ + sourcemeta::jsontoolkit::to_string(value)}); + return result; } else { static_assert(std::is_same_v); return sourcemeta::jsontoolkit::JSON{nullptr}; @@ -296,6 +301,7 @@ struct StepVisitor { HANDLE_STEP("loop", "contains", LoopContains) HANDLE_STEP("control", "label", ControlLabel) HANDLE_STEP("control", "mark", ControlMark) + HANDLE_STEP("control", "evaluate", ControlEvaluate) HANDLE_STEP("control", "jump", ControlJump) HANDLE_STEP("control", "dynamic-anchor-jump", ControlDynamicAnchorJump) diff --git a/src/compiler/default_compiler_2019_09.h b/src/compiler/default_compiler_2019_09.h index 9de486e..88790d0 100644 --- a/src/compiler/default_compiler_2019_09.h +++ b/src/compiler/default_compiler_2019_09.h @@ -100,7 +100,7 @@ auto compiler_2019_09_core_annotation(const Context &context, schema_context.schema.at(dynamic_context.keyword)})}; } -auto compiler_2019_09_applicator_contains_conditional_annotate( +auto compiler_2019_09_applicator_contains_with_options( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const bool annotate) -> Template { if (schema_context.schema.defines("type") && @@ -170,23 +170,23 @@ auto compiler_2019_09_applicator_contains(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_2019_09_applicator_contains_conditional_annotate( + return compiler_2019_09_applicator_contains_with_options( context, schema_context, dynamic_context, false); } auto compiler_2019_09_applicator_additionalproperties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_draft4_applicator_additionalproperties_conditional_annotation( + return compiler_draft4_applicator_additionalproperties_with_options( context, schema_context, dynamic_context, - context.uses_unevaluated_properties || context.mode == Mode::Exhaustive); + context.mode == Mode::Exhaustive, context.uses_unevaluated_properties); } auto compiler_2019_09_applicator_items(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_draft4_applicator_items_conditional_annotation( + return compiler_draft4_applicator_items_with_options( context, schema_context, dynamic_context, context.uses_unevaluated_items || context.mode == Mode::Exhaustive); } @@ -194,7 +194,7 @@ auto compiler_2019_09_applicator_items(const Context &context, auto compiler_2019_09_applicator_additionalitems( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_draft4_applicator_additionalitems_conditional_annotation( + return compiler_draft4_applicator_additionalitems_with_options( context, schema_context, dynamic_context, context.uses_unevaluated_items || context.mode == Mode::Exhaustive); } @@ -247,30 +247,22 @@ auto compiler_2019_09_applicator_unevaluatedproperties( return {}; } - ValueStrings dependencies{"unevaluatedProperties"}; - - if (schema_context.vocabularies.contains( - "https://json-schema.org/draft/2019-09/vocab/applicator")) { - dependencies.push_back("properties"); - dependencies.push_back("patternProperties"); - dependencies.push_back("additionalProperties"); - } - - if (schema_context.vocabularies.contains( - "https://json-schema.org/draft/2020-12/vocab/applicator")) { - dependencies.push_back("properties"); - dependencies.push_back("patternProperties"); - dependencies.push_back("additionalProperties"); - } - Template children{compile(context, schema_context, relative_dynamic_context, sourcemeta::jsontoolkit::empty_pointer, sourcemeta::jsontoolkit::empty_pointer)}; - children.push_back(make( - true, context, schema_context, relative_dynamic_context, ValueNone{})); + + if (context.mode == Mode::Exhaustive) { + children.push_back(make( + true, context, schema_context, relative_dynamic_context, ValueNone{})); + } + + // TODO: Do this out the box on LoopPropertiesUnevaluated? + children.push_back(make(false, context, schema_context, + relative_dynamic_context, + ValuePointer{})); return {make( - true, context, schema_context, dynamic_context, std::move(dependencies), + true, context, schema_context, dynamic_context, ValueNone{}, std::move(children))}; } @@ -297,17 +289,17 @@ auto compiler_2019_09_core_recursiveref(const Context &context, auto compiler_2019_09_applicator_properties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_draft4_applicator_properties_conditional_annotation( + return compiler_draft4_applicator_properties_with_options( context, schema_context, dynamic_context, - context.uses_unevaluated_properties || context.mode == Mode::Exhaustive); + context.mode == Mode::Exhaustive, context.uses_unevaluated_properties); } auto compiler_2019_09_applicator_patternproperties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_draft4_applicator_patternproperties_conditional_annotation( + return compiler_draft4_applicator_patternproperties_with_options( context, schema_context, dynamic_context, - context.uses_unevaluated_properties || context.mode == Mode::Exhaustive); + context.mode == Mode::Exhaustive, context.uses_unevaluated_properties); } } // namespace internal diff --git a/src/compiler/default_compiler_2020_12.h b/src/compiler/default_compiler_2020_12.h index 0781094..fc8cafe 100644 --- a/src/compiler/default_compiler_2020_12.h +++ b/src/compiler/default_compiler_2020_12.h @@ -36,7 +36,7 @@ auto compiler_2020_12_applicator_contains(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_2019_09_applicator_contains_conditional_annotate( + return compiler_2019_09_applicator_contains_with_options( context, schema_context, dynamic_context, context.uses_unevaluated_items || context.mode == Mode::Exhaustive); } diff --git a/src/compiler/default_compiler_draft4.h b/src/compiler/default_compiler_draft4.h index f0e7c62..b6e35bb 100644 --- a/src/compiler/default_compiler_draft4.h +++ b/src/compiler/default_compiler_draft4.h @@ -13,6 +13,8 @@ #include "compile_helpers.h" +#include + static auto parse_regex(const std::string &pattern, const sourcemeta::jsontoolkit::URI &base, const sourcemeta::jsontoolkit::Pointer &schema_location) @@ -382,9 +384,10 @@ auto compiler_draft4_applicator_oneof(const Context &context, std::move(disjunctors))}; } -auto compiler_draft4_applicator_properties_conditional_annotation( +auto compiler_draft4_applicator_properties_with_options( const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const bool annotate) -> Template { + const DynamicContext &dynamic_context, const bool annotate, + const bool track_evaluation) -> Template { assert(schema_context.schema.at(dynamic_context.keyword).is_object()); if (schema_context.schema.at(dynamic_context.keyword).empty()) { return {}; @@ -501,6 +504,12 @@ auto compiler_draft4_applicator_properties_conditional_annotation( Template children; for (auto &&[name, substeps] : properties) { + if (track_evaluation) { + substeps.push_back(make(false, context, schema_context, + relative_dynamic_context, + ValuePointer{name})); + } + if (annotate) { substeps.push_back(make( true, context, schema_context, relative_dynamic_context, @@ -579,13 +588,14 @@ auto compiler_draft4_applicator_properties_conditional_annotation( auto compiler_draft4_applicator_properties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_draft4_applicator_properties_conditional_annotation( - context, schema_context, dynamic_context, false); + return compiler_draft4_applicator_properties_with_options( + context, schema_context, dynamic_context, false, false); } -auto compiler_draft4_applicator_patternproperties_conditional_annotation( +auto compiler_draft4_applicator_patternproperties_with_options( const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const bool annotate) -> Template { + const DynamicContext &dynamic_context, const bool annotate, + const bool track_evaluation) -> Template { assert(schema_context.schema.at(dynamic_context.keyword).is_object()); if (schema_context.schema.at(dynamic_context.keyword).empty()) { return {}; @@ -622,6 +632,12 @@ auto compiler_draft4_applicator_patternproperties_conditional_annotation( ValueNone{})); } + if (track_evaluation) { + substeps.push_back(make(false, context, schema_context, + relative_dynamic_context, + ValuePointer{})); + } + // If the `patternProperties` subschema for the given pattern does // nothing, then we can avoid generating an entire loop for it if (!substeps.empty()) { @@ -649,13 +665,14 @@ auto compiler_draft4_applicator_patternproperties_conditional_annotation( auto compiler_draft4_applicator_patternproperties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_draft4_applicator_patternproperties_conditional_annotation( - context, schema_context, dynamic_context, false); + return compiler_draft4_applicator_patternproperties_with_options( + context, schema_context, dynamic_context, false, false); } -auto compiler_draft4_applicator_additionalproperties_conditional_annotation( +auto compiler_draft4_applicator_additionalproperties_with_options( const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const bool annotate) -> Template { + const DynamicContext &dynamic_context, const bool annotate, + const bool track_evaluation) -> Template { if (schema_context.schema.defines("type") && schema_context.schema.at("type").is_string() && schema_context.schema.at("type").to_string() != "object") { @@ -671,6 +688,12 @@ auto compiler_draft4_applicator_additionalproperties_conditional_annotation( true, context, schema_context, relative_dynamic_context, ValueNone{})); } + if (track_evaluation) { + children.push_back(make(false, context, schema_context, + relative_dynamic_context, + ValuePointer{})); + } + ValuePropertyFilter filter; if (schema_context.schema.defines("properties") && @@ -726,8 +749,8 @@ auto compiler_draft4_applicator_additionalproperties_conditional_annotation( auto compiler_draft4_applicator_additionalproperties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_draft4_applicator_additionalproperties_conditional_annotation( - context, schema_context, dynamic_context, false); + return compiler_draft4_applicator_additionalproperties_with_options( + context, schema_context, dynamic_context, false, false); } auto compiler_draft4_validation_pattern(const Context &context, @@ -895,7 +918,7 @@ auto compiler_draft4_applicator_items_array( std::move(children))}; } -auto compiler_draft4_applicator_items_conditional_annotation( +auto compiler_draft4_applicator_items_with_options( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const bool annotate) -> Template { if (schema_context.schema.defines("type") && @@ -937,8 +960,8 @@ auto compiler_draft4_applicator_items(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_draft4_applicator_items_conditional_annotation( - context, schema_context, dynamic_context, false); + return compiler_draft4_applicator_items_with_options(context, schema_context, + dynamic_context, false); } auto compiler_draft4_applicator_additionalitems_from_cursor( @@ -969,7 +992,7 @@ auto compiler_draft4_applicator_additionalitems_from_cursor( ValueUnsignedInteger{cursor}, std::move(children))}; } -auto compiler_draft4_applicator_additionalitems_conditional_annotation( +auto compiler_draft4_applicator_additionalitems_with_options( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const bool annotate) -> Template { if (schema_context.schema.defines("type") && @@ -998,7 +1021,7 @@ auto compiler_draft4_applicator_additionalitems_conditional_annotation( auto compiler_draft4_applicator_additionalitems( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context) -> Template { - return compiler_draft4_applicator_additionalitems_conditional_annotation( + return compiler_draft4_applicator_additionalitems_with_options( context, schema_context, dynamic_context, false); } diff --git a/src/evaluator/context.cc b/src/evaluator/context.cc index ed1a9bb..ca2ced6 100644 --- a/src/evaluator/context.cc +++ b/src/evaluator/context.cc @@ -3,6 +3,8 @@ #include // assert +#include + namespace sourcemeta::blaze { auto EvaluationContext::prepare(const sourcemeta::jsontoolkit::JSON &instance) @@ -317,4 +319,32 @@ auto EvaluationContext::find_dynamic_anchor(const std::string &anchor) const return std::nullopt; } +auto EvaluationContext::evaluate( + const sourcemeta::jsontoolkit::Pointer &relative_instance_location) + -> void { + std::cerr << "EVALUATED! "; + + // TODO: Improve Pointer API to make this easier + auto new_instance_location = this->instance_location_; + for (const auto &token : relative_instance_location) { + if (token.is_property()) { + new_instance_location.push_back(token.to_property()); + } else { + new_instance_location.push_back(token.to_index()); + } + } + + sourcemeta::jsontoolkit::stringify(new_instance_location, std::cerr); + std::cerr << "\n"; + this->evaluated_.push_back(std::move(new_instance_location)); +} + +auto EvaluationContext::is_evaluated( + const sourcemeta::jsontoolkit::JSON::String &property) const -> bool { + auto expected_instance_location = this->instance_location_; + expected_instance_location.push_back(property); + return std::find(this->evaluated_.cbegin(), this->evaluated_.cend(), + expected_instance_location) != this->evaluated_.cend(); +} + } // namespace sourcemeta::blaze diff --git a/src/evaluator/evaluator.cc b/src/evaluator/evaluator.cc index b844271..a5d1d9f 100644 --- a/src/evaluator/evaluator.cc +++ b/src/evaluator/evaluator.cc @@ -9,6 +9,8 @@ #include // std::numeric_limits #include // std::optional +#include + namespace { auto evaluate_step(const sourcemeta::blaze::Template::value_type &step, @@ -636,6 +638,14 @@ auto evaluate_step(const sourcemeta::blaze::Template::value_type &step, return true; } + case IS_STEP(ControlEvaluate): { + SOURCEMETA_TRACE_START(trace_id, "ControlEvaluate"); + const auto &control{std::get(step)}; + context.evaluate(control.value); + SOURCEMETA_TRACE_END(trace_id, "ControlEvaluate"); + return true; + } + case IS_STEP(ControlJump): { EVALUATE_BEGIN_NO_PRECONDITION(control, ControlJump); result = true; @@ -704,16 +714,9 @@ auto evaluate_step(const sourcemeta::blaze::Template::value_type &step, EVALUATE_BEGIN(loop, AnnotationLoopPropertiesUnevaluated, target.is_object()); result = true; - assert(!loop.value.empty()); for (const auto &entry : target.as_object()) { - // TODO: It might be more efficient to get all the annotations we - // potentially care about as a set first, and the make the loop - // check for O(1) containment in that set? - if (context.defines_sibling_annotation( - loop.value, - // TODO: This conversion implies a string copy - sourcemeta::jsontoolkit::JSON{entry.first})) { + if (context.is_evaluated(entry.first)) { continue; } diff --git a/src/evaluator/include/sourcemeta/blaze/evaluator_context.h b/src/evaluator/include/sourcemeta/blaze/evaluator_context.h index ef14c89..271c82f 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator_context.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator_context.h @@ -91,14 +91,20 @@ class SOURCEMETA_BLAZE_EVALUATOR_EXPORT EvaluationContext { auto find_dynamic_anchor(const std::string &anchor) const -> std::optional; + /////////////////////////////////////////////// + // Evaluation + /////////////////////////////////////////////// + + auto + evaluate(const sourcemeta::jsontoolkit::Pointer &relative_instance_location) + -> void; + auto is_evaluated(const sourcemeta::jsontoolkit::JSON::String &property) const + -> bool; + /////////////////////////////////////////////// // Annotations /////////////////////////////////////////////// - // TODO: At least currently, we only need to mask if a schema - // makes use of `unevaluatedProperties` or `unevaluatedItems` - // Detect if a schema does need this so if not, we avoid - // an unnecessary copy auto mask() -> void; auto annotate( const sourcemeta::jsontoolkit::WeakPointer ¤t_instance_location, @@ -133,6 +139,8 @@ class SOURCEMETA_BLAZE_EVALUATOR_EXPORT EvaluationContext { std::vector resources_; std::map> labels; bool property_as_instance{false}; + // TODO: Turn this into a trie + std::vector evaluated_; // For annotations std::vector annotation_blacklist; diff --git a/src/evaluator/include/sourcemeta/blaze/evaluator_template.h b/src/evaluator/include/sourcemeta/blaze/evaluator_template.h index 409a3b1..050512e 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator_template.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator_template.h @@ -72,6 +72,7 @@ struct LoopItems; struct LoopContains; struct ControlLabel; struct ControlMark; +struct ControlEvaluate; struct ControlJump; struct ControlDynamicAnchorJump; #endif @@ -97,8 +98,8 @@ using Template = std::vector>; + LoopKeys, LoopItems, LoopContains, ControlLabel, ControlMark, + ControlEvaluate, ControlJump, ControlDynamicAnchorJump>>; #if !defined(DOXYGEN) // For fast internal instruction dispatching. It must stay @@ -162,6 +163,7 @@ enum class TemplateIndex : std::uint8_t { LoopContains, ControlLabel, ControlMark, + ControlEvaluate, ControlJump, ControlDynamicAnchorJump }; @@ -367,8 +369,8 @@ DEFINE_STEP_WITH_VALUE(Annotation, BasenameToParent, ValueNone) /// @ingroup evaluator_instructions /// @brief Represents a compiler step that loops over object properties that -/// were not collected as annotations -DEFINE_STEP_APPLICATOR(Annotation, LoopPropertiesUnevaluated, ValueStrings) +/// were not previously evaluated +DEFINE_STEP_APPLICATOR(Annotation, LoopPropertiesUnevaluated, ValueNone) /// @ingroup evaluator_instructions /// @brief Represents a compiler step that loops over array items when the array @@ -478,6 +480,11 @@ DEFINE_STEP_APPLICATOR(Control, Label, ValueUnsignedInteger) /// without executing children instructions DEFINE_STEP_APPLICATOR(Control, Mark, ValueUnsignedInteger) +/// @ingroup evaluator_instructions +/// @brief Represents a compiler step that marks the current instance location +/// as evaluated +DEFINE_STEP_WITH_VALUE(Control, Evaluate, ValuePointer) + /// @ingroup evaluator_instructions /// @brief Represents a compiler step that consists of jumping into a /// pre-registered label diff --git a/src/evaluator/include/sourcemeta/blaze/evaluator_value.h b/src/evaluator/include/sourcemeta/blaze/evaluator_value.h index 31f3dc9..3616970 100644 --- a/src/evaluator/include/sourcemeta/blaze/evaluator_value.h +++ b/src/evaluator/include/sourcemeta/blaze/evaluator_value.h @@ -2,6 +2,7 @@ #define SOURCEMETA_BLAZE_EVALUATOR_VALUE_H #include +#include #include #include // std::uint8_t @@ -101,6 +102,10 @@ using ValuePropertyFilter = std::pair>; /// Represents a compiler step value that consists of two indexes using ValueIndexPair = std::pair; +/// @ingroup evaluator +/// Represents a compiler step value that consists of a pointer +using ValuePointer = sourcemeta::jsontoolkit::Pointer; + } // namespace sourcemeta::blaze #endif diff --git a/test/evaluator/evaluator_2019_09_test.cc b/test/evaluator/evaluator_2019_09_test.cc index e0e826d..4498900 100644 --- a/test/evaluator/evaluator_2019_09_test.cc +++ b/test/evaluator/evaluator_2019_09_test.cc @@ -2614,48 +2614,35 @@ TEST(Evaluator_2019_09, unevaluatedProperties_1) { const sourcemeta::jsontoolkit::JSON instance{ sourcemeta::jsontoolkit::parse("{ \"foo\": \"baz\", \"bar\": true }")}; - EVALUATE_WITH_TRACE_FAST_SUCCESS(schema, instance, 6); + EVALUATE_WITH_TRACE_FAST_SUCCESS(schema, instance, 4); EVALUATE_TRACE_PRE(0, LogicalAnd, "/properties", "#/properties", ""); EVALUATE_TRACE_PRE(1, AssertionTypeStrict, "/properties/foo/type", "#/properties/foo/type", "/foo"); - EVALUATE_TRACE_PRE_ANNOTATION(2, "/properties", "#/properties", ""); - EVALUATE_TRACE_PRE(3, AnnotationLoopPropertiesUnevaluated, + EVALUATE_TRACE_PRE(2, AnnotationLoopPropertiesUnevaluated, "/unevaluatedProperties", "#/unevaluatedProperties", ""); - EVALUATE_TRACE_PRE(4, AssertionTypeStrict, "/unevaluatedProperties/type", + EVALUATE_TRACE_PRE(3, AssertionTypeStrict, "/unevaluatedProperties/type", "#/unevaluatedProperties/type", "/bar"); - EVALUATE_TRACE_PRE_ANNOTATION(5, "/unevaluatedProperties", - "#/unevaluatedProperties", ""); EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict, "/properties/foo/type", "#/properties/foo/type", "/foo"); - EVALUATE_TRACE_POST_ANNOTATION(1, "/properties", "#/properties", "", "foo"); - EVALUATE_TRACE_POST_SUCCESS(2, LogicalAnd, "/properties", "#/properties", ""); - EVALUATE_TRACE_POST_SUCCESS(3, AssertionTypeStrict, + EVALUATE_TRACE_POST_SUCCESS(1, LogicalAnd, "/properties", "#/properties", ""); + EVALUATE_TRACE_POST_SUCCESS(2, AssertionTypeStrict, "/unevaluatedProperties/type", "#/unevaluatedProperties/type", "/bar"); - EVALUATE_TRACE_POST_ANNOTATION(4, "/unevaluatedProperties", - "#/unevaluatedProperties", "", "bar"); - EVALUATE_TRACE_POST_SUCCESS(5, AnnotationLoopPropertiesUnevaluated, + EVALUATE_TRACE_POST_SUCCESS(3, AnnotationLoopPropertiesUnevaluated, "/unevaluatedProperties", "#/unevaluatedProperties", ""); EVALUATE_TRACE_POST_DESCRIBE(instance, 0, "The value was expected to be of type string"); EVALUATE_TRACE_POST_DESCRIBE(instance, 1, - "The object property \"foo\" successfully " - "validated against its property subschema"); - EVALUATE_TRACE_POST_DESCRIBE(instance, 2, "The object value was expected to validate " "against the single defined property subschema"); - EVALUATE_TRACE_POST_DESCRIBE(instance, 3, + EVALUATE_TRACE_POST_DESCRIBE(instance, 2, "The value was expected to be of type boolean"); EVALUATE_TRACE_POST_DESCRIBE( - instance, 4, - "The object property \"bar\" successfully validated against the " - "subschema for unevaluated properties"); - EVALUATE_TRACE_POST_DESCRIBE( - instance, 5, + instance, 3, "The object properties not covered by other object keywords were " "expected to validate against this subschema"); } @@ -2736,58 +2723,43 @@ TEST(Evaluator_2019_09, unevaluatedProperties_2) { const sourcemeta::jsontoolkit::JSON instance{ sourcemeta::jsontoolkit::parse("{ \"foo\": \"baz\", \"bar\": true }")}; - EVALUATE_WITH_TRACE_FAST_SUCCESS(schema, instance, 7); + EVALUATE_WITH_TRACE_FAST_SUCCESS(schema, instance, 5); EVALUATE_TRACE_PRE(0, LogicalAnd, "/allOf", "#/allOf", ""); EVALUATE_TRACE_PRE(1, LogicalAnd, "/allOf/0/properties", "#/allOf/0/properties", ""); EVALUATE_TRACE_PRE(2, AssertionTypeStrict, "/allOf/0/properties/foo/type", "#/allOf/0/properties/foo/type", "/foo"); - EVALUATE_TRACE_PRE_ANNOTATION(3, "/allOf/0/properties", - "#/allOf/0/properties", ""); - EVALUATE_TRACE_PRE(4, AnnotationLoopPropertiesUnevaluated, + EVALUATE_TRACE_PRE(3, AnnotationLoopPropertiesUnevaluated, "/unevaluatedProperties", "#/unevaluatedProperties", ""); - EVALUATE_TRACE_PRE(5, AssertionTypeStrict, "/unevaluatedProperties/type", + EVALUATE_TRACE_PRE(4, AssertionTypeStrict, "/unevaluatedProperties/type", "#/unevaluatedProperties/type", "/bar"); - EVALUATE_TRACE_PRE_ANNOTATION(6, "/unevaluatedProperties", - "#/unevaluatedProperties", ""); EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict, "/allOf/0/properties/foo/type", "#/allOf/0/properties/foo/type", "/foo"); - EVALUATE_TRACE_POST_ANNOTATION(1, "/allOf/0/properties", - "#/allOf/0/properties", "", "foo"); - EVALUATE_TRACE_POST_SUCCESS(2, LogicalAnd, "/allOf/0/properties", + EVALUATE_TRACE_POST_SUCCESS(1, LogicalAnd, "/allOf/0/properties", "#/allOf/0/properties", ""); - EVALUATE_TRACE_POST_SUCCESS(3, LogicalAnd, "/allOf", "#/allOf", ""); - EVALUATE_TRACE_POST_SUCCESS(4, AssertionTypeStrict, + EVALUATE_TRACE_POST_SUCCESS(2, LogicalAnd, "/allOf", "#/allOf", ""); + EVALUATE_TRACE_POST_SUCCESS(3, AssertionTypeStrict, "/unevaluatedProperties/type", "#/unevaluatedProperties/type", "/bar"); - EVALUATE_TRACE_POST_ANNOTATION(5, "/unevaluatedProperties", - "#/unevaluatedProperties", "", "bar"); - EVALUATE_TRACE_POST_SUCCESS(6, AnnotationLoopPropertiesUnevaluated, + EVALUATE_TRACE_POST_SUCCESS(4, AnnotationLoopPropertiesUnevaluated, "/unevaluatedProperties", "#/unevaluatedProperties", ""); EVALUATE_TRACE_POST_DESCRIBE(instance, 0, "The value was expected to be of type string"); EVALUATE_TRACE_POST_DESCRIBE(instance, 1, - "The object property \"foo\" successfully " - "validated against its property subschema"); - EVALUATE_TRACE_POST_DESCRIBE(instance, 2, "The object value was expected to validate " "against the single defined property subschema"); EVALUATE_TRACE_POST_DESCRIBE( - instance, 3, + instance, 2, "The object value was expected to validate against the given subschema"); - EVALUATE_TRACE_POST_DESCRIBE(instance, 4, + EVALUATE_TRACE_POST_DESCRIBE(instance, 3, "The value was expected to be of type boolean"); EVALUATE_TRACE_POST_DESCRIBE( - instance, 5, - "The object property \"bar\" successfully validated against the " - "subschema for unevaluated properties"); - EVALUATE_TRACE_POST_DESCRIBE( - instance, 6, + instance, 4, "The object properties not covered by other object keywords were " "expected to validate against this subschema"); } @@ -2882,51 +2854,44 @@ TEST(Evaluator_2019_09, unevaluatedProperties_3) { const sourcemeta::jsontoolkit::JSON instance{ sourcemeta::jsontoolkit::parse("{ \"foo\": \"baz\", \"bar\": 1 }")}; - EVALUATE_WITH_TRACE_FAST_FAILURE(schema, instance, 6); + EVALUATE_WITH_TRACE_FAST_FAILURE(schema, instance, 5); EVALUATE_TRACE_PRE(0, LogicalAnd, "/allOf", "#/allOf", ""); EVALUATE_TRACE_PRE(1, LogicalAnd, "/allOf/0/properties", "#/allOf/0/properties", ""); EVALUATE_TRACE_PRE(2, AssertionTypeStrict, "/allOf/0/properties/foo/type", "#/allOf/0/properties/foo/type", "/foo"); - EVALUATE_TRACE_PRE_ANNOTATION(3, "/allOf/0/properties", - "#/allOf/0/properties", ""); - EVALUATE_TRACE_PRE(4, AnnotationLoopPropertiesUnevaluated, + EVALUATE_TRACE_PRE(3, AnnotationLoopPropertiesUnevaluated, "/unevaluatedProperties", "#/unevaluatedProperties", ""); - EVALUATE_TRACE_PRE(5, AssertionTypeStrict, "/unevaluatedProperties/type", + EVALUATE_TRACE_PRE(4, AssertionTypeStrict, "/unevaluatedProperties/type", "#/unevaluatedProperties/type", "/bar"); EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict, "/allOf/0/properties/foo/type", "#/allOf/0/properties/foo/type", "/foo"); - EVALUATE_TRACE_POST_ANNOTATION(1, "/allOf/0/properties", - "#/allOf/0/properties", "", "foo"); - EVALUATE_TRACE_POST_SUCCESS(2, LogicalAnd, "/allOf/0/properties", + EVALUATE_TRACE_POST_SUCCESS(1, LogicalAnd, "/allOf/0/properties", "#/allOf/0/properties", ""); - EVALUATE_TRACE_POST_SUCCESS(3, LogicalAnd, "/allOf", "#/allOf", ""); - EVALUATE_TRACE_POST_FAILURE(4, AssertionTypeStrict, + EVALUATE_TRACE_POST_SUCCESS(2, LogicalAnd, "/allOf", "#/allOf", ""); + EVALUATE_TRACE_POST_FAILURE(3, AssertionTypeStrict, "/unevaluatedProperties/type", "#/unevaluatedProperties/type", "/bar"); - EVALUATE_TRACE_POST_FAILURE(5, AnnotationLoopPropertiesUnevaluated, + EVALUATE_TRACE_POST_FAILURE(4, AnnotationLoopPropertiesUnevaluated, "/unevaluatedProperties", "#/unevaluatedProperties", ""); EVALUATE_TRACE_POST_DESCRIBE(instance, 0, "The value was expected to be of type string"); EVALUATE_TRACE_POST_DESCRIBE(instance, 1, - "The object property \"foo\" successfully " - "validated against its property subschema"); - EVALUATE_TRACE_POST_DESCRIBE(instance, 2, "The object value was expected to validate " "against the single defined property subschema"); EVALUATE_TRACE_POST_DESCRIBE( - instance, 3, + instance, 2, "The object value was expected to validate against the given subschema"); - EVALUATE_TRACE_POST_DESCRIBE(instance, 4, + EVALUATE_TRACE_POST_DESCRIBE(instance, 3, "The value was expected to be of type boolean " "but it was of type integer"); EVALUATE_TRACE_POST_DESCRIBE( - instance, 5, + instance, 4, "The object properties not covered by other object keywords were " "expected to validate against this subschema"); } @@ -3010,40 +2975,35 @@ TEST(Evaluator_2019_09, unevaluatedProperties_4) { const sourcemeta::jsontoolkit::JSON instance{ sourcemeta::jsontoolkit::parse("{ \"foo\": \"baz\", \"bar\": true }")}; - EVALUATE_WITH_TRACE_FAST_FAILURE(schema, instance, 5); + EVALUATE_WITH_TRACE_FAST_FAILURE(schema, instance, 4); EVALUATE_TRACE_PRE(0, LogicalAnd, "/properties", "#/properties", ""); EVALUATE_TRACE_PRE(1, AssertionTypeStrict, "/properties/foo/type", "#/properties/foo/type", "/foo"); - EVALUATE_TRACE_PRE_ANNOTATION(2, "/properties", "#/properties", ""); - EVALUATE_TRACE_PRE(3, AnnotationLoopPropertiesUnevaluated, + EVALUATE_TRACE_PRE(2, AnnotationLoopPropertiesUnevaluated, "/unevaluatedProperties", "#/unevaluatedProperties", ""); - EVALUATE_TRACE_PRE(4, AssertionFail, "/unevaluatedProperties", + EVALUATE_TRACE_PRE(3, AssertionFail, "/unevaluatedProperties", "#/unevaluatedProperties", "/bar"); EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict, "/properties/foo/type", "#/properties/foo/type", "/foo"); - EVALUATE_TRACE_POST_ANNOTATION(1, "/properties", "#/properties", "", "foo"); - EVALUATE_TRACE_POST_SUCCESS(2, LogicalAnd, "/properties", "#/properties", ""); - EVALUATE_TRACE_POST_FAILURE(3, AssertionFail, "/unevaluatedProperties", + EVALUATE_TRACE_POST_SUCCESS(1, LogicalAnd, "/properties", "#/properties", ""); + EVALUATE_TRACE_POST_FAILURE(2, AssertionFail, "/unevaluatedProperties", "#/unevaluatedProperties", "/bar"); - EVALUATE_TRACE_POST_FAILURE(4, AnnotationLoopPropertiesUnevaluated, + EVALUATE_TRACE_POST_FAILURE(3, AnnotationLoopPropertiesUnevaluated, "/unevaluatedProperties", "#/unevaluatedProperties", ""); EVALUATE_TRACE_POST_DESCRIBE(instance, 0, "The value was expected to be of type string"); EVALUATE_TRACE_POST_DESCRIBE(instance, 1, - "The object property \"foo\" successfully " - "validated against its property subschema"); - EVALUATE_TRACE_POST_DESCRIBE(instance, 2, "The object value was expected to validate " "against the single defined property subschema"); EVALUATE_TRACE_POST_DESCRIBE( - instance, 3, + instance, 2, "The object value was not expected to define the property \"bar\""); EVALUATE_TRACE_POST_DESCRIBE( - instance, 4, + instance, 3, "The object value was not expected to define unevaluated properties"); } @@ -3097,6 +3057,154 @@ TEST(Evaluator_2019_09, unevaluatedProperties_4_exhaustive) { "The object value was not expected to define unevaluated properties"); } +TEST(Evaluator_2019_09, unevaluatedProperties_5) { + const sourcemeta::jsontoolkit::JSON schema{ + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "additionalProperties": true, + "unevaluatedProperties": false + })JSON")}; + + const sourcemeta::jsontoolkit::JSON instance{ + sourcemeta::jsontoolkit::parse("{ \"foo\": \"baz\" }")}; + + EVALUATE_WITH_TRACE_FAST_SUCCESS(schema, instance, 2); + + EVALUATE_TRACE_PRE(0, LoopProperties, "/additionalProperties", + "#/additionalProperties", ""); + EVALUATE_TRACE_PRE(1, AnnotationLoopPropertiesUnevaluated, + "/unevaluatedProperties", "#/unevaluatedProperties", ""); + + EVALUATE_TRACE_POST_SUCCESS(0, LoopProperties, "/additionalProperties", + "#/additionalProperties", ""); + EVALUATE_TRACE_POST_SUCCESS(1, AnnotationLoopPropertiesUnevaluated, + "/unevaluatedProperties", + "#/unevaluatedProperties", ""); + + EVALUATE_TRACE_POST_DESCRIBE( + instance, 0, + "The object properties not covered by other adjacent object keywords " + "were expected to validate against this subschema"); + EVALUATE_TRACE_POST_DESCRIBE( + instance, 1, + "The object value was not expected to define unevaluated properties"); +} + +TEST(Evaluator_2019_09, unevaluatedProperties_5_exhaustive) { + const sourcemeta::jsontoolkit::JSON schema{ + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "additionalProperties": true, + "unevaluatedProperties": false + })JSON")}; + + const sourcemeta::jsontoolkit::JSON instance{ + sourcemeta::jsontoolkit::parse("{ \"foo\": \"baz\" }")}; + + EVALUATE_WITH_TRACE_EXHAUSTIVE_SUCCESS(schema, instance, 3); + + EVALUATE_TRACE_PRE(0, LoopProperties, "/additionalProperties", + "#/additionalProperties", ""); + EVALUATE_TRACE_PRE_ANNOTATION(1, "/additionalProperties", + "#/additionalProperties", ""); + EVALUATE_TRACE_PRE(2, AnnotationLoopPropertiesUnevaluated, + "/unevaluatedProperties", "#/unevaluatedProperties", ""); + + EVALUATE_TRACE_POST_ANNOTATION(0, "/additionalProperties", + "#/additionalProperties", "", "foo"); + EVALUATE_TRACE_POST_SUCCESS(1, LoopProperties, "/additionalProperties", + "#/additionalProperties", ""); + EVALUATE_TRACE_POST_SUCCESS(2, AnnotationLoopPropertiesUnevaluated, + "/unevaluatedProperties", + "#/unevaluatedProperties", ""); + + EVALUATE_TRACE_POST_DESCRIBE( + instance, 0, + "The object property \"foo\" successfully validated against the " + "additional properties subschema"); + EVALUATE_TRACE_POST_DESCRIBE( + instance, 1, + "The object properties not covered by other adjacent object keywords " + "were expected to validate against this subschema"); + EVALUATE_TRACE_POST_DESCRIBE( + instance, 2, + "The object value was not expected to define unevaluated properties"); +} + +TEST(Evaluator_2019_09, unevaluatedProperties_6) { + const sourcemeta::jsontoolkit::JSON schema{ + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "patternProperties": { "^@": true }, + "unevaluatedProperties": false + })JSON")}; + + const sourcemeta::jsontoolkit::JSON instance{ + sourcemeta::jsontoolkit::parse("{ \"@foo\": \"bar\" }")}; + + EVALUATE_WITH_TRACE_FAST_SUCCESS(schema, instance, 2); + + EVALUATE_TRACE_PRE(0, LogicalWhenType, "/patternProperties", + "#/patternProperties", ""); + EVALUATE_TRACE_PRE(1, AnnotationLoopPropertiesUnevaluated, + "/unevaluatedProperties", "#/unevaluatedProperties", ""); + + EVALUATE_TRACE_POST_SUCCESS(0, LogicalWhenType, "/patternProperties", + "#/patternProperties", ""); + EVALUATE_TRACE_POST_SUCCESS(1, AnnotationLoopPropertiesUnevaluated, + "/unevaluatedProperties", + "#/unevaluatedProperties", ""); + + EVALUATE_TRACE_POST_DESCRIBE( + instance, 0, + "The object value was expected to validate against the single defined " + "pattern property subschema"); + EVALUATE_TRACE_POST_DESCRIBE( + instance, 1, + "The object value was not expected to define unevaluated properties"); +} + +TEST(Evaluator_2019_09, unevaluatedProperties_6_exhaustive) { + const sourcemeta::jsontoolkit::JSON schema{ + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "patternProperties": { "^@": true }, + "unevaluatedProperties": false + })JSON")}; + + const sourcemeta::jsontoolkit::JSON instance{ + sourcemeta::jsontoolkit::parse("{ \"@foo\": \"bar\" }")}; + + EVALUATE_WITH_TRACE_EXHAUSTIVE_SUCCESS(schema, instance, 3); + + EVALUATE_TRACE_PRE(0, LogicalWhenType, "/patternProperties", + "#/patternProperties", ""); + EVALUATE_TRACE_PRE_ANNOTATION(1, "/patternProperties", "#/patternProperties", + ""); + EVALUATE_TRACE_PRE(2, AnnotationLoopPropertiesUnevaluated, + "/unevaluatedProperties", "#/unevaluatedProperties", ""); + + EVALUATE_TRACE_POST_ANNOTATION(0, "/patternProperties", "#/patternProperties", + "", "@foo"); + EVALUATE_TRACE_POST_SUCCESS(1, LogicalWhenType, "/patternProperties", + "#/patternProperties", ""); + EVALUATE_TRACE_POST_SUCCESS(2, AnnotationLoopPropertiesUnevaluated, + "/unevaluatedProperties", + "#/unevaluatedProperties", ""); + + EVALUATE_TRACE_POST_DESCRIBE( + instance, 0, + "The object property \"@foo\" successfully validated against its pattern " + "property subschema"); + EVALUATE_TRACE_POST_DESCRIBE( + instance, 1, + "The object value was expected to validate against the single defined " + "pattern property subschema"); + EVALUATE_TRACE_POST_DESCRIBE( + instance, 2, + "The object value was not expected to define unevaluated properties"); +} + TEST(Evaluator_2019_09, unevaluatedItems_1) { const sourcemeta::jsontoolkit::JSON schema{ sourcemeta::jsontoolkit::parse(R"JSON({