Skip to content

Commit

Permalink
Merge pull request #209 from moberegger/main
Browse files Browse the repository at this point in the history
Support nullable `format`s for Openapi
  • Loading branch information
davishmcclurg authored Jan 28, 2025
2 parents ed3f419 + 0686950 commit 09841e7
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 9 deletions.
6 changes: 4 additions & 2 deletions lib/json_schemer/openapi30/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
14 changes: 10 additions & 4 deletions lib/json_schemer/openapi31/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
224 changes: 221 additions & 3 deletions test/open_api_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -812,16 +812,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
Expand All @@ -842,24 +926,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
Expand Down

0 comments on commit 09841e7

Please sign in to comment.