amritk-scalar-test 0.1.1

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.
@@ -0,0 +1,893 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Code generated by Scalar SDK Generator. DO NOT EDIT.
4
+ require "base64"
5
+ require "json"
6
+ require "net/http"
7
+ require "openssl"
8
+ require "securerandom"
9
+ require "socket"
10
+ require "time"
11
+ require "uri"
12
+
13
+ module ScalarRubyTest
14
+ module Runtime
15
+ RETRYABLE_STATUS = [408, 409, 429, 500, 502, 503, 504].freeze
16
+ module_function
17
+
18
+ def compact_hash(hash)
19
+ hash.each_with_object({}) { |(key, value), result| result[key] = value unless value.nil? }
20
+ end
21
+
22
+ def serialize(value)
23
+ return value.iso8601 if value.respond_to?(:iso8601)
24
+ return value.to_h if value.respond_to?(:to_h)
25
+ return value.map { |item| serialize(item) } if value.is_a?(Array)
26
+ return value.transform_values { |item| serialize(item) } if value.is_a?(Hash)
27
+ value
28
+ end
29
+
30
+ def deserialize(value, descriptor)
31
+ return value if value.nil? || descriptor.nil?
32
+ return value.map { |item| deserialize(item, descriptor.first) } if descriptor.is_a?(Array) && value.is_a?(Array)
33
+ return value unless descriptor.is_a?(String)
34
+ klass = Models.const_get(descriptor, false)
35
+ klass.respond_to?(:from_h) ? klass.from_h(value) : value
36
+ rescue NameError
37
+ value
38
+ end
39
+
40
+ class Page
41
+ include Enumerable
42
+
43
+ attr_reader :items, :response, :next_cursor
44
+
45
+ def initialize(items:, response:, next_cursor: nil, fetch_next: nil)
46
+ @items = items
47
+ @response = response
48
+ @next_cursor = next_cursor
49
+ @fetch_next = fetch_next
50
+ end
51
+
52
+ def each(&block)
53
+ return enum_for(:each) unless block
54
+ @items.each(&block)
55
+ end
56
+
57
+ def each_page
58
+ return enum_for(:each_page) unless block_given?
59
+ page = self
60
+ loop do
61
+ yield page
62
+ break unless page.has_next_page?
63
+ page = page.next_page
64
+ end
65
+ end
66
+
67
+ def auto_paging_each
68
+ return enum_for(:auto_paging_each) unless block_given?
69
+ each_page { |page| page.each { |item| yield item } }
70
+ end
71
+
72
+ def has_next_page?
73
+ !@next_cursor.nil? && !@fetch_next.nil?
74
+ end
75
+
76
+ def next_page
77
+ raise RuntimeError, "No next page available" unless has_next_page?
78
+ @fetch_next.call(@next_cursor)
79
+ end
80
+ end
81
+
82
+ class RawResponse
83
+ attr_reader :data, :status, :headers, :body
84
+
85
+ def initialize(data:, status:, headers:, body:)
86
+ @data = data
87
+ @status = status
88
+ @headers = headers
89
+ @body = body
90
+ end
91
+
92
+ def map
93
+ self.class.new(data: yield(@data), status: @status, headers: @headers, body: @body)
94
+ end
95
+ end
96
+
97
+ class ResultResponse
98
+ attr_reader :data, :error, :status, :headers, :body
99
+
100
+ def initialize(data: nil, error: nil, status:, headers:, body:)
101
+ @data = data
102
+ @error = error
103
+ @status = status
104
+ @headers = headers
105
+ @body = body
106
+ end
107
+ end
108
+
109
+ class WebSocketMessageError < StandardError; end
110
+
111
+ class WebSocketConnection
112
+ attr_reader :socket, :request
113
+
114
+ def initialize(socket, request)
115
+ @socket = socket
116
+ @request = request
117
+ end
118
+
119
+ def send(message)
120
+ send_text(@request.serialize_send(message))
121
+ end
122
+
123
+ def send_text(payload)
124
+ data = payload.to_s.b
125
+ mask = SecureRandom.random_bytes(4)
126
+ header = [0x81]
127
+ if data.bytesize < 126
128
+ header << (0x80 | data.bytesize)
129
+ elsif data.bytesize < 65_536
130
+ header << (0x80 | 126)
131
+ header.concat([data.bytesize].pack("n").bytes)
132
+ else
133
+ header << (0x80 | 127)
134
+ header.concat([data.bytesize].pack("Q>").bytes)
135
+ end
136
+ masked = data.bytes.each_with_index.map { |byte, index| (byte ^ mask.getbyte(index % 4)).chr }.join
137
+ @socket.write(header.pack("C*") + mask + masked)
138
+ end
139
+
140
+ def receive
141
+ text = receive_text
142
+ text.nil? ? nil : @request.deserialize_receive(text)
143
+ end
144
+
145
+ def receive_text
146
+ header = read_exact(2).bytes
147
+ opcode = header[0] & 0x0f
148
+ length_byte = header[1]
149
+ masked = (length_byte & 0x80) != 0
150
+ length = length_byte & 0x7f
151
+ length = read_exact(2).unpack1("n") if length == 126
152
+ length = read_exact(8).unpack1("Q>") if length == 127
153
+ mask = masked ? read_exact(4).bytes : nil
154
+ payload = read_exact(length)
155
+ payload = payload.bytes.each_with_index.map { |byte, index| (byte ^ mask[index % 4]).chr }.join if mask
156
+ return nil if opcode == 0x8
157
+ raise WebSocketMessageError, "unsupported websocket frame opcode #{opcode}" unless opcode == 0x1
158
+ payload.force_encoding("UTF-8")
159
+ end
160
+
161
+ def close
162
+ @socket.close unless @socket.closed?
163
+ end
164
+
165
+ def closed?
166
+ @socket.closed?
167
+ end
168
+
169
+ private
170
+
171
+ def read_exact(size)
172
+ data = @socket.read(size)
173
+ raise WebSocketMessageError, "unexpected websocket EOF" if data.nil? || data.bytesize != size
174
+ data
175
+ end
176
+ end
177
+
178
+ class WebSocketRequest
179
+ attr_reader :url, :headers, :send_descriptor, :receive_descriptor, :event_name_separator, :reconnect_options, :heartbeat_options
180
+
181
+ def initialize(url:, headers: {}, send_descriptor: nil, receive_descriptor: nil, event_name_separator: nil, reconnect_options: nil, heartbeat_options: nil)
182
+ @url = url
183
+ @headers = headers
184
+ @send_descriptor = send_descriptor
185
+ @receive_descriptor = receive_descriptor
186
+ @event_name_separator = event_name_separator
187
+ @reconnect_options = reconnect_options
188
+ @heartbeat_options = heartbeat_options
189
+ end
190
+
191
+ def auth_headers
192
+ @headers.dup
193
+ end
194
+
195
+ def with_headers(extra = {})
196
+ self.class.new(url: @url, headers: @headers.merge(extra || {}), send_descriptor: @send_descriptor, receive_descriptor: @receive_descriptor, event_name_separator: @event_name_separator, reconnect_options: @reconnect_options, heartbeat_options: @heartbeat_options)
197
+ end
198
+
199
+ def with_reconnect(**options)
200
+ self.class.new(url: @url, headers: @headers, send_descriptor: @send_descriptor, receive_descriptor: @receive_descriptor, event_name_separator: @event_name_separator, reconnect_options: options, heartbeat_options: @heartbeat_options)
201
+ end
202
+
203
+ def with_heartbeat(interval:, message:)
204
+ self.class.new(url: @url, headers: @headers, send_descriptor: @send_descriptor, receive_descriptor: @receive_descriptor, event_name_separator: @event_name_separator, reconnect_options: @reconnect_options, heartbeat_options: { interval: interval, message: message })
205
+ end
206
+
207
+ def connect
208
+ uri = URI(@url)
209
+ raise WebSocketMessageError, "only ws:// and wss:// URLs are supported by the stdlib connector" unless ["ws", "wss"].include?(uri.scheme)
210
+ socket = TCPSocket.new(uri.host, uri.port || (uri.scheme == "wss" ? 443 : 80))
211
+ if uri.scheme == "wss"
212
+ context = OpenSSL::SSL::SSLContext.new
213
+ socket = OpenSSL::SSL::SSLSocket.new(socket, context)
214
+ socket.hostname = uri.host if socket.respond_to?(:hostname=)
215
+ socket.sync_close = true
216
+ socket.connect
217
+ end
218
+ key = SecureRandom.base64(16)
219
+ path = uri.path.empty? ? "/" : uri.path
220
+ path = "#{path}?#{uri.query}" if uri.query
221
+ headers = { "Host" => uri.host, "Upgrade" => "websocket", "Connection" => "Upgrade", "Sec-WebSocket-Key" => key, "Sec-WebSocket-Version" => "13" }.merge(auth_headers)
222
+ socket.write("GET #{path} HTTP/1.1\r\n")
223
+ headers.each { |name, value| socket.write("#{name}: #{value}\r\n") }
224
+ socket.write("\r\n")
225
+ status = socket.gets
226
+ raise WebSocketMessageError, "WebSocket upgrade failed: #{status}" unless status&.include?(" 101 ")
227
+ WebSocketConnection.new(socket, self)
228
+ rescue StandardError
229
+ socket&.close
230
+ raise
231
+ end
232
+
233
+ def serialize_send(message)
234
+ payload = Runtime.serialize(message)
235
+ return payload if payload.is_a?(String) && @send_descriptor.nil?
236
+ JSON.generate(payload)
237
+ rescue JSON::GeneratorError => e
238
+ raise WebSocketMessageError, "failed to serialize websocket payload: #{e.message}"
239
+ end
240
+
241
+ def deserialize_receive(payload)
242
+ parsed = parse_receive_payload(payload)
243
+ Runtime.deserialize(parsed, @receive_descriptor)
244
+ rescue JSON::ParserError => e
245
+ raise WebSocketMessageError, "failed to parse websocket payload: #{e.message}"
246
+ end
247
+
248
+ def normalize_event_name(name)
249
+ return name if name.nil? || @event_name_separator.nil? || @event_name_separator.empty?
250
+ name.to_s.tr(@event_name_separator, ".")
251
+ end
252
+
253
+ private
254
+
255
+ def parse_receive_payload(payload)
256
+ return payload if payload.nil? || payload.is_a?(Hash) || payload.is_a?(Array)
257
+ return payload unless payload.respond_to?(:to_str)
258
+ text = payload.to_str
259
+ return text if @receive_descriptor.nil?
260
+ JSON.parse(text)
261
+ end
262
+ end
263
+
264
+ class Stream
265
+ include Enumerable
266
+
267
+ def initialize(source:, descriptor:, format: :sse, handlers: [])
268
+ @source = source
269
+ @descriptor = descriptor
270
+ @format = format
271
+ @handlers = handlers
272
+ @closed = false
273
+ @retry_after = nil
274
+ @last_event_id = nil
275
+ @last_event_name = nil
276
+ end
277
+
278
+ def each(&block)
279
+ return enum_for(:each) unless block
280
+ return if @closed
281
+ each_event do |data|
282
+ break if @closed
283
+ next if stream_action(data) == :skip
284
+ block.call(Runtime.deserialize(Runtime.parse_json(data), @descriptor))
285
+ end
286
+ ensure
287
+ close
288
+ end
289
+
290
+ def close
291
+ @closed = true
292
+ end
293
+
294
+ def closed?
295
+ @closed
296
+ end
297
+
298
+ attr_reader :retry_after, :last_event_id, :last_event_name
299
+
300
+ private
301
+
302
+ def each_event
303
+ return enum_for(:each_event) unless block_given?
304
+ return each_json_line { |line| yield line } if @format == :jsonl
305
+
306
+ buffer = +""
307
+ event = []
308
+ each_raw_event do |chunk|
309
+ buffer << chunk.to_s
310
+ while (line_end = buffer.index("\n"))
311
+ data = consume_sse_line(buffer.slice!(0, line_end + 1), event)
312
+ yield data unless data.nil? || data == "[DONE]" || @closed
313
+ end
314
+ end
315
+ data = flush_sse_event(event)
316
+ yield data unless data.nil? || data == "[DONE]" || @closed
317
+ end
318
+
319
+ def each_raw_event
320
+ @source.call do |chunk|
321
+ yield chunk unless @closed
322
+ !@closed
323
+ end
324
+ end
325
+
326
+ def each_json_line
327
+ buffer = +""
328
+ each_raw_event do |chunk|
329
+ buffer << chunk.to_s
330
+ while (line_end = buffer.index("\n"))
331
+ line = buffer.slice!(0, line_end + 1).strip
332
+ yield line unless line.empty? || @closed
333
+ end
334
+ end
335
+ line = buffer.strip
336
+ yield line unless line.empty? || @closed
337
+ end
338
+
339
+ def consume_sse_line(raw_line, event)
340
+ line = raw_line.delete_suffix("\n").delete_suffix("\r")
341
+ return flush_sse_event(event) if line.empty?
342
+ return nil if line.start_with?(":")
343
+ if line.start_with?("event:")
344
+ @last_event_name = line.delete_prefix("event:").strip
345
+ return nil
346
+ end
347
+ if line.start_with?("id:")
348
+ @last_event_id = line.delete_prefix("id:").strip
349
+ return nil
350
+ end
351
+ if line.start_with?("retry:")
352
+ @retry_after = line.delete_prefix("retry:").strip.to_i
353
+ return nil
354
+ end
355
+ return nil unless line.start_with?("data:")
356
+
357
+ data = line.delete_prefix("data:")
358
+ event << data.delete_prefix(" ")
359
+ nil
360
+ end
361
+
362
+ def flush_sse_event(event)
363
+ return nil if event.empty?
364
+ data = event.join("\n")
365
+ event.clear
366
+ data
367
+ end
368
+
369
+ def stream_action(data)
370
+ handler = @handlers.find { |entry| handler_matches?(entry, data) }
371
+ return :yield if handler.nil?
372
+ handle = handler["handle"] || handler[:handle]
373
+ return :skip if ["continue", "ignore"].include?(handle)
374
+ raise WebSocketMessageError, data if handle == "error"
375
+ :yield
376
+ end
377
+
378
+ def handler_matches?(handler, data)
379
+ kind = handler["kind"] || handler[:kind]
380
+ return false if kind == "event" && !event_type_matches?(handler)
381
+ prefix = handler["dataStartsWith"] || handler[:dataStartsWith]
382
+ return false if prefix && !data.to_s.start_with?(prefix.to_s)
383
+ true
384
+ end
385
+
386
+ def event_type_matches?(handler)
387
+ expected = handler["eventType"] || handler[:eventType]
388
+ return @last_event_name.nil? if expected.nil?
389
+ return expected.include?(@last_event_name) if expected.is_a?(Array)
390
+ expected.to_s == @last_event_name.to_s
391
+ end
392
+ end
393
+
394
+ def extra_keys(data, known)
395
+ data.reject { |key, _value| known.include?(key) }
396
+ end
397
+
398
+ def parse_json(text)
399
+ return nil if text.nil? || text.empty?
400
+ JSON.parse(text)
401
+ rescue JSON::ParserError
402
+ text
403
+ end
404
+
405
+ def value_at(value, path)
406
+ path.reduce(value) do |current, key|
407
+ break nil if current.nil?
408
+ current.is_a?(Hash) ? current[key] || current[key.to_s] : nil
409
+ end
410
+ end
411
+
412
+ def header_value(headers, name)
413
+ return nil unless headers.is_a?(Hash)
414
+ value = headers[name] || headers[name.to_s.downcase] || headers[name.to_s]
415
+ value.is_a?(Array) ? value.first : value
416
+ end
417
+
418
+ def link_header_url(headers, name, rel)
419
+ header = header_value(headers, name)
420
+ return nil if header.nil?
421
+ header.to_s.split(",").each do |part|
422
+ url = part[/<([^>]+)>/, 1]
423
+ rel_value = part[/rel="?([^";,]+)"?/, 1]
424
+ return url if url && rel_value == rel
425
+ end
426
+ nil
427
+ end
428
+
429
+ def extract_error_message(body)
430
+ return nil unless body.is_a?(Hash)
431
+ candidate = body["message"] || body["error"] || body["detail"]
432
+ return candidate if candidate.is_a?(String)
433
+ candidate["message"] if candidate.is_a?(Hash) && candidate["message"].is_a?(String)
434
+ end
435
+
436
+ def request_options_hash(options)
437
+ return {} if options.nil?
438
+ value = options.respond_to?(:to_h) ? options.to_h : options
439
+ value.is_a?(Hash) ? value : {}
440
+ end
441
+
442
+ def request_option_hash(options, *keys)
443
+ keys.each do |key|
444
+ value = request_option_value(options, key)
445
+ return value if value.is_a?(Hash)
446
+ end
447
+ {}
448
+ end
449
+
450
+ def request_option_value(options, key)
451
+ value = request_options_hash(options)
452
+ value[key] || value[key.to_s]
453
+ end
454
+
455
+ def safe_header_value(value)
456
+ text = value.to_s
457
+ raise ArgumentError, "header values cannot contain CR or LF" if text.match?(/[\r\n]/)
458
+ text
459
+ end
460
+
461
+ def safe_header_name(value)
462
+ text = value.to_s
463
+ raise ArgumentError, "header names cannot be empty" if text.empty?
464
+ raise ArgumentError, "invalid header name #{text.inspect}" unless text.match?(/\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/)
465
+ text
466
+ end
467
+
468
+ def safe_cookie_pair(name, value)
469
+ "#{URI.encode_www_form_component(name.to_s)}=#{URI.encode_www_form_component(safe_header_value(value))}"
470
+ end
471
+
472
+ def body_encoding_for_content_type(content_type)
473
+ value = content_type.to_s.downcase
474
+ return :multipart if value.include?("multipart/")
475
+ return :form if value.include?("x-www-form-urlencoded")
476
+ return :json if value.include?("json")
477
+ :binary
478
+ end
479
+ end
480
+
481
+ class APIClient
482
+ def initialize(base_url:, max_retries: 2, timeout: 60.0, initial_retry_delay: 0.5, max_retry_delay: 8.0, idempotency_header: "Idempotency-Key", logger: nil, default_headers: {}, default_query: {})
483
+ @base_url = base_url.to_s.delete_suffix("/")
484
+ @max_retries = max_retries
485
+ @timeout = timeout
486
+ @initial_retry_delay = initial_retry_delay
487
+ @max_retry_delay = max_retry_delay
488
+ @idempotency_header = idempotency_header
489
+ @logger = logger
490
+ @default_headers = default_headers
491
+ @default_query = default_query
492
+ end
493
+
494
+ def request(
495
+ method:, path:,
496
+ path_params: {}, path_settings: {},
497
+ query: {}, query_settings: {},
498
+ cookies: {}, cookie_settings: {},
499
+ body: nil, body_type: :json, content_type: nil,
500
+ headers: nil, request_options: nil
501
+ )
502
+ options = normalize_options(request_options)
503
+ merged_query = @default_query.merge(auth_query).merge(option_hash(options, :query, :extra_query)).merge(query || {})
504
+ # Link-header pagination passes the exact next-page URL via `url:`; use it
505
+ # directly instead of rebuilding base_url + path + query.
506
+ exact_url = option_value(options, :url)
507
+ uri = exact_url ? URI.parse(exact_url.to_s) : build_uri(
508
+ option_value(options, :base_url) || @base_url,
509
+ path,
510
+ path_params,
511
+ path_settings,
512
+ merged_query,
513
+ query_settings
514
+ )
515
+ cookie_header = build_cookie_header(cookies || {}, cookie_settings || {})
516
+ headers = (headers || {}).merge("Cookie" => [headers && headers["Cookie"], cookie_header].compact.reject(&:empty?).join("; ")) unless cookie_header.empty?
517
+ request = build_request(method, uri, body, body_type, content_type, headers || {}, options)
518
+ response = execute_with_retry(uri, request, option_value(options, :max_retries) || @max_retries, option_value(options, :timeout) || @timeout)
519
+ parsed = parse_response_body(response)
520
+ raise APIError.from_response(response, parsed) unless response.is_a?(Net::HTTPSuccess)
521
+ parsed
522
+ end
523
+
524
+ def request_raw(
525
+ method:, path:,
526
+ path_params: {}, path_settings: {},
527
+ query: {}, query_settings: {},
528
+ cookies: {}, cookie_settings: {},
529
+ body: nil, body_type: :json, content_type: nil,
530
+ headers: nil, request_options: nil
531
+ )
532
+ options = normalize_options(request_options)
533
+ merged_query = @default_query.merge(auth_query).merge(option_hash(options, :query, :extra_query)).merge(query || {})
534
+ exact_url = option_value(options, :url)
535
+ uri = exact_url ? URI.parse(exact_url.to_s) : build_uri(
536
+ option_value(options, :base_url) || @base_url,
537
+ path,
538
+ path_params,
539
+ path_settings,
540
+ merged_query,
541
+ query_settings
542
+ )
543
+ cookie_header = build_cookie_header(cookies || {}, cookie_settings || {})
544
+ headers = (headers || {}).merge("Cookie" => [headers && headers["Cookie"], cookie_header].compact.reject(&:empty?).join("; ")) unless cookie_header.empty?
545
+ request = build_request(method, uri, body, body_type, content_type, headers || {}, options)
546
+ response = execute_with_retry(uri, request, option_value(options, :max_retries) || @max_retries, option_value(options, :timeout) || @timeout)
547
+ parsed = parse_response_body(response)
548
+ raise APIError.from_response(response, parsed) unless response.is_a?(Net::HTTPSuccess)
549
+ Runtime::RawResponse.new(data: parsed, status: response.code.to_i, headers: response.to_hash, body: response.body)
550
+ end
551
+
552
+ def request_stream(
553
+ method:, path:,
554
+ path_params: {}, path_settings: {},
555
+ query: {}, query_settings: {},
556
+ cookies: {}, cookie_settings: {},
557
+ body: nil, body_type: :json, content_type: nil,
558
+ headers: nil, stream_format: :sse, descriptor: nil, handlers: [], request_options: nil
559
+ )
560
+ options = normalize_options(request_options)
561
+ merged_query = @default_query.merge(auth_query).merge(option_hash(options, :query, :extra_query)).merge(query || {})
562
+ uri = build_uri(
563
+ option_value(options, :base_url) || @base_url,
564
+ path,
565
+ path_params,
566
+ path_settings,
567
+ merged_query,
568
+ query_settings
569
+ )
570
+ cookie_header = build_cookie_header(cookies || {}, cookie_settings || {})
571
+ headers = (headers || {}).merge("Cookie" => [headers && headers["Cookie"], cookie_header].compact.reject(&:empty?).join("; ")) unless cookie_header.empty?
572
+ request = build_request(method, uri, body, body_type, content_type, headers || {}, options)
573
+ source = proc { |&chunk_handler| execute_stream_with_retry(uri, request, option_value(options, :max_retries) || @max_retries, option_value(options, :timeout) || @timeout, &chunk_handler) }
574
+ Runtime::Stream.new(source: source, descriptor: descriptor, format: stream_format, handlers: handlers)
575
+ end
576
+
577
+ def websocket_request(
578
+ method: "GET", path:,
579
+ path_params: {}, path_settings: {},
580
+ query: {}, query_settings: {},
581
+ cookies: {}, cookie_settings: {},
582
+ body: nil, body_type: :json, content_type: nil,
583
+ headers: nil, send_descriptor: nil, receive_descriptor: nil, event_name_separator: nil, request_options: nil
584
+ )
585
+ options = normalize_options(request_options)
586
+ merged_query = @default_query.merge(auth_query).merge(option_hash(options, :query, :extra_query)).merge(query || {})
587
+ uri = build_uri(
588
+ option_value(options, :base_url) || @base_url,
589
+ path,
590
+ path_params,
591
+ path_settings,
592
+ merged_query,
593
+ query_settings
594
+ )
595
+ uri.scheme = uri.scheme == "https" ? "wss" : "ws"
596
+ request_headers = sanitize_headers(@default_headers.merge(auth_headers_for(uri, options)).merge(headers || {}))
597
+ Runtime::WebSocketRequest.new(url: uri.to_s, headers: request_headers, send_descriptor: send_descriptor, receive_descriptor: receive_descriptor, event_name_separator: event_name_separator)
598
+ end
599
+
600
+ def auth_headers
601
+ {}
602
+ end
603
+
604
+ def auth_query
605
+ {}
606
+ end
607
+
608
+ private
609
+
610
+ def normalize_options(options)
611
+ Runtime.request_options_hash(options)
612
+ end
613
+
614
+ def option_value(options, *keys)
615
+ keys.each do |key|
616
+ value = Runtime.request_option_value(options, key)
617
+ return value unless value.nil?
618
+ end
619
+ nil
620
+ end
621
+
622
+ def option_hash(options, *keys)
623
+ Runtime.request_option_hash(options, *keys)
624
+ end
625
+
626
+ def sanitize_headers(headers)
627
+ headers.each_with_object({}) do |(key, value), out|
628
+ next if value.nil?
629
+ out[Runtime.safe_header_name(key)] = Runtime.safe_header_value(value)
630
+ end
631
+ end
632
+
633
+ def auth_headers_for(uri, options)
634
+ # Both `base_url:` and `url:` overrides suppress auth when the target
635
+ # crosses origins, unless the caller explicitly allows it.
636
+ override = option_value(options, :base_url) || option_value(options, :url)
637
+ return auth_headers if override.nil? || option_value(options, :allow_auth_to_base_url)
638
+ same_origin?(URI.parse(@base_url), uri) ? auth_headers : {}
639
+ rescue URI::InvalidURIError
640
+ {}
641
+ end
642
+
643
+ def same_origin?(left, right)
644
+ left.scheme == right.scheme && left.host == right.host && left.port == right.port
645
+ end
646
+
647
+ def log_debug(message)
648
+ return if @logger.nil?
649
+ @logger.respond_to?(:call) ? @logger.call(message) : @logger.puts(message)
650
+ end
651
+
652
+ def build_uri(base_url, path, path_params, path_settings, query, query_settings)
653
+ resolved = path.dup
654
+ path_params.each { |key, value| resolved = resolved.gsub("{#{key}}", serialize_path_param(key, value, path_settings[key] || {})) }
655
+ uri = URI.parse(base_url.to_s.delete_suffix("/") + resolved)
656
+ pairs = URI.decode_www_form(uri.query.to_s)
657
+ query.each { |key, value| append_query(pairs, key, value, query_settings[key] || {}) }
658
+ uri.query = pairs.empty? ? nil : URI.encode_www_form(pairs)
659
+ uri
660
+ end
661
+
662
+ def append_query(pairs, key, raw_value, setting = {})
663
+ value = parameter_value(raw_value, setting)
664
+ return if value.nil?
665
+ style = setting[:style] || "form"
666
+ explode = setting.key?(:explode) ? setting[:explode] : true
667
+ if value.is_a?(Array)
668
+ return value.each { |item| append_query(pairs, key, item, setting.merge(explode: false)) } if ["form", "spaceDelimited", "pipeDelimited"].include?(style) && explode
669
+ sep = style == "spaceDelimited" ? " " : style == "pipeDelimited" ? "|" : ","
670
+ pairs << [key, value.map { |item| parameter_value(item, setting).to_s }.join(sep)]
671
+ elsif value.is_a?(Hash)
672
+ entries = value.map { |prop, prop_value| [prop, parameter_value(prop_value, setting)] }.reject { |_, prop_value| prop_value.nil? }
673
+ return entries.each { |prop, prop_value| pairs << ["#{key}[#{prop}]", prop_value.to_s] } if style == "deepObject"
674
+ return entries.each { |prop, prop_value| pairs << [prop.to_s, prop_value.to_s] } if style == "form" && explode
675
+ pairs << [key, style == "form" ? entries.flat_map { |prop, prop_value| [prop, prop_value] }.join(",") : JSON.generate(value)]
676
+ else
677
+ pairs << [key, value.to_s]
678
+ end
679
+ end
680
+
681
+ def parameter_value(value, setting)
682
+ return setting[:default] if value.nil? && setting.key?(:default)
683
+ return "" if value.nil? && (setting[:nullable] || setting[:required] || setting[:allow_empty_value])
684
+ value
685
+ end
686
+
687
+ def encode_part(value, allow_reserved: false)
688
+ encoded = URI.encode_www_form_component(value.to_s)
689
+ return encoded unless allow_reserved
690
+ encoded.gsub(/%3A/i, ":").gsub(/%2F/i, "/").gsub(/%3F/i, "?").gsub(/%23/i, "#").gsub(/%5B/i, "[").gsub(/%5D/i, "]").gsub(/%40/i, "@").gsub(/%21/i, "!").gsub(/%24/i, "$").gsub(/%26/i, "&").gsub(/%27/i, "'").gsub(/%28/i, "(").gsub(/%29/i, ")").gsub(/%2A/i, "*").gsub(/%2B/i, "+").gsub(/%2C/i, ",").gsub(/%3B/i, ";").gsub(/%3D/i, "=")
691
+ end
692
+
693
+ def serialize_path_param(key, raw_value, setting = {})
694
+ value = parameter_value(raw_value, setting) || ""
695
+ style = setting[:style] || "simple"
696
+ explode = setting.key?(:explode) ? setting[:explode] : false
697
+ # `allow_reserved` keeps RFC 6570 reserved characters (notably `/`) so a path-like value such as a
698
+ # file path stays nested under the route instead of collapsing to a single `%2F`-encoded segment.
699
+ allow_reserved = setting.key?(:allow_reserved) ? setting[:allow_reserved] : false
700
+ return label_path_value(value, explode, allow_reserved: allow_reserved) if style == "label"
701
+ if style == "matrix"
702
+ return value.map { |item| ";#{encode_part(key)}=#{encode_part(parameter_value(item, setting) || "", allow_reserved: allow_reserved)}" }.join if value.is_a?(Array) && explode
703
+ return value.map { |prop, prop_value| ";#{encode_part(prop)}=#{encode_part(parameter_value(prop_value, setting) || "", allow_reserved: allow_reserved)}" }.join if value.is_a?(Hash) && explode
704
+ return ";#{encode_part(key)}=#{simple_path_value(value, false, allow_reserved: allow_reserved)}"
705
+ end
706
+ simple_path_value(value, explode, allow_reserved: allow_reserved)
707
+ end
708
+
709
+ def label_path_value(value, explode, allow_reserved: false)
710
+ return "." + value.map { |item| encode_part(parameter_value(item, {}) || "", allow_reserved: allow_reserved) }.join(".") if value.is_a?(Array) && explode
711
+ return "." + value.map { |prop, prop_value| "#{encode_part(prop)}=#{encode_part(parameter_value(prop_value, {}) || "", allow_reserved: allow_reserved)}" }.join(".") if value.is_a?(Hash) && explode
712
+ "." + simple_path_value(value, false, allow_reserved: allow_reserved)
713
+ end
714
+
715
+ def simple_path_value(value, explode, allow_reserved: false)
716
+ return value.map { |item| encode_part(parameter_value(item, {}) || "", allow_reserved: allow_reserved) }.join(",") if value.is_a?(Array)
717
+ return value.map { |prop, prop_value| explode ? "#{encode_part(prop)}=#{encode_part(parameter_value(prop_value, {}) || "", allow_reserved: allow_reserved)}" : [encode_part(prop), encode_part(parameter_value(prop_value, {}) || "", allow_reserved: allow_reserved)] }.flatten.join(",") if value.is_a?(Hash)
718
+ encode_part(value, allow_reserved: allow_reserved)
719
+ end
720
+
721
+ def build_cookie_header(cookies, cookie_settings)
722
+ parts = []
723
+ cookies.each do |key, raw_value|
724
+ setting = cookie_settings[key] || {}
725
+ value = parameter_value(raw_value, setting)
726
+ next if value.nil?
727
+ explode = setting.key?(:explode) ? setting[:explode] : true
728
+ if value.is_a?(Array) && explode
729
+ value.each { |item| parts << "#{encode_part(key)}=#{encode_part(parameter_value(item, setting) || "")}" }
730
+ elsif value.is_a?(Hash) && explode
731
+ value.each { |prop, prop_value| parts << "#{encode_part(prop)}=#{encode_part(parameter_value(prop_value, setting) || "")}" }
732
+ else
733
+ serialized = value.is_a?(Array) ? value.map { |item| parameter_value(item, setting).to_s }.join(",") : value.is_a?(Hash) ? value.flat_map { |prop, prop_value| [prop, parameter_value(prop_value, setting)] }.join(",") : value.to_s
734
+ parts << "#{encode_part(key)}=#{encode_part(serialized)}"
735
+ end
736
+ end
737
+ parts.join("; ")
738
+ end
739
+
740
+ def build_request(method, uri, body, body_type, content_type, extra_headers, options = {})
741
+ request = request_class(method).new(uri)
742
+ headers = { "Accept" => "application/json" }.merge(@default_headers).merge(auth_headers_for(uri, options)).merge(option_hash(options, :extra_headers)).merge(extra_headers)
743
+ if write_method?(method) && !@idempotency_header.nil? && !@idempotency_header.empty?
744
+ idempotency_key = option_value(options, :idempotency_key)
745
+ headers[@idempotency_header] = idempotency_key.to_s unless idempotency_key.nil? || idempotency_key.to_s.empty?
746
+ end
747
+ encoded_body, encoded_content_type = encode_body(body, body_type, content_type, options)
748
+ headers["Content-Type"] ||= encoded_content_type unless encoded_content_type.nil?
749
+ sanitize_headers(headers).each { |key, value| request[key] = value }
750
+ request.body = encoded_body unless encoded_body.nil?
751
+ request
752
+ end
753
+
754
+ def write_method?(method)
755
+ !["GET", "HEAD", "OPTIONS"].include?(method.to_s.upcase)
756
+ end
757
+
758
+ def request_class(method)
759
+ { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post, "PUT" => Net::HTTP::Put, "PATCH" => Net::HTTP::Patch, "DELETE" => Net::HTTP::Delete, "HEAD" => Net::HTTP::Head, "OPTIONS" => Net::HTTP::Options }.fetch(method.to_s.upcase)
760
+ end
761
+
762
+ def encode_body(body, body_type, content_type, options = {})
763
+ return [nil, nil] if body.nil?
764
+ value = Runtime.serialize(body)
765
+ extra_body = option_hash(options, :extra_body)
766
+ value = value.merge(extra_body) if value.is_a?(Hash) && !extra_body.empty?
767
+ return [URI.encode_www_form(value || {}), "application/x-www-form-urlencoded"] if body_type == :form
768
+ return encode_multipart(value || {}, content_type) if body_type == :multipart
769
+ return [value, content_type || "application/octet-stream"] if body_type == :binary
770
+ [JSON.generate(value), content_type || "application/json"]
771
+ end
772
+
773
+ def encode_multipart(value, content_type)
774
+ boundary = "scalar-sdk-generator-#{object_id}-#{rand(1_000_000)}"
775
+ chunks = []
776
+ value.each do |key, part|
777
+ Array(part).each do |item|
778
+ filename = multipart_filename(item)
779
+ part_content_type = multipart_content_type(item)
780
+ chunks << "--#{boundary}\r\n"
781
+ disposition = "Content-Disposition: form-data; name=\"#{multipart_disposition_param(key)}\""
782
+ disposition += "; filename=\"#{multipart_disposition_param(filename)}\"" unless filename.nil?
783
+ chunks << "#{disposition}\r\n"
784
+ chunks << "Content-Type: #{multipart_header_value(part_content_type)}\r\n" unless part_content_type.nil?
785
+ chunks << "\r\n"
786
+ chunks << multipart_value(item)
787
+ chunks << "\r\n"
788
+ end
789
+ end
790
+ chunks << "--#{boundary}--\r\n"
791
+ [chunks.join, content_type || "multipart/form-data; boundary=#{boundary}"]
792
+ end
793
+
794
+ def multipart_disposition_param(value)
795
+ value.to_s.gsub(/["\\\r\n]/, "_")
796
+ end
797
+
798
+ def multipart_header_value(value)
799
+ value.to_s.gsub(/[\r\n]/, "")
800
+ end
801
+
802
+ def multipart_filename(value)
803
+ return File.basename(value.filename.to_s) if value.respond_to?(:filename) && value.filename
804
+ return File.basename(value.path) if value.respond_to?(:path) && value.path
805
+ nil
806
+ end
807
+
808
+ def multipart_content_type(value)
809
+ return value.content_type if value.respond_to?(:content_type) && value.content_type
810
+ return "application/octet-stream" unless multipart_filename(value).nil?
811
+ nil
812
+ end
813
+
814
+ def multipart_value(value)
815
+ source = value.respond_to?(:io) ? value.io : value
816
+ return source.read if source.respond_to?(:read)
817
+ source.to_s
818
+ end
819
+
820
+ def parse_response_body(response)
821
+ content_type = response["Content-Type"].to_s.downcase
822
+ body = response.body
823
+ return body unless content_type.empty? || content_type.include?("json") || content_type.start_with?("text/") || content_type.include?("xml")
824
+ Runtime.parse_json(body)
825
+ end
826
+
827
+ def execute_with_retry(uri, request, retries_left, timeout)
828
+ response = nil
829
+ log_debug("#{request.method} #{uri}")
830
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", read_timeout: timeout, open_timeout: timeout) { |http| response = http.request(request) }
831
+ log_debug("#{response.code} #{request.method} #{uri}")
832
+ if Runtime::RETRYABLE_STATUS.include?(response.code.to_i) && retries_left.positive?
833
+ sleep retry_delay(retries_left, response)
834
+ return execute_with_retry(uri, request, retries_left - 1, timeout)
835
+ end
836
+ response
837
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
838
+ raise APIConnectionTimeoutError.new(e.message) unless retries_left.positive?
839
+ sleep retry_delay(retries_left)
840
+ execute_with_retry(uri, request, retries_left - 1, timeout)
841
+ rescue StandardError => e
842
+ raise APIConnectionError.new(e.message) unless retries_left.positive?
843
+ sleep retry_delay(retries_left)
844
+ execute_with_retry(uri, request, retries_left - 1, timeout)
845
+ end
846
+
847
+ def execute_stream_with_retry(uri, request, retries_left, timeout, &chunk_handler)
848
+ log_debug("#{request.method} #{uri} stream")
849
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", read_timeout: timeout, open_timeout: timeout) do |http|
850
+ http.request(request) do |response|
851
+ log_debug("#{response.code} #{request.method} #{uri} stream")
852
+ unless response.is_a?(Net::HTTPSuccess)
853
+ error_body = +""
854
+ response.read_body { |chunk| error_body << chunk.to_s }
855
+ if Runtime::RETRYABLE_STATUS.include?(response.code.to_i) && retries_left.positive?
856
+ sleep retry_delay(retries_left, response)
857
+ return execute_stream_with_retry(uri, request, retries_left - 1, timeout, &chunk_handler)
858
+ end
859
+ raise APIError.from_response(response, Runtime.parse_json(error_body))
860
+ end
861
+
862
+ response.read_body do |chunk|
863
+ break if chunk_handler.call(chunk) == false
864
+ end
865
+ end
866
+ end
867
+ rescue APIError
868
+ raise
869
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
870
+ raise APIConnectionTimeoutError.new(e.message) unless retries_left.positive?
871
+ sleep retry_delay(retries_left)
872
+ execute_stream_with_retry(uri, request, retries_left - 1, timeout, &chunk_handler)
873
+ rescue StandardError => e
874
+ raise APIConnectionError.new(e.message) unless retries_left.positive?
875
+ sleep retry_delay(retries_left)
876
+ execute_stream_with_retry(uri, request, retries_left - 1, timeout, &chunk_handler)
877
+ end
878
+
879
+ def retry_delay(retries_left, response = nil)
880
+ retry_after = response&.[]("Retry-After")
881
+ unless retry_after.nil? || retry_after.empty?
882
+ seconds = Float(retry_after, exception: false)
883
+ return seconds if !seconds.nil? && seconds >= 0
884
+ begin
885
+ return [Time.httpdate(retry_after) - Time.now, 0].max
886
+ rescue ArgumentError
887
+ # Fall through to exponential delay.
888
+ end
889
+ end
890
+ [@initial_retry_delay * (2**(@max_retries - retries_left)), @max_retry_delay].min + rand * 0.25
891
+ end
892
+ end
893
+ end