Skip to content

Commit 02a246f

Browse files
authored
Collect and expose annotations in SimpleOutput (#402)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 20cc03f commit 02a246f

9 files changed

Lines changed: 285 additions & 16 deletions

File tree

DEPENDENCIES

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
vendorpull https://github.com/sourcemeta/vendorpull dea311b5bfb53b6926a4140267959ae334d3ecf4
2-
core https://github.com/sourcemeta/core eb96c64fdf4658db1a75a4c51884a2e9b797f05f
2+
core https://github.com/sourcemeta/core 89c1394849a488667f03ec5ee1844326570f486d
33
jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite 4ba013d58e747ecaf48c8bb7cf248cb0d564afbc

src/compiler/compile_output_simple.cc

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ auto SimpleOutput::operator()(
3737
}
3838

3939
assert(evaluate_path.back().is_property());
40+
auto effective_evaluate_path{evaluate_path.resolve_from(this->base_)};
41+
if (effective_evaluate_path.empty()) {
42+
return;
43+
}
44+
45+
if (is_annotation(step.type)) {
46+
if (type == EvaluationType::Post) {
47+
Location location{instance_location, std::move(effective_evaluate_path)};
48+
this->annotations_[std::move(location)].push_back(annotation);
49+
}
50+
51+
return;
52+
}
4053

4154
if (type == EvaluationType::Pre) {
4255
assert(result);
@@ -49,6 +62,16 @@ auto SimpleOutput::operator()(
4962
} else if (type == EvaluationType::Post &&
5063
this->mask.contains(evaluate_path)) {
5164
this->mask.erase(evaluate_path);
65+
} else if (type == EvaluationType::Post && !result &&
66+
!this->annotations_.empty()) {
67+
for (auto iterator = this->annotations_.begin();
68+
iterator != this->annotations_.end();) {
69+
if (iterator->first.evaluate_path.starts_with_initial(evaluate_path)) {
70+
iterator = this->annotations_.erase(iterator);
71+
} else {
72+
iterator++;
73+
}
74+
}
5275
}
5376

5477
// Ignore successful or masked steps
@@ -59,11 +82,6 @@ auto SimpleOutput::operator()(
5982
return;
6083
}
6184

62-
auto effective_evaluate_path{evaluate_path.resolve_from(this->base_)};
63-
if (effective_evaluate_path.empty()) {
64-
return;
65-
}
66-
6785
this->output.push_back(
6886
{describe(result, step, evaluate_path, instance_location, this->instance_,
6987
annotation),

src/compiler/include/sourcemeta/blaze/compiler_output.h

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010

1111
#include <sourcemeta/blaze/evaluator.h>
1212

13+
#include <map> // std::map
1314
#include <set> // std::set
1415
#include <string> // std::string
1516
#include <string_view> // std::string_view
17+
#include <tuple> // std::tie
1618
#include <vector> // std::vector
1719

1820
namespace sourcemeta::blaze {
@@ -89,6 +91,21 @@ class SOURCEMETA_BLAZE_COMPILER_EXPORT SimpleOutput {
8991
auto cbegin() const -> const_iterator;
9092
auto cend() const -> const_iterator;
9193

94+
/// Access annotations that were collected during evaluation, indexed by
95+
/// instance location and evaluation path
96+
auto annotations() const -> const auto & { return this->annotations_; }
97+
98+
struct Location {
99+
auto operator<(const Location &other) const noexcept -> bool {
100+
// Perform a lexicographical comparison
101+
return std::tie(instance_location, evaluate_path) <
102+
std::tie(other.instance_location, other.evaluate_path);
103+
}
104+
105+
const sourcemeta::core::WeakPointer instance_location;
106+
const sourcemeta::core::WeakPointer evaluate_path;
107+
};
108+
92109
private:
93110
// Exporting symbols that depends on the standard C++ library is considered
94111
// safe.
@@ -100,10 +117,11 @@ class SOURCEMETA_BLAZE_COMPILER_EXPORT SimpleOutput {
100117
const sourcemeta::core::WeakPointer base_;
101118
container_type output;
102119
std::set<sourcemeta::core::WeakPointer> mask;
120+
std::map<Location, std::vector<sourcemeta::core::JSON>> annotations_;
103121
#if defined(_MSC_VER)
104122
#pragma warning(default : 4251)
105123
#endif
106-
};
124+
}; // namespace sourcemeta::blaze
107125

108126
/// @ingroup compiler
109127
///

test/compiler/compiler_output_simple_test.cc

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,41 @@
1515
EXPECT_EQ(sourcemeta::core::to_string(traces.at((index)).evaluate_path), \
1616
expected_evaluate_path);
1717

18+
#define EXPECT_ANNOTATION_COUNT(output, expected_count) \
19+
EXPECT_EQ(output.annotations().size(), (expected_count));
20+
21+
#define EXPECT_ANNOTATION_ENTRY(output, expected_instance_location, \
22+
expected_evaluate_path, expected_entry_count) \
23+
{ \
24+
const auto instance_location{ \
25+
sourcemeta::core::to_pointer(expected_instance_location)}; \
26+
const auto evaluate_path{ \
27+
sourcemeta::core::to_pointer(expected_evaluate_path)}; \
28+
EXPECT_TRUE(output.annotations().contains( \
29+
{sourcemeta::core::to_weak_pointer(instance_location), \
30+
sourcemeta::core::to_weak_pointer(evaluate_path)})); \
31+
EXPECT_EQ(output.annotations() \
32+
.at({sourcemeta::core::to_weak_pointer(instance_location), \
33+
sourcemeta::core::to_weak_pointer(evaluate_path)}) \
34+
.size(), \
35+
(expected_entry_count)); \
36+
}
37+
38+
#define EXPECT_ANNOTATION_VALUE(output, expected_instance_location, \
39+
expected_evaluate_path, expected_entry_index, \
40+
expected_value) \
41+
{ \
42+
const auto instance_location{ \
43+
sourcemeta::core::to_pointer(expected_instance_location)}; \
44+
const auto evaluate_path{ \
45+
sourcemeta::core::to_pointer(expected_evaluate_path)}; \
46+
EXPECT_EQ(output.annotations() \
47+
.at({sourcemeta::core::to_weak_pointer(instance_location), \
48+
sourcemeta::core::to_weak_pointer(evaluate_path)}) \
49+
.at(expected_entry_index), \
50+
(expected_value)); \
51+
}
52+
1853
TEST(Compiler_output_simple, success_string_1) {
1954
const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({
2055
"$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -37,6 +72,8 @@ TEST(Compiler_output_simple, success_string_1) {
3772
std::vector<sourcemeta::blaze::SimpleOutput::Entry> traces{output.cbegin(),
3873
output.cend()};
3974
EXPECT_TRUE(traces.empty());
75+
76+
EXPECT_ANNOTATION_COUNT(output, 0);
4077
}
4178

4279
TEST(Compiler_output_simple, fail_meaningless_if_1) {
@@ -82,6 +119,8 @@ TEST(Compiler_output_simple, fail_meaningless_if_1) {
82119
EXPECT_OUTPUT(
83120
traces, 1, "/foo", "/properties/foo/unevaluatedProperties",
84121
"The object value was not expected to define unevaluated properties");
122+
123+
EXPECT_ANNOTATION_COUNT(output, 0);
85124
}
86125

87126
TEST(Compiler_output_simple, success_dynamic_anchor_1) {
@@ -112,6 +151,8 @@ TEST(Compiler_output_simple, success_dynamic_anchor_1) {
112151
std::vector<sourcemeta::blaze::SimpleOutput::Entry> traces{output.cbegin(),
113152
output.cend()};
114153
EXPECT_TRUE(traces.empty());
154+
155+
EXPECT_ANNOTATION_COUNT(output, 0);
115156
}
116157

117158
TEST(Compiler_output_simple, success_oneof_1) {
@@ -139,6 +180,8 @@ TEST(Compiler_output_simple, success_oneof_1) {
139180
std::vector<sourcemeta::blaze::SimpleOutput::Entry> traces{output.cbegin(),
140181
output.cend()};
141182
EXPECT_TRUE(traces.empty());
183+
184+
EXPECT_ANNOTATION_COUNT(output, 0);
142185
}
143186

144187
TEST(Compiler_output_simple, fail_string) {
@@ -167,6 +210,8 @@ TEST(Compiler_output_simple, fail_string) {
167210
EXPECT_OUTPUT(
168211
traces, 0, "", "/type",
169212
"The value was expected to be of type string but it was of type integer");
213+
214+
EXPECT_ANNOTATION_COUNT(output, 0);
170215
}
171216

172217
TEST(Compiler_output_simple, fail_string_over_ref) {
@@ -200,6 +245,7 @@ TEST(Compiler_output_simple, fail_string_over_ref) {
200245
EXPECT_OUTPUT(
201246
traces, 0, "", "/$ref/type",
202247
"The value was expected to be of type string but it was of type integer");
248+
EXPECT_ANNOTATION_COUNT(output, 0);
203249
}
204250

205251
TEST(Compiler_output_simple, fail_string_with_matching_base) {
@@ -235,6 +281,7 @@ TEST(Compiler_output_simple, fail_string_with_matching_base) {
235281
EXPECT_OUTPUT(
236282
traces, 0, "", "/type",
237283
"The value was expected to be of type string but it was of type integer");
284+
EXPECT_ANNOTATION_COUNT(output, 0);
238285
}
239286

240287
TEST(Compiler_output_simple, fail_string_with_non_matching_base) {
@@ -269,6 +316,7 @@ TEST(Compiler_output_simple, fail_string_with_non_matching_base) {
269316
EXPECT_OUTPUT(
270317
traces, 0, "", "/$ref/type",
271318
"The value was expected to be of type string but it was of type integer");
319+
EXPECT_ANNOTATION_COUNT(output, 0);
272320
}
273321

274322
TEST(Compiler_output_simple, fail_oneof_1) {
@@ -300,6 +348,7 @@ TEST(Compiler_output_simple, fail_oneof_1) {
300348
EXPECT_OUTPUT(traces, 0, "", "/oneOf",
301349
"The string value was expected to validate against one and "
302350
"only one of the 2 given subschemas");
351+
EXPECT_ANNOTATION_COUNT(output, 0);
303352
}
304353

305354
TEST(Compiler_output_simple, fail_not_1) {
@@ -330,6 +379,7 @@ TEST(Compiler_output_simple, fail_not_1) {
330379
EXPECT_OUTPUT(traces, 0, "", "/not",
331380
"The string value was expected to not validate against the "
332381
"given subschema, but it did");
382+
EXPECT_ANNOTATION_COUNT(output, 0);
333383
}
334384

335385
TEST(Compiler_output_simple, fail_not_not_1) {
@@ -362,6 +412,7 @@ TEST(Compiler_output_simple, fail_not_not_1) {
362412
EXPECT_OUTPUT(traces, 0, "", "/not",
363413
"The integer value was expected to not validate against the "
364414
"given subschema, but it did");
415+
EXPECT_ANNOTATION_COUNT(output, 0);
365416
}
366417

367418
TEST(Compiler_output_simple, fail_anyof_1) {
@@ -400,6 +451,7 @@ TEST(Compiler_output_simple, fail_anyof_1) {
400451
EXPECT_OUTPUT(
401452
traces, 0, "", "/allOf/1/type",
402453
"The value was expected to be of type integer but it was of type object");
454+
EXPECT_ANNOTATION_COUNT(output, 0);
403455
}
404456

405457
TEST(Compiler_output_simple, fail_anyof_2) {
@@ -433,4 +485,150 @@ TEST(Compiler_output_simple, fail_anyof_2) {
433485
EXPECT_OUTPUT(traces, 0, "", "/anyOf",
434486
"The object value was expected to validate against at least "
435487
"one of the 2 given subschemas");
488+
EXPECT_ANNOTATION_COUNT(output, 0);
489+
}
490+
491+
TEST(Compiler_output_simple, annotations_success_1) {
492+
const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({
493+
"$schema": "https://json-schema.org/draft/2020-12/schema",
494+
"title": "My Schema",
495+
"properties": {
496+
"foo": {
497+
"title": "Foo",
498+
"deprecated": true,
499+
"$ref": "#/$defs/test"
500+
},
501+
"bar": true
502+
},
503+
"$defs": {
504+
"test": {
505+
"title": "Test"
506+
}
507+
}
508+
})JSON")};
509+
510+
const auto schema_template{sourcemeta::blaze::compile(
511+
schema, sourcemeta::core::schema_official_walker,
512+
sourcemeta::core::schema_official_resolver,
513+
sourcemeta::blaze::default_schema_compiler,
514+
sourcemeta::blaze::Mode::Exhaustive)};
515+
516+
const auto instance{sourcemeta::core::parse_json(R"JSON({
517+
"foo": "bar",
518+
"bar": "baz"
519+
})JSON")};
520+
521+
sourcemeta::blaze::SimpleOutput output{instance};
522+
sourcemeta::blaze::Evaluator evaluator;
523+
const auto result{
524+
evaluator.validate(schema_template, instance, std::ref(output))};
525+
EXPECT_TRUE(result);
526+
527+
EXPECT_ANNOTATION_COUNT(output, 5);
528+
529+
EXPECT_ANNOTATION_ENTRY(output, "", "/properties", 2);
530+
EXPECT_ANNOTATION_VALUE(output, "", "/properties", 0,
531+
sourcemeta::core::JSON{"bar"});
532+
EXPECT_ANNOTATION_VALUE(output, "", "/properties", 1,
533+
sourcemeta::core::JSON{"foo"});
534+
535+
EXPECT_ANNOTATION_ENTRY(output, "", "/title", 1);
536+
EXPECT_ANNOTATION_VALUE(output, "", "/title", 0,
537+
sourcemeta::core::JSON{"My Schema"});
538+
539+
EXPECT_ANNOTATION_ENTRY(output, "/foo", "/properties/foo/$ref/title", 1);
540+
EXPECT_ANNOTATION_VALUE(output, "/foo", "/properties/foo/$ref/title", 0,
541+
sourcemeta::core::JSON{"Test"});
542+
543+
EXPECT_ANNOTATION_ENTRY(output, "/foo", "/properties/foo/title", 1);
544+
EXPECT_ANNOTATION_VALUE(output, "/foo", "/properties/foo/title", 0,
545+
sourcemeta::core::JSON{"Foo"});
546+
547+
EXPECT_ANNOTATION_ENTRY(output, "/foo", "/properties/foo/deprecated", 1);
548+
EXPECT_ANNOTATION_VALUE(output, "/foo", "/properties/foo/deprecated", 0,
549+
sourcemeta::core::JSON{true});
550+
}
551+
552+
TEST(Compiler_output_simple, annotations_success_2) {
553+
const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({
554+
"$schema": "https://json-schema.org/draft/2020-12/schema",
555+
"anyOf": [
556+
{ "title": "First branch" },
557+
{ "type": "string", "title": "Second branch" },
558+
{ "title": "Third branch" }
559+
]
560+
})JSON")};
561+
562+
const auto schema_template{sourcemeta::blaze::compile(
563+
schema, sourcemeta::core::schema_official_walker,
564+
sourcemeta::core::schema_official_resolver,
565+
sourcemeta::blaze::default_schema_compiler,
566+
sourcemeta::blaze::Mode::Exhaustive)};
567+
568+
const sourcemeta::core::JSON instance{5};
569+
570+
sourcemeta::blaze::SimpleOutput output{instance};
571+
sourcemeta::blaze::Evaluator evaluator;
572+
const auto result{
573+
evaluator.validate(schema_template, instance, std::ref(output))};
574+
EXPECT_TRUE(result);
575+
576+
EXPECT_ANNOTATION_COUNT(output, 2);
577+
578+
EXPECT_ANNOTATION_ENTRY(output, "", "/anyOf/0/title", 1);
579+
EXPECT_ANNOTATION_VALUE(output, "", "/anyOf/0/title", 0,
580+
sourcemeta::core::JSON{"First branch"});
581+
582+
EXPECT_ANNOTATION_ENTRY(output, "", "/anyOf/2/title", 1);
583+
EXPECT_ANNOTATION_VALUE(output, "", "/anyOf/2/title", 0,
584+
sourcemeta::core::JSON{"Third branch"});
585+
}
586+
587+
TEST(Compiler_output_simple, annotations_success_3) {
588+
const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({
589+
"$schema": "https://json-schema.org/draft/2020-12/schema",
590+
"not": {
591+
"title": "First branch",
592+
"type": "string"
593+
}
594+
})JSON")};
595+
596+
const auto schema_template{sourcemeta::blaze::compile(
597+
schema, sourcemeta::core::schema_official_walker,
598+
sourcemeta::core::schema_official_resolver,
599+
sourcemeta::blaze::default_schema_compiler,
600+
sourcemeta::blaze::Mode::Exhaustive)};
601+
602+
const sourcemeta::core::JSON instance{5};
603+
604+
sourcemeta::blaze::SimpleOutput output{instance};
605+
sourcemeta::blaze::Evaluator evaluator;
606+
const auto result{
607+
evaluator.validate(schema_template, instance, std::ref(output))};
608+
EXPECT_TRUE(result);
609+
610+
EXPECT_ANNOTATION_COUNT(output, 0);
611+
}
612+
613+
TEST(Compiler_output_simple, annotations_failure_1) {
614+
const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({
615+
"$schema": "https://json-schema.org/draft/2020-12/schema",
616+
"title": "Foo",
617+
"type": "string"
618+
})JSON")};
619+
620+
const auto schema_template{sourcemeta::blaze::compile(
621+
schema, sourcemeta::core::schema_official_walker,
622+
sourcemeta::core::schema_official_resolver,
623+
sourcemeta::blaze::default_schema_compiler,
624+
sourcemeta::blaze::Mode::Exhaustive)};
625+
626+
const sourcemeta::core::JSON instance{5};
627+
628+
sourcemeta::blaze::SimpleOutput output{instance};
629+
sourcemeta::blaze::Evaluator evaluator;
630+
const auto result{
631+
evaluator.validate(schema_template, instance, std::ref(output))};
632+
EXPECT_FALSE(result);
633+
EXPECT_ANNOTATION_COUNT(output, 0);
436634
}

0 commit comments

Comments
 (0)