Skip to content

Commit 1f08b59

Browse files
authored
Revise unnecessary_allof_wrapper rules to only cover $ref (#2049)
See: sourcemeta/jsonschema#536 Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 0e963f3 commit 1f08b59

12 files changed

+1075
-383
lines changed

src/extension/alterschema/CMakeLists.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,8 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema
5050
linter/content_schema_without_media_type.h
5151
linter/additional_items_with_schema_items.h
5252
linter/non_applicable_type_specific_keywords.h
53-
linter/unnecessary_allof_wrapper_modern.h
54-
linter/unnecessary_allof_wrapper_draft.h
55-
linter/unnecessary_allof_wrapper_properties.h
53+
linter/unnecessary_allof_ref_wrapper_modern.h
54+
linter/unnecessary_allof_ref_wrapper_draft.h
5655
linter/duplicate_allof_branches.h
5756
linter/duplicate_anyof_branches.h
5857
linter/else_without_if.h

src/extension/alterschema/alterschema.cc

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,8 @@ inline auto APPLIES_TO_POINTERS(std::vector<Pointer> &&keywords)
9898
#include "linter/unevaluated_items_default.h"
9999
#include "linter/unevaluated_properties_default.h"
100100
#include "linter/unknown_keywords_prefix.h"
101-
#include "linter/unnecessary_allof_wrapper_draft.h"
102-
#include "linter/unnecessary_allof_wrapper_modern.h"
103-
#include "linter/unnecessary_allof_wrapper_properties.h"
101+
#include "linter/unnecessary_allof_ref_wrapper_draft.h"
102+
#include "linter/unnecessary_allof_ref_wrapper_modern.h"
104103
#include "linter/unsatisfiable_max_contains.h"
105104
#include "linter/unsatisfiable_min_properties.h"
106105

@@ -118,9 +117,8 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void {
118117
bundle.add<ContentSchemaWithoutMediaType>();
119118
bundle.add<DraftOfficialDialectWithoutEmptyFragment>();
120119
bundle.add<NonApplicableTypeSpecificKeywords>();
121-
bundle.add<UnnecessaryAllOfWrapperModern>();
122-
bundle.add<UnnecessaryAllOfWrapperDraft>();
123-
bundle.add<UnnecessaryAllOfWrapperProperties>();
120+
bundle.add<UnnecessaryAllOfRefWrapperModern>();
121+
bundle.add<UnnecessaryAllOfRefWrapperDraft>();
124122
bundle.add<DuplicateAllOfBranches>();
125123
bundle.add<DuplicateAnyOfBranches>();
126124
bundle.add<ElseWithoutIf>();
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
class UnnecessaryAllOfRefWrapperDraft final : public SchemaTransformRule {
2+
public:
3+
UnnecessaryAllOfRefWrapperDraft()
4+
: SchemaTransformRule{"unnecessary_allof_ref_wrapper_draft",
5+
"Wrapping `$ref` in `allOf` is only necessary if "
6+
"there are other sibling keywords"} {};
7+
8+
[[nodiscard]] auto
9+
condition(const sourcemeta::core::JSON &schema,
10+
const sourcemeta::core::JSON &,
11+
const sourcemeta::core::Vocabularies &vocabularies,
12+
const sourcemeta::core::SchemaFrame &,
13+
const sourcemeta::core::SchemaFrame::Location &,
14+
const sourcemeta::core::SchemaWalker &,
15+
const sourcemeta::core::SchemaResolver &) const
16+
-> sourcemeta::core::SchemaTransformRule::Result override {
17+
ONLY_CONTINUE_IF(contains_any(vocabularies,
18+
{"http://json-schema.org/draft-07/schema#",
19+
"http://json-schema.org/draft-06/schema#",
20+
"http://json-schema.org/draft-04/schema#"}));
21+
ONLY_CONTINUE_IF(schema.is_object() && schema.size() == 1 &&
22+
schema.defines("allOf") && schema.at("allOf").is_array());
23+
24+
const auto &all_of{schema.at("allOf")};
25+
26+
// In Draft 7 and older, `$ref` overrides sibling keywords, so we can only
27+
// elevate it if it is the only keyword of the only branch, and the outer
28+
// subschema only declares `allOf`
29+
ONLY_CONTINUE_IF(all_of.size() == 1);
30+
const auto &entry{all_of.at(0)};
31+
ONLY_CONTINUE_IF(entry.is_object());
32+
ONLY_CONTINUE_IF(entry.size() == 1 && entry.defines("$ref"));
33+
34+
return APPLIES_TO_POINTERS({{"allOf", 0, "$ref"}});
35+
}
36+
37+
auto transform(JSON &schema, const Result &) const -> void override {
38+
auto value{schema.at("allOf").at(0).at("$ref")};
39+
schema.at("allOf").into(std::move(value));
40+
schema.rename("allOf", "$ref");
41+
}
42+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
class UnnecessaryAllOfRefWrapperModern final : public SchemaTransformRule {
2+
public:
3+
UnnecessaryAllOfRefWrapperModern()
4+
: SchemaTransformRule{"unnecessary_allof_ref_wrapper_modern",
5+
"Wrapping `$ref` in `allOf` was only necessary in "
6+
"JSON Schema Draft 7 and older"} {};
7+
8+
[[nodiscard]] auto
9+
condition(const sourcemeta::core::JSON &schema,
10+
const sourcemeta::core::JSON &,
11+
const sourcemeta::core::Vocabularies &vocabularies,
12+
const sourcemeta::core::SchemaFrame &,
13+
const sourcemeta::core::SchemaFrame::Location &,
14+
const sourcemeta::core::SchemaWalker &,
15+
const sourcemeta::core::SchemaResolver &) const
16+
-> sourcemeta::core::SchemaTransformRule::Result override {
17+
ONLY_CONTINUE_IF(contains_any(
18+
vocabularies,
19+
{"https://json-schema.org/draft/2020-12/vocab/applicator",
20+
"https://json-schema.org/draft/2019-09/vocab/applicator"}));
21+
ONLY_CONTINUE_IF(schema.is_object() && schema.defines("allOf") &&
22+
schema.at("allOf").is_array());
23+
24+
const auto &all_of{schema.at("allOf")};
25+
26+
// Don't do anything if there is more than one branch and ALL branches
27+
// define `$ref` (a common multiple composition pattern)
28+
ONLY_CONTINUE_IF(
29+
!(all_of.size() > 1 &&
30+
std::ranges::all_of(all_of.as_array(), [](const auto &entry) {
31+
return entry.is_object() && entry.defines("$ref");
32+
})));
33+
34+
std::vector<Pointer> locations;
35+
for (std::size_t index = 0; index < all_of.size(); index++) {
36+
const auto &entry{all_of.at(index)};
37+
if (entry.is_object() && entry.defines("$ref") &&
38+
// We cannot safely elevate a reference on a subschema with its own
39+
// base URI
40+
// TODO: In theory we can if the URI is absolute
41+
!entry.defines("$id") && !schema.defines("$ref")) {
42+
locations.push_back(Pointer{"allOf", index, "$ref"});
43+
}
44+
}
45+
46+
ONLY_CONTINUE_IF(!locations.empty());
47+
return APPLIES_TO_POINTERS(std::move(locations));
48+
}
49+
50+
auto transform(JSON &schema, const Result &result) const -> void override {
51+
for (const auto &location : result.locations) {
52+
assert(location.size() == 3);
53+
const auto allof_index{location.at(1).to_index()};
54+
const auto &keyword{location.at(2).to_property()};
55+
56+
if (!schema.defines(keyword)) {
57+
schema.try_assign_before(
58+
keyword, schema.at("allOf").at(allof_index).at(keyword), "allOf");
59+
schema.at("allOf").at(allof_index).erase(keyword);
60+
}
61+
}
62+
63+
schema.at("allOf").erase_if(sourcemeta::core::is_empty_schema);
64+
65+
if (schema.at("allOf").empty()) {
66+
schema.erase("allOf");
67+
}
68+
}
69+
};

src/extension/alterschema/linter/unnecessary_allof_wrapper_draft.h

Lines changed: 0 additions & 96 deletions
This file was deleted.

src/extension/alterschema/linter/unnecessary_allof_wrapper_modern.h

Lines changed: 0 additions & 103 deletions
This file was deleted.

0 commit comments

Comments
 (0)