diff --git a/src/alterschema/CMakeLists.txt b/src/alterschema/CMakeLists.txt index 2d9a6ab86..4997de859 100644 --- a/src/alterschema/CMakeLists.txt +++ b/src/alterschema/CMakeLists.txt @@ -34,6 +34,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema canonicalizer/next/implicit_array_keywords.h canonicalizer/next/implicit_object_keywords.h canonicalizer/next/type_with_applicator_to_allof.h + canonicalizer/next/unsatisfiable_exclusive_equal_bounds.h canonicalizer/next/unsatisfiable_type_and_enum.h # Common diff --git a/src/alterschema/alterschema.cc b/src/alterschema/alterschema.cc index 3988d2e1a..994cee814 100644 --- a/src/alterschema/alterschema.cc +++ b/src/alterschema/alterschema.cc @@ -7,6 +7,7 @@ // For built-in rules #include // std::sort, std::unique, std::ranges::none_of #include // std::array +#include // std::popcount #include // assert #include // std::floor #include // std::size_t @@ -131,6 +132,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "canonicalizer/next/implicit_array_keywords.h" #include "canonicalizer/next/implicit_object_keywords.h" #include "canonicalizer/next/type_with_applicator_to_allof.h" +#include "canonicalizer/next/unsatisfiable_exclusive_equal_bounds.h" #include "canonicalizer/next/unsatisfiable_type_and_enum.h" #include "canonicalizer/properties_implicit.h" #include "canonicalizer/type_array_to_any_of.h" @@ -239,6 +241,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); } diff --git a/src/alterschema/canonicalizer/next/dependencies_to_any_of.h b/src/alterschema/canonicalizer/next/dependencies_to_any_of.h index feab15ae8..498676056 100644 --- a/src/alterschema/canonicalizer/next/dependencies_to_any_of.h +++ b/src/alterschema/canonicalizer/next/dependencies_to_any_of.h @@ -30,8 +30,7 @@ class DependenciesToAnyOf final : public SchemaTransformRule { } auto transform(JSON &schema, const Result &) const -> void override { - auto result_branches{schema.defines("anyOf") ? schema.at("anyOf") - : JSON::make_array()}; + auto result_branches{JSON::make_array()}; std::vector processed; for (const auto &entry : schema.at("dependencies").as_object()) { diff --git a/src/alterschema/canonicalizer/next/exclusive_bounds_false_drop.h b/src/alterschema/canonicalizer/next/exclusive_bounds_false_drop.h index c7b3ae4c2..8db608ce6 100644 --- a/src/alterschema/canonicalizer/next/exclusive_bounds_false_drop.h +++ b/src/alterschema/canonicalizer/next/exclusive_bounds_false_drop.h @@ -21,7 +21,8 @@ class ExclusiveBoundsFalseDrop final : public SchemaTransformRule { vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_4) && schema.is_object() && schema.defines("type") && schema.at("type").is_string() && - schema.at("type").to_string() == "integer"); + (schema.at("type").to_string() == "integer" || + schema.at("type").to_string() == "number")); this->has_exclusive_min_ = schema.defines("exclusiveMinimum") && schema.at("exclusiveMinimum").is_boolean() && diff --git a/src/alterschema/canonicalizer/next/exclusive_maximum_boolean_integer_fold.h b/src/alterschema/canonicalizer/next/exclusive_maximum_boolean_integer_fold.h index b1db874b4..31778fda9 100644 --- a/src/alterschema/canonicalizer/next/exclusive_maximum_boolean_integer_fold.h +++ b/src/alterschema/canonicalizer/next/exclusive_maximum_boolean_integer_fold.h @@ -30,9 +30,37 @@ class ExclusiveMaximumBooleanIntegerFold final : public SchemaTransformRule { } auto transform(JSON &schema, const Result &) const -> void override { - auto new_maximum = schema.at("maximum"); - new_maximum += sourcemeta::core::JSON{-1}; - schema.assign("maximum", std::move(new_maximum)); + const auto &maximum{schema.at("maximum")}; + if (maximum.is_integer()) { + auto new_maximum = maximum; + new_maximum += sourcemeta::core::JSON{-1}; + schema.assign("maximum", std::move(new_maximum)); + } else if (maximum.is_decimal()) { + auto current{maximum.to_decimal()}; + auto floored{current.to_integral()}; + if (floored > current) { + floored -= sourcemeta::core::Decimal{1}; + } + + if (current.is_integer()) { + floored -= sourcemeta::core::Decimal{1}; + } + + if (floored.is_int64()) { + schema.assign("maximum", sourcemeta::core::JSON{floored.to_int64()}); + } else { + schema.assign("maximum", sourcemeta::core::JSON{std::move(floored)}); + } + } else { + const auto value{maximum.to_real()}; + auto floored{static_cast(std::floor(value))}; + if (std::floor(value) == value) { + floored -= 1; + } + + schema.assign("maximum", sourcemeta::core::JSON{floored}); + } + schema.erase("exclusiveMaximum"); } }; diff --git a/src/alterschema/canonicalizer/next/exclusive_minimum_boolean_integer_fold.h b/src/alterschema/canonicalizer/next/exclusive_minimum_boolean_integer_fold.h index b813d3b13..9e79176a5 100644 --- a/src/alterschema/canonicalizer/next/exclusive_minimum_boolean_integer_fold.h +++ b/src/alterschema/canonicalizer/next/exclusive_minimum_boolean_integer_fold.h @@ -30,9 +30,37 @@ class ExclusiveMinimumBooleanIntegerFold final : public SchemaTransformRule { } auto transform(JSON &schema, const Result &) const -> void override { - auto new_minimum = schema.at("minimum"); - new_minimum += sourcemeta::core::JSON{1}; - schema.assign("minimum", std::move(new_minimum)); + const auto &minimum{schema.at("minimum")}; + if (minimum.is_integer()) { + auto new_minimum = minimum; + new_minimum += sourcemeta::core::JSON{1}; + schema.assign("minimum", std::move(new_minimum)); + } else if (minimum.is_decimal()) { + auto current{minimum.to_decimal()}; + auto ceiled{current.to_integral()}; + if (ceiled < current) { + ceiled += sourcemeta::core::Decimal{1}; + } + + if (current.is_integer()) { + ceiled += sourcemeta::core::Decimal{1}; + } + + if (ceiled.is_int64()) { + schema.assign("minimum", sourcemeta::core::JSON{ceiled.to_int64()}); + } else { + schema.assign("minimum", sourcemeta::core::JSON{std::move(ceiled)}); + } + } else { + const auto value{minimum.to_real()}; + auto ceiled{static_cast(std::ceil(value))}; + if (std::ceil(value) == value) { + ceiled += 1; + } + + schema.assign("minimum", sourcemeta::core::JSON{ceiled}); + } + schema.erase("exclusiveMinimum"); } }; 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 058ef886b..9fd2b7ba7 100644 --- a/src/alterschema/canonicalizer/next/type_with_applicator_to_allof.h +++ b/src/alterschema/canonicalizer/next/type_with_applicator_to_allof.h @@ -209,7 +209,10 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { {allof_keyword, this->typed_branch_index_, keyword})}; return target.rebase(old_prefix, new_prefix); } else { - const Pointer new_prefix{current.concat({allof_keyword, 1, keyword})}; + const std::size_t typed_index{static_cast( + std::popcount(this->applicator_indices_))}; + const Pointer new_prefix{ + current.concat({allof_keyword, typed_index, keyword})}; return target.rebase(old_prefix, new_prefix); } } diff --git a/src/alterschema/canonicalizer/next/unsatisfiable_exclusive_equal_bounds.h b/src/alterschema/canonicalizer/next/unsatisfiable_exclusive_equal_bounds.h new file mode 100644 index 000000000..d6902813f --- /dev/null +++ b/src/alterschema/canonicalizer/next/unsatisfiable_exclusive_equal_bounds.h @@ -0,0 +1,44 @@ +class UnsatisfiableExclusiveEqualBounds final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; + UnsatisfiableExclusiveEqualBounds() + : SchemaTransformRule{ + "unsatisfiable_exclusive_equal_bounds", + "When `minimum` equals `maximum` and either boolean " + "`exclusiveMinimum` or `exclusiveMaximum` is true, " + "no value can satisfy the schema"} {}; + + [[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 + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_4) && + schema.is_object() && schema.defines("type") && + schema.at("type").is_string() && + (schema.at("type").to_string() == "number" || + schema.at("type").to_string() == "integer") && + schema.defines("minimum") && schema.at("minimum").is_number() && + schema.defines("maximum") && schema.at("maximum").is_number() && + schema.at("minimum") == schema.at("maximum")); + + const bool exclusive_min{schema.defines("exclusiveMinimum") && + schema.at("exclusiveMinimum").is_boolean() && + schema.at("exclusiveMinimum").to_boolean()}; + const bool exclusive_max{schema.defines("exclusiveMaximum") && + schema.at("exclusiveMaximum").is_boolean() && + schema.at("exclusiveMaximum").to_boolean()}; + ONLY_CONTINUE_IF(exclusive_min || exclusive_max); + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.into(JSON{false}); + } +}; diff --git a/src/alterschema/common/equal_numeric_bounds_to_enum.h b/src/alterschema/common/equal_numeric_bounds_to_enum.h index 40d6e99f2..4681b0912 100644 --- a/src/alterschema/common/equal_numeric_bounds_to_enum.h +++ b/src/alterschema/common/equal_numeric_bounds_to_enum.h @@ -28,7 +28,13 @@ class EqualNumericBoundsToEnum final : public SchemaTransformRule { schema.at("type").to_string() == "number") && schema.defines("minimum") && schema.at("minimum").is_number() && schema.defines("maximum") && schema.at("maximum").is_number() && - schema.at("minimum") == schema.at("maximum")); + schema.at("minimum") == schema.at("maximum") && + !(schema.defines("exclusiveMinimum") && + schema.at("exclusiveMinimum").is_boolean() && + schema.at("exclusiveMinimum").to_boolean()) && + !(schema.defines("exclusiveMaximum") && + schema.at("exclusiveMaximum").is_boolean() && + schema.at("exclusiveMaximum").to_boolean())); return APPLIES_TO_KEYWORDS("minimum", "maximum"); } diff --git a/test/alterschema/alterschema_canonicalize_draft4_test.cc b/test/alterschema/alterschema_canonicalize_draft4_test.cc index 4109cc29a..2a6c65cba 100644 --- a/test/alterschema/alterschema_canonicalize_draft4_test.cc +++ b/test/alterschema/alterschema_canonicalize_draft4_test.cc @@ -2030,7 +2030,7 @@ TEST_F(CanonicalizerDraft4Test, CANONICALIZE_NEXT(document, expected, *compiled_meta_); } -TEST_F(CanonicalizerDraft4Test, number_exclusive_minimum_false_kept) { +TEST_F(CanonicalizerDraft4Test, number_exclusive_minimum_false_dropped) { auto document = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema#", "type": "number", @@ -2041,7 +2041,6 @@ TEST_F(CanonicalizerDraft4Test, number_exclusive_minimum_false_kept) { const auto expected = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema#", "type": "number", - "exclusiveMinimum": false, "minimum": 0 })JSON"); @@ -2957,3 +2956,511 @@ TEST_F(CanonicalizerDraft4Test, type_allof_ref_and_typed_not) { CANONICALIZE_NEXT(document, expected, *compiled_meta_); } + +TEST_F(CanonicalizerDraft4Test, exclusive_maximum_fold_non_integral) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "maximum": 10.5, + "exclusiveMaximum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "maximum": 10, + "multipleOf": 1 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, exclusive_minimum_fold_non_integral) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "minimum": 5.2, + "exclusiveMinimum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "minimum": 6, + "multipleOf": 1 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, dependencies_with_existing_anyof) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "anyOf": [ + { "required": [ "a" ] }, + { "required": [ "b" ] } + ], + "dependencies": { "x": [ "y" ] } + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "allOf": [ + { + "anyOf": [ + { + "anyOf": [ + { "enum": [ null ] }, + { "enum": [ false, true ] }, + { + "type": "object", + "required": [ "a" ], + "minProperties": 1, + "properties": { "a": true }, + "patternProperties": {}, + "additionalProperties": true + }, + { "type": "array", "minItems": 0, "uniqueItems": false, "items": true }, + { "type": "string", "minLength": 0 }, + { "type": "number" } + ] + }, + { + "anyOf": [ + { "enum": [ null ] }, + { "enum": [ false, true ] }, + { + "type": "object", + "required": [ "b" ], + "minProperties": 1, + "properties": { "b": true }, + "patternProperties": {}, + "additionalProperties": true + }, + { "type": "array", "minItems": 0, "uniqueItems": false, "items": true }, + { "type": "string", "minLength": 0 }, + { "type": "number" } + ] + } + ] + }, + { + "allOf": [ + { + "anyOf": [ + { + "not": { + "type": "object", + "required": [ "x" ], + "minProperties": 1, + "properties": { "x": true }, + "patternProperties": {}, + "additionalProperties": true + } + }, + { + "type": "object", + "required": [ "x", "y" ], + "minProperties": 2, + "properties": { "x": true, "y": true }, + "patternProperties": {}, + "additionalProperties": true + } + ] + } + ] + }, + { + "type": "object", + "minProperties": 0, + "properties": {}, + "patternProperties": {}, + "additionalProperties": true + } + ] + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, full_restructure_ref_in_typed_keyword) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "pos": { "type": "integer", "minimum": 0 } + }, + "type": "object", + "not": { "required": [ "forbidden" ] }, + "anyOf": [ + { "required": [ "a" ] }, + { "required": [ "b" ] } + ], + "properties": { + "x": { "$ref": "#/definitions/pos" } + } + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "allOf": [ + { + "not": { + "anyOf": [ + { "enum": [ null ] }, + { "enum": [ false, true ] }, + { + "type": "object", + "required": [ "forbidden" ], + "minProperties": 1, + "properties": { "forbidden": true }, + "patternProperties": {}, + "additionalProperties": true + }, + { "type": "array", "minItems": 0, "uniqueItems": false, "items": true }, + { "type": "string", "minLength": 0 }, + { "type": "number" } + ] + } + }, + { + "anyOf": [ + { + "anyOf": [ + { "enum": [ null ] }, + { "enum": [ false, true ] }, + { + "type": "object", + "required": [ "a" ], + "minProperties": 1, + "properties": { "a": true }, + "patternProperties": {}, + "additionalProperties": true + }, + { "type": "array", "minItems": 0, "uniqueItems": false, "items": true }, + { "type": "string", "minLength": 0 }, + { "type": "number" } + ] + }, + { + "anyOf": [ + { "enum": [ null ] }, + { "enum": [ false, true ] }, + { + "type": "object", + "required": [ "b" ], + "minProperties": 1, + "properties": { "b": true }, + "patternProperties": {}, + "additionalProperties": true + }, + { "type": "array", "minItems": 0, "uniqueItems": false, "items": true }, + { "type": "string", "minLength": 0 }, + { "type": "number" } + ] + } + ] + }, + { + "type": "object", + "minProperties": 0, + "properties": { "x": true }, + "patternProperties": {}, + "additionalProperties": true + } + ] + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, + equal_bounds_with_exclusive_minimum_unsatisfiable) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "minimum": 5, + "maximum": 5, + "exclusiveMinimum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON( + false + )JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, + equal_bounds_with_exclusive_maximum_unsatisfiable) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "minimum": 5, + "maximum": 5, + "exclusiveMaximum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON( + false + )JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, exclusive_maximum_fold_exponential_notation) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "maximum": 1e1, + "exclusiveMaximum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "maximum": 9, + "multipleOf": 1 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, exclusive_minimum_fold_exponential_notation) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "minimum": 1e1, + "exclusiveMinimum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "minimum": 11, + "multipleOf": 1 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, + exclusive_maximum_fold_non_integral_exponential) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "maximum": 1.05e1, + "exclusiveMaximum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "maximum": 10, + "multipleOf": 1 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, + exclusive_minimum_fold_non_integral_exponential) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "minimum": 5.2e0, + "exclusiveMinimum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "minimum": 6, + "multipleOf": 1 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, equal_bounds_to_enum_exponential) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "minimum": 1e1, + "maximum": 1e1 + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "enum": [ 10 ] + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, + equal_bounds_exclusive_exponential_unsatisfiable) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "minimum": 1e1, + "maximum": 1e1, + "exclusiveMinimum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON( + false + )JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, integer_minimum_large_decimal) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "minimum": 1e2 + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "minimum": 100, + "multipleOf": 1 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, integer_maximum_large_decimal) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "maximum": 1e3 + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "maximum": 1000, + "multipleOf": 1 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, number_multiple_of_exponential) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "multipleOf": 1e-1 + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "multipleOf": 1e-1 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, exclusive_maximum_fold_real_integral) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "maximum": 10.0, + "exclusiveMaximum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "maximum": 9, + "multipleOf": 1 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, exclusive_minimum_fold_real_integral) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "minimum": 5.0, + "exclusiveMinimum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer", + "minimum": 6, + "multipleOf": 1 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, + exclusive_equal_bounds_without_type_not_unsatisfiable) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "minimum": 5, + "maximum": 5, + "exclusiveMinimum": true + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "anyOf": [ + { "enum": [ null ] }, + { "enum": [ false, true ] }, + { + "type": "object", + "minProperties": 0, + "properties": {}, + "patternProperties": {}, + "additionalProperties": true + }, + { "type": "array", "minItems": 0, "uniqueItems": false, "items": true }, + { "type": "string", "minLength": 0 }, + false + ] + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, exclusive_minimum_false_dropped_on_number) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "minimum": 5, + "exclusiveMinimum": false + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "minimum": 5 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +} + +TEST_F(CanonicalizerDraft4Test, exclusive_maximum_false_dropped_on_number) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "maximum": 10, + "exclusiveMaximum": false + })JSON"); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "maximum": 10 + })JSON"); + + CANONICALIZE_NEXT(document, expected, *compiled_meta_); +}