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.
- checksums.yaml +7 -0
- data/README.md +107 -0
- data/lib/petstore/client.rb +45 -0
- data/lib/petstore/config.rb +54 -0
- data/lib/petstore/environment.rb +7 -0
- data/lib/petstore/error.rb +40 -0
- data/lib/petstore/errors/api_error.rb +9 -0
- data/lib/petstore/errors/client_error.rb +43 -0
- data/lib/petstore/errors/connection_error.rb +11 -0
- data/lib/petstore/errors/redirect_error.rb +9 -0
- data/lib/petstore/errors/response_error.rb +92 -0
- data/lib/petstore/errors/server_error.rb +25 -0
- data/lib/petstore/errors/timeout_error.rb +9 -0
- data/lib/petstore/http/auth.rb +19 -0
- data/lib/petstore/http/oauth_token_manager.rb +91 -0
- data/lib/petstore/internal/errors/constraint_error.rb +10 -0
- data/lib/petstore/internal/errors/type_error.rb +10 -0
- data/lib/petstore/internal/form/request.rb +110 -0
- data/lib/petstore/internal/http/base_request.rb +64 -0
- data/lib/petstore/internal/http/event_stream.rb +106 -0
- data/lib/petstore/internal/http/raw_client.rb +426 -0
- data/lib/petstore/internal/http/response_body.rb +35 -0
- data/lib/petstore/internal/iterators/cursor_item_iterator.rb +28 -0
- data/lib/petstore/internal/iterators/cursor_page_iterator.rb +72 -0
- data/lib/petstore/internal/iterators/item_iterator.rb +72 -0
- data/lib/petstore/internal/iterators/offset_item_iterator.rb +30 -0
- data/lib/petstore/internal/iterators/offset_page_iterator.rb +110 -0
- data/lib/petstore/internal/json/request.rb +61 -0
- data/lib/petstore/internal/json/serializable.rb +25 -0
- data/lib/petstore/internal/multipart/multipart_encoder.rb +141 -0
- data/lib/petstore/internal/multipart/multipart_form_data.rb +78 -0
- data/lib/petstore/internal/multipart/multipart_form_data_part.rb +51 -0
- data/lib/petstore/internal/multipart/multipart_request.rb +49 -0
- data/lib/petstore/internal/raw/request.rb +88 -0
- data/lib/petstore/internal/types/array.rb +47 -0
- data/lib/petstore/internal/types/boolean.rb +34 -0
- data/lib/petstore/internal/types/enum.rb +56 -0
- data/lib/petstore/internal/types/hash.rb +36 -0
- data/lib/petstore/internal/types/model/field.rb +38 -0
- data/lib/petstore/internal/types/model.rb +246 -0
- data/lib/petstore/internal/types/type.rb +35 -0
- data/lib/petstore/internal/types/union.rb +193 -0
- data/lib/petstore/internal/types/unknown.rb +15 -0
- data/lib/petstore/internal/types/utils.rb +137 -0
- data/lib/petstore/models/open_model.rb +30 -0
- data/lib/petstore/models/serializable.rb +63 -0
- data/lib/petstore/models/union_type.rb +12 -0
- data/lib/petstore/pet/client.rb +196 -0
- data/lib/petstore/pet/types/delete_pet_request.rb +13 -0
- data/lib/petstore/pet/types/find_pets_by_status_request.rb +11 -0
- data/lib/petstore/pet/types/find_pets_by_status_request_status.rb +24 -0
- data/lib/petstore/pet/types/find_pets_by_tags_request.rb +11 -0
- data/lib/petstore/pet/types/get_pet_by_id_request.rb +11 -0
- data/lib/petstore/pet/types/pet.rb +25 -0
- data/lib/petstore/pet/types/update_pet_with_form_request.rb +15 -0
- data/lib/petstore/serializers/form_serializer.rb +41 -0
- data/lib/petstore/serializers/json_serializer.rb +41 -0
- data/lib/petstore/serializers/xml_serializer.rb +56 -0
- data/lib/petstore/store/client.rb +89 -0
- data/lib/petstore/store/types/delete_order_request.rb +11 -0
- data/lib/petstore/store/types/get_order_by_id_request.rb +11 -0
- data/lib/petstore/store/types/order.rb +25 -0
- data/lib/petstore/types/api_response.rb +20 -0
- data/lib/petstore/types/category.rb +19 -0
- data/lib/petstore/types/order_status.rb +22 -0
- data/lib/petstore/types/pet_status.rb +22 -0
- data/lib/petstore/types/tag.rb +19 -0
- data/lib/petstore/user/client.rb +149 -0
- data/lib/petstore/user/types/delete_user_request.rb +11 -0
- data/lib/petstore/user/types/get_user_by_name_request.rb +11 -0
- data/lib/petstore/user/types/login_user_request.rb +13 -0
- data/lib/petstore/user/types/user.rb +27 -0
- data/lib/petstore/validator.rb +42 -0
- data/lib/petstore/version.rb +5 -0
- data/lib/petstore.rb +78 -0
- 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
|