Skip to content

Commit 35c2f2f

Browse files
authored
Simplify nested logical operators in linter and canonicalizer (#736)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 8213b33 commit 35c2f2f

15 files changed

Lines changed: 1367 additions & 876 deletions

src/alterschema/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,14 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema
7272
common/content_schema_without_media_type.h
7373
common/dependencies_property_tautology.h
7474
common/dependent_required_tautology.h
75+
common/double_negation_elimination.h
7576
common/draft_official_dialect_with_https.h
7677
common/draft_official_dialect_without_empty_fragment.h
7778
common/draft_ref_siblings.h
7879
common/drop_allof_empty_schemas.h
7980
common/duplicate_allof_branches.h
81+
common/flatten_nested_allof.h
82+
common/flatten_nested_anyof.h
8083
common/duplicate_anyof_branches.h
8184
common/dynamic_ref_to_static_ref.h
8285
common/duplicate_enum_values.h

src/alterschema/alterschema.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame,
175175
#include "common/content_schema_without_media_type.h"
176176
#include "common/dependencies_property_tautology.h"
177177
#include "common/dependent_required_tautology.h"
178+
#include "common/double_negation_elimination.h"
178179
#include "common/draft_official_dialect_with_https.h"
179180
#include "common/draft_official_dialect_without_empty_fragment.h"
180181
#include "common/draft_ref_siblings.h"
@@ -190,6 +191,8 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame,
190191
#include "common/equal_numeric_bounds_to_enum.h"
191192
#include "common/exclusive_maximum_number_and_maximum.h"
192193
#include "common/exclusive_minimum_number_and_minimum.h"
194+
#include "common/flatten_nested_allof.h"
195+
#include "common/flatten_nested_anyof.h"
193196
#include "common/if_without_then_else.h"
194197
#include "common/ignored_metaschema.h"
195198
#include "common/max_contains_without_contains.h"
@@ -301,10 +304,13 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void {
301304
bundle.add<AnyOfTrueSimplify>();
302305
bundle.add<DuplicateAllOfBranches>();
303306
bundle.add<DuplicateAnyOfBranches>();
307+
bundle.add<FlattenNestedAllOf>();
308+
bundle.add<FlattenNestedAnyOf>();
304309
bundle.add<UnsatisfiableInPlaceApplicatorType>();
305310
bundle.add<AllOfFalseSimplify>();
306311
bundle.add<AnyOfFalseSimplify>();
307312
bundle.add<OneOfFalseSimplify>();
313+
bundle.add<DoubleNegationElimination>();
308314
bundle.add<OneOfToAnyOfDisjointTypes>();
309315
bundle.add<UnsatisfiableDropValidation>();
310316
bundle.add<ElseWithoutIf>();

src/alterschema/canonicalizer/single_branch_allof.h

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,7 @@ class SingleBranchAllOf final : public SchemaTransformRule {
5151
return;
5252
}
5353

54-
for (const auto &entry : branch.as_object()) {
55-
schema.assign(entry.first, entry.second);
56-
}
54+
schema.merge(branch.as_object());
5755
schema.erase("allOf");
5856
}
5957

src/alterschema/canonicalizer/single_branch_anyof.h

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,7 @@ class SingleBranchAnyOf final : public SchemaTransformRule {
5050
return;
5151
}
5252

53-
for (const auto &entry : branch.as_object()) {
54-
schema.assign(entry.first, entry.second);
55-
}
53+
schema.merge(branch.as_object());
5654
schema.erase("anyOf");
5755
}
5856

src/alterschema/canonicalizer/single_branch_oneof.h

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,7 @@ class SingleBranchOneOf final : public SchemaTransformRule {
5050
return;
5151
}
5252

