diff --git a/lib/json_schemer/openapi30/meta.rb b/lib/json_schemer/openapi30/meta.rb index 2a833f9..17e3604 100644 --- a/lib/json_schemer/openapi30/meta.rb +++ b/lib/json_schemer/openapi30/meta.rb @@ -4,8 +4,10 @@ module OpenAPI30 BASE_URI = URI('json-schemer://openapi30/schema') # https://spec.openapis.org/oas/v3.0.3#data-types FORMATS = OpenAPI31::FORMATS.merge( - 'byte' => proc { |instance, _value| ContentEncoding::BASE64.call(instance).first }, - 'binary' => proc { |instance, _value| instance.is_a?(String) && instance.encoding == Encoding::BINARY }, + '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 ) SCHEMA = { diff --git a/lib/json_schemer/openapi31/meta.rb b/lib/json_schemer/openapi31/meta.rb index e9bb7ac..bb20db0 100644 --- a/lib/json_schemer/openapi31/meta.rb +++ b/lib/json_schemer/openapi31/meta.rb @@ -4,10 +4,16 @@ module OpenAPI31 BASE_URI = URI('https://spec.openapis.org/oas/3.1/dialect/base') # https://spec.openapis.org/oas/v3.1.0#data-types FORMATS = { - 'int32' => proc { |instance, _format| instance.is_a?(Integer) && instance.bit_length <= 32 }, - 'int64' => proc { |instance, _format| instance.is_a?(Integer) && instance.bit_length <= 64 }, - 'float' => proc { |instance, _format| instance.is_a?(Float) }, - 'double' => proc { |instance, _format| instance.is_a?(Float) }, + '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 + 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 + 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) }, 'password' => proc { |_instance, _format| true } } SCHEMA = { diff --git a/test/open_api_test.rb b/test/open_api_test.rb index fcdc36c..ebf1c78 100644 --- a/test/open_api_test.rb +++ b/test/open_api_test.rb @@ -714,16 +714,100 @@ def test_openapi31_formats schemer = JSONSchemer.schema(schema, :meta_schema => JSONSchemer.openapi31) 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 })) + # 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 })) + # float assert(schemer.valid?({ 'c' => 2.0 })) + assert(schemer.valid?({ 'c' => 2.to_s })) refute(schemer.valid?({ 'c' => 2 })) + # double assert(schemer.valid?({ 'd' => 2.0 })) + assert(schemer.valid?({ 'd' => 2.to_s })) refute(schemer.valid?({ 'd' => 2 })) + # password + assert(schemer.valid?({ 'e' => 'anything' })) assert(schemer.valid?({ 'e' => 2 })) + end + + def test_openapi31_formats_with_type + schema = { + 'properties' => { + 'a' => { 'type' => 'integer', 'format' => 'int32' }, + 'b' => { 'type' => 'integer', 'format' => 'int64' }, + 'c' => { 'type' => 'number', 'format' => 'float' }, + 'd' => { 'type' => 'number', 'format' => 'double' }, + 'e' => { 'type' => 'string', 'format' => 'password' } + } + } + + schemer = JSONSchemer.schema(schema, :meta_schema => JSONSchemer.openapi31) + + 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 })) + # 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 })) + # float + assert(schemer.valid?({ 'c' => 2.0 })) + refute(schemer.valid?({ 'c' => 2 })) + # double + assert(schemer.valid?({ 'd' => 2.0 })) + refute(schemer.valid?({ 'd' => 2 })) + # password assert(schemer.valid?({ 'e' => 'anything' })) + refute(schemer.valid?({ 'e' => 2 })) + end + + def test_openapi31_formats_with_multiple_types + schema = { + 'properties' => { + 'a' => { 'type' => ['integer', 'null'], 'format' => 'int32' }, + 'b' => { 'type' => ['integer', 'null'], 'format' => 'int64' }, + 'c' => { 'type' => ['number', 'null'], 'format' => 'float' }, + 'd' => { 'type' => ['number', 'null'], 'format' => 'double' }, + 'e' => { 'type' => ['string', 'null'], 'format' => 'password' } + } + } + + schemer = JSONSchemer.schema(schema, :meta_schema => JSONSchemer.openapi31) + + assert(schemer.valid_schema?) + # int32 + assert(schemer.valid?({ 'a' => 2.pow(31) })) + assert(schemer.valid?({ 'a' => nil })) + refute(schemer.valid?({ 'a' => 2.pow(32) })) + # int64 + assert(schemer.valid?({ 'b' => 2.pow(63) })) + assert(schemer.valid?({ 'b' => nil })) + refute(schemer.valid?({ 'b' => 2.pow(64) })) + # float + assert(schemer.valid?({ 'c' => 2.0 })) + assert(schemer.valid?({ 'c' => nil })) + refute(schemer.valid?({ 'c' => 2 })) + # double + assert(schemer.valid?({ 'd' => 2.0 })) + assert(schemer.valid?({ 'd' => nil })) + refute(schemer.valid?({ 'd' => 2 })) + # password + assert(schemer.valid?({ 'e' => 'anything' })) + assert(schemer.valid?({ 'e' => nil })) + refute(schemer.valid?({ 'e' => 2 })) end def test_openapi30_formats @@ -744,24 +828,158 @@ def test_openapi30_formats schemer = JSONSchemer.schema(schema, :meta_schema => JSONSchemer.openapi30) 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 })) + assert(schemer.valid?({ 'a' => 2.pow(32).to_f })) refute(schemer.valid?({ 'a' => 2.pow(32) })) + # 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 })) + assert(schemer.valid?({ 'b' => 2.pow(64).to_f })) refute(schemer.valid?({ 'b' => 2.pow(64) })) + # float assert(schemer.valid?({ 'c' => 2.0 })) + assert(schemer.valid?({ 'c' => 2.to_s })) refute(schemer.valid?({ 'c' => 2 })) + # double assert(schemer.valid?({ 'd' => 2.0 })) + assert(schemer.valid?({ 'd' => 2.to_s })) refute(schemer.valid?({ 'd' => 2 })) - assert(schemer.valid?({ 'e' => 2 })) + # password assert(schemer.valid?({ 'e' => 'anything' })) - refute(schemer.valid?({ 'f' => '!' })) + assert(schemer.valid?({ 'e' => 2 })) + # byte assert(schemer.valid?({ 'f' => 'IQ==' })) - refute(schemer.valid?({ 'g' => '!' })) + assert(schemer.valid?({ 'f' => 123 })) + refute(schemer.valid?({ 'f' => '!' })) + # binary assert(schemer.valid?({ 'g' => '!'.b })) + assert(schemer.valid?({ 'g' => 123 })) + refute(schemer.valid?({ 'g' => '!' })) + # date + assert(schemer.valid?({ 'h' => '2001-02-03' })) + assert(schemer.valid?({ 'h' => 123 })) refute(schemer.valid?({ 'h' => '2001-02-03T04:05:06.123456789+07:00' })) + # date-time + assert(schemer.valid?({ 'i' => '2001-02-03T04:05:06.123456789+07:00' })) + assert(schemer.valid?({ 'i' => 123 })) + refute(schemer.valid?({ 'i' => '2001-02-03' })) + end + + def test_openapi30_formats_with_type + schema = { + 'properties' => { + 'a' => { 'type' => 'integer', 'format' => 'int32' }, + 'b' => { 'type' => 'integer', 'format' => 'int64' }, + 'c' => { 'type' => 'number', 'format' => 'float' }, + 'd' => { 'type' => 'number', 'format' => 'double' }, + 'e' => { 'type' => 'string', 'format' => 'password' }, + 'f' => { 'type' => 'string', 'format' => 'byte' }, + 'g' => { 'type' => 'string', 'format' => 'binary' }, + 'h' => { 'type' => 'string', 'format' => 'date' }, + 'i' => { 'type' => 'string', 'format' => 'date-time' } + } + } + + schemer = JSONSchemer.schema(schema, :meta_schema => JSONSchemer.openapi30) + + 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 })) + # 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 })) + # float + assert(schemer.valid?({ 'c' => 2.0 })) + refute(schemer.valid?({ 'c' => 2 })) + refute(schemer.valid?({ 'c' => 2.to_s })) + # double + assert(schemer.valid?({ 'd' => 2.0 })) + refute(schemer.valid?({ 'd' => 2 })) + refute(schemer.valid?({ 'd' => 2.to_s })) + # password + assert(schemer.valid?({ 'e' => 'anything' })) + refute(schemer.valid?({ 'e' => 2 })) + # byte + assert(schemer.valid?({ 'f' => 'IQ==' })) + refute(schemer.valid?({ 'f' => '!' })) + refute(schemer.valid?({ 'f' => 123 })) + # binary + assert(schemer.valid?({ 'g' => '!'.b })) + refute(schemer.valid?({ 'g' => '!' })) + refute(schemer.valid?({ 'g' => 123 })) + # date assert(schemer.valid?({ 'h' => '2001-02-03' })) + refute(schemer.valid?({ 'h' => '2001-02-03T04:05:06.123456789+07:00' })) + refute(schemer.valid?({ 'h' => 123 })) + # date-time + assert(schemer.valid?({ 'i' => '2001-02-03T04:05:06.123456789+07:00' })) refute(schemer.valid?({ 'i' => '2001-02-03' })) + refute(schemer.valid?({ 'i' => 123 })) + end + + def test_openapi30_nullable_formats + schema = { + 'properties' => { + 'a' => { 'type' => 'integer', 'format' => 'int32', 'nullable' => true }, + 'b' => { 'type' => 'integer', 'format' => 'int64', 'nullable' => true }, + 'c' => { 'type' => 'number', 'format' => 'float', 'nullable' => true }, + 'd' => { 'type' => 'number', 'format' => 'double', 'nullable' => true }, + 'e' => { 'type' => 'string', 'format' => 'password', 'nullable' => true }, + 'f' => { 'type' => 'string', 'format' => 'byte', 'nullable' => true }, + 'g' => { 'type' => 'string', 'format' => 'binary', 'nullable' => true }, + 'h' => { 'type' => 'string', 'format' => 'date', 'nullable' => true }, + 'i' => { 'type' => 'string', 'format' => 'date-time', 'nullable' => true } + } + } + + schemer = JSONSchemer.schema(schema, :meta_schema => JSONSchemer.openapi30) + + assert(schemer.valid_schema?) + # int32 + assert(schemer.valid?({ 'a' => 2.pow(31) })) + assert(schemer.valid?({ 'a' => nil })) + refute(schemer.valid?({ 'a' => 2.pow(32) })) + # int64 + assert(schemer.valid?({ 'b' => 2.pow(63) })) + assert(schemer.valid?({ 'b' => nil })) + refute(schemer.valid?({ 'b' => 2.pow(64) })) + # float + assert(schemer.valid?({ 'c' => 2.0 })) + assert(schemer.valid?({ 'c' => nil })) + refute(schemer.valid?({ 'c' => 2 })) + # double + assert(schemer.valid?({ 'd' => 2.0 })) + assert(schemer.valid?({ 'd' => nil })) + refute(schemer.valid?({ 'd' => 2 })) + # password + assert(schemer.valid?({ 'e' => 'anything' })) + assert(schemer.valid?({ 'e' => nil })) + refute(schemer.valid?({ 'e' => 2 })) + # byte + assert(schemer.valid?({ 'f' => 'IQ==' })) + assert(schemer.valid?({ 'f' => nil })) + refute(schemer.valid?({ 'f' => '!' })) + # binary + assert(schemer.valid?({ 'g' => '!'.b })) + assert(schemer.valid?({ 'g' => nil })) + refute(schemer.valid?({ 'g' => '!' })) + # date + assert(schemer.valid?({ 'h' => '2001-02-03' })) + assert(schemer.valid?({ 'h' => nil })) + refute(schemer.valid?({ 'h' => '2001-02-03T04:05:06.123456789+07:00' })) + # date-time assert(schemer.valid?({ 'i' => '2001-02-03T04:05:06.123456789+07:00' })) + assert(schemer.valid?({ 'i' => nil })) + refute(schemer.valid?({ 'i' => '2001-02-03' })) end def test_unsupported_openapi_version