diff --git a/src/extension/alterschema/CMakeLists.txt b/src/extension/alterschema/CMakeLists.txt index ee43cbbab..1e7632925 100644 --- a/src/extension/alterschema/CMakeLists.txt +++ b/src/extension/alterschema/CMakeLists.txt @@ -80,6 +80,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema linter/duplicate_examples.h linter/enum_to_const.h linter/equal_numeric_bounds_to_const.h + linter/forbid_empty_enum.h linter/items_array_default.h linter/items_schema_default.h linter/multiple_of_default.h diff --git a/src/extension/alterschema/alterschema.cc b/src/extension/alterschema/alterschema.cc index 131b20c7b..ac8095862 100644 --- a/src/extension/alterschema/alterschema.cc +++ b/src/extension/alterschema/alterschema.cc @@ -108,6 +108,7 @@ inline auto APPLIES_TO_POINTERS(std::vector &&keywords) #include "linter/duplicate_examples.h" #include "linter/enum_to_const.h" #include "linter/equal_numeric_bounds_to_const.h" +#include "linter/forbid_empty_enum.h" #include "linter/items_array_default.h" #include "linter/items_schema_default.h" #include "linter/multiple_of_default.h" @@ -222,6 +223,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); diff --git a/src/extension/alterschema/linter/forbid_empty_enum.h b/src/extension/alterschema/linter/forbid_empty_enum.h new file mode 100644 index 000000000..8f7957160 --- /dev/null +++ b/src/extension/alterschema/linter/forbid_empty_enum.h @@ -0,0 +1,31 @@ +class ForbidEmptyEnum final : public SchemaTransformRule { +public: + using mutates = std::false_type; + using reframe_after_transform = std::false_type; + ForbidEmptyEnum() + : SchemaTransformRule{"forbid_empty_enum", + "enum must contain at least one value"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> sourcemeta::core::SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1}) && + schema.is_object() && schema.defines("enum") && + schema.at("enum").is_array() && schema.at("enum").empty()); + return APPLIES_TO_KEYWORDS("enum"); + } +}; diff --git a/test/alterschema/alterschema_lint_2019_09_test.cc b/test/alterschema/alterschema_lint_2019_09_test.cc index b68b20641..0777dc833 100644 --- a/test/alterschema/alterschema_lint_2019_09_test.cc +++ b/test/alterschema/alterschema_lint_2019_09_test.cc @@ -4494,3 +4494,71 @@ TEST(AlterSchema_lint_2019_09, empty_object_as_true_1) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_2019_09, forbid_empty_enum_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Example", + "description": "Example schema", + "examples": [1], + "enum": [] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "forbid_empty_enum", + "enum must contain at least one value", false); +} + +TEST(AlterSchema_lint_2019_09, forbid_empty_enum_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Example", + "description": "Example schema", + "examples": [1], + "enum": [1, 2] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2019_09, forbid_empty_enum_3) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Example", + "description": "Example schema", + "examples": [1], + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2019_09, forbid_empty_enum_4) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Example", + "description": "Example schema", + "examples": [{}], + "properties": { + "foo": { + "enum": [] + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "forbid_empty_enum", + "enum must contain at least one value", false); +} diff --git a/test/alterschema/alterschema_lint_2020_12_test.cc b/test/alterschema/alterschema_lint_2020_12_test.cc index ba496eff7..f70c8545a 100644 --- a/test/alterschema/alterschema_lint_2020_12_test.cc +++ b/test/alterschema/alterschema_lint_2020_12_test.cc @@ -9664,3 +9664,71 @@ TEST(AlterSchema_lint_2020_12, object_oneof_required_not_required_6) { "can be elevated", true); } + +TEST(AlterSchema_lint_2020_12, forbid_empty_enum_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Example", + "description": "Example schema", + "examples": [1], + "enum": [] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "forbid_empty_enum", + "enum must contain at least one value", false); +} + +TEST(AlterSchema_lint_2020_12, forbid_empty_enum_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Example", + "description": "Example schema", + "examples": [1], + "enum": [1, 2] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, forbid_empty_enum_3) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Example", + "description": "Example schema", + "examples": [1], + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, forbid_empty_enum_4) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Example", + "description": "Example schema", + "examples": [{}], + "properties": { + "foo": { + "enum": [] + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "forbid_empty_enum", + "enum must contain at least one value", false); +} diff --git a/test/alterschema/alterschema_lint_draft4_test.cc b/test/alterschema/alterschema_lint_draft4_test.cc index 6c62d61a6..134f958b3 100644 --- a/test/alterschema/alterschema_lint_draft4_test.cc +++ b/test/alterschema/alterschema_lint_draft4_test.cc @@ -2604,3 +2604,67 @@ TEST(AlterSchema_lint_draft4, EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_draft4, forbid_empty_enum_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Example", + "description": "Example schema", + "enum": [] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "forbid_empty_enum", + "enum must contain at least one value", false); +} + +TEST(AlterSchema_lint_draft4, forbid_empty_enum_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Example", + "description": "Example schema", + "enum": [1, 2] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft4, forbid_empty_enum_3) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Example", + "description": "Example schema", + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft4, forbid_empty_enum_4) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Example", + "description": "Example schema", + "properties": { + "foo": { + "enum": [] + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "forbid_empty_enum", + "enum must contain at least one value", false); +} diff --git a/test/alterschema/alterschema_lint_draft6_test.cc b/test/alterschema/alterschema_lint_draft6_test.cc index 6193d80bb..a13895961 100644 --- a/test/alterschema/alterschema_lint_draft6_test.cc +++ b/test/alterschema/alterschema_lint_draft6_test.cc @@ -2929,3 +2929,71 @@ TEST(AlterSchema_lint_draft6, empty_object_as_true_1) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_draft6, forbid_empty_enum_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "Example", + "description": "Example schema", + "examples": [1], + "enum": [] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "forbid_empty_enum", + "enum must contain at least one value", false); +} + +TEST(AlterSchema_lint_draft6, forbid_empty_enum_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "Example", + "description": "Example schema", + "examples": [1], + "enum": [1, 2] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft6, forbid_empty_enum_3) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "Example", + "description": "Example schema", + "examples": [1], + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft6, forbid_empty_enum_4) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "Example", + "description": "Example schema", + "examples": [{}], + "properties": { + "foo": { + "enum": [] + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "forbid_empty_enum", + "enum must contain at least one value", false); +} diff --git a/test/alterschema/alterschema_lint_draft7_test.cc b/test/alterschema/alterschema_lint_draft7_test.cc index 14b0734c2..eae69230b 100644 --- a/test/alterschema/alterschema_lint_draft7_test.cc +++ b/test/alterschema/alterschema_lint_draft7_test.cc @@ -3507,3 +3507,71 @@ TEST(AlterSchema_lint_draft7, "illustrate the expected data", false); } + +TEST(AlterSchema_lint_draft7, forbid_empty_enum_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Example", + "description": "Example schema", + "examples": [1], + "enum": [] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "forbid_empty_enum", + "enum must contain at least one value", false); +} + +TEST(AlterSchema_lint_draft7, forbid_empty_enum_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Example", + "description": "Example schema", + "examples": [1], + "enum": [1, 2] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft7, forbid_empty_enum_3) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Example", + "description": "Example schema", + "examples": [1], + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft7, forbid_empty_enum_4) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Example", + "description": "Example schema", + "examples": [{}], + "properties": { + "foo": { + "enum": [] + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "forbid_empty_enum", + "enum must contain at least one value", false); +}