diff --git a/src/alterschema/canonicalizer/next/type_with_applicator_to_allof.h b/src/alterschema/canonicalizer/next/type_with_applicator_to_allof.h index 0d0f8c96c..0ad5fd017 100644 --- a/src/alterschema/canonicalizer/next/type_with_applicator_to_allof.h +++ b/src/alterschema/canonicalizer/next/type_with_applicator_to_allof.h @@ -124,7 +124,8 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { for (const auto &entry : schema.as_object()) { if (entry.first == "not" || entry.first == "anyOf" || entry.first == "allOf" || entry.first == "oneOf" || - entry.first == "$schema" || entry.first == "id") { + entry.first == "$schema" || entry.first == "id" || + entry.first == "$id") { continue; } typed_branch.assign(entry.first, entry.second); @@ -186,6 +187,9 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { if (schema.defines("id")) { new_schema.assign("id", schema.at("id")); } + if (schema.defines("$id")) { + new_schema.assign("$id", schema.at("$id")); + } new_schema.assign("allOf", std::move(new_allof)); schema.into(std::move(new_schema)); } diff --git a/src/alterschema/common/enum_with_type.h b/src/alterschema/common/enum_with_type.h index ec34be038..1d6e2fd33 100644 --- a/src/alterschema/common/enum_with_type.h +++ b/src/alterschema/common/enum_with_type.h @@ -30,9 +30,14 @@ class EnumWithType final : public SchemaTransformRule { schema.defines("enum") && schema.at("enum").is_array()); const auto current_types{parse_schema_type(schema.at("type"))}; + const bool integer_matches_integral{ + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_6) && + current_types.test(std::to_underlying(JSON::Type::Integer))}; ONLY_CONTINUE_IF(std::ranges::all_of( - schema.at("enum").as_array(), [¤t_types](const auto &item) { - return current_types.test(std::to_underlying(item.type())); + schema.at("enum").as_array(), + [¤t_types, integer_matches_integral](const auto &item) { + return current_types.test(std::to_underlying(item.type())) || + (integer_matches_integral && item.is_integral()); })); return APPLIES_TO_KEYWORDS("enum", "type"); diff --git a/test/alterschema/alterschema_canonicalize_draft6_test.cc b/test/alterschema/alterschema_canonicalize_draft6_test.cc index e06ee4fb9..3cb7d5830 100644 --- a/test/alterschema/alterschema_canonicalize_draft6_test.cc +++ b/test/alterschema/alterschema_canonicalize_draft6_test.cc @@ -4476,3 +4476,192 @@ TEST_F(CanonicalizerDraft6Test, enum_assertion_uniqueItems_wrapped) { CANONICALIZE_NEXT(document, expected, *compiled_meta_); } + +TEST_F(CanonicalizerDraft6Test, type_applicator_preserves_dollar_id) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://example.com/schema", + "type": "string", + "not": { "pattern": "^admin" } + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://example.com/schema", + "allOf": [ + { + "not": { + "anyOf": [ + { "enum": [ null ] }, + { "enum": [ false, true ] }, + { + "type": "object", + "minProperties": 0, + "propertyNames": true, + "properties": {}, + "patternProperties": {}, + "additionalProperties": true + }, + { + "type": "array", + "minItems": 0, + "uniqueItems": false, + "items": true + }, + { + "type": "string", + "pattern": "^admin", + "minLength": 0 + }, + { "type": "number" } + ] + } + }, + { "type": "string", "minLength": 0 } + ] + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft6Test, const_with_incompatible_type) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "string", + "const": 42 + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON( + false + )JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft6Test, const_null_with_string_type) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "string", + "const": null + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON( + false + )JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft6Test, integer_enum_with_real_equivalent) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "integer", + "enum": [ 3, 3.0, "hello" ] + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "allOf": [ + { + "anyOf": [ + { "enum": [ null ] }, + { "enum": [ false, true ] }, + { + "type": "object", + "minProperties": 0, + "propertyNames": true, + "properties": {}, + "patternProperties": {}, + "additionalProperties": true + }, + { + "type": "array", + "minItems": 0, + "uniqueItems": false, + "items": true + }, + { "type": "string", "minLength": 0 }, + { "type": "number", "multipleOf": 1 } + ] + }, + { "enum": [ 3, 3.0 ] } + ] + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft6Test, property_names_false_restricts_objects) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "propertyNames": false + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "minProperties": 0, + "propertyNames": false, + "properties": {}, + "patternProperties": {}, + "additionalProperties": true + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft6Test, contains_false_restricts_arrays) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "array", + "contains": false + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "array", + "minItems": 0, + "uniqueItems": false, + "contains": false, + "items": true + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft6Test, exclusive_minimum_subsumes_minimum) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "number", + "minimum": 5, + "exclusiveMinimum": 5 + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "number", + "exclusiveMinimum": 5 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft6Test, draft4_id_becomes_unknown_keyword) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "id": "http://old.com", + "$id": "http://new.com", + "type": "string" + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://new.com", + "x-id": "http://old.com", + "type": "string", + "minLength": 0 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +}