From 386c2a6fe089350c61775716643ef0600898060e Mon Sep 17 00:00:00 2001 From: David Harsha Date: Mon, 27 Jan 2025 17:29:06 -0800 Subject: [PATCH] Fix `bit_length` checks Need to save a bit because `int32` and `int64` are signed integers. `MAX_INT_WITH_ACCURATE_FLOAT` is necessary because XL integers can't be stored accurately: ``` >> (2.pow(63) - 1) => 9223372036854775807 >> (2.pow(63) - 1).to_f => 9.223372036854776e+18 >> (2.pow(63) - 1).to_f.to_i > (2.pow(63) - 1) => true ``` --- lib/json_schemer/openapi30/meta.rb | 4 +- lib/json_schemer/openapi31/meta.rb | 4 +- test/open_api_test.rb | 92 ++++++++++++++++-------------- 3 files changed, 53 insertions(+), 47 deletions(-) diff --git a/lib/json_schemer/openapi30/meta.rb b/lib/json_schemer/openapi30/meta.rb index 17e3604..3fc0f3f 100644 --- a/lib/json_schemer/openapi30/meta.rb +++ b/lib/json_schemer/openapi30/meta.rb @@ -4,8 +4,8 @@ module OpenAPI30 BASE_URI = URI('json-schemer://openapi30/schema') # https://spec.openapis.org/oas/v3.0.3#data-types FORMATS = OpenAPI31::FORMATS.merge( - 'int32' => proc { |instance, _format| !instance.is_a?(Integer) || instance.floor.bit_length <= 32 }, - 'int64' => proc { |instance, _format| !instance.is_a?(Integer) || instance.floor.bit_length <= 64 }, + 'int32' => proc { |instance, _format| !instance.is_a?(Integer) || instance.floor.bit_length < 32 }, + 'int64' => proc { |instance, _format| !instance.is_a?(Integer) || instance.floor.bit_length < 64 }, 'byte' => proc { |instance, _value| !instance.is_a?(String) || ContentEncoding::BASE64.call(instance).first }, 'binary' => proc { |instance, _value| !instance.is_a?(String) || instance.encoding == Encoding::BINARY }, 'date' => Format::DATE diff --git a/lib/json_schemer/openapi31/meta.rb b/lib/json_schemer/openapi31/meta.rb index bb20db0..5bc793e 100644 --- a/lib/json_schemer/openapi31/meta.rb +++ b/lib/json_schemer/openapi31/meta.rb @@ -6,11 +6,11 @@ module OpenAPI31 FORMATS = { 'int32' => proc do |instance, _format| valid_type = instance.is_a?(Numeric) && (instance.is_a?(Integer) || instance.floor == instance) - !valid_type || instance.floor.bit_length <= 32 + !valid_type || instance.floor.bit_length < 32 end, 'int64' => proc do |instance, _format| valid_type = instance.is_a?(Numeric) && (instance.is_a?(Integer) || instance.floor == instance) - !valid_type || instance.floor.bit_length <= 64 + !valid_type || instance.floor.bit_length < 64 end, 'float' => proc { |instance, _format| !instance.is_a?(Numeric) || instance.is_a?(Float) }, 'double' => proc { |instance, _format| !instance.is_a?(Numeric) || instance.is_a?(Float) }, diff --git a/test/open_api_test.rb b/test/open_api_test.rb index 2f56cbf..bbae3b4 100644 --- a/test/open_api_test.rb +++ b/test/open_api_test.rb @@ -1,6 +1,10 @@ require 'test_helper' class OpenAPITest < Minitest::Test + MAX_INT32 = 2.pow(31) - 1 + MAX_INT64 = 2.pow(63) - 1 + MAX_INT_WITH_ACCURATE_FLOAT = 2.pow(53) + CAT_SCHEMA = { 'type' => 'object', 'properties' => { @@ -183,7 +187,7 @@ def test_discriminator_specification_example invalid_pack_size = { 'petType' => 'Dog', 'name' => 'Heaven', - 'packSize' => 2.pow(32) + 'packSize' => 2.pow(31) } missing_pet_type = { 'name' => 'Brian' @@ -813,17 +817,18 @@ def test_openapi31_formats assert(schemer.valid_schema?) # int32 - assert(schemer.valid?({ 'a' => 2.pow(31) })) - assert(schemer.valid?({ 'a' => 2.pow(31).to_f })) - assert(schemer.valid?({ 'a' => 2.pow(31).to_s })) - refute(schemer.valid?({ 'a' => 2.pow(32) })) - refute(schemer.valid?({ 'a' => 2.pow(32).to_f })) + assert(schemer.valid?({ 'a' => MAX_INT32 })) + assert(schemer.valid?({ 'a' => MAX_INT32.to_f })) + assert(schemer.valid?({ 'a' => MAX_INT32.to_s })) + refute(schemer.valid?({ 'a' => 2.pow(31) })) + refute(schemer.valid?({ 'a' => 2.pow(31).to_f })) # int64 - assert(schemer.valid?({ 'b' => 2.pow(63) })) - assert(schemer.valid?({ 'b' => 2.pow(63).to_f })) - assert(schemer.valid?({ 'a' => 2.pow(63).to_s })) - refute(schemer.valid?({ 'b' => 2.pow(64) })) - refute(schemer.valid?({ 'b' => 2.pow(64).to_f })) + assert(schemer.valid?({ 'b' => MAX_INT64 })) + assert(schemer.valid?({ 'b' => MAX_INT_WITH_ACCURATE_FLOAT })) + assert(schemer.valid?({ 'b' => MAX_INT_WITH_ACCURATE_FLOAT.to_f })) + assert(schemer.valid?({ 'a' => MAX_INT64.to_s })) + refute(schemer.valid?({ 'b' => 2.pow(63) })) + refute(schemer.valid?({ 'b' => 2.pow(63).to_f })) # float assert(schemer.valid?({ 'c' => 2.0 })) assert(schemer.valid?({ 'c' => 2.to_s })) @@ -852,15 +857,16 @@ def test_openapi31_formats_with_type assert(schemer.valid_schema?) # int32 - assert(schemer.valid?({ 'a' => 2.pow(31) })) - assert(schemer.valid?({ 'a' => 2.pow(31).to_f })) - refute(schemer.valid?({ 'a' => 2.pow(32) })) - refute(schemer.valid?({ 'a' => 2.pow(32).to_f })) + assert(schemer.valid?({ 'a' => MAX_INT32 })) + assert(schemer.valid?({ 'a' => MAX_INT32.to_f })) + refute(schemer.valid?({ 'a' => 2.pow(31) })) + refute(schemer.valid?({ 'a' => 2.pow(31).to_f })) # int64 - assert(schemer.valid?({ 'b' => 2.pow(63) })) - assert(schemer.valid?({ 'b' => 2.pow(63).to_f })) - refute(schemer.valid?({ 'b' => 2.pow(64) })) - refute(schemer.valid?({ 'b' => 2.pow(64).to_f })) + assert(schemer.valid?({ 'b' => MAX_INT64 })) + assert(schemer.valid?({ 'b' => MAX_INT_WITH_ACCURATE_FLOAT })) + assert(schemer.valid?({ 'b' => MAX_INT_WITH_ACCURATE_FLOAT.to_f })) + refute(schemer.valid?({ 'b' => 2.pow(63) })) + refute(schemer.valid?({ 'b' => 2.pow(63).to_f })) # float assert(schemer.valid?({ 'c' => 2.0 })) refute(schemer.valid?({ 'c' => 2 })) @@ -887,13 +893,13 @@ def test_openapi31_formats_with_multiple_types assert(schemer.valid_schema?) # int32 - assert(schemer.valid?({ 'a' => 2.pow(31) })) + assert(schemer.valid?({ 'a' => MAX_INT32 })) assert(schemer.valid?({ 'a' => nil })) - refute(schemer.valid?({ 'a' => 2.pow(32) })) + refute(schemer.valid?({ 'a' => 2.pow(31) })) # int64 - assert(schemer.valid?({ 'b' => 2.pow(63) })) + assert(schemer.valid?({ 'b' => MAX_INT64 })) assert(schemer.valid?({ 'b' => nil })) - refute(schemer.valid?({ 'b' => 2.pow(64) })) + refute(schemer.valid?({ 'b' => 2.pow(63) })) # float assert(schemer.valid?({ 'c' => 2.0 })) assert(schemer.valid?({ 'c' => nil })) @@ -927,17 +933,17 @@ def test_openapi30_formats assert(schemer.valid_schema?) # int32 - assert(schemer.valid?({ 'a' => 2.pow(31) })) + assert(schemer.valid?({ 'a' => MAX_INT32 })) + assert(schemer.valid?({ 'a' => MAX_INT32.to_f })) + assert(schemer.valid?({ 'a' => MAX_INT32.to_s })) assert(schemer.valid?({ 'a' => 2.pow(31).to_f })) - assert(schemer.valid?({ 'a' => 2.pow(31).to_s })) - assert(schemer.valid?({ 'a' => 2.pow(32).to_f })) - refute(schemer.valid?({ 'a' => 2.pow(32) })) + refute(schemer.valid?({ 'a' => 2.pow(31) })) # int64 - assert(schemer.valid?({ 'b' => 2.pow(63) })) + assert(schemer.valid?({ 'b' => MAX_INT64 })) + assert(schemer.valid?({ 'b' => MAX_INT64.to_f })) + assert(schemer.valid?({ 'a' => MAX_INT64.to_s })) assert(schemer.valid?({ 'b' => 2.pow(63).to_f })) - assert(schemer.valid?({ 'a' => 2.pow(63).to_s })) - assert(schemer.valid?({ 'b' => 2.pow(64).to_f })) - refute(schemer.valid?({ 'b' => 2.pow(64) })) + refute(schemer.valid?({ 'b' => 2.pow(63) })) # float assert(schemer.valid?({ 'c' => 2.0 })) assert(schemer.valid?({ 'c' => 2.to_s })) @@ -986,15 +992,15 @@ def test_openapi30_formats_with_type assert(schemer.valid_schema?) # int32 - assert(schemer.valid?({ 'a' => 2.pow(31) })) - refute(schemer.valid?({ 'a' => 2.pow(31).to_s })) - refute(schemer.valid?({ 'a' => 2.pow(32) })) - refute(schemer.valid?({ 'a' => 2.pow(32).to_f })) + assert(schemer.valid?({ 'a' => MAX_INT32 })) + refute(schemer.valid?({ 'a' => MAX_INT32.to_s })) + refute(schemer.valid?({ 'a' => 2.pow(31) })) + refute(schemer.valid?({ 'a' => 2.pow(31).to_f })) # int64 - assert(schemer.valid?({ 'b' => 2.pow(63) })) - refute(schemer.valid?({ 'a' => 2.pow(63).to_s })) - refute(schemer.valid?({ 'b' => 2.pow(64) })) - refute(schemer.valid?({ 'b' => 2.pow(64).to_f })) + assert(schemer.valid?({ 'b' => MAX_INT64 })) + refute(schemer.valid?({ 'a' => MAX_INT64.to_s })) + refute(schemer.valid?({ 'b' => 2.pow(63) })) + refute(schemer.valid?({ 'b' => 2.pow(63).to_f })) # float assert(schemer.valid?({ 'c' => 2.0 })) refute(schemer.valid?({ 'c' => 2 })) @@ -1043,13 +1049,13 @@ def test_openapi30_nullable_formats assert(schemer.valid_schema?) # int32 - assert(schemer.valid?({ 'a' => 2.pow(31) })) + assert(schemer.valid?({ 'a' => MAX_INT32 })) assert(schemer.valid?({ 'a' => nil })) - refute(schemer.valid?({ 'a' => 2.pow(32) })) + refute(schemer.valid?({ 'a' => 2.pow(31) })) # int64 - assert(schemer.valid?({ 'b' => 2.pow(63) })) + assert(schemer.valid?({ 'b' => MAX_INT64 })) assert(schemer.valid?({ 'b' => nil })) - refute(schemer.valid?({ 'b' => 2.pow(64) })) + refute(schemer.valid?({ 'b' => 2.pow(63) })) # float assert(schemer.valid?({ 'c' => 2.0 })) assert(schemer.valid?({ 'c' => nil }))