Skip to content

Commit 9b692e5

Browse files
authored
fix(ruby-client): Fix incorrect boolean parsing in OneOf/AnyOf (#21943)
Changes the logic of parsing of OneOf/AnyOf to fix incorrect type coercion in OneOf/AnyOf contexts. The guard which checks whether typed_data is true-ish fails for booleans. If the oneOf includes a boolean type and the data is false then it will correctly parse this value as a boolean. However, the guard class will determine that false is not true-ish and therefore returns nil. So the result of the type coercion of false will be nil. This can result in problems when dealing with default true. For example how a false can turn into a true with defaults: 1. The API returns false 2. The type coercion determines false becomes nil 3. When storing this retrieved record with a default of true for this column the stored value is suddenly true while the API specifically returned false This fix removes this guard and will rely on the exception raised to return nil when type coercion fails.
1 parent d523903 commit 9b692e5

File tree

20 files changed

+576
-15
lines changed

20 files changed

+576
-15
lines changed

modules/openapi-generator/src/main/resources/ruby-client/partial_anyof_module.mustache

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@
2929
openapi_any_of.each do |klass|
3030
begin
3131
next if klass == :AnyType # "nullable: true"
32-
typed_data = find_and_cast_into_type(klass, data)
33-
return typed_data if typed_data
32+
return find_and_cast_into_type(klass, data)
3433
rescue # rescue all errors so we keep iterating even if the current item lookup raises
3534
end
3635
end

modules/openapi-generator/src/main/resources/ruby-client/partial_oneof_module.mustache

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@
6969
openapi_one_of.each do |klass|
7070
begin
7171
next if klass == :AnyType # "nullable: true"
72-
typed_data = find_and_cast_into_type(klass, data)
73-
return typed_data if typed_data
72+
return find_and_cast_into_type(klass, data)
7473
rescue # rescue all errors so we keep iterating even if the current item lookup raises
7574
end
7675
end

modules/openapi-generator/src/test/resources/3_0/ruby/petstore-with-fake-endpoints-models-for-testing.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2162,3 +2162,33 @@ components:
21622162
type: string
21632163
type_:
21642164
type: string
2165+
OneOfPrimitiveTypes:
2166+
oneOf:
2167+
- type: string
2168+
- type: number
2169+
- type: integer
2170+
- type: boolean
2171+
- type: string
2172+
format: date
2173+
- type: string
2174+
format: date-time
2175+
- type: array
2176+
items:
2177+
type: string
2178+
- type: array
2179+
items:
2180+
type: number
2181+
- type: array
2182+
items:
2183+
type: integer
2184+
- type: array
2185+
items:
2186+
type: boolean
2187+
- type: array
2188+
items:
2189+
type: string
2190+
format: date
2191+
- type: array
2192+
items:
2193+
type: string
2194+
format: date-time

samples/client/petstore/ruby-httpx/.openapi-generator/FILES

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ docs/Name.md
4848
docs/NullableClass.md
4949
docs/NumberOnly.md
5050
docs/ObjectWithDeprecatedFields.md
51+
docs/OneOfPrimitiveTypes.md
5152
docs/Order.md
5253
docs/OuterComposite.md
5354
docs/OuterEnum.md
@@ -118,6 +119,7 @@ lib/petstore/models/name.rb
118119
lib/petstore/models/nullable_class.rb
119120
lib/petstore/models/number_only.rb
120121
lib/petstore/models/object_with_deprecated_fields.rb
122+
lib/petstore/models/one_of_primitive_types.rb
121123
lib/petstore/models/order.rb
122124
lib/petstore/models/outer_composite.rb
123125
lib/petstore/models/outer_enum.rb

samples/client/petstore/ruby-httpx/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ Class | Method | HTTP request | Description
164164
- [Petstore::NullableClass](docs/NullableClass.md)
165165
- [Petstore::NumberOnly](docs/NumberOnly.md)
166166
- [Petstore::ObjectWithDeprecatedFields](docs/ObjectWithDeprecatedFields.md)
167+
- [Petstore::OneOfPrimitiveTypes](docs/OneOfPrimitiveTypes.md)
167168
- [Petstore::Order](docs/Order.md)
168169
- [Petstore::OuterComposite](docs/OuterComposite.md)
169170
- [Petstore::OuterEnum](docs/OuterEnum.md)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Petstore::OneOfPrimitiveTypes
2+
3+
## Class instance methods
4+
5+
### `openapi_one_of`
6+
7+
Returns the list of classes defined in oneOf.
8+
9+
#### Example
10+
11+
```ruby
12+
require 'petstore'
13+
14+
Petstore::OneOfPrimitiveTypes.openapi_one_of
15+
# =>
16+
# [
17+
# :'Array<Boolean>',
18+
# :'Array<Date>',
19+
# :'Array<Float>',
20+
# :'Array<Integer>',
21+
# :'Array<String>',
22+
# :'Array<Time>',
23+
# :'Boolean',
24+
# :'Date',
25+
# :'Float',
26+
# :'Integer',
27+
# :'String',
28+
# :'Time'
29+
# ]
30+
```
31+
32+
### build
33+
34+
Find the appropriate object from the `openapi_one_of` list and casts the data into it.
35+
36+
#### Example
37+
38+
```ruby
39+
require 'petstore'
40+
41+
Petstore::OneOfPrimitiveTypes.build(data)
42+
# => #<Array<Boolean>:0x00007fdd4aab02a0>
43+
44+
Petstore::OneOfPrimitiveTypes.build(data_that_doesnt_match)
45+
# => nil
46+
```
47+
48+
#### Parameters
49+
50+
| Name | Type | Description |
51+
| ---- | ---- | ----------- |
52+
| **data** | **Mixed** | data to be matched against the list of oneOf items |
53+
54+
#### Return type
55+
56+
- `Array<Boolean>`
57+
- `Array<Date>`
58+
- `Array<Float>`
59+
- `Array<Integer>`
60+
- `Array<String>`
61+
- `Array<Time>`
62+
- `Boolean`
63+
- `Date`
64+
- `Float`
65+
- `Integer`
66+
- `String`
67+
- `Time`
68+
- `nil` (if no type matches)
69+

samples/client/petstore/ruby-httpx/lib/petstore.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
require 'petstore/models/nullable_class'
5454
require 'petstore/models/number_only'
5555
require 'petstore/models/object_with_deprecated_fields'
56+
require 'petstore/models/one_of_primitive_types'
5657
require 'petstore/models/order'
5758
require 'petstore/models/outer_composite'
5859
require 'petstore/models/outer_enum'

samples/client/petstore/ruby-httpx/lib/petstore/models/mammal_anyof.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ def build(data)
3737
openapi_any_of.each do |klass|
3838
begin
3939
next if klass == :AnyType # "nullable: true"
40-
typed_data = find_and_cast_into_type(klass, data)
41-
return typed_data if typed_data
40+
return find_and_cast_into_type(klass, data)
4241
rescue # rescue all errors so we keep iterating even if the current item lookup raises
4342
end
4443
end

samples/client/petstore/ruby-httpx/lib/petstore/models/mammal_without_discriminator.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ def build(data)
3838
openapi_one_of.each do |klass|
3939
begin
4040
next if klass == :AnyType # "nullable: true"
41-
typed_data = find_and_cast_into_type(klass, data)
42-
return typed_data if typed_data
41+
return find_and_cast_into_type(klass, data)
4342
rescue # rescue all errors so we keep iterating even if the current item lookup raises
4443
end
4544
end
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
=begin
2+
#OpenAPI Petstore
3+
4+
#This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
5+
6+
The version of the OpenAPI document: 1.0.0
7+
8+
Generated by: https://openapi-generator.tech
9+
Generator version: 7.16.0-SNAPSHOT
10+
11+
=end
12+
13+
require 'date'
14+
require 'time'
15+
16+
module Petstore
17+
module OneOfPrimitiveTypes
18+
class << self
19+
# List of class defined in oneOf (OpenAPI v3)
20+
def openapi_one_of
21+
[
22+
:'Array<Boolean>',
23+
:'Array<Date>',
24+
:'Array<Float>',
25+
:'Array<Integer>',
26+
:'Array<String>',
27+
:'Array<Time>',
28+
:'Boolean',
29+
:'Date',
30+
:'Float',
31+
:'Integer',
32+
:'String',
33+
:'Time'
34+
]
35+
end
36+
37+
# Builds the object
38+
# @param [Mixed] Data to be matched against the list of oneOf items
39+
# @return [Object] Returns the model or the data itself
40+
def build(data)
41+
# Go through the list of oneOf items and attempt to identify the appropriate one.
42+
# Note:
43+
# - We do not attempt to check whether exactly one item matches.
44+
# - No advanced validation of types in some cases (e.g. "x: { type: string }" will happily match { x: 123 })
45+
# due to the way the deserialization is made in the base_object template (it just casts without verifying).
46+
# - TODO: scalar values are de facto behaving as if they were nullable.
47+
# - TODO: logging when debugging is set.
48+
openapi_one_of.each do |klass|
49+
begin
50+
next if klass == :AnyType # "nullable: true"
51+
return find_and_cast_into_type(klass, data)
52+
rescue # rescue all errors so we keep iterating even if the current item lookup raises
53+
end
54+
end
55+
56+
openapi_one_of.include?(:AnyType) ? data : nil
57+
end
58+
59+
private
60+
61+
SchemaMismatchError = Class.new(StandardError)
62+
63+
# Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse.
64+
def find_and_cast_into_type(klass, data)
65+
return if data.nil?
66+
67+
case klass.to_s
68+
when 'Boolean'
69+
return data if data.instance_of?(TrueClass) || data.instance_of?(FalseClass)
70+
when 'Float'
71+
return data if data.instance_of?(Float)
72+
when 'Integer'
73+
return data if data.instance_of?(Integer)
74+
when 'Time'
75+
return Time.parse(data)
76+
when 'Date'
77+
return Date.iso8601(data)
78+
when 'String'
79+
return data if data.instance_of?(String)
80+
when 'Object' # "type: object"
81+
return data if data.instance_of?(Hash)
82+
when /\AArray<(?<sub_type>.+)>\z/ # "type: array"
83+
if data.instance_of?(Array)
84+
sub_type = Regexp.last_match[:sub_type]
85+
return data.map { |item| find_and_cast_into_type(sub_type, item) }
86+
end
87+
when /\AHash<String, (?<sub_type>.+)>\z/ # "type: object" with "additionalProperties: { ... }"
88+
if data.instance_of?(Hash) && data.keys.all? { |k| k.instance_of?(Symbol) || k.instance_of?(String) }
89+
sub_type = Regexp.last_match[:sub_type]
90+
return data.each_with_object({}) { |(k, v), hsh| hsh[k] = find_and_cast_into_type(sub_type, v) }
91+
end
92+
else # model
93+
const = Petstore.const_get(klass)
94+
if const
95+
if const.respond_to?(:openapi_one_of) # nested oneOf model
96+
model = const.build(data)
97+
return model if model
98+
else
99+
# raise if data contains keys that are not known to the model
100+
raise if const.respond_to?(:acceptable_attributes) && !(data.keys - const.acceptable_attributes).empty?
101+
model = const.build_from_hash(data)
102+
return model if model
103+
end
104+
end
105+
end
106+
107+
raise # if no match by now, raise
108+
rescue
109+
raise SchemaMismatchError, "#{data} doesn't match the #{klass} type"
110+
end
111+
end
112+
end
113+
114+
end

0 commit comments

Comments
 (0)