Skip to content

Commit bc0cc34

Browse files
committed
WIP
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent f76dc85 commit bc0cc34

6 files changed

Lines changed: 214 additions & 148 deletions

File tree

src/alterschema/alterschema.cc

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,70 @@ inline auto APPLIES_TO_POINTERS(std::vector<Pointer> &&keywords)
3535
return {std::move(keywords)};
3636
}
3737

38+
// TODO: Move upstream
39+
inline auto IS_IN_PLACE_APPLICATOR(const SchemaKeywordType type) -> bool {
40+
return type == SchemaKeywordType::ApplicatorValueOrElementsInPlace ||
41+
type == SchemaKeywordType::ApplicatorMembersInPlaceSome ||
42+
type == SchemaKeywordType::ApplicatorElementsInPlace ||
43+
type == SchemaKeywordType::ApplicatorElementsInPlaceSome ||
44+
type == SchemaKeywordType::ApplicatorElementsInPlaceSomeNegate ||
45+
type == SchemaKeywordType::ApplicatorValueInPlaceMaybe ||
46+
type == SchemaKeywordType::ApplicatorValueInPlaceOther ||
47+
type == SchemaKeywordType::ApplicatorValueInPlaceNegate;
48+
}
49+
50+
// Walk up from a schema location, continuing as long as the traversal
51+
// predicate returns true for each keyword type encountered. Returns a
52+
// reference to the pointer of the ancestor where the match callback returned
53+
// true, or nullopt if no match was found or the traversal predicate stopped
54+
// the walk.
55+
template <typename TraversePredicate, typename MatchCallback>
56+
auto WALK_UP(const JSON &root, const SchemaFrame &frame,
57+
const SchemaFrame::Location &location, const SchemaWalker &walker,
58+
const SchemaResolver &resolver,
59+
const TraversePredicate &should_continue,
60+
const MatchCallback &matches)
61+
-> std::optional<std::reference_wrapper<const WeakPointer>> {
62+
auto current_pointer{location.pointer};
63+
auto current_parent{location.parent};
64+
65+
while (current_parent.has_value()) {
66+
const auto &parent_pointer{current_parent.value()};
67+
const auto relative_pointer{current_pointer.resolve_from(parent_pointer)};
68+
assert(!relative_pointer.empty() && relative_pointer.at(0).is_property());
69+
const auto parent{frame.traverse(frame.uri(parent_pointer).value().get())};
70+
assert(parent.has_value());
71+
const auto keyword_type{
72+
walker(relative_pointer.at(0).to_property(),
73+
frame.vocabularies(parent.value().get(), resolver))
74+
.type};
75+
76+
if (!should_continue(keyword_type)) {
77+
return std::nullopt;
78+
}
79+
80+
if (matches(get(root, parent_pointer))) {
81+
return std::cref(parent.value().get().pointer);
82+
}
83+
84+
current_pointer = parent_pointer;
85+
current_parent = parent.value().get().parent;
86+
}
87+
88+
return std::nullopt;
89+
}
90+
91+
template <typename MatchCallback>
92+
auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame,
93+
const SchemaFrame::Location &location,
94+
const SchemaWalker &walker,
95+
const SchemaResolver &resolver,
96+
const MatchCallback &matches)
97+
-> std::optional<std::reference_wrapper<const WeakPointer>> {
98+
return WALK_UP(root, frame, location, walker, resolver,
99+
IS_IN_PLACE_APPLICATOR, matches);
100+
}
101+
38102
#define ONLY_CONTINUE_IF(condition) \
39103
if (!(condition)) { \
40104
return false; \
@@ -55,6 +119,7 @@ inline auto APPLIES_TO_POINTERS(std::vector<Pointer> &&keywords)
55119
#include "canonicalizer/properties_implicit.h"
56120
#include "canonicalizer/type_array_to_any_of.h"
57121
#include "canonicalizer/type_boolean_as_enum.h"
122+
#include "canonicalizer/type_inherit_in_place.h"
58123
#include "canonicalizer/type_null_as_enum.h"
59124
#include "canonicalizer/type_union_implicit.h"
60125

@@ -155,6 +220,7 @@ namespace sourcemeta::blaze {
155220
auto add(sourcemeta::core::SchemaTransformer &bundle,
156221
const AlterSchemaMode mode) -> void {
157222
if (mode == AlterSchemaMode::Canonicalizer) {
223+
bundle.add<TypeInheritInPlace>();
158224
bundle.add<TypeUnionImplicit>();
159225
bundle.add<TypeArrayToAnyOf>();
160226
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
class TypeInheritInPlace final : public SchemaTransformRule {
2+
public:
3+
using mutates = std::true_type;
4+
using reframe_after_transform = std::true_type;
5+
TypeInheritInPlace()
6+
: SchemaTransformRule{
7+
"type_inherit_in_place",
8+
"An untyped schema inside an in-place applicator inherits "
9+
"the type from its nearest typed ancestor"} {};
10+
11+
[[nodiscard]] auto
12+
condition(const sourcemeta::core::JSON &schema,
13+
const sourcemeta::core::JSON &root,
14+
const sourcemeta::core::Vocabularies &vocabularies,
15+
const sourcemeta::core::SchemaFrame &frame,
16+
const sourcemeta::core::SchemaFrame::Location &location,
17+
const sourcemeta::core::SchemaWalker &walker,
18+
const sourcemeta::core::SchemaResolver &resolver) const
19+
-> sourcemeta::core::SchemaTransformRule::Result override {
20+
using namespace sourcemeta::core;
21+
ONLY_CONTINUE_IF(schema.is_object());
22+
ONLY_CONTINUE_IF(vocabularies.contains_any(
23+
{Vocabularies::Known::JSON_Schema_2020_12_Validation,
24+
Vocabularies::Known::JSON_Schema_2019_09_Validation,
25+
Vocabularies::Known::JSON_Schema_Draft_7,
26+
Vocabularies::Known::JSON_Schema_Draft_6,
27+
Vocabularies::Known::JSON_Schema_Draft_4,
28+
Vocabularies::Known::JSON_Schema_Draft_3,
29+
Vocabularies::Known::JSON_Schema_Draft_2,
30+
Vocabularies::Known::JSON_Schema_Draft_1,
31+
Vocabularies::Known::JSON_Schema_Draft_0}));
32+
ONLY_CONTINUE_IF(!schema.defines("type"));
33+
ONLY_CONTINUE_IF(!schema.defines("enum"));
34+
ONLY_CONTINUE_IF(!vocabularies.contains_any(
35+
{Vocabularies::Known::JSON_Schema_2020_12_Validation,
36+
Vocabularies::Known::JSON_Schema_2019_09_Validation,
37+
Vocabularies::Known::JSON_Schema_Draft_7,
38+
Vocabularies::Known::JSON_Schema_Draft_6}) ||
39+
!schema.defines("const"));
40+
41+
for (const auto &entry : schema.as_object()) {
42+
const auto &keyword_type{walker(entry.first, vocabularies).type};
43+
ONLY_CONTINUE_IF(keyword_type != SchemaKeywordType::Reference);
44+
ONLY_CONTINUE_IF(keyword_type ==
45+
SchemaKeywordType::ApplicatorValueInPlaceOther ||
46+
!IS_IN_PLACE_APPLICATOR(keyword_type));
47+
}
48+
49+
// Walk up through in-place applicators excluding `allOf`. In `allOf` the
50+
// parent's type already constrains all branches (a conjunction), and other
51+
// rules may want to lift type out of conjunctions
52+
const auto ancestor{WALK_UP(
53+
root, frame, location, walker, resolver,
54+
[](const SchemaKeywordType keyword_type) {
55+
return IS_IN_PLACE_APPLICATOR(keyword_type) &&
56+
keyword_type != SchemaKeywordType::ApplicatorElementsInPlace;
57+
},
58+
[](const JSON &ancestor_schema) {
59+
return ancestor_schema.defines("type");
60+
})};
61+
62+
ONLY_CONTINUE_IF(ancestor.has_value());
63+
this->inherited_type_ = get(root, ancestor.value().get()).at("type");
64+
return true;
65+
}
66+
67+
auto transform(JSON &schema, const Result &) const -> void override {
68+
schema.assign("type", this->inherited_type_);
69+
}
70+
71+
private:
72+
mutable sourcemeta::core::JSON inherited_type_{
73+
sourcemeta::core::JSON{nullptr}};
74+
};

src/alterschema/canonicalizer/type_union_implicit.h

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,12 @@ class TypeUnionImplicit final : public SchemaTransformRule {
4040
for (const auto &entry : schema.as_object()) {
4141
const auto &keyword_type{walker(entry.first, vocabularies).type};
4242

43-
// References point to other schemas that may have type constraints
4443
ONLY_CONTINUE_IF(keyword_type != SchemaKeywordType::Reference);
45-
46-
// Logical in-place applicators apply without affecting the instance
47-
// location, meaning they impose constraints on the same instance. Adding
48-
// an implicit type union alongside these would create redundant branches
49-
// that need complex simplification
5044
ONLY_CONTINUE_IF(
51-
keyword_type != SchemaKeywordType::ApplicatorValueOrElementsInPlace &&
52-
keyword_type != SchemaKeywordType::ApplicatorMembersInPlaceSome &&
53-
keyword_type != SchemaKeywordType::ApplicatorElementsInPlace &&
54-
keyword_type != SchemaKeywordType::ApplicatorElementsInPlaceSome &&
55-
keyword_type !=
56-
SchemaKeywordType::ApplicatorElementsInPlaceSomeNegate &&
57-
keyword_type != SchemaKeywordType::ApplicatorValueInPlaceMaybe &&
58-
keyword_type != SchemaKeywordType::ApplicatorValueInPlaceNegate);
45+
// Applicators like `contentSchema` applies to decoded content, not
46+
// the current instance
47+
keyword_type == SchemaKeywordType::ApplicatorValueInPlaceOther ||
48+
!IS_IN_PLACE_APPLICATOR(keyword_type));
5949
}
6050

6151
return true;

src/alterschema/common/required_properties_in_properties.h

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,13 @@ class RequiredPropertiesInProperties final : public SchemaTransformRule {
4040
for (const auto &property : schema.at("required").as_array()) {
4141
if (property.is_string() &&
4242
!this->defined_in_properties_sibling(schema, property.to_string()) &&
43-
!this->defined_in_properties_parent(root, frame, location, walker,
44-
resolver, property.to_string())) {
43+
!WALK_UP_IN_PLACE_APPLICATORS(
44+
root, frame, location, walker, resolver,
45+
[&](const JSON &ancestor) {
46+
return this->defined_in_properties_sibling(
47+
ancestor, property.to_string());
48+
})
49+
.has_value()) {
4550
locations.push_back(Pointer{"required", index});
4651
}
4752

@@ -71,43 +76,4 @@ class RequiredPropertiesInProperties final : public SchemaTransformRule {
7176
schema.at("properties").is_object() &&
7277
schema.at("properties").defines(property);
7378
};
74-
75-
[[nodiscard]] auto
76-
defined_in_properties_parent(const JSON &root, const SchemaFrame &frame,
77-
const SchemaFrame::Location &location,
78-
const SchemaWalker &walker,
79-
const SchemaResolver &resolver,
80-
const JSON::String &property) const -> bool {
81-
auto current_pointer = location.pointer;
82-
auto current_parent = location.parent;
83-
84-
while (current_parent.has_value()) {
85-
const auto &parent_pointer{current_parent.value()};
86-
const auto relative_pointer{current_pointer.resolve_from(parent_pointer)};
87-
assert(!relative_pointer.empty() && relative_pointer.at(0).is_property());
88-
const auto parent{
89-
frame.traverse(frame.uri(parent_pointer).value().get())};
90-
assert(parent.has_value());
91-
const auto type{walker(relative_pointer.at(0).to_property(),
92-
frame.vocabularies(parent.value().get(), resolver))
93-
.type};
94-
if (type != SchemaKeywordType::ApplicatorElementsInPlaceSome &&
95-
type != SchemaKeywordType::ApplicatorElementsInPlace &&
96-
type != SchemaKeywordType::ApplicatorValueInPlaceMaybe &&
97-
type != SchemaKeywordType::ApplicatorValueInPlaceNegate &&
98-
type != SchemaKeywordType::ApplicatorValueInPlaceOther) {
99-
return false;
100-
}
101-
102-
if (this->defined_in_properties_sibling(get(root, parent_pointer),
103-
property)) {
104-
return true;
105-
}
106-
107-
current_pointer = parent_pointer;
108-
current_parent = parent.value().get().parent;
109-
}
110-
111-
return false;
112-
};
11379
};

test/alterschema/alterschema_canonicalize_2020_12_test.cc

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,19 +1148,13 @@ TEST(AlterSchema_canonicalize_2020_12,
11481148
const auto expected = sourcemeta::core::parse_json(R"JSON({
11491149
"$schema": "https://json-schema.org/draft/2020-12/schema",
11501150
"type": "string",
1151-
"minLength": 0,
11521151
"anyOf": [
11531152
{
1154-
"anyOf": [
1155-
{ "enum": [ null ] },
1156-
{ "enum": [ false, true ] },
1157-
{ "type": "object", "minProperties": 0, "properties": {} },
1158-
{ "type": "array", "minItems": 0, "items": true },
1159-
{ "type": "string", "minLength": 1 },
1160-
{ "type": "number" }
1161-
]
1153+
"minLength": 1,
1154+
"type": "string"
11621155
}
1163-
]
1156+
],
1157+
"minLength": 0
11641158
})JSON");
11651159

11661160
EXPECT_EQ(document, expected);
@@ -1187,14 +1181,8 @@ TEST(AlterSchema_canonicalize_2020_12,
11871181
"oneOf": [
11881182
false,
11891183
{
1190-
"anyOf": [
1191-
{ "enum": [ null ] },
1192-
{ "enum": [ false, true ] },
1193-
{ "type": "object", "minProperties": 0, "properties": {} },
1194-
{ "type": "array", "minItems": 0, "items": true },
1195-
{ "type": "string", "minLength": 0 },
1196-
{ "type": "number", "minimum": 0 }
1197-
]
1184+
"minimum": 0,
1185+
"type": "number"
11981186
}
11991187
]
12001188
})JSON");
@@ -1328,3 +1316,52 @@ TEST(AlterSchema_canonicalize_2020_12, items_implicit_2) {
13281316

13291317
EXPECT_EQ(document, expected);
13301318
}
1319+
1320+
TEST(AlterSchema_canonicalize_2020_12, object_anyof_untyped_branches_1) {
1321+
auto document = sourcemeta::core::parse_json(R"JSON({
1322+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1323+
"type": "object",
1324+
"title": "Pet",
1325+
"properties": {
1326+
"kind": { "const": "cat" }
1327+
},
1328+
"anyOf": [
1329+
{ "properties": { "indoor": { "type": "boolean" } } },
1330+
{ "properties": { "outdoor": { "type": "boolean" } } }
1331+
]
1332+
})JSON");
1333+
1334+
CANONICALIZE(document, result, traces);
1335+
1336+
EXPECT_TRUE(result.first);
1337+
1338+
const auto expected = sourcemeta::core::parse_json(R"JSON({
1339+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1340+
"type": "object",
1341+
"title": "Pet",
1342+
"properties": {
1343+
"kind": {
1344+
"enum": [ "cat" ]
1345+
}
1346+
},
1347+
"anyOf": [
1348+
{
1349+
"type": "object",
1350+
"properties": {
1351+
"indoor": { "enum": [ false, true ] }
1352+
},
1353+
"minProperties": 0
1354+
},
1355+
{
1356+
"type": "object",
1357+
"properties": {
1358+
"outdoor": { "enum": [ false, true ] }
1359+
},
1360+
"minProperties": 0
1361+
}
1362+
],
1363+
"minProperties": 0
1364+
})JSON");
1365+
1366+
EXPECT_EQ(document, expected);
1367+
}

0 commit comments

Comments
 (0)