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,426 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Required here rather than inherited from elsewhere in the load order: this file is the only one
4
+ # in the generated runtime that talks to Net::HTTP, and TIMEOUT_ERRORS names its exception classes
5
+ # at class-definition time.
6
+ require 'net/http'
7
+ require 'openssl'
8
+ require 'uri'
9
+
10
+ module Petstore
11
+ module Internal
12
+ module Http
13
+ # @api private
14
+ class RawClient
15
+ # The retry policy this SDK was generated with. Every value is overridable per client via
16
+ # `retry_config:`, and the keys are the ones the rest of the generated SDKs already use
17
+ # (the legacy Ruby `HTTP::Connection` and the Python `RetryHandler`), so there is one shape
18
+ # to learn rather than a Ruby-only third one.
19
+ DEFAULT_RETRY_CONFIG = {
20
+ # Retried status codes. `retry_all_server_errors` covers the open-ended 5xx range that
21
+ # cannot be enumerated.
22
+ status_codes_to_retry: [408, 429].freeze,
23
+ retry_all_server_errors: true,
24
+ # HTTP methods allowed to retry at all. A method absent here is never retried; a method
25
+ # present here is retried only if replaying it is safe (see `#retryable_method?`).
26
+ http_methods_to_retry: %w[GET POST PUT DELETE PATCH HEAD OPTIONS].freeze,
27
+ # Exponential backoff, in milliseconds: delay_ms * (backoff_factor ** attempt), capped at
28
+ # max_delay_ms, plus up to jitter_ms of random jitter.
29
+ delay_ms: 150,
30
+ max_delay_ms: 5_000,
31
+ backoff_factor: 2,
32
+ jitter_ms: 50,
33
+ # Ceiling for a server-directed wait parsed from Retry-After. Separate from max_delay_ms
34
+ # because a server asking for a long wait is legitimate; this only guards against a
35
+ # pathological value.
36
+ max_retry_after_delay_ms: 60_000,
37
+ }.freeze
38
+
39
+ # Methods RFC 9110 defines as idempotent: replaying one cannot cause a second effect, so a
40
+ # retry is safe. POST and PATCH are absent deliberately — see `#retryable_method?`.
41
+ IDEMPOTENT_METHODS = %w[GET HEAD PUT DELETE OPTIONS TRACE].freeze
42
+
43
+ # Request headers that carry a caller-supplied idempotency key. A server that honours one
44
+ # deduplicates a replay, which is what makes retrying a non-idempotent method safe.
45
+ IDEMPOTENCY_KEY_HEADERS = %w[Idempotency-Key].freeze
46
+
47
+ # Net::HTTP's three timeout exceptions, all reachable because `#send` sets open_timeout,
48
+ # read_timeout AND write_timeout. Deliberately not widened to their shared parent
49
+ # `Timeout::Error`, which would also swallow an unrelated `Timeout.timeout` block in caller
50
+ # code.
51
+ # `IO::TimeoutError` is the fourth and is NOT a Net::HTTP class: since Ruby 3.2 an expired
52
+ # connect deadline surfaces from the IO layer, and it descends from IOError rather than
53
+ # Timeout::Error, so none of the three above catch it and CONNECTION_ERRORS excludes bare
54
+ # IOError. Without it a timed-out connect escaped unwrapped on ruby 3.3 -- the version the
55
+ # gemspec floors at and the e2e image runs -- while raising Net::OpenTimeout on 4.x, so it
56
+ # looked correct locally. No `defined?` guard: the gemspec requires >= 3.3.
57
+ TIMEOUT_ERRORS = [::Net::OpenTimeout, ::Net::ReadTimeout, ::Net::WriteTimeout, ::IO::TimeoutError].freeze
58
+
59
+ # Failures that mean the request never reached the server. `SocketError` covers DNS
60
+ # resolution (`Socket::ResolutionError` derives from it); `EOFError` is the peer closing
61
+ # mid-exchange; `OpenSSL::SSL::SSLError` is a handshake or certificate failure, which is
62
+ # still a connection that could not be established. Bare `IOError` is deliberately absent:
63
+ # it is broad enough to catch an unrelated failure from a caller-supplied IO body.
64
+ CONNECTION_ERRORS = [
65
+ ::SocketError,
66
+ ::EOFError,
67
+ ::Errno::ECONNREFUSED,
68
+ ::Errno::ECONNRESET,
69
+ ::Errno::ECONNABORTED,
70
+ ::Errno::EHOSTUNREACH,
71
+ ::Errno::ENETUNREACH,
72
+ ::Errno::EPIPE,
73
+ ::Errno::ETIMEDOUT,
74
+ ::OpenSSL::SSL::SSLError,
75
+ ].freeze
76
+
77
+ # Both sets, in the order `#wrap_transport_error` classifies them. TIMEOUT_ERRORS is
78
+ # checked first because Net::WriteTimeout is an IOError descendant and would otherwise be
79
+ # ambiguous.
80
+ TRANSPORT_ERRORS = (TIMEOUT_ERRORS + CONNECTION_ERRORS).freeze
81
+
82
+ # Open/read/write timeout in seconds when neither the client nor the call names one. Held as
83
+ # a constant rather than a keyword default so the root client can offer a `timeout:` of its
84
+ # own without restating the number.
85
+ DEFAULT_TIMEOUT = 60.0
86
+
87
+ # @return [String] The base URL for requests
88
+ attr_reader :base_url
89
+
90
+ # @param base_url [String] The base url for the request.
91
+ # @param max_retries [Integer] The number of times to retry a failed request, defaults to 2.
92
+ # @param timeout [Float, nil] The timeout for the request; DEFAULT_TIMEOUT when nil.
93
+ # @param headers [Hash] The headers for the request.
94
+ # @param token_provider [Proc, nil] Optional callable returning the Authorization header
95
+ # value; invoked on every request so dynamic (OAuth/refresh) credentials stay current.
96
+ # @param retry_config [Hash] Overrides for any key of DEFAULT_RETRY_CONFIG.
97
+ def initialize(base_url:, max_retries: 2, timeout: nil, headers: {}, token_provider: nil, retry_config: {})
98
+ @base_url = base_url
99
+ @max_retries = max_retries
100
+ @timeout = timeout || DEFAULT_TIMEOUT
101
+ @token_provider = token_provider
102
+ @retry_config = DEFAULT_RETRY_CONFIG.merge(retry_config.to_h.transform_keys(&:to_sym))
103
+ @default_headers = {
104
+ 'User-Agent': 'postman-codegen/3.3.0 petstore/0.1.0 (ruby)',
105
+ 'X-Fern-Language': 'Ruby',
106
+ 'X-Fern-SDK-Name': 'petstore',
107
+ 'X-Fern-SDK-Version': '0.1.0',
108
+ }.merge(headers)
109
+ end
110
+
111
+ # @param request [::Petstore::Internal::Http::BaseRequest] The HTTP request.
112
+ # @return [Net::HTTPResponse] The HTTP response.
113
+ # @raise [::Petstore::Errors::TimeoutError] The request timed out and no retry remained.
114
+ # @raise [::Petstore::Errors::ConnectionError] The request never reached the server.
115
+ def send(request)
116
+ url = build_url(request)
117
+ timeout = resolve_timeout(request)
118
+ attempt = 0
119
+ response = nil
120
+
121
+ loop do
122
+ # Refresh the dynamic auth header (OAuth/refresh) before each attempt, if configured.
123
+ @default_headers.merge!(resolve_auth_headers)
124
+
125
+ # Encoded ONCE per attempt and then reused for the retry decision. `encode_headers` is
126
+ # not a pure getter — `Multipart::Request` reads its body's content type off the
127
+ # encoder — so asking the request again from inside `should_retry?` re-ran that work on
128
+ # every attempt for no gain.
129
+ headers = request.encode_headers(protected_keys: protected_header_keys)
130
+
131
+ http_request = build_http_request(
132
+ url:,
133
+ method: request.method,
134
+ headers:,
135
+ body: request.encode_body,
136
+ )
137
+
138
+ begin
139
+ conn = connect(url)
140
+ conn.open_timeout = timeout
141
+ conn.read_timeout = timeout
142
+ conn.write_timeout = timeout
143
+ conn.continue_timeout = timeout
144
+
145
+ response = conn.request(http_request)
146
+ rescue *TRANSPORT_ERRORS => e
147
+ raise wrap_transport_error(e) unless retry_transport_error?(request, attempt, headers)
148
+
149
+ sleep(retry_delay(nil, attempt))
150
+ attempt += 1
151
+ next
152
+ end
153
+
154
+ break unless should_retry?(request, response, attempt, headers)
155
+
156
+ delay = retry_delay(response, attempt)
157
+ sleep(delay)
158
+ attempt += 1
159
+ end
160
+
161
+ response
162
+ end
163
+
164
+ # The read/open/write timeout for this request, in seconds: `request_options[:timeout]`
165
+ # when the caller supplied one for this call, else the client's own.
166
+ # @param request [::Petstore::Internal::Http::BaseRequest] The HTTP request.
167
+ # @return [Float, Integer, nil] The timeout in seconds.
168
+ def resolve_timeout(request)
169
+ options = request.request_options
170
+ per_request = options&.dig(:timeout) || options&.dig('timeout')
171
+ per_request || @timeout
172
+ end
173
+
174
+ # Turns a transport-level exception into this SDK's own error type, so a caller can rescue
175
+ # a timeout or a failed connection without importing Net::HTTP's or Socket's exception
176
+ # classes. Ruby records the original on `#cause`, so nothing is lost. No status code is
177
+ # invented: neither failure produced a response, so there is none to report.
178
+ # @param error [StandardError] The transport exception.
179
+ # @return [::Petstore::Errors::ApiError] The SDK-owned error to raise.
180
+ def wrap_transport_error(error)
181
+ return ::Petstore::Errors::TimeoutError.new(error.message) if TIMEOUT_ERRORS.any? { |klass| error.is_a?(klass) }
182
+
183
+ ::Petstore::Errors::ConnectionError.new(error.message)
184
+ end
185
+
186
+ # Whether a transport failure may be replayed. A timeout leaves the request's fate unknown —
187
+ # the server may have applied it and only failed to answer in time — so this reuses the same
188
+ # idempotency gate as a status-code retry rather than a looser one. A connection failure is
189
+ # held to the same gate: a reset or a broken pipe can happen after the server accepted the
190
+ # request, so it is no safer to replay than a timeout.
191
+ # @param request [::Petstore::Internal::Http::BaseRequest] The HTTP request.
192
+ # @param attempt [Integer] The current retry attempt (0-indexed).
193
+ # @param headers [Hash] The headers already encoded for this attempt by `#send`.
194
+ # @return [Boolean] Whether the request should be retried.
195
+ def retry_transport_error?(request, attempt, headers)
196
+ return false if attempt >= @max_retries
197
+
198
+ retryable_method?(request, headers)
199
+ end
200
+
201
+ # Determines if a request should be retried, from the response status AND the request
202
+ # method. The method half is not optional: a retry re-sends the request, so retrying a
203
+ # non-idempotent write can duplicate it — a second charge, a second row — and a 503 is
204
+ # exactly the case where the server may well have applied the first attempt before failing
205
+ # to answer.
206
+ # @param request [::Petstore::Internal::Http::BaseRequest] The HTTP request.
207
+ # @param response [Net::HTTPResponse] The HTTP response.
208
+ # @param attempt [Integer] The current retry attempt (0-indexed).
209
+ # @param headers [Hash] The headers already encoded for this attempt by `#send`.
210
+ # @return [Boolean] Whether the request should be retried.
211
+ def should_retry?(request, response, attempt, headers)
212
+ return false if attempt >= @max_retries
213
+ return false unless retryable_method?(request, headers)
214
+
215
+ retryable_status?(response.code.to_i)
216
+ end
217
+
218
+ # @param status [Integer] The response status code.
219
+ # @return [Boolean] Whether the status code is retryable.
220
+ def retryable_status?(status)
221
+ return true if @retry_config[:retry_all_server_errors] && status >= 500
222
+
223
+ @retry_config[:status_codes_to_retry].include?(status)
224
+ end
225
+
226
+ # Whether this request may be replayed.
227
+ #
228
+ # Two gates. The configured method list decides whether the method is eligible at all.
229
+ # Eligibility alone is not enough for a non-idempotent method, though: replaying a POST or
230
+ # a PATCH can duplicate its effect, so one is retried only when the caller supplied an
231
+ # idempotency key for the server to deduplicate on.
232
+ # @param request [::Petstore::Internal::Http::BaseRequest] The HTTP request.
233
+ # @param headers [Hash] The headers already encoded for this attempt by `#send`.
234
+ # @return [Boolean] Whether the request is safe to replay.
235
+ def retryable_method?(request, headers)
236
+ method = request.method.to_s.upcase
237
+ return false unless @retry_config[:http_methods_to_retry].include?(method)
238
+ return true if IDEMPOTENT_METHODS.include?(method)
239
+
240
+ idempotency_key?(headers)
241
+ end
242
+
243
+ # Reads the headers `#send` already encoded rather than asking the request to encode them
244
+ # again: these are the headers actually going on the wire, and re-encoding per retry
245
+ # decision repeated work for an answer that cannot differ.
246
+ # @param headers [Hash] The encoded HTTP request headers.
247
+ # @return [Boolean] Whether the request carries a non-empty idempotency key header.
248
+ def idempotency_key?(headers)
249
+ wanted = IDEMPOTENCY_KEY_HEADERS.map(&:downcase)
250
+ headers.any? { |name, value| wanted.include?(name.to_s.downcase) && !value.to_s.empty? }
251
+ end
252
+
253
+ # The default-header names a per-request header may not override.
254
+ # @return [Array<Symbol>] The protected header names.
255
+ def protected_header_keys
256
+ @default_headers.keys
257
+ end
258
+
259
+ # The dynamic auth headers for this attempt, re-resolved per attempt so a refreshed OAuth
260
+ # token reaches the retry. Empty when the client was built without a token provider.
261
+ # @return [Hash] The auth headers.
262
+ def resolve_auth_headers
263
+ return {} unless @token_provider
264
+
265
+ { Authorization: @token_provider.call }
266
+ end
267
+
268
+ # Calculates the delay before the next retry attempt using exponential backoff with jitter.
269
+ # Respects Retry-After header if present.
270
+ # @param response [Net::HTTPResponse, nil] The HTTP response, or nil after a transport failure.
271
+ # @param attempt [Integer] The current retry attempt (0-indexed).
272
+ # @return [Float] The delay in seconds before the next retry.
273
+ def retry_delay(response, attempt)
274
+ # Check for Retry-After header (can be seconds or HTTP date). Nil when the attempt ended
275
+ # in a transport failure rather than a response, which has no headers to read.
276
+ retry_after = response['Retry-After'] if response
277
+ if retry_after
278
+ delay = parse_retry_after(retry_after)
279
+ return [delay, @retry_config[:max_retry_after_delay_ms] / 1000.0].min if delay&.positive?
280
+ end
281
+
282
+ # Exponential backoff: delay_ms * (backoff_factor ** attempt), capped, plus jitter.
283
+ base_ms = @retry_config[:delay_ms] * (@retry_config[:backoff_factor]**attempt)
284
+ capped_ms = [base_ms, @retry_config[:max_delay_ms]].min
285
+ add_jitter(capped_ms) / 1000.0
286
+ end
287
+
288
+ # Spreads retries so concurrent clients do not resynchronise on the same backoff schedule.
289
+ # `to_i` because `jitter_ms` is overridable per client and only an Integer range gives
290
+ # `rand` a whole number of milliseconds to pick from — a Float bound answers with a Float,
291
+ # and a String bound (`"50"`) has no ordering against 0 at all.
292
+ # @param delay_ms [Numeric] The un-jittered delay in milliseconds.
293
+ # @return [Numeric] The delay in milliseconds with jitter added.
294
+ def add_jitter(delay_ms)
295
+ delay_ms + rand(0..@retry_config[:jitter_ms].to_i)
296
+ end
297
+
298
+ # Parses the Retry-After header value.
299
+ # @param value [String] The Retry-After header value (seconds or HTTP date).
300
+ # @return [Float, nil] The delay in seconds, or nil if parsing fails.
301
+ def parse_retry_after(value)
302
+ # Try parsing as integer (seconds)
303
+ seconds = Integer(value, exception: false)
304
+ return seconds.to_f if seconds
305
+
306
+ # Try parsing as HTTP date
307
+ begin
308
+ retry_time = Time.httpdate(value)
309
+ delay = retry_time - Time.now
310
+ delay.positive? ? delay : nil
311
+ rescue ArgumentError
312
+ nil
313
+ end
314
+ end
315
+
316
+ LOCALHOST_HOSTS = %w[localhost 127.0.0.1 [::1]].freeze
317
+
318
+ # @param request [::Petstore::Internal::Http::BaseRequest] The HTTP request.
319
+ # @return [URI::Generic] The URL.
320
+ def build_url(request)
321
+ encoded_query = request.encode_query
322
+
323
+ # If the path is already an absolute URL, use it directly
324
+ if request.path.start_with?('http://', 'https://')
325
+ url = request.path
326
+ url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
327
+ parsed = URI.parse(url)
328
+ validate_https!(parsed)
329
+ return parsed
330
+ end
331
+
332
+ path = request.path.start_with?('/') ? request.path[1..] : request.path
333
+ base = request.base_url || @base_url
334
+ url = "#{base.chomp('/')}/#{path}"
335
+ url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
336
+ parsed = URI.parse(url)
337
+ validate_https!(parsed)
338
+ parsed
339
+ end
340
+
341
+ # Raises if the URL uses http:// for a non-localhost host, which would
342
+ # send authentication credentials in plaintext.
343
+ # @param url [URI::Generic] The parsed URL.
344
+ def validate_https!(url)
345
+ return if url.scheme != 'http'
346
+ return if LOCALHOST_HOSTS.include?(url.host)
347
+
348
+ raise ArgumentError,
349
+ "Refusing to send request to non-HTTPS URL: #{url}. " \
350
+ 'HTTP is only allowed for localhost. Use HTTPS or pass a localhost URL.'
351
+ end
352
+
353
+ # @param url [URI::Generic] The url to the resource.
354
+ # @param method [String] The HTTP method to use.
355
+ # @param headers [Hash] The headers for the request.
356
+ # @param body [String, nil] The body for the request.
357
+ # @return [HTTP::Request] The HTTP request.
358
+ def build_http_request(url:, method:, headers: {}, body: nil)
359
+ request = Net::HTTPGenericRequest.new(
360
+ method,
361
+ !body.nil?,
362
+ method != 'HEAD',
363
+ url,
364
+ )
365
+
366
+ request_headers = @default_headers.merge(headers)
367
+ request_headers.each { |name, value| request[name] = value }
368
+ request.body = body if body
369
+
370
+ # Net::HTTP silently turns OFF its transparent gzip/deflate decoding as soon as the request
371
+ # carries an explicit Accept-Encoding, handing back a compressed body. Re-assert it.
372
+ request.extend(DecodeContent) if request_headers.keys.any? { |name| name.to_s.casecmp('accept-encoding').zero? }
373
+
374
+ request
375
+ end
376
+
377
+ # Keeps Net::HTTP's transparent gzip/deflate response decoding enabled even when an
378
+ # Accept-Encoding header is set explicitly on the request.
379
+ # @api private
380
+ module DecodeContent
381
+ def decode_content # rubocop:disable Naming/PredicateMethod
382
+ true
383
+ end
384
+ end
385
+
386
+ # @param query [Hash, Array<String>] The query for the request. An Array carries parts
387
+ # that +Serializers::Params+ has already style-serialized and percent-encoded (see
388
+ # +BaseRequest#encode_query+), so it is joined as-is — re-encoding it would escape the
389
+ # `=`, `&` and style delimiters it is built from.
390
+ # @return [String, nil] The encoded query.
391
+ def encode_query(query)
392
+ return query.empty? ? nil : query.join('&') if query.is_a?(::Array)
393
+
394
+ query.to_h.empty? ? nil : URI.encode_www_form(query)
395
+ end
396
+
397
+ # @param url [URI::Generic] The url to connect to.
398
+ # @return [Net::HTTP] The HTTP connection.
399
+ def connect(url)
400
+ is_https = (url.scheme == 'https')
401
+
402
+ port = if url.port
403
+ url.port
404
+ elsif is_https
405
+ Net::HTTP.https_default_port
406
+ else
407
+ Net::HTTP.http_default_port
408
+ end
409
+
410
+ http = Net::HTTP.new(url.host, port)
411
+ http.use_ssl = is_https
412
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER if is_https
413
+ # NOTE: We handle retries at the application level with HTTP status code awareness,
414
+ # so we set max_retries to 0 to disable Net::HTTP's built-in network-level retries.
415
+ http.max_retries = 0
416
+ http
417
+ end
418
+
419
+ # @return [String]
420
+ def inspect
421
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} @base_url=#{@base_url.inspect}>"
422
+ end
423
+ end
424
+ end
425
+ end
426
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ module Http
6
+ # Decoding a 2xx body. The service methods decide WHICH schema applies from the DECLARED
7
+ # status code and media type; this holds the one runtime question they cannot answer at
8
+ # generation time: whether there is a body at all.
9
+ #
10
+ # A status declaring several media types is dispatched on the declaration too, so the
11
+ # response's actual `Content-Type` is deliberately not consulted here. The reasoning is in
12
+ # `buildFernDeserializeExpression`: the alternative needs a union return type in a
13
+ # statically-typed sibling language, and trusting the declaration is what keeps a body the
14
+ # server mislabelled parseable.
15
+ #
16
+ # @api private
17
+ module ResponseBody
18
+ # Yields the body for decoding, or returns nil when there is nothing to decode.
19
+ #
20
+ # A 2xx with an empty body is not an error — a server is entitled to answer a declared 200
21
+ # with no content — but handing "" or nil to a JSON parse raises from inside the SDK
22
+ # (`TypeError: no implicit conversion of nil into String`, or `JSON::ParserError` on the
23
+ # empty string), which is the failure this guards.
24
+ # @param response [Net::HTTPResponse] The HTTP response.
25
+ # @return [Object, nil] The block's value, or nil for an absent body.
26
+ def self.decode(response)
27
+ body = response.body
28
+ return nil if body.nil? || body.empty?
29
+
30
+ yield body
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ class CursorItemIterator < ItemIterator
6
+ # Instantiates a CursorItemIterator, an Enumerable class which wraps calls to a cursor-based paginated API and yields individual items from it.
7
+ #
8
+ # @param initial_cursor [String] The initial cursor to use when iterating, if any.
9
+ # @param cursor_field [Symbol] The field in API responses to extract the next cursor from.
10
+ # @param item_field [Symbol] The field in API responses to extract the items to iterate over.
11
+ # @param block [Proc] A block which is responsible for receiving a cursor to use and returning the given page from the API.
12
+ # @return [::Petstore::Internal::CursorItemIterator]
13
+ def initialize(initial_cursor:, cursor_field:, item_field:, &)
14
+ super()
15
+ @item_field = item_field
16
+ @page_iterator = CursorPageIterator.new(initial_cursor:, cursor_field:, &)
17
+ @page = nil
18
+ end
19
+
20
+ # Returns the CursorPageIterator mediating access to the underlying API.
21
+ #
22
+ # @return [::Petstore::Internal::CursorPageIterator]
23
+ def pages
24
+ @page_iterator
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ class CursorPageIterator
6
+ include Enumerable
7
+
8
+ # The raw HTTP response from the most recent page response.
9
+ # @return [Net::HTTPResponse, nil]
10
+ attr_reader :http_response
11
+
12
+ # Instantiates a CursorPageIterator, an Enumerable class which wraps calls to a cursor-based paginated API and yields pages of items.
13
+ #
14
+ # @param initial_cursor [String] The initial cursor to use when iterating, if any.
15
+ # @param cursor_field [Symbol] The name of the field in API responses to extract the next cursor from.
16
+ # @param block [Proc] A block which is responsible for receiving a cursor to use and returning the given page from the API.
17
+ # The block should return a two-element array: [parsed_page, raw_http_response].
18
+ # @return [::Petstore::Internal::CursorPageIterator]
19
+ def initialize(initial_cursor:, cursor_field:, &block)
20
+ @need_initial_load = initial_cursor.nil?
21
+ @cursor = initial_cursor
22
+ @cursor_field = cursor_field
23
+ @get_next_page = block
24
+ @http_response = nil
25
+ end
26
+
27
+ # Iterates over each page returned by the API.
28
+ #
29
+ # @yield [page] Each retrieved page.
30
+ # @return [NilClass]
31
+ def each
32
+ while (page = next_page)
33
+ yield(page)
34
+ end
35
+ end
36
+
37
+ # Whether another page will be available from the API.
38
+ #
39
+ # @return [Boolean]
40
+ def next?
41
+ @need_initial_load || !@cursor.nil?
42
+ end
43
+
44
+ # Retrieves the next page from the API.
45
+ #
46
+ # @return [Object, nil]
47
+ def next_page
48
+ return if !@need_initial_load && @cursor.nil?
49
+
50
+ @need_initial_load = false
51
+ result = @get_next_page.call(@cursor)
52
+ if result.is_a?(Array)
53
+ fetched_page, raw_response = result
54
+ @http_response = raw_response
55
+ else
56
+ fetched_page = result
57
+ end
58
+ @cursor = resolve_field(fetched_page, @cursor_field)
59
+ fetched_page
60
+ end
61
+
62
+ private
63
+
64
+ # Resolves a field accessor (Symbol) or nested accessor path (Array<Symbol>) against a page.
65
+ def resolve_field(page, field)
66
+ return nil if page.nil? || field.nil?
67
+
68
+ Array(field).reduce(page) { |value, accessor| value&.send(accessor) }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ class ItemIterator
6
+ include Enumerable
7
+
8
+ # The raw HTTP response from the most recent page response.
9
+ # @return [Net::HTTPResponse, nil]
10
+ def http_response
11
+ @page_iterator&.http_response
12
+ end
13
+
14
+ # Iterates over each item returned by the API.
15
+ #
16
+ # @yield [item] Each retrieved item.
17
+ # @return [NilClass]
18
+ def each
19
+ while (item = next_element)
20
+ yield(item)
21
+ end
22
+ end
23
+
24
+ # Whether another item will be available from the API.
25
+ #
26
+ # @return [Boolean]
27
+ def next?
28
+ load_next_page if @page.nil?
29
+ return false if @page.nil?
30
+
31
+ return true if any_items_in_cached_page?
32
+
33
+ load_next_page
34
+ any_items_in_cached_page?
35
+ end
36
+
37
+ # Retrieves the next item from the API.
38
+ def next_element
39
+ item = next_item_from_cached_page
40
+ return item if item
41
+
42
+ load_next_page
43
+ next_item_from_cached_page
44
+ end
45
+
46
+ private
47
+
48
+ def next_item_from_cached_page
49
+ return unless @page
50
+
51
+ resolve_field(@page, @item_field)&.shift
52
+ end
53
+
54
+ def any_items_in_cached_page?
55
+ return false unless @page
56
+
57
+ !resolve_field(@page, @item_field).to_a.empty?
58
+ end
59
+
60
+ def load_next_page
61
+ @page = @page_iterator.next_page
62
+ end
63
+
64
+ # Resolves a field accessor (Symbol) or nested accessor path (Array<Symbol>) against a page.
65
+ def resolve_field(page, field)
66
+ return nil if page.nil? || field.nil?
67
+
68
+ Array(field).reduce(page) { |value, accessor| value&.send(accessor) }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ class OffsetItemIterator < ItemIterator
6
+ # Instantiates an OffsetItemIterator, an Enumerable class which wraps calls to an offset-based paginated API and yields the individual items from it.
7
+ #
8
+ # @param initial_page [Integer] The initial page or offset to start from when iterating.
9
+ # @param item_field [Symbol] The name of the field in API responses to extract the items to iterate over.
10
+ # @param has_next_field [Symbol] The name of the field in API responses containing a boolean of whether another page exists.
11
+ # @param step [Boolean] If true, treats the page number as a true offset (i.e. increments the page number by the number of items returned from each call rather than just 1)
12
+ # @param block [Proc] A block which is responsible for receiving a page number to use and returning the given page from the API.
13
+ #
14
+ # @return [::Petstore::Internal::OffsetItemIterator]
15
+ def initialize(initial_page:, item_field:, has_next_field:, step:, &)
16
+ super()
17
+ @item_field = item_field
18
+ @page_iterator = OffsetPageIterator.new(initial_page:, item_field:, has_next_field:, step:, &)
19
+ @page = nil
20
+ end
21
+
22
+ # Returns the OffsetPageIterator that is mediating access to the underlying API.
23
+ #
24
+ # @return [::Petstore::Internal::OffsetPageIterator]
25
+ def pages
26
+ @page_iterator
27
+ end
28
+ end
29
+ end
30
+ end