diff --git a/src/compiler/compile_helpers.h b/src/compiler/compile_helpers.h index a36190e9e..a72e2e0b9 100644 --- a/src/compiler/compile_helpers.h +++ b/src/compiler/compile_helpers.h @@ -336,6 +336,13 @@ inline auto requires_evaluation(const Context &context, return requires_evaluation(context, entry.pointer); } +inline auto annotations_enabled(const Context &context, + const std::string_view keyword) -> bool { + return context.mode == Mode::Exhaustive && + (!context.tweaks.annotations.has_value() || + context.tweaks.annotations.value().contains(keyword)); +} + // TODO: Elevate to Core and test inline auto diff --git a/src/compiler/default_compiler.cc b/src/compiler/default_compiler.cc index 5fe917fec..192b6a174 100644 --- a/src/compiler/default_compiler.cc +++ b/src/compiler/default_compiler.cc @@ -626,7 +626,7 @@ auto sourcemeta::blaze::default_schema_compiler( return {}; } - if (context.mode == Mode::FastValidation || + if (!annotations_enabled(context, dynamic_context.keyword) || schema_context.is_property_name) { return {}; } diff --git a/src/compiler/default_compiler_2019_09.h b/src/compiler/default_compiler_2019_09.h index 133ece3cd..2e53084a7 100644 --- a/src/compiler/default_compiler_2019_09.h +++ b/src/compiler/default_compiler_2019_09.h @@ -158,7 +158,7 @@ auto compiler_2019_09_applicator_contains_with_options( sourcemeta::core::empty_weak_pointer, sourcemeta::core::empty_weak_pointer)}; - if (annotate) { + if (annotate && annotations_enabled(context, dynamic_context.keyword)) { children.push_back( make(sourcemeta::blaze::InstructionIndex::AnnotationBasenameToParent, context, schema_context, relative_dynamic_context(), ValueNone{})); @@ -282,7 +282,7 @@ auto compiler_2019_09_applicator_unevaluateditems( sourcemeta::core::empty_weak_pointer, sourcemeta::core::empty_weak_pointer)}; - if (context.mode == Mode::Exhaustive) { + if (annotations_enabled(context, dynamic_context.keyword)) { children.push_back( make(sourcemeta::blaze::InstructionIndex::AnnotationToParent, context, schema_context, relative_dynamic_context(), @@ -322,7 +322,7 @@ auto compiler_2019_09_applicator_unevaluatedproperties( sourcemeta::core::empty_weak_pointer, sourcemeta::core::empty_weak_pointer)}; - if (context.mode == Mode::Exhaustive) { + if (annotations_enabled(context, dynamic_context.keyword)) { children.push_back( make(sourcemeta::blaze::InstructionIndex::AnnotationBasenameToParent, context, schema_context, relative_dynamic_context(), ValueNone{})); @@ -451,7 +451,7 @@ auto compiler_2019_09_content_contentencoding( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (context.mode == Mode::FastValidation) { + if (!annotations_enabled(context, dynamic_context.keyword)) { return {}; } @@ -470,7 +470,7 @@ auto compiler_2019_09_content_contentmediatype( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (context.mode == Mode::FastValidation) { + if (!annotations_enabled(context, dynamic_context.keyword)) { return {}; } @@ -489,7 +489,7 @@ auto compiler_2019_09_content_contentschema( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (context.mode == Mode::FastValidation) { + if (!annotations_enabled(context, dynamic_context.keyword)) { return {}; } diff --git a/src/compiler/default_compiler_draft3.h b/src/compiler/default_compiler_draft3.h index 2ed75e42a..fbe5e3c1d 100644 --- a/src/compiler/default_compiler_draft3.h +++ b/src/compiler/default_compiler_draft3.h @@ -546,6 +546,9 @@ auto compiler_draft3_applicator_properties_with_options( return {}; } + const bool emit_annotation{ + annotate && annotations_enabled(context, dynamic_context.keyword)}; + if (properties_as_loop(context, schema_context, schema_context.schema.at(dynamic_context.keyword))) { ValueNamedIndexes indexes; @@ -562,7 +565,7 @@ auto compiler_draft3_applicator_properties_with_options( schema_context, relative_dynamic_context(), ValuePointer{name})); } - if (annotate) { + if (emit_annotation) { substeps.push_back( make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, schema_context, relative_dynamic_context(), @@ -715,7 +718,7 @@ auto compiler_draft3_applicator_properties_with_options( bool fusion_possible{attempt_object_fusion}; for (auto &&[name, substeps] : properties) { - if (annotate) { + if (emit_annotation) { substeps.push_back( make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, schema_context, effective_dynamic_context, @@ -1016,7 +1019,7 @@ auto compiler_draft3_applicator_patternproperties_with_options( auto substeps{compile(context, schema_context, relative_dynamic_context(), sourcemeta::blaze::make_weak_pointer(pattern))}; - if (annotate) { + if (annotate && annotations_enabled(context, dynamic_context.keyword)) { substeps.push_back(make( sourcemeta::blaze::InstructionIndex::AnnotationBasenameToParent, context, schema_context, relative_dynamic_context(), ValueNone{})); @@ -1141,7 +1144,7 @@ auto compiler_draft3_applicator_additionalproperties_with_options( sourcemeta::core::empty_weak_pointer, sourcemeta::core::empty_weak_pointer)}; - if (annotate) { + if (annotate && annotations_enabled(context, dynamic_context.keyword)) { children.push_back( make(sourcemeta::blaze::InstructionIndex::AnnotationBasenameToParent, context, schema_context, relative_dynamic_context(), ValueNone{})); @@ -1316,6 +1319,9 @@ auto compiler_draft3_applicator_items_array( return {}; } + const bool emit_annotation{ + annotate && annotations_enabled(context, dynamic_context.keyword)}; + // Precompile subschemas std::vector subschemas; subschemas.reserve(items_size); @@ -1336,7 +1342,7 @@ auto compiler_draft3_applicator_items_array( } } - if (annotate) { + if (emit_annotation) { subchildren.push_back( make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, schema_context, relative_dynamic_context(), @@ -1355,7 +1361,7 @@ auto compiler_draft3_applicator_items_array( } } - if (annotate) { + if (emit_annotation) { tail.push_back(make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, schema_context, relative_dynamic_context(), sourcemeta::core::JSON{children.size() - 1})); @@ -1433,8 +1439,11 @@ auto compiler_draft3_applicator_items_with_options( return {}; } + const bool emit_annotation{ + annotate && annotations_enabled(context, dynamic_context.keyword)}; + if (is_schema(schema_context.schema.at(dynamic_context.keyword))) { - if (annotate || track_evaluation) { + if (emit_annotation || track_evaluation) { Instructions subchildren{compile(context, schema_context, relative_dynamic_context(), sourcemeta::core::empty_weak_pointer, @@ -1448,13 +1457,13 @@ auto compiler_draft3_applicator_items_with_options( ValueNone{}, std::move(subchildren))); } - if (!annotate && !track_evaluation) { + if (!emit_annotation && !track_evaluation) { return children; } Instructions tail; - if (annotate) { + if (emit_annotation) { tail.push_back(make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, schema_context, relative_dynamic_context(), sourcemeta::core::JSON{true})); @@ -1562,6 +1571,9 @@ auto compiler_draft3_applicator_additionalitems_from_cursor( return {}; } + const bool emit_annotation{ + annotate && annotations_enabled(context, dynamic_context.keyword)}; + Instructions subchildren{compile(context, schema_context, relative_dynamic_context(), sourcemeta::core::empty_weak_pointer, @@ -1586,13 +1598,13 @@ auto compiler_draft3_applicator_additionalitems_from_cursor( } // Avoid one extra wrapper instruction if possible - if (!annotate && !track_evaluation) { + if (!emit_annotation && !track_evaluation) { return children; } Instructions tail; - if (annotate) { + if (emit_annotation) { tail.push_back(make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, schema_context, relative_dynamic_context(), sourcemeta::core::JSON{true})); @@ -2577,7 +2589,7 @@ auto compiler_draft3_validation_format(const Context &context, make(sourcemeta::blaze::InstructionIndex::AssertionStringType, context, schema_context, dynamic_context, type)}; - if (context.mode == Mode::Exhaustive) { + if (annotations_enabled(context, dynamic_context.keyword)) { Instructions annotation_children{ make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, schema_context, dynamic_context, @@ -2593,7 +2605,7 @@ auto compiler_draft3_validation_format(const Context &context, } if (is_2019_09_format || is_2020_12_format_annotation) { - if (context.mode == Mode::FastValidation) { + if (!annotations_enabled(context, dynamic_context.keyword)) { return {}; } diff --git a/src/compiler/include/sourcemeta/blaze/compiler.h b/src/compiler/include/sourcemeta/blaze/compiler.h index f823bc30d..8fcf6c897 100644 --- a/src/compiler/include/sourcemeta/blaze/compiler.h +++ b/src/compiler/include/sourcemeta/blaze/compiler.h @@ -25,6 +25,7 @@ #include // std::string_view #include // std::tuple #include // std::unordered_map +#include // std::unordered_set #include // std::vector /// @defgroup compiler Compiler @@ -98,6 +99,10 @@ struct Tweaks { std::size_t target_inline_threshold{50}; /// When set, force `format` to be compiled as an assertion bool format_assertion{false}; + /// Select which keywords emit annotations in exhaustive mode. When not set, + /// every annotation keyword is emitted + std::optional> + annotations{}; }; /// @ingroup compiler diff --git a/test/evaluator/evaluator_2020_12_test.cc b/test/evaluator/evaluator_2020_12_test.cc index f9ee9ce27..0bc252f68 100644 --- a/test/evaluator/evaluator_2020_12_test.cc +++ b/test/evaluator/evaluator_2020_12_test.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -307,6 +308,230 @@ TEST(Evaluator_2020_12, prop_type_integer_bounded_6_exhaustive) { "against the single defined property subschema"); } +TEST(Evaluator_2020_12, + prop_type_integer_bounded_6_exhaustive_annotations_none) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "x": { "type": "integer", "minimum": 0, "maximum": 100 } + } + })JSON")}; + + const sourcemeta::core::JSON instance{ + sourcemeta::core::parse_json(R"JSON({ "x": 3.0 })JSON")}; + + sourcemeta::blaze::Tweaks tweaks; + tweaks.annotations.emplace(); + + EVALUATE_WITH_TRACE_EXHAUSTIVE_SUCCESS_TWEAKED(schema, instance, 4, "", + tweaks); + + EVALUATE_TRACE_PRE(0, LogicalWhenType, "/properties", "#/properties", ""); + EVALUATE_TRACE_PRE(1, AssertionType, "/properties/x/type", + "#/properties/x/type", "/x"); + EVALUATE_TRACE_PRE(2, AssertionLessEqual, "/properties/x/maximum", + "#/properties/x/maximum", "/x"); + EVALUATE_TRACE_PRE(3, AssertionGreaterEqual, "/properties/x/minimum", + "#/properties/x/minimum", "/x"); + + EVALUATE_TRACE_POST_SUCCESS(0, AssertionType, "/properties/x/type", + "#/properties/x/type", "/x"); + EVALUATE_TRACE_POST_SUCCESS(1, AssertionLessEqual, "/properties/x/maximum", + "#/properties/x/maximum", "/x"); + EVALUATE_TRACE_POST_SUCCESS(2, AssertionGreaterEqual, "/properties/x/minimum", + "#/properties/x/minimum", "/x"); + EVALUATE_TRACE_POST_SUCCESS(3, LogicalWhenType, "/properties", "#/properties", + ""); + + EVALUATE_TRACE_POST_DESCRIBE(instance, 0, + "The value was expected to be of type integer"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 1, + "The number value 3.0 was expected to be less " + "than or equal to the integer 100"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 2, + "The number value 3.0 was expected to be " + "greater than or equal to the integer 0"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 3, + "The object value was expected to validate " + "against the single defined property subschema"); +} + +TEST(Evaluator_2020_12, + prop_type_integer_bounded_6_exhaustive_annotations_selected) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "x": { "type": "integer", "minimum": 0, "maximum": 100 } + } + })JSON")}; + + const sourcemeta::core::JSON instance{ + sourcemeta::core::parse_json(R"JSON({ "x": 3.0 })JSON")}; + + sourcemeta::blaze::Tweaks tweaks; + tweaks.annotations = + std::unordered_set{"properties"}; + + EVALUATE_WITH_TRACE_EXHAUSTIVE_SUCCESS_TWEAKED(schema, instance, 5, "", + tweaks); + + EVALUATE_TRACE_PRE(0, LogicalWhenType, "/properties", "#/properties", ""); + EVALUATE_TRACE_PRE(1, AssertionType, "/properties/x/type", + "#/properties/x/type", "/x"); + EVALUATE_TRACE_PRE(2, AssertionLessEqual, "/properties/x/maximum", + "#/properties/x/maximum", "/x"); + EVALUATE_TRACE_PRE(3, AssertionGreaterEqual, "/properties/x/minimum", + "#/properties/x/minimum", "/x"); + EVALUATE_TRACE_PRE_ANNOTATION(4, "/properties", "#/properties", ""); + + EVALUATE_TRACE_POST_SUCCESS(0, AssertionType, "/properties/x/type", + "#/properties/x/type", "/x"); + EVALUATE_TRACE_POST_SUCCESS(1, AssertionLessEqual, "/properties/x/maximum", + "#/properties/x/maximum", "/x"); + EVALUATE_TRACE_POST_SUCCESS(2, AssertionGreaterEqual, "/properties/x/minimum", + "#/properties/x/minimum", "/x"); + EVALUATE_TRACE_POST_ANNOTATION(3, "/properties", "#/properties", "", "x"); + EVALUATE_TRACE_POST_SUCCESS(4, LogicalWhenType, "/properties", "#/properties", + ""); + + EVALUATE_TRACE_POST_DESCRIBE(instance, 0, + "The value was expected to be of type integer"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 1, + "The number value 3.0 was expected to be less " + "than or equal to the integer 100"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 2, + "The number value 3.0 was expected to be " + "greater than or equal to the integer 0"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 3, + "The object property \"x\" successfully " + "validated against its property subschema"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 4, + "The object value was expected to validate " + "against the single defined property subschema"); +} + +TEST(Evaluator_2020_12, annotation_contains_nested_not_short_circuited) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { "title": "x", "type": "string" } + })JSON")}; + + const sourcemeta::core::JSON instance{ + sourcemeta::core::parse_json(R"JSON([ "a", "b" ])JSON")}; + + sourcemeta::blaze::Tweaks tweaks; + tweaks.annotations = + std::unordered_set{"title"}; + + EVALUATE_WITH_TRACE_EXHAUSTIVE_SUCCESS_TWEAKED(schema, instance, 5, "", + tweaks); + + EVALUATE_TRACE_PRE(0, LoopContains, "/contains", "#/contains", ""); + EVALUATE_TRACE_PRE_ANNOTATION(1, "/contains/title", "#/contains/title", "/0"); + EVALUATE_TRACE_PRE(2, AssertionTypeStrict, "/contains/type", + "#/contains/type", "/0"); + EVALUATE_TRACE_PRE_ANNOTATION(3, "/contains/title", "#/contains/title", "/1"); + EVALUATE_TRACE_PRE(4, AssertionTypeStrict, "/contains/type", + "#/contains/type", "/1"); + + EVALUATE_TRACE_POST_ANNOTATION(0, "/contains/title", "#/contains/title", "/0", + "x"); + EVALUATE_TRACE_POST_SUCCESS(1, AssertionTypeStrict, "/contains/type", + "#/contains/type", "/0"); + EVALUATE_TRACE_POST_ANNOTATION(2, "/contains/title", "#/contains/title", "/1", + "x"); + EVALUATE_TRACE_POST_SUCCESS(3, AssertionTypeStrict, "/contains/type", + "#/contains/type", "/1"); + EVALUATE_TRACE_POST_SUCCESS(4, LoopContains, "/contains", "#/contains", ""); + + EVALUATE_TRACE_POST_DESCRIBE( + instance, 0, "The title of the instance location \"/0\" was \"x\""); + EVALUATE_TRACE_POST_DESCRIBE(instance, 1, + "The value was expected to be of type string"); + EVALUATE_TRACE_POST_DESCRIBE( + instance, 2, "The title of the instance location \"/1\" was \"x\""); + EVALUATE_TRACE_POST_DESCRIBE(instance, 3, + "The value was expected to be of type string"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 4, + "The array value was expected to contain at " + "least 1 item that validates against the given " + "subschema"); +} + +TEST(Evaluator_2020_12, annotation_custom_keyword_selected) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "My title", + "x-test-custom": "hello" + })JSON")}; + + const sourcemeta::core::JSON instance{"foo"}; + + sourcemeta::blaze::Tweaks tweaks; + tweaks.annotations = + std::unordered_set{"x-test-custom"}; + + EVALUATE_WITH_TRACE_EXHAUSTIVE_SUCCESS_TWEAKED(schema, instance, 1, "", + tweaks); + + EVALUATE_TRACE_PRE_ANNOTATION(0, "/x-test-custom", "#/x-test-custom", ""); + EVALUATE_TRACE_POST_ANNOTATION(0, "/x-test-custom", "#/x-test-custom", "", + "hello"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 0, + "The unrecognized keyword \"x-test-custom\" was " + "collected as the annotation \"hello\""); +} + +TEST(Evaluator_2020_12, annotation_fast_mode_ignores_tweak) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "title": "My title" + })JSON")}; + + const sourcemeta::core::JSON instance{"foo"}; + + sourcemeta::blaze::Tweaks tweaks; + tweaks.annotations = + std::unordered_set{"title"}; + + EVALUATE_WITH_TRACE_FAST_SUCCESS_TWEAKED(schema, instance, 1, "", tweaks); + + EVALUATE_TRACE_PRE(0, AssertionTypeStrict, "/type", "#/type", ""); + EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict, "/type", "#/type", ""); + EVALUATE_TRACE_POST_DESCRIBE(instance, 0, + "The value was expected to be of type string"); +} + +TEST(Evaluator_2020_12, unevaluated_properties_annotations_none_still_tracks) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { "a": { "type": "integer" } }, + "unevaluatedProperties": false + })JSON")}; + + const sourcemeta::core::JSON instance{ + sourcemeta::core::parse_json(R"JSON({ "a": 1, "b": 2 })JSON")}; + + sourcemeta::blaze::Tweaks tweaks; + tweaks.annotations.emplace(); + + const auto compiled_schema{sourcemeta::blaze::compile( + schema, sourcemeta::blaze::schema_walker, + sourcemeta::blaze::schema_resolver, + sourcemeta::blaze::default_schema_compiler, + sourcemeta::blaze::Mode::Exhaustive, "", "", "", tweaks)}; + + // Even with all annotations filtered out, evaluation tracking still marks "a" + // as evaluated, so the unevaluated "b" is rejected + sourcemeta::blaze::Evaluator evaluator; + EXPECT_FALSE(evaluator.validate(compiled_schema, instance)); + + const sourcemeta::core::JSON evaluated{ + sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; + EXPECT_TRUE(evaluator.validate(compiled_schema, evaluated)); +} + TEST(Evaluator_2020_12, prop_type_integer_lower_bound_4) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", diff --git a/test/evaluator/evaluator_draft7_test.cc b/test/evaluator/evaluator_draft7_test.cc index 02cfa67b9..4fd041650 100644 --- a/test/evaluator/evaluator_draft7_test.cc +++ b/test/evaluator/evaluator_draft7_test.cc @@ -2993,3 +2993,50 @@ TEST(Evaluator_draft7, x_assertion_true_without_format_no_tweak_fast) { EVALUATE_WITH_TRACE_FAST_SUCCESS(schema, instance, 0, ""); } + +TEST(Evaluator_draft7, additionalitems_annotations_none_no_empty_wrapper) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "items": [ { "type": "string" } ], + "additionalItems": { "type": "number" } + })JSON")}; + + const sourcemeta::core::JSON instance{ + sourcemeta::core::parse_json(R"JSON([ "a", 1 ])JSON")}; + + sourcemeta::blaze::Tweaks tweaks; + tweaks.annotations.emplace(); + + EVALUATE_WITH_TRACE_EXHAUSTIVE_SUCCESS_TWEAKED(schema, instance, 4, "", + tweaks); + + EVALUATE_TRACE_PRE(0, AssertionArrayPrefix, "/items", "#/items", ""); + EVALUATE_TRACE_PRE(1, AssertionTypeStrict, "/items/0/type", "#/items/0/type", + "/0"); + EVALUATE_TRACE_PRE(2, LoopItemsFrom, "/additionalItems", "#/additionalItems", + ""); + EVALUATE_TRACE_PRE(3, AssertionTypeStrictAny, "/additionalItems/type", + "#/additionalItems/type", "/1"); + + EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict, "/items/0/type", + "#/items/0/type", "/0"); + EVALUATE_TRACE_POST_SUCCESS(1, AssertionArrayPrefix, "/items", "#/items", ""); + EVALUATE_TRACE_POST_SUCCESS(2, AssertionTypeStrictAny, + "/additionalItems/type", "#/additionalItems/type", + "/1"); + EVALUATE_TRACE_POST_SUCCESS(3, LoopItemsFrom, "/additionalItems", + "#/additionalItems", ""); + + EVALUATE_TRACE_POST_DESCRIBE(instance, 0, + "The value was expected to be of type string"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 1, + "The first item of the array value was expected " + "to validate against the corresponding " + "subschemas"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 2, + "The value was expected to be of type number"); + EVALUATE_TRACE_POST_DESCRIBE(instance, 3, + "Every item in the array value except for the " + "first one was expected to validate against the " + "given subschema"); +}