Skip to content

Commit

Permalink
Stricter int32 and int64 formats
Browse files Browse the repository at this point in the history
  • Loading branch information
moberegger committed Jan 27, 2025
1 parent 229a47c commit 0686950
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 21 deletions.
2 changes: 2 additions & 0 deletions lib/json_schemer/openapi30/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +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 },
'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
Expand Down
10 changes: 8 additions & 2 deletions lib/json_schemer/openapi31/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ 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 },
'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 }
Expand Down
155 changes: 136 additions & 19 deletions test/open_api_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,45 @@ def test_discriminator_non_object_and_missing_property_name
end

def test_openapi31_formats
schema = {
'properties' => {
'a' => { 'format' => 'int32' },
'b' => { 'format' => 'int64' },
'c' => { 'format' => 'float' },
'd' => { 'format' => 'double' },
'e' => { '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 }))
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' },
Expand All @@ -714,63 +753,123 @@ 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 }))
refute(schemer.valid?({ 'a' => 2.pow(31).to_s }))
refute(schemer.valid?({ 'a' => 2.pow(32) }))
refute(schemer.valid?({ 'a' => 123.123 }))
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?({ 'a' => 2.pow(63).to_s }))
refute(schemer.valid?({ 'b' => 2.pow(64) }))
refute(schemer.valid?({ 'b' => 123.123 }))
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 }))
end

def test_openapi31_formats_multiple_types
def test_openapi31_formats_with_multiple_types
schema = {
'properties' => {
'a' => { 'type' => ['integer', 'boolean', 'null'], 'format' => 'int32' },
'b' => { 'type' => ['integer', 'boolean', 'null'], 'format' => 'int64' },
'c' => { 'type' => ['number', 'boolean', 'null'], 'format' => 'float' },
'd' => { 'type' => ['number', 'boolean', 'null'], 'format' => 'double' },
'e' => { 'type' => ['string', 'boolean', 'null'], 'format' => 'password' }
'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' => true }))
assert(schemer.valid?({ 'a' => nil }))
refute(schemer.valid?({ 'a' => 2.pow(32) }))
# int64
assert(schemer.valid?({ 'b' => 2.pow(63) }))
assert(schemer.valid?({ 'b' => true }))
assert(schemer.valid?({ 'b' => nil }))
refute(schemer.valid?({ 'b' => 2.pow(64) }))
# float
assert(schemer.valid?({ 'c' => 2.0 }))
assert(schemer.valid?({ 'c' => true }))
assert(schemer.valid?({ 'c' => nil }))
refute(schemer.valid?({ 'c' => 2 }))
# double
assert(schemer.valid?({ 'd' => 2.0 }))
assert(schemer.valid?({ 'd' => true }))
assert(schemer.valid?({ 'd' => nil }))
refute(schemer.valid?({ 'd' => 2 }))
# password
assert(schemer.valid?({ 'e' => 'anything' }))
assert(schemer.valid?({ 'e' => true }))
assert(schemer.valid?({ 'e' => nil }))
refute(schemer.valid?({ 'e' => 2 }))
end

def test_openapi30_formats
schema = {
'properties' => {
'a' => { 'format' => 'int32' },
'b' => { 'format' => 'int64' },
'c' => { 'format' => 'float' },
'd' => { 'format' => 'double' },
'e' => { 'format' => 'password' },
'f' => { 'format' => 'byte' },
'g' => { 'format' => 'binary' },
'h' => { 'format' => 'date' },
'i' => { 'format' => 'date-time' }
}
}

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 }))
# password
assert(schemer.valid?({ 'e' => 'anything' }))
assert(schemer.valid?({ 'e' => 2 }))
# byte
assert(schemer.valid?({ 'f' => 'IQ==' }))
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' },
Expand All @@ -788,31 +887,40 @@ def test_openapi30_formats
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' => 123.123 }))
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' => 123.123 }))
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 }))
Expand All @@ -836,30 +944,39 @@ def test_openapi30_nullable_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' => 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' }))
Expand Down

0 comments on commit 0686950

Please sign in to comment.