fsdk-2237-ruby-e2e 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +107 -0
  3. data/lib/petstore/client.rb +45 -0
  4. data/lib/petstore/config.rb +54 -0
  5. data/lib/petstore/environment.rb +7 -0
  6. data/lib/petstore/error.rb +40 -0
  7. data/lib/petstore/errors/api_error.rb +9 -0
  8. data/lib/petstore/errors/client_error.rb +43 -0
  9. data/lib/petstore/errors/connection_error.rb +11 -0
  10. data/lib/petstore/errors/redirect_error.rb +9 -0
  11. data/lib/petstore/errors/response_error.rb +92 -0
  12. data/lib/petstore/errors/server_error.rb +25 -0
  13. data/lib/petstore/errors/timeout_error.rb +9 -0
  14. data/lib/petstore/http/auth.rb +19 -0
  15. data/lib/petstore/http/oauth_token_manager.rb +91 -0
  16. data/lib/petstore/internal/errors/constraint_error.rb +10 -0
  17. data/lib/petstore/internal/errors/type_error.rb +10 -0
  18. data/lib/petstore/internal/form/request.rb +110 -0
  19. data/lib/petstore/internal/http/base_request.rb +64 -0
  20. data/lib/petstore/internal/http/event_stream.rb +106 -0
  21. data/lib/petstore/internal/http/raw_client.rb +426 -0
  22. data/lib/petstore/internal/http/response_body.rb +35 -0
  23. data/lib/petstore/internal/iterators/cursor_item_iterator.rb +28 -0
  24. data/lib/petstore/internal/iterators/cursor_page_iterator.rb +72 -0
  25. data/lib/petstore/internal/iterators/item_iterator.rb +72 -0
  26. data/lib/petstore/internal/iterators/offset_item_iterator.rb +30 -0
  27. data/lib/petstore/internal/iterators/offset_page_iterator.rb +110 -0
  28. data/lib/petstore/internal/json/request.rb +61 -0
  29. data/lib/petstore/internal/json/serializable.rb +25 -0
  30. data/lib/petstore/internal/multipart/multipart_encoder.rb +141 -0
  31. data/lib/petstore/internal/multipart/multipart_form_data.rb +78 -0
  32. data/lib/petstore/internal/multipart/multipart_form_data_part.rb +51 -0
  33. data/lib/petstore/internal/multipart/multipart_request.rb +49 -0
  34. data/lib/petstore/internal/raw/request.rb +88 -0
  35. data/lib/petstore/internal/types/array.rb +47 -0
  36. data/lib/petstore/internal/types/boolean.rb +34 -0
  37. data/lib/petstore/internal/types/enum.rb +56 -0
  38. data/lib/petstore/internal/types/hash.rb +36 -0
  39. data/lib/petstore/internal/types/model/field.rb +38 -0
  40. data/lib/petstore/internal/types/model.rb +246 -0
  41. data/lib/petstore/internal/types/type.rb +35 -0
  42. data/lib/petstore/internal/types/union.rb +193 -0
  43. data/lib/petstore/internal/types/unknown.rb +15 -0
  44. data/lib/petstore/internal/types/utils.rb +137 -0
  45. data/lib/petstore/models/open_model.rb +30 -0
  46. data/lib/petstore/models/serializable.rb +63 -0
  47. data/lib/petstore/models/union_type.rb +12 -0
  48. data/lib/petstore/pet/client.rb +196 -0
  49. data/lib/petstore/pet/types/delete_pet_request.rb +13 -0
  50. data/lib/petstore/pet/types/find_pets_by_status_request.rb +11 -0
  51. data/lib/petstore/pet/types/find_pets_by_status_request_status.rb +24 -0
  52. data/lib/petstore/pet/types/find_pets_by_tags_request.rb +11 -0
  53. data/lib/petstore/pet/types/get_pet_by_id_request.rb +11 -0
  54. data/lib/petstore/pet/types/pet.rb +25 -0
  55. data/lib/petstore/pet/types/update_pet_with_form_request.rb +15 -0
  56. data/lib/petstore/serializers/form_serializer.rb +41 -0
  57. data/lib/petstore/serializers/json_serializer.rb +41 -0
  58. data/lib/petstore/serializers/xml_serializer.rb +56 -0
  59. data/lib/petstore/store/client.rb +89 -0
  60. data/lib/petstore/store/types/delete_order_request.rb +11 -0
  61. data/lib/petstore/store/types/get_order_by_id_request.rb +11 -0
  62. data/lib/petstore/store/types/order.rb +25 -0
  63. data/lib/petstore/types/api_response.rb +20 -0
  64. data/lib/petstore/types/category.rb +19 -0
  65. data/lib/petstore/types/order_status.rb +22 -0
  66. data/lib/petstore/types/pet_status.rb +22 -0
  67. data/lib/petstore/types/tag.rb +19 -0
  68. data/lib/petstore/user/client.rb +149 -0
  69. data/lib/petstore/user/types/delete_user_request.rb +11 -0
  70. data/lib/petstore/user/types/get_user_by_name_request.rb +11 -0
  71. data/lib/petstore/user/types/login_user_request.rb +13 -0
  72. data/lib/petstore/user/types/user.rb +27 -0
  73. data/lib/petstore/validator.rb +42 -0
  74. data/lib/petstore/version.rb +5 -0
  75. data/lib/petstore.rb +78 -0
  76. metadata +236 -0
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ module Types
6
+ # Module for defining enums
7
+ module Enum
8
+ include Type
9
+
10
+ # @api private
11
+ #
12
+ # @return [Array<Object>]
13
+ def values
14
+ @values ||= constants.map { |c| const_get(c) }
15
+ end
16
+
17
+ # @api private
18
+ def finalize!
19
+ values
20
+ end
21
+
22
+ # @api private
23
+ def strict?
24
+ @strict ||= false
25
+ end
26
+
27
+ # @api private
28
+ def strict!
29
+ @strict = true
30
+ end
31
+
32
+ def coerce(value, strict: strict?)
33
+ coerced_value = Utils.coerce(Symbol, value)
34
+
35
+ return coerced_value if values.include?(coerced_value)
36
+
37
+ raise Errors::TypeError, "`#{value}` not in enum #{self}" if strict
38
+
39
+ value
40
+ end
41
+
42
+ # Parse JSON string and coerce to the enum value
43
+ #
44
+ # @param str [String] JSON string to parse
45
+ # @return [String] The enum value
46
+ def load(str)
47
+ coerce(::JSON.parse(str))
48
+ end
49
+
50
+ def inspect
51
+ "#{name}[#{values.join(', ')}]"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ module Types
6
+ class Hash
7
+ include Type
8
+
9
+ attr_reader :key_type, :value_type
10
+
11
+ class << self
12
+ def [](key_type, value_type)
13
+ new(key_type, value_type)
14
+ end
15
+ end
16
+
17
+ def initialize(key_type, value_type)
18
+ @key_type = key_type
19
+ @value_type = value_type
20
+ end
21
+
22
+ def coerce(value, strict: strict?)
23
+ unless value.is_a?(::Hash)
24
+ raise Errors::TypeError, 'not hash' if strict
25
+
26
+ return value
27
+ end
28
+
29
+ value.to_h do |k, v|
30
+ [Utils.coerce(key_type, k, strict: strict), Utils.coerce(value_type, v, strict: strict)]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ module Types
6
+ class Model
7
+ # Definition of a field on a model
8
+ class Field
9
+ SENSITIVE_FIELD_NAMES = %i[
10
+ password secret token api_key apikey access_token refresh_token
11
+ client_secret client_id credential bearer authorization
12
+ ].freeze
13
+
14
+ attr_reader :name, :type, :optional, :nullable, :api_name, :value, :default
15
+
16
+ def initialize(name:, type:, optional: false, nullable: false, api_name: nil, value: nil, default: nil)
17
+ @name = name.to_sym
18
+ @type = type
19
+ @optional = optional
20
+ @nullable = nullable
21
+ @api_name = api_name || name.to_s
22
+ @value = value
23
+ @default = default
24
+ end
25
+
26
+ def literal?
27
+ !value.nil?
28
+ end
29
+
30
+ def sensitive?
31
+ SENSITIVE_FIELD_NAMES.include?(@name) ||
32
+ SENSITIVE_FIELD_NAMES.any? { |sensitive| @name.to_s.include?(sensitive.to_s) }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ module Types
6
+ # @abstract
7
+ #
8
+ # An abstract model that all data objects will inherit from
9
+ class Model
10
+ include Type
11
+
12
+ class << self
13
+ # The defined fields for this model
14
+ #
15
+ # @api private
16
+ #
17
+ # @return [Hash<Symbol, Field>]
18
+ def fields
19
+ @fields ||= if self < ::Petstore::Internal::Types::Model
20
+ superclass.fields.dup
21
+ else
22
+ {}
23
+ end
24
+ end
25
+
26
+ # Any extra fields that have been created from instantiation
27
+ #
28
+ # @api private
29
+ #
30
+ # @return [Hash<Symbol, Field>]
31
+ def extra_fields
32
+ @extra_fields ||= {}
33
+ end
34
+
35
+ # Define a new field on this model
36
+ #
37
+ # @param name [Symbol] The name of the field
38
+ # @param type [Class] Type of the field
39
+ # @option optional [Boolean] If it is an optional field
40
+ # @option nullable [Boolean] If it is a nullable field
41
+ # @option api_name [Symbol, String] Name in the API of this field. When serializing/deserializing, will use
42
+ # this field name
43
+ # @return [void]
44
+ def field(name, type, optional: false, nullable: false, api_name: nil, default: nil)
45
+ add_field_definition(name: name, type: type, optional: optional, nullable: nullable, api_name: api_name,
46
+ default: default)
47
+
48
+ define_accessor(name)
49
+ define_setter(name)
50
+ end
51
+
52
+ # Define a new literal for this model
53
+ #
54
+ # @param name [Symbol]
55
+ # @param value [Object]
56
+ # @option api_name [Symbol, String]
57
+ # @return [void]
58
+ def literal(name, value, api_name: nil)
59
+ add_field_definition(name: name, type: value.class, optional: false, nullable: false, api_name: api_name,
60
+ value: value)
61
+
62
+ define_accessor(name)
63
+ end
64
+
65
+ # Adds a new field definition into the class's extra fields registry
66
+ #
67
+ # @api private
68
+ #
69
+ # @param name [Symbol]
70
+ # @param type [Class]
71
+ # @option required [Boolean]
72
+ # @option optional [Boolean]
73
+ # @return [void]
74
+ def add_extra_field_definition(name:, type:)
75
+ return if extra_fields.key?(name.to_sym)
76
+
77
+ extra_fields[name.to_sym] = Field.new(name: name, type: type, optional: true, nullable: false)
78
+
79
+ define_accessor(name)
80
+ define_setter(name)
81
+ end
82
+
83
+ def coerce(value, strict: (respond_to?(:strict?) ? strict? : false))
84
+ return value if value.is_a?(self)
85
+
86
+ return value unless value.is_a?(::Hash)
87
+
88
+ new(value)
89
+ end
90
+
91
+ def load(str)
92
+ coerce(::JSON.parse(str, symbolize_names: true))
93
+ end
94
+
95
+ def ===(instance)
96
+ instance.class.ancestors.include?(self)
97
+ end
98
+
99
+ private
100
+
101
+ # Adds a new field definition into the class's fields registry
102
+ #
103
+ # @api private
104
+ #
105
+ # @param name [Symbol]
106
+ # @param type [Class]
107
+ # @option optional [Boolean]
108
+ # @return [void]
109
+ def add_field_definition(name:, type:, optional:, nullable:, api_name:, default: nil, value: nil)
110
+ fields[name.to_sym] =
111
+ Field.new(name: name, type: type, optional: optional, nullable: nullable, api_name: api_name,
112
+ value: value, default: default)
113
+ end
114
+
115
+ # @api private
116
+ def define_accessor(name)
117
+ method_name = name.to_sym
118
+
119
+ define_method(method_name) do
120
+ @data[name]
121
+ end
122
+ end
123
+
124
+ # @api private
125
+ def define_setter(name)
126
+ method_name = :"#{name}="
127
+
128
+ define_method(method_name) do |val|
129
+ @data[name] = val
130
+ end
131
+ end
132
+ end
133
+
134
+ # Creates a new instance of this model
135
+ # TODO: Should all this logic be in `#coerce` instead?
136
+ #
137
+ # @param values [Hash]
138
+ # @option strict [Boolean]
139
+ # @return [self]
140
+ def initialize(values = {})
141
+ @data = {}
142
+
143
+ values = Utils.symbolize_keys(values.dup)
144
+
145
+ self.class.fields.each do |field_name, field|
146
+ # Presence, not truthiness: `values[key]` cannot tell "absent" from "explicitly nil",
147
+ # and `to_h` needs that distinction — an explicit null is "delete this field" to a
148
+ # `merge-patch` endpoint, while an absent one must stay absent.
149
+ provided_key = [field.api_name.to_sym, field.api_name, field_name].find { |key| values.key?(key) }
150
+ value = provided_key.nil? ? nil : values.delete(provided_key)
151
+
152
+ # Presence again, not truthiness: `value || default` replaced an explicitly-passed
153
+ # `false` with the literal/default, `false` being falsy in Ruby. An explicit nil now
154
+ # wins over the default too — asking for null is not asking for the default.
155
+ field_value = if !provided_key.nil?
156
+ value
157
+ elsif field.literal?
158
+ field.value
159
+ elsif field.default
160
+ field.default
161
+ end
162
+
163
+ # An optional field the caller never mentioned is left OUT of `@data` rather than
164
+ # materialized as an explicit nil, which `to_h` would have serialized as `null`.
165
+ next if provided_key.nil? && field_value.nil? && field.optional
166
+
167
+ @data[field_name] = Utils.coerce(field.type, field_value)
168
+ end
169
+
170
+ # Any remaining values in the input become extra fields
171
+ values.each do |name, value|
172
+ self.class.add_extra_field_definition(name: name, type: value.class)
173
+
174
+ @data[name.to_sym] = value
175
+ end
176
+ end
177
+
178
+ def to_h
179
+ result = self.class.fields.merge(self.class.extra_fields).each_with_object({}) do |(name, field), acc|
180
+ # Keyed on PRESENCE first, then on nullability:
181
+ # absent -> omitted, for the reason `initialize` gives above
182
+ # explicit nil, nullable -> `null`; that is exactly what the caller asked for
183
+ # explicit nil, NOT nullable -> omitted; the declared schema forbids null
184
+
185
+ value = @data[name]
186
+
187
+ next if field.optional && !@data.key?(name)
188
+ next if value.nil? && field.optional && !field.nullable
189
+
190
+ acc[field.api_name] = serialize_value(value)
191
+ end
192
+
193
+ # Inject union discriminant if this instance was coerced from a discriminated union
194
+ # and the discriminant key is not already present in the result
195
+ discriminant_key = instance_variable_get(:@_fern_union_discriminant_key)
196
+ discriminant_value = instance_variable_get(:@_fern_union_discriminant_value)
197
+ result[discriminant_key] = discriminant_value if discriminant_key && discriminant_value && !result.key?(discriminant_key)
198
+
199
+ result
200
+ end
201
+
202
+ def ==(other)
203
+ self.class == other.class && to_h == other.to_h
204
+ end
205
+
206
+ # @return [String]
207
+ def inspect
208
+ attrs = @data.map do |name, value|
209
+ field = self.class.fields[name] || self.class.extra_fields[name]
210
+ display_value = field&.sensitive? ? '[REDACTED]' : value.inspect
211
+ "#{name}=#{display_value}"
212
+ end
213
+
214
+ "#<#{self.class.name}:0x#{object_id&.to_s(16)} #{attrs.join(' ')}>"
215
+ end
216
+
217
+ private
218
+
219
+ # `::Hash#to_h` returns itself rather than recursing, so a model nested inside a
220
+ # dictionary-typed field's VALUES (not just a direct model field) needs an explicit
221
+ # recursive walk here rather than the bare `value.to_h` this used to do. `Symbol` is
222
+ # handled the same way: enum members are coerced to `Symbol` on the way in
223
+ # (`Internal::Types::Enum#coerce`) so they round-trip through `==`/`Set` lookups, but the
224
+ # wire shape is always the original string value.
225
+ def serialize_value(value)
226
+ case value
227
+ when ::Symbol
228
+ value.to_s
229
+ when ::Array
230
+ value.map { |item| serialize_value(item) }
231
+ when ::Hash
232
+ value.transform_values { |item| serialize_value(item) }
233
+ else
234
+ # `respond_to?(:to_h)`, not `is_a?(Model)`: a free-form/`any`-typed field can hold
235
+ # whatever a caller constructs it with, not only this SDK's own model classes --
236
+ # `Struct`, `OpenStruct`, `Data`, and similar objects respond to `#to_h` too, and
237
+ # narrowing to `Model` would silently fall through to `#to_s` in `JSON.generate` for
238
+ # any of them. `Model#to_h` itself routes back through this same method for every
239
+ # field, so a nested `Model` value still serializes correctly through this branch.
240
+ !value.nil? && value.respond_to?(:to_h) ? value.to_h : value
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ module Types
6
+ # @abstract
7
+ module Type
8
+ include ::Petstore::Internal::JSON::Serializable
9
+
10
+ # Coerces a value to this type
11
+ #
12
+ # @param value [unknown]
13
+ # @option strict [Boolean] If we should strictly coerce this value
14
+ def coerce(value, strict: strict?)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ # Returns if strictness is on for this type, defaults to `false`
19
+ #
20
+ # @return [Boolean]
21
+ def strict?
22
+ @strict ||= false
23
+ end
24
+
25
+ # Enable strictness by default for this type
26
+ #
27
+ # @return [void]
28
+ def strict!
29
+ @strict = true
30
+ self
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ module Types
6
+ # Define a union between two types
7
+ module Union
8
+ include ::Petstore::Internal::Types::Type
9
+
10
+ def members
11
+ @members ||= []
12
+ end
13
+
14
+ # Add a member to this union
15
+ #
16
+ # @param type [Object]
17
+ # @option key [Symbol, String]
18
+ # @return [void]
19
+ def member(type, key: nil)
20
+ members.push([key, Utils.wrap_type(type)])
21
+ self
22
+ end
23
+
24
+ def type_member?(type)
25
+ members.any? { |_key, type_fn| type == type_fn.call }
26
+ end
27
+
28
+ # Set the discriminant for this union
29
+ #
30
+ # @param key [Symbol, String]
31
+ # @return [void]
32
+ def discriminant(key)
33
+ @discriminant = key
34
+ end
35
+
36
+ def coerce(value, strict: strict?)
37
+ type = resolve_member(value)
38
+
39
+ unless type
40
+ return value unless strict
41
+
42
+ if discriminated?
43
+ raise Errors::TypeError,
44
+ "value of type `#{value.class}` not member of union #{self}"
45
+ end
46
+
47
+ raise Errors::TypeError, "could not resolve to member of union #{self}"
48
+ end
49
+
50
+ coerced = Utils.coerce(type, value, strict: strict)
51
+
52
+ # For discriminated unions, store the discriminant info on the coerced instance
53
+ # so it can be injected back during serialization (to_h)
54
+ if discriminated? && value.is_a?(::Hash) && coerced.is_a?(Model)
55
+ discriminant_value = value.fetch(@discriminant, nil) || value.fetch(@discriminant.to_s, nil)
56
+ if discriminant_value
57
+ coerced.instance_variable_set(:@_fern_union_discriminant_key, @discriminant.to_s)
58
+ coerced.instance_variable_set(:@_fern_union_discriminant_value, discriminant_value)
59
+ end
60
+ end
61
+
62
+ coerced
63
+ end
64
+
65
+ # Parse JSON string and coerce to the correct union member type
66
+ #
67
+ # @param str [String] JSON string to parse
68
+ # @return [Object] Coerced value matching a union member
69
+ def load(str)
70
+ coerce(::JSON.parse(str, symbolize_names: true))
71
+ end
72
+
73
+ # FSDK-1660: like +load+, but RAISES when the value matches no member instead of handing
74
+ # the raw parsed value back.
75
+ #
76
+ # +load+ returns the value untouched in that case, because +coerce+ only raises under
77
+ # strictness and +strict?+ defaults to false. That is correct for a REQUEST body, where the
78
+ # caller's value is theirs to send. It is wrong for a DECLARED SUCCESS RESPONSE: the
79
+ # method's return type is this union, so a bare ::Hash violates it, and the caller found
80
+ # out through a +NoMethodError+ from +result.field+ a long way from the real cause.
81
+ #
82
+ # Only member RESOLUTION is enforced here. The matched member is coerced exactly as +load+
83
+ # coerces it — non-strict — so nothing that used to deserialize stops deserializing; the
84
+ # only bodies that change behaviour are the ones that produced no typed value at all.
85
+ #
86
+ # Note a generated union module defines its OWN +self.load+, which shadows the one above;
87
+ # this method is unshadowed and is deliberately self-contained rather than delegating, so
88
+ # it does not depend on which +load+ wins.
89
+ #
90
+ # @param str [String] JSON string to parse
91
+ # @return [Object] Coerced value matching a union member
92
+ # @raise [Errors::TypeError] If the value matches no member of this union.
93
+ def load!(str)
94
+ value = ::JSON.parse(str, symbolize_names: true)
95
+ unless resolve_member(value)
96
+ raise Errors::TypeError,
97
+ "response body could not be resolved to a member of union #{self}"
98
+ end
99
+
100
+ coerce(value)
101
+ end
102
+
103
+ private
104
+
105
+ # @api private
106
+ def discriminated?
107
+ !@discriminant.nil?
108
+ end
109
+
110
+ # Check if value matches a type, handling type wrapper instances
111
+ # (Internal::Types::Hash and Internal::Types::Array instances)
112
+ #
113
+ # @param value [Object]
114
+ # @param member_type [Object]
115
+ # @return [Boolean]
116
+ def type_matches?(value, member_type)
117
+ case member_type
118
+ when ::Petstore::Internal::Types::Hash
119
+ value.is_a?(::Hash)
120
+ when ::Petstore::Internal::Types::Array
121
+ value.is_a?(::Array)
122
+ when Class, Module
123
+ value.is_a?(member_type)
124
+ else
125
+ false
126
+ end
127
+ end
128
+
129
+ # Resolves the type of a value to be one of the members
130
+ #
131
+ # @param value [Object]
132
+ # @return [Class]
133
+ def resolve_member(value)
134
+ if discriminated? && value.is_a?(::Hash)
135
+ # Try both symbol and string keys for the discriminant
136
+ discriminant_value = value.fetch(@discriminant, nil) || value.fetch(@discriminant.to_s, nil)
137
+
138
+ return if discriminant_value.nil?
139
+
140
+ # Convert to string for consistent comparison
141
+ discriminant_str = discriminant_value.to_s
142
+
143
+ # First try exact match
144
+ members_hash = members.to_h
145
+ result = members_hash[discriminant_str]&.call
146
+ return result if result
147
+
148
+ # Try case-insensitive match as fallback
149
+ discriminant_lower = discriminant_str.downcase
150
+ matching_keys = members_hash.keys.select { |k| k.to_s.downcase == discriminant_lower }
151
+
152
+ # Only use case-insensitive match if exactly one key matches (avoid ambiguity)
153
+ return members_hash[matching_keys.first]&.call if matching_keys.length == 1
154
+
155
+ nil
156
+ else
157
+ # First try exact type matching
158
+ result = members.find do |_key, mem|
159
+ member_type = Utils.unwrap_type(mem)
160
+ type_matches?(value, member_type)
161
+ end&.last&.call
162
+
163
+ return result if result
164
+
165
+ # For Hash values, try to coerce into Model member types
166
+ if value.is_a?(::Hash)
167
+ members.find do |_key, mem|
168
+ member_type = Utils.unwrap_type(mem)
169
+ # Check if member_type is a Model class
170
+ next unless member_type.is_a?(Class) && member_type <= Model
171
+
172
+ # Try to coerce the hash into this model type with strict mode
173
+ begin
174
+ candidate = Utils.coerce(member_type, value, strict: true)
175
+
176
+ # Validate that all required (non-optional) fields are present
177
+ # This ensures undiscriminated unions properly distinguish between member types
178
+ member_type.fields.each do |field_name, field|
179
+ raise Errors::TypeError, "Required field `#{field_name}` missing for union member #{member_type.name}" if candidate.instance_variable_get(:@data)[field_name].nil? && !field.optional
180
+ end
181
+
182
+ true
183
+ rescue Errors::TypeError
184
+ false
185
+ end
186
+ end&.last&.call
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ module Types
6
+ module Unknown
7
+ include ::Petstore::Internal::Types::Type
8
+
9
+ def coerce(value)
10
+ value
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end