diff --git a/src/core/jsonschema/transformer.cc b/src/core/jsonschema/transformer.cc index e79cc7be9..20e98aab8 100644 --- a/src/core/jsonschema/transformer.cc +++ b/src/core/jsonschema/transformer.cc @@ -26,7 +26,6 @@ auto calculate_health_percentage(const std::size_t subschemas, -> std::uint8_t { assert(failed_subschemas <= subschemas); const auto result{100 - (failed_subschemas * 100 / subschemas)}; - assert(result >= 0); assert(result <= 100); return static_cast(result); } diff --git a/src/extension/alterschema/CMakeLists.txt b/src/extension/alterschema/CMakeLists.txt index 9bb24d580..6c3fa8edf 100644 --- a/src/extension/alterschema/CMakeLists.txt +++ b/src/extension/alterschema/CMakeLists.txt @@ -57,6 +57,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema linter/duplicate_anyof_branches.h linter/else_without_if.h linter/if_without_then_else.h + linter/required_properties_default.h linter/max_contains_without_contains.h linter/min_contains_without_contains.h linter/modern_official_dialect_with_empty_fragment.h diff --git a/src/extension/alterschema/alterschema.cc b/src/extension/alterschema/alterschema.cc index 3312b4825..0dfe82ddf 100644 --- a/src/extension/alterschema/alterschema.cc +++ b/src/extension/alterschema/alterschema.cc @@ -71,6 +71,7 @@ contains_any(const Vocabularies &container, #include "linter/properties_default.h" #include "linter/property_names_default.h" #include "linter/property_names_type_default.h" +#include "linter/required_properties_default.h" #include "linter/single_type_array.h" #include "linter/then_empty.h" #include "linter/then_without_if.h" @@ -114,6 +115,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); diff --git a/src/extension/alterschema/linter/required_properties_default.h b/src/extension/alterschema/linter/required_properties_default.h new file mode 100644 index 000000000..1d4b45ba4 --- /dev/null +++ b/src/extension/alterschema/linter/required_properties_default.h @@ -0,0 +1,73 @@ +class RequiredPropertiesDefault final : public SchemaTransformRule { +public: + RequiredPropertiesDefault() + : SchemaTransformRule{"required_properties_default", + "Properties not listed in `required` add " + "unnecessary constraints to `properties`"} {}; + + [[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 { + return contains_any( + vocabularies, + {"https://json-schema.org/draft/2020-12/vocab/validation", + "https://json-schema.org/draft/2019-09/vocab/validation", + "http://json-schema.org/draft-07/schema#", + "http://json-schema.org/draft-06/schema#", + "http://json-schema.org/draft-04/schema#"}) && + schema.is_object() && schema.defines("required") && + schema.at("required").is_array() && !schema.at("required").empty() && + schema.defines("properties") && + schema.at("properties").is_object() && + !schema.at("properties").empty() && has_extra_properties(schema); + } + + auto transform(JSON &schema) const -> void override { + const auto &required_array = schema.at("required").as_array(); + std::unordered_set required_names; + for (const auto &item : required_array) { + if (item.is_string()) { + required_names.insert(item.to_string()); + } + } + + std::vector properties_to_remove; + for (const auto &entry : schema.at("properties").as_object()) { + if (required_names.find(entry.first) == required_names.end()) { + properties_to_remove.emplace_back(entry.first); + } + } + + for (const auto &property_name : properties_to_remove) { + schema.at("properties").erase(property_name); + } + } + +private: + auto has_extra_properties(const sourcemeta::core::JSON &schema) const + -> bool { + const auto &required_array = schema.at("required").as_array(); + const auto &properties_obj = schema.at("properties").as_object(); + + std::unordered_set required_names; + for (const auto &item : required_array) { + if (item.is_string()) { + required_names.insert(item.to_string()); + } + } + + for (const auto &entry : properties_obj) { + if (required_names.find(entry.first) == required_names.end()) { + return true; + } + } + + return false; + } +}; diff --git a/test/alterschema/alterschema_lint_2019_09_test.cc b/test/alterschema/alterschema_lint_2019_09_test.cc index 171d82775..d34c6da86 100644 --- a/test/alterschema/alterschema_lint_2019_09_test.cc +++ b/test/alterschema/alterschema_lint_2019_09_test.cc @@ -2294,3 +2294,55 @@ TEST(AlterSchema_lint_2019_09, property_names_default_1) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_2019_09, required_properties_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "required": [ "a", "b" ], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" }, + "c": { "type": "boolean" } + } + })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": "object", + "required": [ "a", "b" ], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, required_properties_default_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "required": [ "name" ], + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" }, + "email": { "type": "string" }, + "active": { "type": "boolean" } + } + })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", + "required": [ "name" ], + "properties": { + "name": { "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 bbad7d002..bd1a8a1b7 100644 --- a/test/alterschema/alterschema_lint_2020_12_test.cc +++ b/test/alterschema/alterschema_lint_2020_12_test.cc @@ -2528,3 +2528,55 @@ TEST(AlterSchema_lint_2020_12, property_names_default_1) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_2020_12, required_properties_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": [ "a", "b" ], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" }, + "c": { "type": "boolean" } + } + })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": "object", + "required": [ "a", "b" ], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, required_properties_default_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "required": [ "name" ], + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" }, + "email": { "type": "string" }, + "active": { "type": "boolean" } + } + })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", + "required": [ "name" ], + "properties": { + "name": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/alterschema/alterschema_lint_draft4_test.cc b/test/alterschema/alterschema_lint_draft4_test.cc index 2321c065b..b0edef270 100644 --- a/test/alterschema/alterschema_lint_draft4_test.cc +++ b/test/alterschema/alterschema_lint_draft4_test.cc @@ -1141,3 +1141,55 @@ TEST(AlterSchema_lint_draft4, EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_draft4, required_properties_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": [ "a", "b" ], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" }, + "c": { "type": "boolean" } + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": [ "a", "b" ], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft4, required_properties_default_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "required": [ "name" ], + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" }, + "email": { "type": "string" }, + "active": { "type": "boolean" } + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "required": [ "name" ], + "properties": { + "name": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/alterschema/alterschema_lint_draft6_test.cc b/test/alterschema/alterschema_lint_draft6_test.cc index 71a90b24b..db2b86133 100644 --- a/test/alterschema/alterschema_lint_draft6_test.cc +++ b/test/alterschema/alterschema_lint_draft6_test.cc @@ -1462,3 +1462,55 @@ TEST(AlterSchema_lint_draft6, property_names_default_1) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_draft6, required_properties_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "required": [ "a", "b" ], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" }, + "c": { "type": "boolean" } + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "required": [ "a", "b" ], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft6, required_properties_default_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "required": [ "name" ], + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" }, + "email": { "type": "string" }, + "active": { "type": "boolean" } + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "required": [ "name" ], + "properties": { + "name": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/alterschema/alterschema_lint_draft7_test.cc b/test/alterschema/alterschema_lint_draft7_test.cc index 3a4268637..704232f7f 100644 --- a/test/alterschema/alterschema_lint_draft7_test.cc +++ b/test/alterschema/alterschema_lint_draft7_test.cc @@ -1718,3 +1718,55 @@ TEST(AlterSchema_lint_draft7, property_names_default_1) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_draft7, required_properties_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ "a", "b" ], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" }, + "c": { "type": "boolean" } + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ "a", "b" ], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft7, required_properties_default_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ "name" ], + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" }, + "email": { "type": "string" }, + "active": { "type": "boolean" } + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ "name" ], + "properties": { + "name": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +}