From b4d61c81a342b2f37ef219d79877710fa7831f6f Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 23 Dec 2025 21:10:34 -0400 Subject: [PATCH] Implement a linter rule for simpler property names Signed-off-by: Juan Cruz Viotti --- config.cmake.in | 2 + .../sourcemeta/core/jsonschema_transform.h | 9 +- src/extension/alterschema/CMakeLists.txt | 3 + src/extension/alterschema/alterschema.cc | 3 + .../common/simple_properties_identifiers.h | 65 ++++++ .../alterschema_canonicalize_2020_12_test.cc | 45 ++++ .../alterschema_canonicalize_draft7_test.cc | 40 ++++ .../alterschema_lint_2019_09_test.cc | 101 +++++++++ .../alterschema_lint_2020_12_test.cc | 211 ++++++++++++++++++ .../alterschema_lint_draft4_test.cc | 89 ++++++++ .../alterschema_lint_draft6_test.cc | 94 ++++++++ .../alterschema_lint_draft7_test.cc | 94 ++++++++ test/alterschema/alterschema_test_utils.h | 26 ++- .../jsonschema/jsonschema_transformer_test.cc | 19 +- 14 files changed, 782 insertions(+), 19 deletions(-) create mode 100644 src/extension/alterschema/common/simple_properties_identifiers.h diff --git a/config.cmake.in b/config.cmake.in index 2db4b0aaa..2a663c02b 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -92,6 +92,8 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) find_dependency(mpdecimal CONFIG) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") + find_dependency(PCRE2 CONFIG) + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_regex.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonpointer.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonschema.cmake") diff --git a/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h b/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h index 6b23e9443..eb6090614 100644 --- a/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h +++ b/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h @@ -251,10 +251,11 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaTransformer { const SchemaTransformRule::Result &)>; /// Apply the bundle of rules to a schema - auto apply(JSON &schema, const SchemaWalker &walker, - const SchemaResolver &resolver, const Callback &callback, - const std::optional &default_dialect = std::nullopt, - const std::optional &default_id = std::nullopt) const + [[nodiscard]] auto + apply(JSON &schema, const SchemaWalker &walker, + const SchemaResolver &resolver, const Callback &callback, + const std::optional &default_dialect = std::nullopt, + const std::optional &default_id = std::nullopt) const -> std::pair; /// Report back the rules from the bundle that need to be applied to a schema diff --git a/src/extension/alterschema/CMakeLists.txt b/src/extension/alterschema/CMakeLists.txt index a695ee7f7..808167af0 100644 --- a/src/extension/alterschema/CMakeLists.txt +++ b/src/extension/alterschema/CMakeLists.txt @@ -51,6 +51,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema common/non_applicable_type_specific_keywords.h common/not_false.h common/required_properties_in_properties.h + common/simple_properties_identifiers.h common/single_type_array.h common/then_empty.h common/then_without_if.h @@ -96,3 +97,5 @@ endif() target_link_libraries(sourcemeta_core_alterschema PUBLIC sourcemeta::core::jsonschema) +target_link_libraries(sourcemeta_core_alterschema PRIVATE + sourcemeta::core::regex) diff --git a/src/extension/alterschema/alterschema.cc b/src/extension/alterschema/alterschema.cc index 058dbb516..85e72cd40 100644 --- a/src/extension/alterschema/alterschema.cc +++ b/src/extension/alterschema/alterschema.cc @@ -1,4 +1,5 @@ #include +#include // For built-in rules #include // std::sort, std::unique @@ -78,6 +79,7 @@ inline auto APPLIES_TO_POINTERS(std::vector &&keywords) #include "common/not_false.h" #include "common/orphan_definitions.h" #include "common/required_properties_in_properties.h" +#include "common/simple_properties_identifiers.h" #include "common/single_type_array.h" #include "common/then_empty.h" #include "common/then_without_if.h" @@ -166,6 +168,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); if (mode == AlterSchemaMode::Canonicalizer) { diff --git a/src/extension/alterschema/common/simple_properties_identifiers.h b/src/extension/alterschema/common/simple_properties_identifiers.h new file mode 100644 index 000000000..e4e365191 --- /dev/null +++ b/src/extension/alterschema/common/simple_properties_identifiers.h @@ -0,0 +1,65 @@ +class SimplePropertiesIdentifiers final : public SchemaTransformRule { +public: + SimplePropertiesIdentifiers() + // Inspired by + // https://json-structure.github.io/core/draft-vasters-json-structure-core.html#section-3.6 + : SchemaTransformRule{ + "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily mapped to " + "programming languages (matching [A-Za-z_][A-Za-z0-9_]*)"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &root, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &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_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + 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_2_Hyper, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_1_Hyper})); + ONLY_CONTINUE_IF(schema.is_object() && schema.defines("properties") && + schema.at("properties").is_object() && + !schema.at("properties").empty()); + + if (vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Core, + Vocabularies::Known::JSON_Schema_2019_09_Core})) { + // Skip meta-schemas with `$vocabulary` (2019-09+) + // We check the current schema resource (not root) to handle bundled + // schemas + const auto base_location{frame.traverse(location.base)}; + if (base_location.has_value()) { + const auto &resource{get(root, base_location->get().pointer)}; + ONLY_CONTINUE_IF(!resource.is_object() || + !resource.defines("$vocabulary")); + } + } else { + // Skip pre-vocabulary meta-schemas + ONLY_CONTINUE_IF(location.base != location.dialect && + (location.base + "#") != location.dialect); + } + + std::vector offenders; + for (const auto &entry : schema.at("properties").as_object()) { + static const Regex IDENTIFIER_PATTERN{ + to_regex("^[A-Za-z_][A-Za-z0-9_]*$").value()}; + if (!matches(IDENTIFIER_PATTERN, entry.first)) { + offenders.push_back(Pointer{"properties", entry.first}); + } + } + + ONLY_CONTINUE_IF(!offenders.empty()); + return APPLIES_TO_POINTERS(std::move(offenders)); + } +}; diff --git a/test/alterschema/alterschema_canonicalize_2020_12_test.cc b/test/alterschema/alterschema_canonicalize_2020_12_test.cc index a39933c37..50a5c4bef 100644 --- a/test/alterschema/alterschema_canonicalize_2020_12_test.cc +++ b/test/alterschema/alterschema_canonicalize_2020_12_test.cc @@ -1322,3 +1322,48 @@ TEST(AlterSchema_canonicalize_2020_12, EXPECT_EQ(document, expected); } + +TEST(AlterSchema_canonicalize_2020_12, + simple_properties_identifiers_skip_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/my-metaschema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object", + "minProperties": 0, + "properties": { + "foo-bar": { "type": "string", "minLength": 0 } + } + })JSON"); + + CANONICALIZE_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_canonicalize_2020_12, + simple_properties_identifiers_applies_non_meta) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/my-schema", + "type": "object", + "minProperties": 0, + "properties": { + "foo-bar": { "type": "string", "minLength": 0 } + } + })JSON"); + + CANONICALIZE_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily " + "mapped to programming languages (matching " + "[A-Za-z_][A-Za-z0-9_]*)"); +} diff --git a/test/alterschema/alterschema_canonicalize_draft7_test.cc b/test/alterschema/alterschema_canonicalize_draft7_test.cc index 4d8fc9ea7..81cd81060 100644 --- a/test/alterschema/alterschema_canonicalize_draft7_test.cc +++ b/test/alterschema/alterschema_canonicalize_draft7_test.cc @@ -569,3 +569,43 @@ TEST(AlterSchema_canonicalize_draft7, min_properties_implicit_2) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_canonicalize_draft7, + simple_properties_identifiers_skip_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "type": "object", + "minProperties": 0, + "properties": { + "foo-bar": { "type": "string", "minLength": 0 } + } + })JSON"); + + CANONICALIZE_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_canonicalize_draft7, + simple_properties_identifiers_applies_non_meta) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/my-schema", + "type": "object", + "minProperties": 0, + "properties": { + "foo-bar": { "type": "string", "minLength": 0 } + } + })JSON"); + + CANONICALIZE_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily " + "mapped to programming languages (matching " + "[A-Za-z_][A-Za-z0-9_]*)"); +} diff --git a/test/alterschema/alterschema_lint_2019_09_test.cc b/test/alterschema/alterschema_lint_2019_09_test.cc index 8e47265bf..362a565ba 100644 --- a/test/alterschema/alterschema_lint_2019_09_test.cc +++ b/test/alterschema/alterschema_lint_2019_09_test.cc @@ -3896,3 +3896,104 @@ TEST(AlterSchema_lint_2019_09, EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_2019_09, simple_properties_identifiers_skip_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/my-metaschema", + "title": "My meta-schema", + "description": "A custom meta-schema", + "examples": [ {} ], + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true, + "https://json-schema.org/draft/2019-09/vocab/applicator": true + }, + "properties": { + "foo-bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2019_09, + simple_properties_identifiers_skip_metaschema_nested) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/my-metaschema", + "title": "My meta-schema", + "description": "A custom meta-schema", + "examples": [ {} ], + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true, + "https://json-schema.org/draft/2019-09/vocab/applicator": true + }, + "properties": { + "valid": { + "properties": { + "also-invalid": { "type": "number" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2019_09, simple_properties_identifiers_applies_non_meta) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/my-schema", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo-bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily " + "mapped to programming languages (matching " + "[A-Za-z_][A-Za-z0-9_]*)"); +} + +TEST(AlterSchema_lint_2019_09, + simple_properties_identifiers_skip_bundled_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/main", + "title": "Main schema", + "description": "A schema that bundles a meta-schema", + "examples": [ {} ], + "$ref": "https://example.com/my-metaschema", + "$defs": { + "metaschema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/my-metaschema", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true, + "https://json-schema.org/draft/2019-09/vocab/applicator": true + }, + "properties": { + "foo-bar": { "type": "string" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} diff --git a/test/alterschema/alterschema_lint_2020_12_test.cc b/test/alterschema/alterschema_lint_2020_12_test.cc index 3c67bd04d..b91827a64 100644 --- a/test/alterschema/alterschema_lint_2020_12_test.cc +++ b/test/alterschema/alterschema_lint_2020_12_test.cc @@ -6441,3 +6441,214 @@ TEST(AlterSchema_lint_2020_12, circular_ref_through_defs_1) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_2020_12, simple_properties_identifiers_valid) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string" }, + "bar_baz": { "type": "number" }, + "_private": { "type": "boolean" }, + "CamelCase": { "type": "object" }, + "name123": { "type": "array" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, simple_properties_identifiers_hyphen) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo-bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE( + traces, 0, "", "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily mapped " + "to programming languages (matching [A-Za-z_][A-Za-z0-9_]*)"); +} + +TEST(AlterSchema_lint_2020_12, + simple_properties_identifiers_starts_with_digit) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "123abc": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE( + traces, 0, "", "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily mapped " + "to programming languages (matching [A-Za-z_][A-Za-z0-9_]*)"); +} + +TEST(AlterSchema_lint_2020_12, simple_properties_identifiers_special_char) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo@bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE( + traces, 0, "", "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily mapped " + "to programming languages (matching [A-Za-z_][A-Za-z0-9_]*)"); +} + +TEST(AlterSchema_lint_2020_12, simple_properties_identifiers_space) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE( + traces, 0, "", "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily mapped " + "to programming languages (matching [A-Za-z_][A-Za-z0-9_]*)"); +} + +TEST(AlterSchema_lint_2020_12, simple_properties_identifiers_nested) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "valid": { + "type": "object", + "properties": { + "invalid-name": { "type": "string" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE( + traces, 0, "/properties/valid", "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily mapped " + "to programming languages (matching [A-Za-z_][A-Za-z0-9_]*)"); +} + +TEST(AlterSchema_lint_2020_12, simple_properties_identifiers_skip_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/my-metaschema", + "title": "My meta-schema", + "description": "A custom meta-schema", + "examples": [ {} ], + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true + }, + "properties": { + "foo-bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, + simple_properties_identifiers_skip_metaschema_nested) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/my-metaschema", + "title": "My meta-schema", + "description": "A custom meta-schema", + "examples": [ {} ], + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true + }, + "properties": { + "valid": { + "properties": { + "also-invalid": { "type": "number" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, + simple_properties_identifiers_skip_bundled_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/main", + "title": "Main schema", + "description": "A schema that bundles a meta-schema", + "examples": [ {} ], + "$ref": "https://example.com/my-metaschema", + "$defs": { + "metaschema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/my-metaschema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true + }, + "properties": { + "foo-bar": { "type": "string" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} diff --git a/test/alterschema/alterschema_lint_draft4_test.cc b/test/alterschema/alterschema_lint_draft4_test.cc index 2f13cdec5..e1ab51be4 100644 --- a/test/alterschema/alterschema_lint_draft4_test.cc +++ b/test/alterschema/alterschema_lint_draft4_test.cc @@ -2204,3 +2204,92 @@ TEST(AlterSchema_lint_draft4, EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_draft4, simple_properties_identifiers_skip_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://json-schema.org/draft-04/schema#", + "title": "My Meta-Schema", + "description": "A meta-schema for testing", + "properties": { + "foo-bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft4, + simple_properties_identifiers_skip_metaschema_nested) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://json-schema.org/draft-04/schema#", + "title": "My Meta-Schema", + "description": "A meta-schema for testing", + "properties": { + "valid": { + "properties": { + "also-invalid": { "type": "number" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft4, simple_properties_identifiers_applies_non_meta) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://example.com/my-schema", + "title": "Test", + "description": "A test schema", + "properties": { + "foo-bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily " + "mapped to programming languages (matching " + "[A-Za-z_][A-Za-z0-9_]*)"); +} + +TEST(AlterSchema_lint_draft4, + simple_properties_identifiers_skip_bundled_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://example.com/main", + "title": "Main schema", + "description": "A schema that bundles a meta-schema", + "allOf": [ + { "$ref": "#/definitions/metaschema" } + ], + "definitions": { + "metaschema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://json-schema.org/draft-04/schema#", + "title": "Bundled Meta-Schema", + "description": "A bundled meta-schema", + "properties": { + "foo-bar": { "type": "string" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} diff --git a/test/alterschema/alterschema_lint_draft6_test.cc b/test/alterschema/alterschema_lint_draft6_test.cc index 123e57e6b..1bfda0bd2 100644 --- a/test/alterschema/alterschema_lint_draft6_test.cc +++ b/test/alterschema/alterschema_lint_draft6_test.cc @@ -2461,3 +2461,97 @@ TEST(AlterSchema_lint_draft6, EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_draft6, simple_properties_identifiers_skip_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://json-schema.org/draft-06/schema#", + "title": "My Meta-Schema", + "description": "A meta-schema for testing", + "examples": [ {} ], + "properties": { + "foo-bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft6, + simple_properties_identifiers_skip_metaschema_nested) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://json-schema.org/draft-06/schema#", + "title": "My Meta-Schema", + "description": "A meta-schema for testing", + "examples": [ {} ], + "properties": { + "valid": { + "properties": { + "also-invalid": { "type": "number" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft6, simple_properties_identifiers_applies_non_meta) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "https://example.com/my-schema", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo-bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily " + "mapped to programming languages (matching " + "[A-Za-z_][A-Za-z0-9_]*)"); +} + +TEST(AlterSchema_lint_draft6, + simple_properties_identifiers_skip_bundled_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "https://example.com/main", + "title": "Main schema", + "description": "A schema that bundles a meta-schema", + "examples": [ {} ], + "allOf": [ + { "$ref": "#/definitions/metaschema" } + ], + "definitions": { + "metaschema": { + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://json-schema.org/draft-06/schema#", + "title": "Bundled Meta-Schema", + "description": "A bundled meta-schema", + "examples": [ {} ], + "properties": { + "foo-bar": { "type": "string" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} diff --git a/test/alterschema/alterschema_lint_draft7_test.cc b/test/alterschema/alterschema_lint_draft7_test.cc index 58a40c041..a967ee03e 100644 --- a/test/alterschema/alterschema_lint_draft7_test.cc +++ b/test/alterschema/alterschema_lint_draft7_test.cc @@ -2803,3 +2803,97 @@ TEST(AlterSchema_lint_draft7, EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_draft7, simple_properties_identifiers_skip_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "My Meta-Schema", + "description": "A meta-schema for testing", + "examples": [ {} ], + "properties": { + "foo-bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft7, + simple_properties_identifiers_skip_metaschema_nested) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "My Meta-Schema", + "description": "A meta-schema for testing", + "examples": [ {} ], + "properties": { + "valid": { + "properties": { + "also-invalid": { "type": "number" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft7, simple_properties_identifiers_applies_non_meta) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/my-schema", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo-bar": { "type": "string" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "simple_properties_identifiers", + "Set `properties` to identifier names that can be easily " + "mapped to programming languages (matching " + "[A-Za-z_][A-Za-z0-9_]*)"); +} + +TEST(AlterSchema_lint_draft7, + simple_properties_identifiers_skip_bundled_metaschema) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/main", + "title": "Main schema", + "description": "A schema that bundles a meta-schema", + "examples": [ {} ], + "allOf": [ + { "$ref": "#/definitions/metaschema" } + ], + "definitions": { + "metaschema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Bundled Meta-Schema", + "description": "A bundled meta-schema", + "examples": [ {} ], + "properties": { + "foo-bar": { "type": "string" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} diff --git a/test/alterschema/alterschema_test_utils.h b/test/alterschema/alterschema_test_utils.h index 6548530de..d92f4f571 100644 --- a/test/alterschema/alterschema_test_utils.h +++ b/test/alterschema/alterschema_test_utils.h @@ -46,16 +46,30 @@ static auto alterschema_test_resolver(std::string_view identifier) #define LINT_AND_FIX(traces) \ sourcemeta::core::SchemaTransformer bundle; \ sourcemeta::core::add(bundle, sourcemeta::core::AlterSchemaMode::Linter); \ - bundle.apply(document, sourcemeta::core::schema_walker, \ - alterschema_test_resolver, \ - [](const auto &, const auto &, const auto &, const auto &) {}); + [[maybe_unused]] const auto lint_result = bundle.apply( \ + document, sourcemeta::core::schema_walker, alterschema_test_resolver, \ + [](const auto &, const auto &, const auto &, const auto &) {}); #define CANONICALIZE(document) \ sourcemeta::core::SchemaTransformer bundle; \ sourcemeta::core::add(bundle, \ sourcemeta::core::AlterSchemaMode::Canonicalizer); \ - bundle.apply(document, sourcemeta::core::schema_walker, \ - alterschema_test_resolver, \ - [](const auto &, const auto &, const auto &, const auto &) {}); + [[maybe_unused]] const auto canonicalize_result = bundle.apply( \ + document, sourcemeta::core::schema_walker, alterschema_test_resolver, \ + [](const auto &, const auto &, const auto &, const auto &) {}); + +#define CANONICALIZE_WITHOUT_FIX(document, result, traces) \ + std::vector> \ + traces; \ + sourcemeta::core::SchemaTransformer bundle; \ + sourcemeta::core::add(bundle, \ + sourcemeta::core::AlterSchemaMode::Canonicalizer); \ + const auto result = bundle.check( \ + document, sourcemeta::core::schema_walker, alterschema_test_resolver, \ + [&traces](const auto &pointer, const auto &name, const auto &message, \ + const auto &outcome) { \ + traces.emplace_back(pointer, name, message, outcome); \ + }); #endif diff --git a/test/jsonschema/jsonschema_transformer_test.cc b/test/jsonschema/jsonschema_transformer_test.cc index c8fc66ddd..b7efd3690 100644 --- a/test/jsonschema/jsonschema_transformer_test.cc +++ b/test/jsonschema/jsonschema_transformer_test.cc @@ -213,10 +213,10 @@ TEST(JSONSchema_transformer, throw_if_no_dialect_invalid_default) { "qux": "xxx" })JSON"); - EXPECT_THROW(bundle.apply(document, sourcemeta::core::schema_walker, - sourcemeta::core::schema_resolver, - transformer_callback_noop, - "https://example.com/invalid"), + EXPECT_THROW(static_cast(bundle.apply( + document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, transformer_callback_noop, + "https://example.com/invalid")), sourcemeta::core::SchemaResolutionError); const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ @@ -298,8 +298,9 @@ TEST(JSONSchema_transformer, throw_on_rules_called_twice) { })JSON"); try { - bundle.apply(document, sourcemeta::core::schema_walker, - sourcemeta::core::schema_resolver, transformer_callback_noop); + [[maybe_unused]] const auto result = bundle.apply( + document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, transformer_callback_noop); FAIL(); } catch ( const sourcemeta::core::SchemaTransformRuleProcessedTwiceError &error) { @@ -1053,9 +1054,9 @@ TEST(JSONSchema_transformer, rereference_not_fixed_ref) { TestTransformTraces entries; try { - bundle.apply(document, sourcemeta::core::schema_walker, - sourcemeta::core::schema_resolver, - transformer_callback_trace(entries)); + [[maybe_unused]] const auto result = bundle.apply( + document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, transformer_callback_trace(entries)); FAIL() << "The transformation was expected to throw"; } catch (const sourcemeta::core::SchemaBrokenReferenceError &error) { EXPECT_EQ(error.identifier(), "#/definitions/foo");