53-
for (const auto &entry : branch.as_object()) {
54-
schema.assign(entry.first, entry.second);
55-
}
53+
schema.merge(branch.as_object());
5654
schema.erase("oneOf");
5755
}
5856

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
class DoubleNegationElimination final : public SchemaTransformRule {
2+
public:
3+
using mutates = std::true_type;
4+
using reframe_after_transform = std::true_type;
5+
DoubleNegationElimination()
6+
: SchemaTransformRule{
7+
"double_negation_elimination",
8+
"A `not` whose value is a schema containing only another "
9+
"`not` is equivalent to the inner value"} {};
10+
11+
[[nodiscard]] auto
12+
condition(const sourcemeta::core::JSON &schema,
13+
const sourcemeta::core::JSON &,
14+
const sourcemeta::core::Vocabularies &vocabularies,
15+
const sourcemeta::core::SchemaFrame &frame,
16+
const sourcemeta::core::SchemaFrame::Location &location,
17+
const sourcemeta::core::SchemaWalker &,
18+
const sourcemeta::core::SchemaResolver &) const
19+
-> SchemaTransformRule::Result override {
20+
static const JSON::String KEYWORD{"not"};
21+
ONLY_CONTINUE_IF(vocabularies.contains_any(
22+
{Vocabularies::Known::JSON_Schema_2020_12_Applicator,
23+
Vocabularies::Known::JSON_Schema_2019_09_Applicator,
24+
Vocabularies::Known::JSON_Schema_Draft_7,
25+
Vocabularies::Known::JSON_Schema_Draft_6,
26+
Vocabularies::Known::JSON_Schema_Draft_4}) &&
27+
schema.is_object() && schema.defines(KEYWORD) &&
28+
schema.at(KEYWORD).is_object() &&
29+
schema.at(KEYWORD).size() == 1 &&
30+
schema.at(KEYWORD).defines(KEYWORD) &&
31+
!(schema.at(KEYWORD).at(KEYWORD).is_boolean() &&
32+
!schema.at(KEYWORD).at(KEYWORD).to_boolean()));
33+
ONLY_CONTINUE_IF(
34+
!(vocabularies.contains_any(
35+
{Vocabularies::Known::JSON_Schema_2020_12_Unevaluated,
36+
Vocabularies::Known::JSON_Schema_2019_09_Applicator}) &&
37+
(schema.defines("unevaluatedProperties") ||
38+
schema.defines("unevaluatedItems"))));
39+
ONLY_CONTINUE_IF(!frame.has_references_through(
40+
location.pointer, WeakPointer::Token{std::cref(KEYWORD)}));
41+
return APPLIES_TO_KEYWORDS(KEYWORD);
42+
}
43+
44+
auto transform(JSON &schema, const Result &) const -> void override {
45+
auto inner{schema.at("not").at("not")};
46+
schema.erase("not");
47+
48+
while (inner.is_object() && inner.size() == 1 && inner.defines("not") &&
49+
inner.at("not").is_object() && inner.at("not").size() == 1 &&
50+
inner.at("not").defines("not") &&
51+
!(inner.at("not").at("not").is_boolean() &&
52+
!inner.at("not").at("not").to_boolean())) {
53+
auto next{inner.at("not").at("not")};
54+
inner = std::move(next);
55+
}
56+
57+
if (inner.is_object()) {
58+
schema.merge(inner.as_object());
59+
}
60+
}
61+
62+
[[nodiscard]] auto rereference(const std::string_view, const Pointer &,
63+
const Pointer &target,
64+
const Pointer &current) const
65+
-> Pointer override {
66+
auto old_prefix{current.concat({"not", "not"})};
67+
while (target.starts_with(old_prefix.concat({"not", "not"}))) {
68+
old_prefix = old_prefix.concat({"not", "not"});
69+
}
70+
if (!target.starts_with(old_prefix)) {
71+
return target;
72+
}
73+
return target.rebase(old_prefix, current);
74+
}
75+
};
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
class FlattenNestedAllOf final : public SchemaTransformRule {
2+
public:
3+
using mutates = std::true_type;
4+
using reframe_after_transform = std::true_type;
5+
FlattenNestedAllOf()
6+
: SchemaTransformRule{
7+
"flatten_nested_allof",
8+
"An `allOf` branch that only contains another `allOf` can "
9+
"be flattened into the parent `allOf`"} {};
10+
11+
[[nodiscard]] auto
12+
condition(const sourcemeta::core::JSON &schema,
13+
const sourcemeta::core::JSON &,
14+
const sourcemeta::core::Vocabularies &vocabularies,
15+
const sourcemeta::core::SchemaFrame &frame,
16+
const sourcemeta::core::SchemaFrame::Location &location,
17+
const sourcemeta::core::SchemaWalker &,
18+
const sourcemeta::core::SchemaResolver &) const
19+
-> SchemaTransformRule::Result override {
20+
static const JSON::String KEYWORD{"allOf"};
21+
ONLY_CONTINUE_IF(vocabularies.contains_any(
22+
{Vocabularies::Known::JSON_Schema_2020_12_Applicator,
23+
Vocabularies::Known::JSON_Schema_2019_09_Applicator,
24+
Vocabularies::Known::JSON_Schema_Draft_7,
25+
Vocabularies::Known::JSON_Schema_Draft_6,
26+
Vocabularies::Known::JSON_Schema_Draft_4}) &&
27+
schema.is_object() && schema.defines(KEYWORD) &&
28+
schema.at(KEYWORD).is_array());
29+
30+
this->flatten_indices_.clear();
31+
const auto &branches{schema.at(KEYWORD)};
32+
for (std::size_t index = 0; index < branches.size(); ++index) {
33+
const auto &branch{branches.at(index)};
34+
if (branch.is_object() && branch.size() == 1 && branch.defines(KEYWORD) &&
35+
branch.at(KEYWORD).is_array()) {
36+
this->flatten_indices_.push_back(index);
37+
}
38+
}
39+
40+
ONLY_CONTINUE_IF(!this->flatten_indices_.empty());
41+
ONLY_CONTINUE_IF(!frame.has_references_through(
42+
location.pointer, WeakPointer::Token{std::cref(KEYWORD)}));
43+
return APPLIES_TO_KEYWORDS(KEYWORD);
44+
}
45+
46+
auto transform(JSON &schema, const Result &) const -> void override {
47+
static const JSON::String KEYWORD{"allOf"};
48+
this->index_mapping_.clear();
49+
const auto &original{schema.at(KEYWORD)};
50+
auto result{JSON::make_array()};
51+
std::size_t new_index{0};
52+
std::size_t flatten_cursor{0};
53+
54+
for (std::size_t index = 0; index < original.size(); ++index) {
55+
if (flatten_cursor < this->flatten_indices_.size() &&
56+
this->flatten_indices_[flatten_cursor] == index) {
57+
this->collect_leaves_(original.at(index), KEYWORD, index, result,
58+
new_index);
59+
++flatten_cursor;
60+
} else {
61+
this->index_mapping_.emplace_back(index, std::nullopt, new_index);
62+
result.push_back(original.at(index));
63+
++new_index;
64+
}
65+
}
66+
67+
schema.assign(KEYWORD, std::move(result));
68+
}
69+
70+
[[nodiscard]] auto rereference(const std::string_view, const Pointer &,
71+
const Pointer &target,
72+
const Pointer &current) const
73+
-> Pointer override {
74+
static const JSON::String KEYWORD{"allOf"};
75+
const auto prefix{current.concat({KEYWORD})};
76+
if (!target.starts_with(prefix)) {
77+
return target;
78+
}
79+
const auto relative{target.resolve_from(prefix)};
80+
if (relative.empty() || !relative.at(0).is_index()) {
81+
return target;
82+
}
83+
const auto old_index{relative.at(0).to_index()};
84+
for (const auto &[outer, inner, mapped] : this->index_mapping_) {
85+
if (outer == old_index && inner.has_value()) {
86+
const Pointer old_prefix{
87+
prefix.concat({old_index, KEYWORD, inner.value()})};
88+
if (target.starts_with(old_prefix)) {
89+
const Pointer new_prefix{prefix.concat({mapped})};
90+
return target.rebase(old_prefix, new_prefix);
91+
}
92+
} else if (outer == old_index) {
93+
const Pointer old_prefix{prefix.concat({old_index})};
94+
const Pointer new_prefix{prefix.concat({mapped})};
95+
return target.rebase(old_prefix, new_prefix);
96+
}
97+
}
98+
return target;
99+
}
100+
101+
private:
102+
auto collect_leaves_(const JSON &node, const JSON::String &keyword,
103+
std::size_t outer_index, JSON &result,
104+
std::size_t &new_index) const -> void {
105+
const auto &inner{node.at(keyword)};
106+
for (std::size_t inner_index = 0; inner_index < inner.size();
107+
++inner_index) {
108+
const auto &child{inner.at(inner_index)};
109+
if (child.is_object() && child.size() == 1 && child.defines(keyword) &&
110+
child.at(keyword).is_array()) {
111+
this->collect_leaves_(child, keyword, outer_index, result, new_index);
112+
} else {
113+
this->index_mapping_.emplace_back(outer_index, inner_index, new_index);
114+
result.push_back(child);
115+
++new_index;
116+
}
117+
}
118+
}
119+
120+
mutable std::vector<std::size_t> flatten_indices_;
121+
mutable std::vector<
122+
std::tuple<std::size_t, std::optional<std::size_t>, std::size_t>>
123+
index_mapping_;
124+
};

0 commit comments

Comments
 (0)