voltaria_sdk 2.34.5

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 (209) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +69 -0
  3. data/README.md +202 -0
  4. data/Rakefile +20 -0
  5. data/custom.gemspec.rb +16 -0
  6. data/lib/voltaria/accounts/client.rb +168 -0
  7. data/lib/voltaria/accounts/types/get_client_account_request.rb +12 -0
  8. data/lib/voltaria/accounts/types/list_client_account_fields_request.rb +11 -0
  9. data/lib/voltaria/accounts/types/list_client_accounts_request.rb +14 -0
  10. data/lib/voltaria/accounts/types/partner_client_account_create_request.rb +23 -0
  11. data/lib/voltaria/accounts/types/partner_client_account_create_request_status.rb +14 -0
  12. data/lib/voltaria/client.rb +74 -0
  13. data/lib/voltaria/clients/client.rb +557 -0
  14. data/lib/voltaria/clients/types/approve_onboarding_request.rb +11 -0
  15. data/lib/voltaria/clients/types/client_create_payload.rb +16 -0
  16. data/lib/voltaria/clients/types/client_data_create_payload.rb +12 -0
  17. data/lib/voltaria/clients/types/client_user_invite_request.rb +16 -0
  18. data/lib/voltaria/clients/types/delete_client_request.rb +11 -0
  19. data/lib/voltaria/clients/types/get_client_by_id_request.rb +11 -0
  20. data/lib/voltaria/clients/types/get_limit_request_request.rb +11 -0
  21. data/lib/voltaria/clients/types/limit_request_create_payload.rb +14 -0
  22. data/lib/voltaria/clients/types/limit_request_create_payload_requested_limit.rb +15 -0
  23. data/lib/voltaria/clients/types/list_client_checklist_summaries_request.rb +13 -0
  24. data/lib/voltaria/clients/types/list_client_waivers_request.rb +15 -0
  25. data/lib/voltaria/clients/types/list_clients_request.rb +14 -0
  26. data/lib/voltaria/clients/types/list_limit_requests_request.rb +15 -0
  27. data/lib/voltaria/clients/types/list_onboarding_clients_request.rb +12 -0
  28. data/lib/voltaria/clients/types/reject_onboarding_request.rb +11 -0
  29. data/lib/voltaria/documents/client.rb +221 -0
  30. data/lib/voltaria/documents/types/delete_document_request.rb +11 -0
  31. data/lib/voltaria/documents/types/document_create_payload.rb +14 -0
  32. data/lib/voltaria/documents/types/get_document_by_id_request.rb +11 -0
  33. data/lib/voltaria/documents/types/list_documents_request.rb +18 -0
  34. data/lib/voltaria/drawdowns/client.rb +141 -0
  35. data/lib/voltaria/drawdowns/types/drawdown_create_payload.rb +12 -0
  36. data/lib/voltaria/drawdowns/types/drawdown_create_payload_amount.rb +15 -0
  37. data/lib/voltaria/drawdowns/types/list_drawdown_checklists_request.rb +15 -0
  38. data/lib/voltaria/drawdowns/types/list_drawdowns_request.rb +14 -0
  39. data/lib/voltaria/environment.rb +8 -0
  40. data/lib/voltaria/errors/api_error.rb +8 -0
  41. data/lib/voltaria/errors/client_error.rb +17 -0
  42. data/lib/voltaria/errors/redirect_error.rb +8 -0
  43. data/lib/voltaria/errors/response_error.rb +42 -0
  44. data/lib/voltaria/errors/server_error.rb +11 -0
  45. data/lib/voltaria/errors/timeout_error.rb +8 -0
  46. data/lib/voltaria/installments/client.rb +292 -0
  47. data/lib/voltaria/installments/types/delete_installment_request.rb +11 -0
  48. data/lib/voltaria/installments/types/get_installment_by_id_request.rb +11 -0
  49. data/lib/voltaria/installments/types/installment_create_payload.rb +12 -0
  50. data/lib/voltaria/installments/types/installment_edit_payload.rb +13 -0
  51. data/lib/voltaria/installments/types/installment_edit_payload_amount.rb +15 -0
  52. data/lib/voltaria/installments/types/list_installments_request.rb +16 -0
  53. data/lib/voltaria/installments/types/list_payment_promises_request.rb +16 -0
  54. data/lib/voltaria/installments/types/payment_promise_create_payload.rb +14 -0
  55. data/lib/voltaria/installments/types/payment_promise_create_payload_amount.rb +15 -0
  56. data/lib/voltaria/internal/errors/constraint_error.rb +10 -0
  57. data/lib/voltaria/internal/errors/type_error.rb +10 -0
  58. data/lib/voltaria/internal/http/base_request.rb +51 -0
  59. data/lib/voltaria/internal/http/raw_client.rb +214 -0
  60. data/lib/voltaria/internal/iterators/cursor_item_iterator.rb +28 -0
  61. data/lib/voltaria/internal/iterators/cursor_page_iterator.rb +51 -0
  62. data/lib/voltaria/internal/iterators/item_iterator.rb +59 -0
  63. data/lib/voltaria/internal/iterators/offset_item_iterator.rb +30 -0
  64. data/lib/voltaria/internal/iterators/offset_page_iterator.rb +83 -0
  65. data/lib/voltaria/internal/json/request.rb +41 -0
  66. data/lib/voltaria/internal/json/serializable.rb +25 -0
  67. data/lib/voltaria/internal/multipart/multipart_encoder.rb +141 -0
  68. data/lib/voltaria/internal/multipart/multipart_form_data.rb +78 -0
  69. data/lib/voltaria/internal/multipart/multipart_form_data_part.rb +51 -0
  70. data/lib/voltaria/internal/multipart/multipart_request.rb +40 -0
  71. data/lib/voltaria/internal/types/array.rb +47 -0
  72. data/lib/voltaria/internal/types/boolean.rb +34 -0
  73. data/lib/voltaria/internal/types/enum.rb +56 -0
  74. data/lib/voltaria/internal/types/hash.rb +36 -0
  75. data/lib/voltaria/internal/types/model/field.rb +38 -0
  76. data/lib/voltaria/internal/types/model.rb +208 -0
  77. data/lib/voltaria/internal/types/type.rb +35 -0
  78. data/lib/voltaria/internal/types/union.rb +161 -0
  79. data/lib/voltaria/internal/types/unknown.rb +15 -0
  80. data/lib/voltaria/internal/types/utils.rb +116 -0
  81. data/lib/voltaria/investors/client.rb +312 -0
  82. data/lib/voltaria/investors/types/investor_get_client_request.rb +11 -0
  83. data/lib/voltaria/investors/types/investor_get_installment_request.rb +11 -0
  84. data/lib/voltaria/investors/types/investor_get_loan_request.rb +11 -0
  85. data/lib/voltaria/investors/types/investor_list_clients_request.rb +14 -0
  86. data/lib/voltaria/investors/types/investor_list_installments_request.rb +16 -0
  87. data/lib/voltaria/investors/types/investor_list_loans_request.rb +15 -0
  88. data/lib/voltaria/investors/types/investor_list_repayments_request.rb +17 -0
  89. data/lib/voltaria/loans/client.rb +271 -0
  90. data/lib/voltaria/loans/types/bulk_loan_create_payload.rb +11 -0
  91. data/lib/voltaria/loans/types/delete_loan_request.rb +11 -0
  92. data/lib/voltaria/loans/types/get_bulk_loan_status_request.rb +11 -0
  93. data/lib/voltaria/loans/types/get_loan_by_id_request.rb +11 -0
  94. data/lib/voltaria/loans/types/list_loans_request.rb +15 -0
  95. data/lib/voltaria/loans/types/loan_create_payload.rb +17 -0
  96. data/lib/voltaria/loans/types/loan_create_payload_amount.rb +15 -0
  97. data/lib/voltaria/loans/types/loan_default_payload.rb +13 -0
  98. data/lib/voltaria/loans/types/loan_default_payload_sold_amount.rb +15 -0
  99. data/lib/voltaria/partners/client.rb +129 -0
  100. data/lib/voltaria/partners/types/list_partner_waterfalls_request.rb +14 -0
  101. data/lib/voltaria/partners/types/partner_data_create_payload.rb +11 -0
  102. data/lib/voltaria/prefix_routing.rb +80 -0
  103. data/lib/voltaria/repayments/client.rb +183 -0
  104. data/lib/voltaria/repayments/types/bulk_repayment_create_payload.rb +11 -0
  105. data/lib/voltaria/repayments/types/get_bulk_repayment_status_request.rb +11 -0
  106. data/lib/voltaria/repayments/types/list_repayment_logs_request.rb +17 -0
  107. data/lib/voltaria/repayments/types/repayment_create_payload.rb +17 -0
  108. data/lib/voltaria/repayments/types/repayment_create_payload_amount.rb +15 -0
  109. data/lib/voltaria/sandbox/client.rb +124 -0
  110. data/lib/voltaria/sandbox/types/client_update_sandbox.rb +13 -0
  111. data/lib/voltaria/sandbox/types/client_update_sandbox_limit.rb +15 -0
  112. data/lib/voltaria/sandbox/types/loan_update_sandbox.rb +12 -0
  113. data/lib/voltaria/sandbox/types/webhook_test_sandbox.rb +12 -0
  114. data/lib/voltaria/types/account_address.rb +14 -0
  115. data/lib/voltaria/types/account_holder_type_enum.rb +12 -0
  116. data/lib/voltaria/types/account_status_enum.rb +13 -0
  117. data/lib/voltaria/types/available_document_categories_response.rb +9 -0
  118. data/lib/voltaria/types/available_funding.rb +15 -0
  119. data/lib/voltaria/types/bulk_loan_item_payload.rb +15 -0
  120. data/lib/voltaria/types/bulk_loan_item_payload_amount.rb +13 -0
  121. data/lib/voltaria/types/bulk_loan_item_result.rb +15 -0
  122. data/lib/voltaria/types/bulk_loan_result.rb +13 -0
  123. data/lib/voltaria/types/bulk_loan_task_response.rb +11 -0
  124. data/lib/voltaria/types/bulk_loan_task_status.rb +14 -0
  125. data/lib/voltaria/types/bulk_repayment_item_payload.rb +15 -0
  126. data/lib/voltaria/types/bulk_repayment_item_payload_amount.rb +13 -0
  127. data/lib/voltaria/types/bulk_repayment_item_result.rb +15 -0
  128. data/lib/voltaria/types/bulk_repayment_result.rb +13 -0
  129. data/lib/voltaria/types/bulk_repayment_task_response.rb +11 -0
  130. data/lib/voltaria/types/bulk_repayment_task_status.rb +14 -0
  131. data/lib/voltaria/types/checklist_summary_partner_response.rb +14 -0
  132. data/lib/voltaria/types/checklist_type_enum.rb +12 -0
  133. data/lib/voltaria/types/client_account_response.rb +21 -0
  134. data/lib/voltaria/types/client_base_info.rb +13 -0
  135. data/lib/voltaria/types/client_data_response.rb +12 -0
  136. data/lib/voltaria/types/client_investor_response.rb +17 -0
  137. data/lib/voltaria/types/client_limit_response.rb +16 -0
  138. data/lib/voltaria/types/client_response.rb +19 -0
  139. data/lib/voltaria/types/client_status_enum.rb +18 -0
  140. data/lib/voltaria/types/client_type_enum.rb +12 -0
  141. data/lib/voltaria/types/client_user_response.rb +24 -0
  142. data/lib/voltaria/types/client_user_status_enum.rb +13 -0
  143. data/lib/voltaria/types/currency_enum.rb +16 -0
  144. data/lib/voltaria/types/currency_field_spec.rb +10 -0
  145. data/lib/voltaria/types/document_response.rb +20 -0
  146. data/lib/voltaria/types/drawdown_checklist_response.rb +16 -0
  147. data/lib/voltaria/types/drawdown_response.rb +15 -0
  148. data/lib/voltaria/types/drawdown_status_enum.rb +13 -0
  149. data/lib/voltaria/types/http_validation_error.rb +9 -0
  150. data/lib/voltaria/types/installment_response.rb +24 -0
  151. data/lib/voltaria/types/installment_response_with_client_info.rb +25 -0
  152. data/lib/voltaria/types/installment_status_enum.rb +19 -0
  153. data/lib/voltaria/types/jurisdiction_enum.rb +13 -0
  154. data/lib/voltaria/types/kyc_status_enum.rb +14 -0
  155. data/lib/voltaria/types/limit_request_response.rb +16 -0
  156. data/lib/voltaria/types/limit_request_status_enum.rb +13 -0
  157. data/lib/voltaria/types/loan_installment_create_payload.rb +10 -0
  158. data/lib/voltaria/types/loan_installment_create_payload_amount.rb +13 -0
  159. data/lib/voltaria/types/loan_installment_response.rb +15 -0
  160. data/lib/voltaria/types/loan_investor_response.rb +27 -0
  161. data/lib/voltaria/types/loan_payment_status_enum.rb +12 -0
  162. data/lib/voltaria/types/loan_response_with_client_info.rb +26 -0
  163. data/lib/voltaria/types/loan_response_with_installments.rb +26 -0
  164. data/lib/voltaria/types/loan_status_enum.rb +21 -0
  165. data/lib/voltaria/types/paginated_response_checklist_summary_partner_response.rb +16 -0
  166. data/lib/voltaria/types/paginated_response_client_account_response.rb +16 -0
  167. data/lib/voltaria/types/paginated_response_client_investor_response.rb +16 -0
  168. data/lib/voltaria/types/paginated_response_client_response.rb +16 -0
  169. data/lib/voltaria/types/paginated_response_document_response.rb +16 -0
  170. data/lib/voltaria/types/paginated_response_drawdown_checklist_response.rb +16 -0
  171. data/lib/voltaria/types/paginated_response_drawdown_response.rb +16 -0
  172. data/lib/voltaria/types/paginated_response_installment_response_with_client_info.rb +16 -0
  173. data/lib/voltaria/types/paginated_response_limit_request_response.rb +16 -0
  174. data/lib/voltaria/types/paginated_response_loan_investor_response.rb +16 -0
  175. data/lib/voltaria/types/paginated_response_loan_response_with_client_info.rb +16 -0
  176. data/lib/voltaria/types/paginated_response_payment_promise_response.rb +16 -0
  177. data/lib/voltaria/types/paginated_response_repayment_response_with_client_info.rb +16 -0
  178. data/lib/voltaria/types/paginated_response_waiver_response.rb +16 -0
  179. data/lib/voltaria/types/paginated_response_waterfall_response.rb +16 -0
  180. data/lib/voltaria/types/paginated_response_webhook_log_response.rb +16 -0
  181. data/lib/voltaria/types/paginated_response_webhook_subscription_response.rb +16 -0
  182. data/lib/voltaria/types/partner_data_response.rb +11 -0
  183. data/lib/voltaria/types/payment_promise_response.rb +19 -0
  184. data/lib/voltaria/types/payment_promise_status_enum.rb +14 -0
  185. data/lib/voltaria/types/repayment_response.rb +21 -0
  186. data/lib/voltaria/types/repayment_response_with_client_info.rb +22 -0
  187. data/lib/voltaria/types/role_response.rb +11 -0
  188. data/lib/voltaria/types/validation_error.rb +13 -0
  189. data/lib/voltaria/types/validation_error_loc_item.rb +12 -0
  190. data/lib/voltaria/types/waiver_response.rb +18 -0
  191. data/lib/voltaria/types/waiver_status_enum.rb +14 -0
  192. data/lib/voltaria/types/waterfall_response.rb +19 -0
  193. data/lib/voltaria/types/waterfall_status_enum.rb +12 -0
  194. data/lib/voltaria/types/webhook_event_type_enum.rb +15 -0
  195. data/lib/voltaria/types/webhook_log_response.rb +19 -0
  196. data/lib/voltaria/types/webhook_log_response_request_body.rb +13 -0
  197. data/lib/voltaria/types/webhook_status_enum.rb +13 -0
  198. data/lib/voltaria/types/webhook_subscription_response.rb +17 -0
  199. data/lib/voltaria/version.rb +5 -0
  200. data/lib/voltaria/webhooks/client.rb +241 -0
  201. data/lib/voltaria/webhooks/types/delete_webhook_subscription_request.rb +11 -0
  202. data/lib/voltaria/webhooks/types/get_webhook_subscription_request.rb +11 -0
  203. data/lib/voltaria/webhooks/types/list_webhook_logs_request.rb +13 -0
  204. data/lib/voltaria/webhooks/types/list_webhook_subscriptions_request.rb +13 -0
  205. data/lib/voltaria/webhooks/types/webhook_create_payload.rb +16 -0
  206. data/lib/voltaria/webhooks/types/webhook_update_payload.rb +17 -0
  207. data/lib/voltaria.rb +205 -0
  208. data/reference.md +5135 -0
  209. metadata +250 -0
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Voltaria
4
+ module Internal
5
+ module Http
6
+ # @api private
7
+ class RawClient
8
+ # Default HTTP status codes that trigger a retry
9
+ RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504, 521, 522, 524].freeze
10
+ # Initial delay between retries in seconds
11
+ INITIAL_RETRY_DELAY = 0.5
12
+ # Maximum delay between retries in seconds
13
+ MAX_RETRY_DELAY = 60.0
14
+ # Jitter factor for randomizing retry delays (20%)
15
+ JITTER_FACTOR = 0.2
16
+
17
+ # @return [String] The base URL for requests
18
+ attr_reader :base_url
19
+
20
+ # @param base_url [String] The base url for the request.
21
+ # @param max_retries [Integer] The number of times to retry a failed request, defaults to 2.
22
+ # @param timeout [Float] The timeout for the request, defaults to 60.0 seconds.
23
+ # @param headers [Hash] The headers for the request.
24
+ def initialize(base_url:, max_retries: 2, timeout: 60.0, headers: {})
25
+ @base_url = base_url
26
+ @max_retries = max_retries
27
+ @timeout = timeout
28
+ @default_headers = {
29
+ "X-Fern-Language": "Ruby",
30
+ "X-Fern-SDK-Name": "voltaria",
31
+ "X-Fern-SDK-Version": "0.0.1"
32
+ }.merge(headers)
33
+ end
34
+
35
+ # @param request [Voltaria::Internal::Http::BaseRequest] The HTTP request.
36
+ # @return [HTTP::Response] The HTTP response.
37
+ def send(request)
38
+ url = build_url(request)
39
+ attempt = 0
40
+ response = nil
41
+
42
+ loop do
43
+ http_request = build_http_request(
44
+ url:,
45
+ method: request.method,
46
+ headers: request.encode_headers(protected_keys: @default_headers.keys),
47
+ body: request.encode_body
48
+ )
49
+
50
+ conn = connect(url)
51
+ conn.open_timeout = @timeout
52
+ conn.read_timeout = @timeout
53
+ conn.write_timeout = @timeout
54
+ conn.continue_timeout = @timeout
55
+
56
+ response = conn.request(http_request)
57
+
58
+ break unless should_retry?(response, attempt)
59
+
60
+ delay = retry_delay(response, attempt)
61
+ sleep(delay)
62
+ attempt += 1
63
+ end
64
+
65
+ response
66
+ end
67
+
68
+ # Determines if a request should be retried based on the response status code.
69
+ # @param response [Net::HTTPResponse] The HTTP response.
70
+ # @param attempt [Integer] The current retry attempt (0-indexed).
71
+ # @return [Boolean] Whether the request should be retried.
72
+ def should_retry?(response, attempt)
73
+ return false if attempt >= @max_retries
74
+
75
+ status = response.code.to_i
76
+ RETRYABLE_STATUSES.include?(status)
77
+ end
78
+
79
+ # Calculates the delay before the next retry attempt using exponential backoff with jitter.
80
+ # Respects Retry-After header if present.
81
+ # @param response [Net::HTTPResponse] The HTTP response.
82
+ # @param attempt [Integer] The current retry attempt (0-indexed).
83
+ # @return [Float] The delay in seconds before the next retry.
84
+ def retry_delay(response, attempt)
85
+ # Check for Retry-After header (can be seconds or HTTP date)
86
+ retry_after = response["Retry-After"]
87
+ if retry_after
88
+ delay = parse_retry_after(retry_after)
89
+ return [delay, MAX_RETRY_DELAY].min if delay&.positive?
90
+ end
91
+
92
+ # Exponential backoff with jitter: base_delay * 2^attempt
93
+ base_delay = INITIAL_RETRY_DELAY * (2**attempt)
94
+ add_jitter([base_delay, MAX_RETRY_DELAY].min)
95
+ end
96
+
97
+ # Parses the Retry-After header value.
98
+ # @param value [String] The Retry-After header value (seconds or HTTP date).
99
+ # @return [Float, nil] The delay in seconds, or nil if parsing fails.
100
+ def parse_retry_after(value)
101
+ # Try parsing as integer (seconds)
102
+ seconds = Integer(value, exception: false)
103
+ return seconds.to_f if seconds
104
+
105
+ # Try parsing as HTTP date
106
+ begin
107
+ retry_time = Time.httpdate(value)
108
+ delay = retry_time - Time.now
109
+ delay.positive? ? delay : nil
110
+ rescue ArgumentError
111
+ nil
112
+ end
113
+ end
114
+
115
+ # Adds random jitter to a delay value.
116
+ # @param delay [Float] The base delay in seconds.
117
+ # @return [Float] The delay with jitter applied.
118
+ def add_jitter(delay)
119
+ jitter = delay * JITTER_FACTOR * (rand - 0.5) * 2
120
+ [delay + jitter, 0].max
121
+ end
122
+
123
+ LOCALHOST_HOSTS = %w[localhost 127.0.0.1 [::1]].freeze
124
+
125
+ # @param request [Voltaria::Internal::Http::BaseRequest] The HTTP request.
126
+ # @return [URI::Generic] The URL.
127
+ def build_url(request)
128
+ encoded_query = request.encode_query
129
+
130
+ # If the path is already an absolute URL, use it directly
131
+ if request.path.start_with?("http://", "https://")
132
+ url = request.path
133
+ url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
134
+ parsed = URI.parse(url)
135
+ validate_https!(parsed)
136
+ return parsed
137
+ end
138
+
139
+ path = request.path.start_with?("/") ? request.path[1..] : request.path
140
+ base = request.base_url || @base_url
141
+ url = "#{base.chomp("/")}/#{path}"
142
+ url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
143
+ parsed = URI.parse(url)
144
+ validate_https!(parsed)
145
+ parsed
146
+ end
147
+
148
+ # Raises if the URL uses http:// for a non-localhost host, which would
149
+ # send authentication credentials in plaintext.
150
+ # @param url [URI::Generic] The parsed URL.
151
+ def validate_https!(url)
152
+ return if url.scheme != "http"
153
+ return if LOCALHOST_HOSTS.include?(url.host)
154
+
155
+ raise ArgumentError, "Refusing to send request to non-HTTPS URL: #{url}. " \
156
+ "HTTP is only allowed for localhost. Use HTTPS or pass a localhost URL."
157
+ end
158
+
159
+ # @param url [URI::Generic] The url to the resource.
160
+ # @param method [String] The HTTP method to use.
161
+ # @param headers [Hash] The headers for the request.
162
+ # @param body [String, nil] The body for the request.
163
+ # @return [HTTP::Request] The HTTP request.
164
+ def build_http_request(url:, method:, headers: {}, body: nil)
165
+ request = Net::HTTPGenericRequest.new(
166
+ method,
167
+ !body.nil?,
168
+ method != "HEAD",
169
+ url
170
+ )
171
+
172
+ request_headers = @default_headers.merge(headers)
173
+ request_headers.each { |name, value| request[name] = value }
174
+ request.body = body if body
175
+
176
+ request
177
+ end
178
+
179
+ # @param query [Hash] The query for the request.
180
+ # @return [String, nil] The encoded query.
181
+ def encode_query(query)
182
+ query.to_h.empty? ? nil : URI.encode_www_form(query)
183
+ end
184
+
185
+ # @param url [URI::Generic] The url to connect to.
186
+ # @return [Net::HTTP] The HTTP connection.
187
+ def connect(url)
188
+ is_https = (url.scheme == "https")
189
+
190
+ port = if url.port
191
+ url.port
192
+ elsif is_https
193
+ Net::HTTP.https_default_port
194
+ else
195
+ Net::HTTP.http_default_port
196
+ end
197
+
198
+ http = Net::HTTP.new(url.host, port)
199
+ http.use_ssl = is_https
200
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER if is_https
201
+ # NOTE: We handle retries at the application level with HTTP status code awareness,
202
+ # so we set max_retries to 0 to disable Net::HTTP's built-in network-level retries.
203
+ http.max_retries = 0
204
+ http
205
+ end
206
+
207
+ # @return [String]
208
+ def inspect
209
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} @base_url=#{@base_url.inspect}>"
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Voltaria
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 [Voltaria::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 [Voltaria::Internal::CursorPageIterator]
23
+ def pages
24
+ @page_iterator
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Voltaria
4
+ module Internal
5
+ class CursorPageIterator
6
+ include Enumerable
7
+
8
+ # Instantiates a CursorPageIterator, an Enumerable class which wraps calls to a cursor-based paginated API and yields pages of items.
9
+ #
10
+ # @param initial_cursor [String] The initial cursor to use when iterating, if any.
11
+ # @param cursor_field [Symbol] The name of the field in API responses to extract the next cursor from.
12
+ # @param block [Proc] A block which is responsible for receiving a cursor to use and returning the given page from the API.
13
+ # @return [Voltaria::Internal::CursorPageIterator]
14
+ def initialize(initial_cursor:, cursor_field:, &block)
15
+ @need_initial_load = initial_cursor.nil?
16
+ @cursor = initial_cursor
17
+ @cursor_field = cursor_field
18
+ @get_next_page = block
19
+ end
20
+
21
+ # Iterates over each page returned by the API.
22
+ #
23
+ # @param block [Proc] The block which each retrieved page is yielded to.
24
+ # @return [NilClass]
25
+ def each(&block)
26
+ while (page = next_page)
27
+ block.call(page)
28
+ end
29
+ end
30
+
31
+ # Whether another page will be available from the API.
32
+ #
33
+ # @return [Boolean]
34
+ def next?
35
+ @need_initial_load || !@cursor.nil?
36
+ end
37
+
38
+ # Retrieves the next page from the API.
39
+ #
40
+ # @return [Boolean]
41
+ def next_page
42
+ return if !@need_initial_load && @cursor.nil?
43
+
44
+ @need_initial_load = false
45
+ fetched_page = @get_next_page.call(@cursor)
46
+ @cursor = fetched_page.send(@cursor_field)
47
+ fetched_page
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Voltaria
4
+ module Internal
5
+ class ItemIterator
6
+ include Enumerable
7
+
8
+ # Iterates over each item returned by the API.
9
+ #
10
+ # @param block [Proc] The block which each retrieved item is yielded to.
11
+ # @return [NilClass]
12
+ def each(&block)
13
+ while (item = next_element)
14
+ block.call(item)
15
+ end
16
+ end
17
+
18
+ # Whether another item will be available from the API.
19
+ #
20
+ # @return [Boolean]
21
+ def next?
22
+ load_next_page if @page.nil?
23
+ return false if @page.nil?
24
+
25
+ return true if any_items_in_cached_page?
26
+
27
+ load_next_page
28
+ any_items_in_cached_page?
29
+ end
30
+
31
+ # Retrieves the next item from the API.
32
+ def next_element
33
+ item = next_item_from_cached_page
34
+ return item if item
35
+
36
+ load_next_page
37
+ next_item_from_cached_page
38
+ end
39
+
40
+ private
41
+
42
+ def next_item_from_cached_page
43
+ return unless @page
44
+
45
+ @page.send(@item_field).shift
46
+ end
47
+
48
+ def any_items_in_cached_page?
49
+ return false unless @page
50
+
51
+ !@page.send(@item_field).empty?
52
+ end
53
+
54
+ def load_next_page
55
+ @page = @page_iterator.next_page
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Voltaria
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 [Voltaria::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 [Voltaria::Internal::OffsetPageIterator]
25
+ def pages
26
+ @page_iterator
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Voltaria
4
+ module Internal
5
+ class OffsetPageIterator
6
+ include Enumerable
7
+
8
+ # Instantiates an OffsetPageIterator, an Enumerable class which wraps calls to an offset-based paginated API and yields pages of items from it.
9
+ #
10
+ # @param initial_page [Integer] The initial page to use when iterating, if any.
11
+ # @param item_field [Symbol] The field to pull the list of items to iterate over.
12
+ # @param has_next_field [Symbol] The field to pull the boolean of whether a next page exists from, if any.
13
+ # @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)
14
+ # @param block [Proc] A block which is responsible for receiving a page number to use and returning the given page from the API.
15
+ # @return [Voltaria::Internal::OffsetPageIterator]
16
+ def initialize(initial_page:, item_field:, has_next_field:, step:, &block)
17
+ @page_number = initial_page || (step ? 0 : 1)
18
+ @item_field = item_field
19
+ @has_next_field = has_next_field
20
+ @step = step
21
+ @get_next_page = block
22
+
23
+ # A cache of whether the API has another page, if it gives us that information...
24
+ @next_page = nil
25
+ # ...or the actual next page, preloaded, if it doesn't.
26
+ @has_next_page = nil
27
+ end
28
+
29
+ # Iterates over each page returned by the API.
30
+ #
31
+ # @param block [Proc] The block which each retrieved page is yielded to.
32
+ # @return [NilClass]
33
+ def each(&block)
34
+ while (page = next_page)
35
+ block.call(page)
36
+ end
37
+ end
38
+
39
+ # Whether another page will be available from the API.
40
+ #
41
+ # @return [Boolean]
42
+ def next?
43
+ return @has_next_page unless @has_next_page.nil?
44
+ return true if @next_page
45
+
46
+ fetched_page = @get_next_page.call(@page_number)
47
+ fetched_page_items = fetched_page&.send(@item_field)
48
+ if fetched_page_items.nil? || fetched_page_items.empty?
49
+ @has_next_page = false
50
+ else
51
+ @next_page = fetched_page
52
+ true
53
+ end
54
+ end
55
+
56
+ # Returns the next page from the API.
57
+ def next_page
58
+ return nil if @page_number.nil?
59
+
60
+ if @next_page
61
+ this_page = @next_page
62
+ @next_page = nil
63
+ else
64
+ this_page = @get_next_page.call(@page_number)
65
+ end
66
+
67
+ @has_next_page = this_page&.send(@has_next_field) if @has_next_field
68
+
69
+ items = this_page.send(@item_field)
70
+ if items.nil? || items.empty?
71
+ @page_number = nil
72
+ return nil
73
+ elsif @step
74
+ @page_number += items.length
75
+ else
76
+ @page_number += 1
77
+ end
78
+
79
+ this_page
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Voltaria
4
+ module Internal
5
+ module JSON
6
+ # @api private
7
+ class Request < Voltaria::Internal::Http::BaseRequest
8
+ attr_reader :body
9
+
10
+ # @param base_url [String] The base URL for the request
11
+ # @param path [String] The path for the request
12
+ # @param method [Symbol] The HTTP method for the request (:get, :post, etc.)
13
+ # @param headers [Hash] Additional headers for the request (optional)
14
+ # @param query [Hash] Query parameters for the request (optional)
15
+ # @param body [Object, nil] The JSON request body (optional)
16
+ # @param request_options [Voltaria::RequestOptions, Hash{Symbol=>Object}, nil]
17
+ def initialize(base_url:, path:, method:, headers: {}, query: {}, body: nil, request_options: {})
18
+ super(base_url:, path:, method:, headers:, query:, request_options:)
19
+
20
+ @body = body
21
+ end
22
+
23
+ # @return [Hash] The encoded HTTP request headers.
24
+ # @param protected_keys [Array<String>] Header keys set by the SDK client (e.g. auth, metadata)
25
+ # that must not be overridden by additional_headers from request_options.
26
+ def encode_headers(protected_keys: [])
27
+ sdk_headers = {
28
+ "Content-Type" => "application/json",
29
+ "Accept" => "application/json"
30
+ }.merge(@headers)
31
+ merge_additional_headers(sdk_headers, protected_keys:)
32
+ end
33
+
34
+ # @return [String, nil] The encoded HTTP request body.
35
+ def encode_body
36
+ @body.nil? ? nil : ::JSON.generate(@body)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Voltaria
4
+ module Internal
5
+ module JSON
6
+ module Serializable
7
+ # Loads data from JSON into its deserialized form
8
+ #
9
+ # @param str [String] Raw JSON to load into an object
10
+ # @return [Object]
11
+ def load(str)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ # Dumps data from its deserialized form into JSON
16
+ #
17
+ # @param value [Object] The deserialized value
18
+ # @return [String]
19
+ def dump(value)
20
+ raise NotImplementedError
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Voltaria
4
+ module Internal
5
+ module Multipart
6
+ # Encodes parameters into a `multipart/form-data` payload as described by RFC
7
+ # 2388:
8
+ #
9
+ # https://tools.ietf.org/html/rfc2388
10
+ #
11
+ # This is most useful for transferring file-like objects.
12
+ #
13
+ # Parameters should be added with `#encode`. When ready, use `#body` to get
14
+ # the encoded result and `#content_type` to get the value that should be
15
+ # placed in the `Content-Type` header of a subsequent request (which includes
16
+ # a boundary value).
17
+ #
18
+ # This abstraction is heavily inspired by Stripe's multipart/form-data implementation,
19
+ # which can be found here:
20
+ #
21
+ # https://github.com/stripe/stripe-ruby/blob/ca00b676f04ac421cf5cb5ff0325f243651677b6/lib/stripe/multipart_encoder.rb#L18
22
+ #
23
+ # @api private
24
+ class Encoder
25
+ CONTENT_TYPE = "multipart/form-data"
26
+ CRLF = "\r\n"
27
+
28
+ attr_reader :boundary, :body
29
+
30
+ def initialize
31
+ # Chose the same number of random bytes that Go uses in its standard
32
+ # library implementation. Easily enough entropy to ensure that it won't
33
+ # be present in a file we're sending.
34
+ @boundary = SecureRandom.hex(30)
35
+
36
+ @body = String.new
37
+ @closed = false
38
+ @first_field = true
39
+ end
40
+
41
+ # Gets the content type string including the boundary.
42
+ #
43
+ # @return [String] The content type with boundary
44
+ def content_type
45
+ "#{CONTENT_TYPE}; boundary=#{@boundary}"
46
+ end
47
+
48
+ # Encode the given FormData object into a multipart/form-data payload.
49
+ #
50
+ # @param form_data [FormData] The form data to encode
51
+ # @return [String] The encoded body.
52
+ def encode(form_data)
53
+ return "" if form_data.parts.empty?
54
+
55
+ form_data.parts.each do |part|
56
+ write_part(part)
57
+ end
58
+ close
59
+
60
+ @body
61
+ end
62
+
63
+ # Writes a FormDataPart to the encoder.
64
+ #
65
+ # @param part [FormDataPart] The part to write
66
+ # @return [nil]
67
+ def write_part(part)
68
+ raise "Cannot write to closed encoder" if @closed
69
+
70
+ write_field(
71
+ name: part.name,
72
+ data: part.contents,
73
+ filename: part.filename,
74
+ headers: part.headers
75
+ )
76
+
77
+ nil
78
+ end
79
+
80
+ # Writes a field to the encoder.
81
+ #
82
+ # @param name [String] The field name
83
+ # @param data [String] The field data
84
+ # @param filename [String, nil] Optional filename
85
+ # @param headers [Hash<String, String>, nil] Optional additional headers
86
+ # @return [nil]
87
+ def write_field(name:, data:, filename: nil, headers: nil)
88
+ raise "Cannot write to closed encoder" if @closed
89
+
90
+ if @first_field
91
+ @first_field = false
92
+ else
93
+ @body << CRLF
94
+ end
95
+
96
+ @body << "--#{@boundary}#{CRLF}"
97
+ @body << %(Content-Disposition: form-data; name="#{escape(name.to_s)}")
98
+ @body << %(; filename="#{escape(filename)}") if filename
99
+ @body << CRLF
100
+
101
+ if headers
102
+ headers.each do |key, value|
103
+ @body << "#{key}: #{value}#{CRLF}"
104
+ end
105
+ elsif filename
106
+ # Default content type for files.
107
+ @body << "Content-Type: application/octet-stream#{CRLF}"
108
+ end
109
+
110
+ @body << CRLF
111
+ @body << data.to_s
112
+
113
+ nil
114
+ end
115
+
116
+ # Finalizes the encoder by writing the final boundary.
117
+ #
118
+ # @return [nil]
119
+ def close
120
+ raise "Encoder already closed" if @closed
121
+
122
+ @body << CRLF
123
+ @body << "--#{@boundary}--"
124
+ @closed = true
125
+
126
+ nil
127
+ end
128
+
129
+ private
130
+
131
+ # Escapes quotes for use in header values and replaces line breaks with spaces.
132
+ #
133
+ # @param str [String] The string to escape
134
+ # @return [String] The escaped string
135
+ def escape(str)
136
+ str.to_s.gsub('"', "%22").tr("\n", " ").tr("\r", " ")
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end