From ff06610479bdb1fc7c184e8da6c463e313fff168 Mon Sep 17 00:00:00 2001 From: karan-palan Date: Wed, 9 Jul 2025 05:08:34 +0530 Subject: [PATCH 1/4] feat(alterschema): [linter] create rule to remove empty fragment Signed-off-by: karan-palan --- src/extension/alterschema/CMakeLists.txt | 1 + src/extension/alterschema/alterschema.cc | 2 ++ ...ern_official_dialect_with_empty_fragment.h | 36 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 src/extension/alterschema/linter/modern_official_dialect_with_empty_fragment.h diff --git a/src/extension/alterschema/CMakeLists.txt b/src/extension/alterschema/CMakeLists.txt index 3d3942841..fb88d5b10 100644 --- a/src/extension/alterschema/CMakeLists.txt +++ b/src/extension/alterschema/CMakeLists.txt @@ -55,6 +55,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema linter/if_without_then_else.h linter/max_contains_without_contains.h linter/min_contains_without_contains.h + linter/modern_official_dialect_with_empty_fragment.h linter/then_without_if.h) if(SOURCEMETA_CORE_INSTALL) diff --git a/src/extension/alterschema/alterschema.cc b/src/extension/alterschema/alterschema.cc index 77d539de1..48cf3fbfc 100644 --- a/src/extension/alterschema/alterschema.cc +++ b/src/extension/alterschema/alterschema.cc @@ -73,6 +73,7 @@ static auto every_item_is_boolean(const T &container) -> bool { #include "linter/maximum_real_for_integer.h" #include "linter/min_contains_without_contains.h" #include "linter/minimum_real_for_integer.h" +#include "linter/modern_official_dialect_with_empty_fragment.h" #include "linter/multiple_of_default.h" #include "linter/non_applicable_type_specific_keywords.h" #include "linter/pattern_properties_default.h" @@ -113,6 +114,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); diff --git a/src/extension/alterschema/linter/modern_official_dialect_with_empty_fragment.h b/src/extension/alterschema/linter/modern_official_dialect_with_empty_fragment.h new file mode 100644 index 000000000..ff4a0708a --- /dev/null +++ b/src/extension/alterschema/linter/modern_official_dialect_with_empty_fragment.h @@ -0,0 +1,36 @@ +class ModernOfficialDialectWithEmptyFragment final + : public SchemaTransformRule { +public: + ModernOfficialDialectWithEmptyFragment() + : SchemaTransformRule{ + "modern_official_dialect_with_empty_fragment", + "The official dialect URI of Draft 2019-09 and newer versions must " + "not contain the empty fragment"} {}; + + [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> sourcemeta::core::SchemaTransformRule::Result override { + if (!schema.is_object() || !schema.defines("$schema") || + !schema.at("$schema").is_string()) { + return false; + } + + const auto &schema_value = schema.at("$schema").to_string(); + return ( + schema_value == "https://json-schema.org/draft/2019-09/schema#" || + schema_value == "https://json-schema.org/draft/2019-09/hyper-schema#" || + schema_value == "https://json-schema.org/draft/2020-12/schema#" || + schema_value == "https://json-schema.org/draft/2020-12/hyper-schema#"); + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto schema_value = schema.at("$schema").to_string(); + schema_value.pop_back(); + schema.at("$schema").into(sourcemeta::core::JSON{std::move(schema_value)}); + } +}; From 7d407d14ca7a55f9538e4cabae43d0f71fbed0a9 Mon Sep 17 00:00:00 2001 From: karan-palan Date: Mon, 14 Jul 2025 22:35:17 +0530 Subject: [PATCH 2/4] feat(alterschema): [linter] add tests Signed-off-by: karan-palan --- src/core/jsonschema/official_resolver.in.cc | 20 +++++++- .../alterschema_lint_2019_09_test.cc | 48 ++++++++++++++++++ .../alterschema_lint_2020_12_test.cc | 49 +++++++++++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) diff --git a/src/core/jsonschema/official_resolver.in.cc b/src/core/jsonschema/official_resolver.in.cc index fb9ed8c9a..ca1269b18 100644 --- a/src/core/jsonschema/official_resolver.in.cc +++ b/src/core/jsonschema/official_resolver.in.cc @@ -10,6 +10,12 @@ auto sourcemeta::core::schema_official_resolver(std::string_view identifier) "https://json-schema.org/draft/2020-12/hyper-schema") { return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_HYPERSCHEMA_2020_12@)EOF"); + } else if (identifier == + "https://json-schema.org/draft/2020-12/hyper-schema#") { + auto schema = sourcemeta::core::parse_json( + R"EOF(@METASCHEMA_HYPERSCHEMA_2020_12@)EOF"); + schema.at("$id").into(sourcemeta::core::JSON{identifier}); + return schema; } else if (identifier == "https://json-schema.org/draft/2020-12/meta/applicator") { return sourcemeta::core::parse_json( @@ -54,8 +60,10 @@ auto sourcemeta::core::schema_official_resolver(std::string_view identifier) // Just for compatibility given that this is such a common issue } else if (identifier == "https://json-schema.org/draft/2020-12/schema#") { - return sourcemeta::core::parse_json( + auto schema = sourcemeta::core::parse_json( R"EOF(@METASCHEMA_JSONSCHEMA_2020_12@)EOF"); + schema.at("$id").into(sourcemeta::core::JSON{identifier}); + return schema; // JSON Schema 2019-09 } else if (identifier == "https://json-schema.org/draft/2019-09/schema") { @@ -65,6 +73,12 @@ auto sourcemeta::core::schema_official_resolver(std::string_view identifier) "https://json-schema.org/draft/2019-09/hyper-schema") { return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_HYPERSCHEMA_2019_09@)EOF"); + } else if (identifier == + "https://json-schema.org/draft/2019-09/hyper-schema#") { + auto schema = sourcemeta::core::parse_json( + R"EOF(@METASCHEMA_HYPERSCHEMA_2019_09@)EOF"); + schema.at("$id").into(sourcemeta::core::JSON{identifier}); + return schema; } else if (identifier == "https://json-schema.org/draft/2019-09/meta/applicator") { return sourcemeta::core::parse_json( @@ -105,8 +119,10 @@ auto sourcemeta::core::schema_official_resolver(std::string_view identifier) // Just for compatibility given that this is such a common issue } else if (identifier == "https://json-schema.org/draft/2019-09/schema#") { - return sourcemeta::core::parse_json( + auto schema = sourcemeta::core::parse_json( R"EOF(@METASCHEMA_JSONSCHEMA_2019_09@)EOF"); + schema.at("$id").into(sourcemeta::core::JSON{identifier}); + return schema; // JSON Schema Draft7 } else if (identifier == "http://json-schema.org/draft-07/schema#" || diff --git a/test/alterschema/alterschema_lint_2019_09_test.cc b/test/alterschema/alterschema_lint_2019_09_test.cc index fce6f05ce..7e3e6dd72 100644 --- a/test/alterschema/alterschema_lint_2019_09_test.cc +++ b/test/alterschema/alterschema_lint_2019_09_test.cc @@ -1750,3 +1750,51 @@ TEST(AlterSchema_lint_2019_09, multiple_of_default_no_change_1) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_2019_09, modern_official_dialect_with_empty_fragment_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "type": "string" + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "string" + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, modern_official_dialect_with_empty_fragment_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/hyper-schema#", + "type": "string" + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/hyper-schema", + "type": "string" + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, modern_official_dialect_with_empty_fragment_3) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "string" + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "string" + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/alterschema/alterschema_lint_2020_12_test.cc b/test/alterschema/alterschema_lint_2020_12_test.cc index 61af4a426..1f9f13dec 100644 --- a/test/alterschema/alterschema_lint_2020_12_test.cc +++ b/test/alterschema/alterschema_lint_2020_12_test.cc @@ -1912,3 +1912,52 @@ TEST(AlterSchema_lint_2020_12, multiple_of_default_no_change_numeric_value) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_2020_12, modern_official_dialect_with_empty_fragment_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema#", + "type": "string" + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, modern_official_dialect_with_empty_fragment_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/hyper-schema#", + "type": "string" + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/hyper-schema", + "type": "string" + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, modern_official_dialect_with_empty_fragment_3) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + // Should remain unchanged since there's no empty fragment + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + })JSON"); + + EXPECT_EQ(document, expected); +} From ecf01750ec0c40ebd3b66cc789eb4efd98a4225e Mon Sep 17 00:00:00 2001 From: karan-palan Date: Mon, 14 Jul 2025 22:45:39 +0530 Subject: [PATCH 3/4] feat(alterschema): [linter] remove Draft for newer ones Signed-off-by: karan-palan --- .../linter/modern_official_dialect_with_empty_fragment.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/alterschema/linter/modern_official_dialect_with_empty_fragment.h b/src/extension/alterschema/linter/modern_official_dialect_with_empty_fragment.h index ff4a0708a..8188f4985 100644 --- a/src/extension/alterschema/linter/modern_official_dialect_with_empty_fragment.h +++ b/src/extension/alterschema/linter/modern_official_dialect_with_empty_fragment.h @@ -4,7 +4,7 @@ class ModernOfficialDialectWithEmptyFragment final ModernOfficialDialectWithEmptyFragment() : SchemaTransformRule{ "modern_official_dialect_with_empty_fragment", - "The official dialect URI of Draft 2019-09 and newer versions must " + "The official dialect URI of 2019-09 and newer versions must " "not contain the empty fragment"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, From dd5f7fd02abf7a59b6c05063991836d7aa313a11 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Mon, 14 Jul 2025 17:42:17 -0400 Subject: [PATCH 4/4] Minor fixes Signed-off-by: Juan Cruz Viotti --- src/core/jsonschema/jsonschema.cc | 3 ++- src/core/jsonschema/official_resolver.in.cc | 28 ++++++++------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/core/jsonschema/jsonschema.cc b/src/core/jsonschema/jsonschema.cc index 5c23d5369..0c523e084 100644 --- a/src/core/jsonschema/jsonschema.cc +++ b/src/core/jsonschema/jsonschema.cc @@ -443,7 +443,8 @@ auto sourcemeta::core::vocabularies(const SchemaResolver &resolver, // complexity of the generic `id` function. assert(schema_dialect.defines("$id") && schema_dialect.at("$id").is_string() && - schema_dialect.at("$id").to_string() == dialect); + URI::canonicalize(schema_dialect.at("$id").to_string()) == + URI::canonicalize(dialect)); /* * (4) Retrieve the vocabularies explicitly or implicitly declared by the diff --git a/src/core/jsonschema/official_resolver.in.cc b/src/core/jsonschema/official_resolver.in.cc index ca1269b18..d3796f9a3 100644 --- a/src/core/jsonschema/official_resolver.in.cc +++ b/src/core/jsonschema/official_resolver.in.cc @@ -10,12 +10,6 @@ auto sourcemeta::core::schema_official_resolver(std::string_view identifier) "https://json-schema.org/draft/2020-12/hyper-schema") { return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_HYPERSCHEMA_2020_12@)EOF"); - } else if (identifier == - "https://json-schema.org/draft/2020-12/hyper-schema#") { - auto schema = sourcemeta::core::parse_json( - R"EOF(@METASCHEMA_HYPERSCHEMA_2020_12@)EOF"); - schema.at("$id").into(sourcemeta::core::JSON{identifier}); - return schema; } else if (identifier == "https://json-schema.org/draft/2020-12/meta/applicator") { return sourcemeta::core::parse_json( @@ -60,10 +54,12 @@ auto sourcemeta::core::schema_official_resolver(std::string_view identifier) // Just for compatibility given that this is such a common issue } else if (identifier == "https://json-schema.org/draft/2020-12/schema#") { - auto schema = sourcemeta::core::parse_json( + return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_JSONSCHEMA_2020_12@)EOF"); - schema.at("$id").into(sourcemeta::core::JSON{identifier}); - return schema; + } else if (identifier == + "https://json-schema.org/draft/2020-12/hyper-schema#") { + return sourcemeta::core::parse_json( + R"EOF(@METASCHEMA_HYPERSCHEMA_2020_12@)EOF"); // JSON Schema 2019-09 } else if (identifier == "https://json-schema.org/draft/2019-09/schema") { @@ -73,12 +69,6 @@ auto sourcemeta::core::schema_official_resolver(std::string_view identifier) "https://json-schema.org/draft/2019-09/hyper-schema") { return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_HYPERSCHEMA_2019_09@)EOF"); - } else if (identifier == - "https://json-schema.org/draft/2019-09/hyper-schema#") { - auto schema = sourcemeta::core::parse_json( - R"EOF(@METASCHEMA_HYPERSCHEMA_2019_09@)EOF"); - schema.at("$id").into(sourcemeta::core::JSON{identifier}); - return schema; } else if (identifier == "https://json-schema.org/draft/2019-09/meta/applicator") { return sourcemeta::core::parse_json( @@ -119,10 +109,12 @@ auto sourcemeta::core::schema_official_resolver(std::string_view identifier) // Just for compatibility given that this is such a common issue } else if (identifier == "https://json-schema.org/draft/2019-09/schema#") { - auto schema = sourcemeta::core::parse_json( + return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_JSONSCHEMA_2019_09@)EOF"); - schema.at("$id").into(sourcemeta::core::JSON{identifier}); - return schema; + } else if (identifier == + "https://json-schema.org/draft/2019-09/hyper-schema#") { + return sourcemeta::core::parse_json( + R"EOF(@METASCHEMA_HYPERSCHEMA_2019_09@)EOF"); // JSON Schema Draft7 } else if (identifier == "http://json-schema.org/draft-07/schema#" ||