schematichq 1.5.2 → 1.5.4

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 (273) hide show
  1. checksums.yaml +4 -4
  2. data/.fern/metadata.json +4 -4
  3. data/.fern/replay.lock +14 -338
  4. data/.fernignore +8 -0
  5. data/.rubocop.yml +4 -0
  6. data/README.md +141 -0
  7. data/WASM_VERSION +1 -1
  8. data/custom.gemspec.rb +6 -0
  9. data/lib/schematic/accesstokens/client.rb +9 -1
  10. data/lib/schematic/accounts/client.rb +183 -21
  11. data/lib/schematic/accounts/types/update_environment_request_body.rb +2 -0
  12. data/lib/schematic/billing/client.rb +325 -34
  13. data/lib/schematic/billing/types/create_billing_price_request_body.rb +1 -1
  14. data/lib/schematic/billing/types/create_billing_product_request_body.rb +1 -1
  15. data/lib/schematic/billing/types/create_billing_subscription_request_body.rb +1 -1
  16. data/lib/schematic/billing/types/create_coupon_request_body.rb +2 -2
  17. data/lib/schematic/billing/types/create_invoice_request_body.rb +1 -1
  18. data/lib/schematic/billing/types/get_billing_settings_response.rb +13 -0
  19. data/lib/schematic/billing/types/list_billing_prices_params.rb +1 -1
  20. data/lib/schematic/billing/types/list_billing_prices_request.rb +1 -1
  21. data/lib/schematic/billing/types/list_billing_product_prices_params.rb +1 -1
  22. data/lib/schematic/billing/types/list_billing_product_prices_request.rb +1 -1
  23. data/lib/schematic/billing/types/update_billing_settings_request_body.rb +11 -0
  24. data/lib/schematic/billing/types/update_billing_settings_response.rb +13 -0
  25. data/lib/schematic/catalogs/client.rb +121 -14
  26. data/lib/schematic/checkout/client.rb +80 -9
  27. data/lib/schematic/checkout/types/checkout_data_request_body.rb +1 -1
  28. data/lib/schematic/client.rb +5 -3
  29. data/lib/schematic/companies/client.rb +380 -44
  30. data/lib/schematic/components/client.rb +70 -8
  31. data/lib/schematic/components/types/create_component_request_body.rb +1 -1
  32. data/lib/schematic/components/types/update_component_request_body.rb +1 -1
  33. data/lib/schematic/componentspublic/client.rb +8 -1
  34. data/lib/schematic/credits/client.rb +458 -46
  35. data/lib/schematic/credits/leases/check.rb +477 -0
  36. data/lib/schematic/credits/leases/lease_manager.rb +565 -0
  37. data/lib/schematic/credits/leases/lease_store.rb +240 -0
  38. data/lib/schematic/credits/leases/redis_lease_store.rb +358 -0
  39. data/lib/schematic/credits/leases/redis_reservation_store.rb +326 -0
  40. data/lib/schematic/credits/leases/reservation_store.rb +161 -0
  41. data/lib/schematic/credits/leases/server_check.rb +237 -0
  42. data/lib/schematic/credits/leases/track.rb +66 -0
  43. data/lib/schematic/credits/leases/types.rb +329 -0
  44. data/lib/schematic/credits/leases/wire_client.rb +83 -0
  45. data/lib/schematic/credits/types/acquire_credit_lease_request_body.rb +3 -1
  46. data/lib/schematic/credits/types/create_billing_credit_request_body.rb +1 -1
  47. data/lib/schematic/credits/types/create_company_credit_grant.rb +1 -1
  48. data/lib/schematic/credits/types/create_credit_bundle_request_body.rb +1 -1
  49. data/lib/schematic/credits/types/create_credit_spend_policy_request_body.rb +5 -1
  50. data/lib/schematic/credits/types/extend_credit_lease_request_body.rb +3 -1
  51. data/lib/schematic/credits/types/get_credit_balance_time_series_params.rb +18 -0
  52. data/lib/schematic/credits/types/get_credit_balance_time_series_request.rb +17 -0
  53. data/lib/schematic/credits/types/get_credit_balance_time_series_response.rb +13 -0
  54. data/lib/schematic/credits/types/get_credit_spend_policy_usage_params.rb +16 -0
  55. data/lib/schematic/credits/types/get_credit_spend_policy_usage_request.rb +15 -0
  56. data/lib/schematic/credits/types/get_credit_spend_policy_usage_response.rb +13 -0
  57. data/lib/schematic/credits/types/reserve_credits_request_body.rb +1 -1
  58. data/lib/schematic/credits/types/update_credit_spend_policy_request_body.rb +5 -1
  59. data/lib/schematic/dataexports/client.rb +36 -6
  60. data/lib/schematic/datastream/client.rb +32 -2
  61. data/lib/schematic/entitlements/client.rb +406 -25
  62. data/lib/schematic/entitlements/types/count_company_user_usage_params.rb +24 -0
  63. data/lib/schematic/entitlements/types/count_company_user_usage_request.rb +23 -0
  64. data/lib/schematic/entitlements/types/count_company_user_usage_response.rb +13 -0
  65. data/lib/schematic/entitlements/types/create_billing_linked_plan_entitlement_request_body.rb +4 -2
  66. data/lib/schematic/entitlements/types/create_company_override_request_body.rb +3 -1
  67. data/lib/schematic/entitlements/types/create_plan_entitlement_request_body.rb +4 -2
  68. data/lib/schematic/entitlements/types/get_company_user_usage_metrics_params.rb +16 -0
  69. data/lib/schematic/entitlements/types/get_company_user_usage_metrics_request.rb +15 -0
  70. data/lib/schematic/entitlements/types/get_company_user_usage_metrics_response.rb +13 -0
  71. data/lib/schematic/entitlements/types/list_company_user_usage_params.rb +24 -0
  72. data/lib/schematic/entitlements/types/list_company_user_usage_request.rb +23 -0
  73. data/lib/schematic/entitlements/types/list_company_user_usage_response.rb +13 -0
  74. data/lib/schematic/entitlements/types/update_company_override_request_body.rb +3 -1
  75. data/lib/schematic/entitlements/types/update_plan_entitlement_request_body.rb +4 -2
  76. data/lib/schematic/errors/connection_error.rb +11 -0
  77. data/lib/schematic/errors/response_error.rb +23 -2
  78. data/lib/schematic/events/client.rb +78 -9
  79. data/lib/schematic/features/client.rb +160 -18
  80. data/lib/schematic/features/types/check_and_reserve_flag_request_body.rb +1 -1
  81. data/lib/schematic/features/types/create_billing_linked_feature_request_body.rb +2 -0
  82. data/lib/schematic/features/types/create_feature_request_body.rb +2 -0
  83. data/lib/schematic/features/types/update_feature_request_body.rb +2 -0
  84. data/lib/schematic/insights/client.rb +48 -6
  85. data/lib/schematic/integrationsapi/client.rb +120 -14
  86. data/lib/schematic/internal/http/base_request.rb +30 -0
  87. data/lib/schematic/internal/http/raw_client.rb +35 -5
  88. data/lib/schematic/internal/iterators/cursor_item_iterator.rb +7 -2
  89. data/lib/schematic/internal/iterators/cursor_page_iterator.rb +68 -14
  90. data/lib/schematic/internal/iterators/item_iterator.rb +61 -15
  91. data/lib/schematic/internal/iterators/offset_item_iterator.rb +7 -2
  92. data/lib/schematic/internal/iterators/offset_page_iterator.rb +57 -9
  93. data/lib/schematic/internal/json/request.rb +11 -2
  94. data/lib/schematic/internal/multipart/multipart_request.rb +1 -0
  95. data/lib/schematic/internal/types/model.rb +7 -3
  96. data/lib/schematic/internal/types/utils.rb +7 -0
  97. data/lib/schematic/licenses/client.rb +26 -3
  98. data/lib/schematic/planbundle/client.rb +27 -3
  99. data/lib/schematic/plangroups/client.rb +28 -3
  100. data/lib/schematic/plangroups/types/create_plan_group_request_body.rb +2 -0
  101. data/lib/schematic/plangroups/types/update_plan_group_request_body.rb +2 -0
  102. data/lib/schematic/planmigrations/client.rb +104 -11
  103. data/lib/schematic/planmigrations/types/count_migrations_params.rb +2 -0
  104. data/lib/schematic/planmigrations/types/count_migrations_request.rb +2 -0
  105. data/lib/schematic/planmigrations/types/create_migration_input.rb +2 -0
  106. data/lib/schematic/planmigrations/types/list_migrations_params.rb +2 -0
  107. data/lib/schematic/planmigrations/types/list_migrations_request.rb +2 -0
  108. data/lib/schematic/plans/client.rb +213 -23
  109. data/lib/schematic/plans/types/get_plan_version_diff_params.rb +12 -0
  110. data/lib/schematic/plans/types/get_plan_version_diff_request.rb +13 -0
  111. data/lib/schematic/plans/types/get_plan_version_diff_response.rb +13 -0
  112. data/lib/schematic/plans/types/publish_plan_version_request_body.rb +6 -0
  113. data/lib/schematic/plans/types/retry_custom_plan_billing_request_body.rb +2 -0
  114. data/lib/schematic/rules_engine.rb +49 -0
  115. data/lib/schematic/scheduledcheckout/client.rb +35 -4
  116. data/lib/schematic/schematic_client.rb +841 -33
  117. data/lib/schematic/types/billing_coupon_response_data.rb +2 -2
  118. data/lib/schematic/types/billing_credit_grant_response_data.rb +8 -8
  119. data/lib/schematic/types/billing_customer_subscription.rb +1 -1
  120. data/lib/schematic/types/billing_plan_credit_grant_response_data.rb +1 -1
  121. data/lib/schematic/types/billing_price_response_data.rb +1 -1
  122. data/lib/schematic/types/billing_price_view.rb +1 -1
  123. data/lib/schematic/types/billing_product_detail_response_data.rb +2 -2
  124. data/lib/schematic/types/billing_product_for_subscription_response_data.rb +2 -2
  125. data/lib/schematic/types/billing_product_price_response_data.rb +1 -1
  126. data/lib/schematic/types/billing_product_pricing.rb +1 -1
  127. data/lib/schematic/types/billing_product_response_data.rb +2 -2
  128. data/lib/schematic/types/billing_settings_response_data.rb +9 -0
  129. data/lib/schematic/types/billing_subscription_discount_view.rb +2 -2
  130. data/lib/schematic/types/billing_subscription_response_data.rb +1 -1
  131. data/lib/schematic/types/billing_subscription_view.rb +1 -1
  132. data/lib/schematic/types/capture_raw_event.rb +4 -0
  133. data/lib/schematic/types/catalog_config_ordered_plan.rb +2 -0
  134. data/lib/schematic/types/catalog_config_ordered_plan_response_data.rb +2 -0
  135. data/lib/schematic/types/catalog_config_plan_credit.rb +11 -0
  136. data/lib/schematic/types/catalog_config_plan_credit_response_data.rb +11 -0
  137. data/lib/schematic/types/change_subscription_internal_request_body.rb +1 -1
  138. data/lib/schematic/types/change_subscription_request_body.rb +1 -1
  139. data/lib/schematic/types/check_flags_response_data.rb +2 -0
  140. data/lib/schematic/types/checkout_problem_code.rb +54 -0
  141. data/lib/schematic/types/checkout_problem_response_data.rb +31 -0
  142. data/lib/schematic/types/checkout_problem_source.rb +13 -0
  143. data/lib/schematic/types/checkout_subscription.rb +1 -1
  144. data/lib/schematic/types/company_billing_entity_subscription_response_data.rb +1 -1
  145. data/lib/schematic/types/company_credit_balance.rb +3 -3
  146. data/lib/schematic/types/company_credit_balance_response_data.rb +4 -4
  147. data/lib/schematic/types/company_detail_response_data.rb +3 -1
  148. data/lib/schematic/types/company_override_response_data.rb +3 -1
  149. data/lib/schematic/types/company_plan_credit_grant_view.rb +1 -1
  150. data/lib/schematic/types/company_plan_detail_response_data.rb +4 -0
  151. data/lib/schematic/types/company_subscription_response_data.rb +1 -1
  152. data/lib/schematic/types/company_user_usage_metrics_response_data.rb +15 -0
  153. data/lib/schematic/types/company_user_usage_response_data.rb +19 -0
  154. data/lib/schematic/types/company_user_usage_row_response_data.rb +17 -0
  155. data/lib/schematic/types/component_display_settings.rb +2 -0
  156. data/lib/schematic/types/component_response_data.rb +1 -1
  157. data/lib/schematic/types/component_settings_response_data.rb +2 -0
  158. data/lib/schematic/types/condition.rb +3 -1
  159. data/lib/schematic/types/condition_response_data.rb +3 -1
  160. data/lib/schematic/types/condition_view.rb +3 -1
  161. data/lib/schematic/types/coupon_request_body.rb +2 -2
  162. data/lib/schematic/types/create_billing_plan_credit_grant_request_body.rb +1 -1
  163. data/lib/schematic/types/create_entitlement_in_bundle_request_body.rb +4 -2
  164. data/lib/schematic/types/create_entitlement_req_common.rb +3 -1
  165. data/lib/schematic/types/create_or_update_condition_request_body.rb +1 -1
  166. data/lib/schematic/types/credit_balance_event_response_data.rb +21 -0
  167. data/lib/schematic/types/credit_balance_feature_consumption_response_data.rb +13 -0
  168. data/lib/schematic/types/credit_balance_point_response_data.rb +15 -0
  169. data/lib/schematic/types/credit_balance_time_series_response_data.rb +15 -0
  170. data/lib/schematic/types/credit_bundle_currency_price.rb +1 -1
  171. data/lib/schematic/types/credit_bundle_currency_price_request_body.rb +1 -1
  172. data/lib/schematic/types/credit_bundle_currency_price_response_data.rb +1 -1
  173. data/lib/schematic/types/credit_burn_breakdown_line.rb +15 -0
  174. data/lib/schematic/types/credit_company_grant_view.rb +8 -8
  175. data/lib/schematic/types/credit_currency_price.rb +1 -1
  176. data/lib/schematic/types/credit_currency_price_request_body.rb +1 -1
  177. data/lib/schematic/types/credit_currency_price_response_data.rb +1 -1
  178. data/lib/schematic/types/credit_event_ledger_response_data.rb +14 -6
  179. data/lib/schematic/types/credit_event_type.rb +3 -0
  180. data/lib/schematic/types/credit_grant_detail.rb +1 -1
  181. data/lib/schematic/types/credit_lease_response_data.rb +2 -2
  182. data/lib/schematic/types/credit_ledger_entry_kind.rb +17 -0
  183. data/lib/schematic/types/credit_reservation_response_data.rb +2 -2
  184. data/lib/schematic/types/credit_spend_policy.rb +25 -0
  185. data/lib/schematic/types/credit_spend_policy_response_data.rb +13 -1
  186. data/lib/schematic/types/credit_spend_window.rb +11 -0
  187. data/lib/schematic/types/credit_spend_window_unit.rb +13 -0
  188. data/lib/schematic/types/credit_transfer_response_data.rb +1 -1
  189. data/lib/schematic/types/credit_transfer_view.rb +1 -1
  190. data/lib/schematic/types/credit_usage.rb +5 -5
  191. data/lib/schematic/types/credit_usage_aggregation.rb +4 -4
  192. data/lib/schematic/types/credit_usage_reason.rb +1 -0
  193. data/lib/schematic/types/credits_in_plan.rb +11 -0
  194. data/lib/schematic/types/currency.rb +144 -0
  195. data/lib/schematic/types/currency_price_request_body.rb +1 -1
  196. data/lib/schematic/types/custom_plan_billing_response_data.rb +8 -0
  197. data/lib/schematic/types/entitlement_currency_prices_response_data.rb +1 -1
  198. data/lib/schematic/types/environment_detail_response_data.rb +2 -0
  199. data/lib/schematic/types/environment_response_data.rb +2 -0
  200. data/lib/schematic/types/estimated_plan_total.rb +13 -0
  201. data/lib/schematic/types/feature_company_response_data.rb +7 -7
  202. data/lib/schematic/types/feature_detail_response_data.rb +2 -0
  203. data/lib/schematic/types/feature_entitlement.rb +8 -6
  204. data/lib/schematic/types/feature_event_source.rb +12 -0
  205. data/lib/schematic/types/feature_in_plan_response_data.rb +2 -0
  206. data/lib/schematic/types/feature_response_data.rb +2 -0
  207. data/lib/schematic/types/feature_usage_response_data.rb +7 -7
  208. data/lib/schematic/types/feature_view.rb +2 -0
  209. data/lib/schematic/types/flag_check_reservation_response_data.rb +3 -3
  210. data/lib/schematic/types/insights_summary_response_data.rb +1 -1
  211. data/lib/schematic/types/invoice_request_body.rb +1 -1
  212. data/lib/schematic/types/invoice_response_data.rb +1 -1
  213. data/lib/schematic/types/manage_plan_preview_response_response_data.rb +2 -0
  214. data/lib/schematic/types/manage_plan_request.rb +3 -1
  215. data/lib/schematic/types/manage_plan_response_response_data.rb +4 -0
  216. data/lib/schematic/types/mrr_response_data.rb +1 -1
  217. data/lib/schematic/types/onboarding_milestone_view.rb +1 -1
  218. data/lib/schematic/types/ordered_plans_in_group.rb +2 -0
  219. data/lib/schematic/types/pending_migration_response_data.rb +6 -0
  220. data/lib/schematic/types/plan_billing_source.rb +1 -0
  221. data/lib/schematic/types/plan_change_action.rb +1 -0
  222. data/lib/schematic/types/plan_change_amendment_status.rb +15 -0
  223. data/lib/schematic/types/plan_change_response_data.rb +4 -0
  224. data/lib/schematic/types/plan_credit_grant_view.rb +1 -1
  225. data/lib/schematic/types/plan_credit_order.rb +11 -0
  226. data/lib/schematic/types/plan_credit_visibility.rb +11 -0
  227. data/lib/schematic/types/plan_currency_price_request_body.rb +1 -1
  228. data/lib/schematic/types/plan_currency_prices_response_data.rb +1 -1
  229. data/lib/schematic/types/plan_entitlement_response_data.rb +3 -1
  230. data/lib/schematic/types/plan_group_plan_entitlements_order.rb +2 -0
  231. data/lib/schematic/types/plan_version_billing_product_change_response_data.rb +11 -0
  232. data/lib/schematic/types/plan_version_credit_grant_change_response_data.rb +17 -0
  233. data/lib/schematic/types/plan_version_diff_change_type.rb +13 -0
  234. data/lib/schematic/types/plan_version_diff_price_cadence.rb +14 -0
  235. data/lib/schematic/types/plan_version_diff_response_data.rb +23 -0
  236. data/lib/schematic/types/plan_version_entitlement_change_response_data.rb +17 -0
  237. data/lib/schematic/types/plan_version_field_change_response_data.rb +13 -0
  238. data/lib/schematic/types/plan_version_migration_response_data.rb +2 -0
  239. data/lib/schematic/types/plan_version_migration_strategy.rb +1 -0
  240. data/lib/schematic/types/plan_version_price_change_response_data.rb +15 -0
  241. data/lib/schematic/types/plan_view_public_response_data.rb +2 -0
  242. data/lib/schematic/types/preflight_request_body.rb +1 -1
  243. data/lib/schematic/types/preview_subscription_change_response_data.rb +3 -1
  244. data/lib/schematic/types/preview_subscription_discount_response_data.rb +2 -2
  245. data/lib/schematic/types/preview_subscription_finance_response_data.rb +2 -2
  246. data/lib/schematic/types/rules_engine_schema_version.rb +1 -1
  247. data/lib/schematic/types/rulesengine_company.rb +3 -1
  248. data/lib/schematic/types/rulesengine_condition.rb +3 -1
  249. data/lib/schematic/types/rulesengine_credit_postpaid_config.rb +1 -1
  250. data/lib/schematic/types/rulesengine_credit_spend_policy.rb +25 -0
  251. data/lib/schematic/types/rulesengine_credit_spend_policy_scope.rb +13 -0
  252. data/lib/schematic/types/rulesengine_credit_spend_window.rb +11 -0
  253. data/lib/schematic/types/rulesengine_feature_entitlement.rb +8 -6
  254. data/lib/schematic/types/rulesengine_user.rb +2 -0
  255. data/lib/schematic/types/scheduled_downgrade_response_data.rb +1 -1
  256. data/lib/schematic/types/top_feature_by_usage_response_data.rb +1 -1
  257. data/lib/schematic/types/upcoming_invoice_response_data.rb +1 -1
  258. data/lib/schematic/types/update_billing_plan_credit_grant_request_body.rb +1 -1
  259. data/lib/schematic/types/update_entitlement_req_common.rb +3 -1
  260. data/lib/schematic/types/upsert_billing_product_request_body.rb +1 -1
  261. data/lib/schematic/types/usage_based_entitlement_request_body.rb +1 -1
  262. data/lib/schematic/types/usage_based_entitlement_response_data.rb +1 -1
  263. data/lib/schematic/types/user_credit_usage_response_data.rb +2 -2
  264. data/lib/schematic/types/user_daily_credit_point_response_data.rb +1 -1
  265. data/lib/schematic/types/user_feature_usage_response_data.rb +1 -1
  266. data/lib/schematic/types/user_usage_metric.rb +12 -0
  267. data/lib/schematic/version.rb +1 -1
  268. data/lib/schematic/wasm/rulesengine.wasm +0 -0
  269. data/lib/schematic/webhooks/client.rb +88 -10
  270. data/lib/schematic.rb +64 -5
  271. data/lib/schematichq.rb +10 -0
  272. data/reference.md +888 -127
  273. metadata +71 -2
@@ -28,12 +28,42 @@ module Schematic
28
28
  @query.merge(additional_query)
29
29
  end
30
30
 
31
+ # @return [Hash] The additional body parameters from request options, keyed by wire-format name.
32
+ def additional_body_parameters
33
+ @request_options&.dig(:additional_body_parameters) || @request_options&.dig("additional_body_parameters") || {}
34
+ end
35
+
31
36
  # Child classes should implement:
32
37
  # - encode_headers: Returns the encoded HTTP request headers.
33
38
  # - encode_body: Returns the encoded HTTP request body.
34
39
 
35
40
  private
36
41
 
42
+ # Merges additional_body_parameters from request_options on top of the request body.
43
+ # Keys are used as-is (API wire-format names) and are normalized to strings so that a
44
+ # caller-supplied key always overrides the SDK-serialized field with the same name.
45
+ # When the body is nil the additional parameters become the body, except for GET and HEAD
46
+ # requests, which are sent without a body (the additional parameters are ignored). Bodies
47
+ # that are not hash-like (arrays, primitives) are returned unchanged.
48
+ # @param body [Object, nil] The request body.
49
+ # @return [Object, nil] The merged request body.
50
+ def merge_additional_body_parameters(body)
51
+ additional_body = additional_body_parameters
52
+ return body if additional_body.nil? || additional_body.empty?
53
+ return body if body.nil? && bodyless_method?
54
+
55
+ additional_body = additional_body.to_h.transform_keys(&:to_s)
56
+ return additional_body if body.nil?
57
+ return body if body.is_a?(::Array) || !body.respond_to?(:to_h)
58
+
59
+ body.to_h.transform_keys(&:to_s).merge(additional_body)
60
+ end
61
+
62
+ # @return [Boolean] Whether the request method is one that is sent without a body (GET, HEAD).
63
+ def bodyless_method?
64
+ %w[GET HEAD].include?(@method.to_s.upcase)
65
+ end
66
+
37
67
  # Merges additional_headers from request_options into sdk_headers, filtering out
38
68
  # any keys that collide with SDK-set or client-protected headers (case-insensitive).
39
69
  # @param sdk_headers [Hash] Headers set by the SDK for this request type.
@@ -47,6 +47,7 @@ module Schematic
47
47
  # Resolve auth headers once per request (not per retry) so token-based
48
48
  # providers refresh at most once here; static providers are cheap.
49
49
  auth_headers = resolve_auth_headers
50
+ timeout = request_timeout(request)
50
51
  attempt = 0
51
52
  response = nil
52
53
 
@@ -60,12 +61,12 @@ module Schematic
60
61
  )
61
62
 
62
63
  conn = connect(url)
63
- conn.open_timeout = @timeout
64
- conn.read_timeout = @timeout
65
- conn.write_timeout = @timeout
66
- conn.continue_timeout = @timeout
64
+ conn.open_timeout = timeout
65
+ conn.read_timeout = timeout
66
+ conn.write_timeout = timeout
67
+ conn.continue_timeout = timeout
67
68
 
68
- response = conn.request(http_request)
69
+ response = wrap_transport_errors { conn.request(http_request) }
69
70
 
70
71
  break unless should_retry?(response, attempt)
71
72
 
@@ -77,6 +78,35 @@ module Schematic
77
78
  response
78
79
  end
79
80
 
81
+ # Socket-level `Errno` failures. Other `SystemCallError`s, such as file errors raised by a
82
+ # custom HTTP client, are not connection failures and propagate unchanged.
83
+ NETWORK_ERRNOS = [
84
+ Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE,
85
+ Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ENETDOWN, Errno::EHOSTDOWN,
86
+ Errno::EADDRNOTAVAIL
87
+ ].freeze
88
+
89
+ # Runs a single request attempt, re-raising transport failures as SDK errors so that
90
+ # rescuing `Errors::ApiError` covers them. The original exception is kept as `cause`.
91
+ # These failures are not retried: the server may already have processed the request.
92
+ # @return [Net::HTTPResponse] The HTTP response.
93
+ def wrap_transport_errors
94
+ yield
95
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Errno::ETIMEDOUT => e
96
+ raise Schematic::Errors::TimeoutError, e.message
97
+ rescue EOFError, SocketError, OpenSSL::SSL::SSLError, *NETWORK_ERRNOS,
98
+ Net::ProtocolError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError => e
99
+ raise Schematic::Errors::ConnectionError, e.message
100
+ end
101
+
102
+ # @param request [Schematic::Internal::Http::BaseRequest] The HTTP request.
103
+ # @return [Float] The request's `timeout_in_seconds` option, or the client-level timeout.
104
+ def request_timeout(request)
105
+ options = request.request_options || {}
106
+ timeout = options.key?(:timeout_in_seconds) ? options[:timeout_in_seconds] : options["timeout_in_seconds"]
107
+ (timeout.nil? ? @timeout : timeout).to_f
108
+ end
109
+
80
110
  # The client-level header names that `additional_headers` must not replace: every default
81
111
  # header except the API's global headers, which are overridable per request.
82
112
  # @return [Array<Symbol, String>] The protected header names.
@@ -10,11 +10,16 @@ module Schematic
10
10
  # @param item_field [Symbol] The field in API responses to extract the items to iterate over.
11
11
  # @param block [Proc] A block which is responsible for receiving a cursor to use and returning the given page from the API.
12
12
  # @return [Schematic::Internal::CursorItemIterator]
13
- def initialize(initial_cursor:, cursor_field:, item_field:, &)
13
+ def initialize(initial_cursor:, cursor_field:, item_field:, &block)
14
14
  super()
15
15
  @item_field = item_field
16
- @page_iterator = CursorPageIterator.new(initial_cursor:, cursor_field:, &)
16
+ @http_response = nil
17
+ get_page = track_http_responses(block)
18
+ @page_iterator = CursorPageIterator.new(initial_cursor:, cursor_field:, &get_page)
19
+ # Items are read through their own page cursor so that reading `pages` never skips items.
20
+ @item_pages = CursorPageIterator.new(initial_cursor:, cursor_field:, &get_page)
17
21
  @page = nil
22
+ @item_index = 0
18
23
  end
19
24
 
20
25
  # Returns the CursorPageIterator mediating access to the underlying API.
@@ -17,45 +17,99 @@ module Schematic
17
17
  # The block should return a two-element array: [parsed_page, raw_http_response].
18
18
  # @return [Schematic::Internal::CursorPageIterator]
19
19
  def initialize(initial_cursor:, cursor_field:, &block)
20
- @need_initial_load = initial_cursor.nil?
21
- @cursor = initial_cursor
20
+ @initial_cursor = initial_cursor
22
21
  @cursor_field = cursor_field
23
22
  @get_next_page = block
24
23
  @http_response = nil
24
+ @first_page_requested = false
25
+ @first_page = nil
26
+ rewind
25
27
  end
26
28
 
27
- # Iterates over each page returned by the API.
29
+ # Sends the request for the first page now instead of on the first iteration, so an API error for that page
30
+ # is raised here. The page is kept, and every loop that starts from the first page reuses it instead of
31
+ # requesting it again. Does nothing if the first page was already requested.
28
32
  #
29
- # @param block [Proc] The block which each retrieved page is yielded to.
33
+ # @return [self]
34
+ def load_first_page
35
+ return self if @first_page_requested
36
+
37
+ @first_page = fetch_page(@initial_cursor)
38
+ rewind
39
+ self
40
+ end
41
+
42
+ # Uses the first page that `other` loaded with `load_first_page`, unless this iterator already sent a request.
43
+ #
44
+ # @param other [Schematic::Internal::CursorPageIterator]
30
45
  # @return [NilClass]
46
+ def reuse_first_page(other)
47
+ return if @first_page_requested || other.first_page.nil?
48
+
49
+ @first_page = other.first_page
50
+ rewind
51
+ end
52
+
53
+ # Iterates over each page returned by the API, starting again from the first page on every call.
54
+ #
55
+ # @param block [Proc] The block which each retrieved page is yielded to.
56
+ # @return [NilClass, Enumerator] An Enumerator when no block is given.
31
57
  def each(&block)
58
+ return enum_for(:each) unless block_given?
59
+
60
+ rewind
32
61
  while (page = next_page)
33
62
  block.call(page)
34
63
  end
35
64
  end
36
65
 
66
+ # Resets page-by-page iteration (`next_page` / `next?`) to the first page.
67
+ #
68
+ # @return [NilClass]
69
+ def rewind
70
+ @need_initial_load = @initial_cursor.nil?
71
+ @cursor = @initial_cursor
72
+ @at_first_page = true
73
+ nil
74
+ end
75
+
37
76
  # Whether another page will be available from the API.
38
77
  #
39
78
  # @return [Boolean]
40
79
  def next?
41
- @need_initial_load || !@cursor.nil?
80
+ (@at_first_page && !@first_page.nil?) || @need_initial_load || !@cursor.nil?
42
81
  end
43
82
 
44
83
  # Retrieves the next page from the API.
45
84
  #
46
85
  # @return [Object, nil]
47
86
  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
87
+ if @at_first_page && !@first_page.nil?
88
+ page = @first_page
55
89
  else
56
- fetched_page = result
90
+ return if !@need_initial_load && @cursor.nil?
91
+
92
+ page = fetch_page(@cursor)
57
93
  end
58
- @cursor = fetched_page.send(@cursor_field)
94
+ @at_first_page = false
95
+ @need_initial_load = false
96
+ @cursor = page.send(@cursor_field)
97
+ page
98
+ end
99
+
100
+ protected
101
+
102
+ attr_reader :first_page
103
+
104
+ private
105
+
106
+ def fetch_page(cursor)
107
+ result = @get_next_page.call(cursor)
108
+ @first_page_requested = true
109
+ return result unless result.is_a?(Array)
110
+
111
+ fetched_page, raw_response = result
112
+ @http_response = raw_response
59
113
  fetched_page
60
114
  end
61
115
  end
@@ -5,20 +5,35 @@ module Schematic
5
5
  class ItemIterator
6
6
  include Enumerable
7
7
 
8
- # The raw HTTP response from the most recent page response.
8
+ # The raw HTTP response from the most recent page request made by this pager or its `pages`.
9
9
  # @return [Net::HTTPResponse, nil]
10
- def http_response
11
- @page_iterator&.http_response
12
- end
10
+ attr_reader :http_response
13
11
 
14
- # Iterates over each item returned by the API.
12
+ # Iterates over each item returned by the API, starting again from the first page on every call.
13
+ # Each loop is independent of other loops and of `next_element`.
15
14
  #
16
15
  # @param block [Proc] The block which each retrieved item is yielded to.
17
- # @return [NilClass]
18
- def each(&block)
19
- while (item = next_element)
20
- block.call(item)
16
+ # @return [NilClass, Enumerator] An Enumerator when no block is given.
17
+ def each(&)
18
+ return enum_for(:each) unless block_given?
19
+
20
+ # Each loop reads through its own copy of the page stream, so repeated or nested loops never
21
+ # share a position with each other or with `next_element`.
22
+ pass = @item_pages.dup.tap(&:rewind)
23
+ while (page = pass.next_page)
24
+ (page.send(@item_field) || []).each(&)
21
25
  end
26
+ nil
27
+ end
28
+
29
+ # Resets manual item-by-item iteration (`next_element` / `next?`) to the first page.
30
+ #
31
+ # @return [NilClass]
32
+ def rewind
33
+ @item_pages.rewind
34
+ @page = nil
35
+ @item_index = 0
36
+ nil
22
37
  end
23
38
 
24
39
  # Whether another item will be available from the API.
@@ -34,6 +49,19 @@ module Schematic
34
49
  any_items_in_cached_page?
35
50
  end
36
51
 
52
+ # Sends the request for the first page now instead of on the first iteration, so an API error for that page
53
+ # is raised here. The page is kept, and every loop (over items or `pages`) that starts from the first page
54
+ # reuses it instead of requesting it again. Does nothing if this pager already sent a request.
55
+ #
56
+ # @return [self]
57
+ def load_first_page
58
+ return self if @requested
59
+
60
+ @item_pages.load_first_page
61
+ @page_iterator.reuse_first_page(@item_pages)
62
+ self
63
+ end
64
+
37
65
  # Retrieves the next item from the API.
38
66
  def next_element
39
67
  item = next_item_from_cached_page
@@ -45,20 +73,38 @@ module Schematic
45
73
 
46
74
  private
47
75
 
76
+ # Wraps the page-fetching block so that `http_response` reflects the most recent request.
77
+ def track_http_responses(get_page)
78
+ proc do |*args|
79
+ result = get_page.call(*args)
80
+ @requested = true
81
+ @http_response = result[1] if result.is_a?(Array)
82
+ result
83
+ end
84
+ end
85
+
86
+ def cached_page_items
87
+ return [] unless @page
88
+
89
+ @page.send(@item_field) || []
90
+ end
91
+
48
92
  def next_item_from_cached_page
49
- return unless @page
93
+ items = cached_page_items
94
+ return if @item_index >= items.length
50
95
 
51
- @page.send(@item_field).shift
96
+ item = items[@item_index]
97
+ @item_index += 1
98
+ item
52
99
  end
53
100
 
54
101
  def any_items_in_cached_page?
55
- return false unless @page
56
-
57
- !@page.send(@item_field).empty?
102
+ @item_index < cached_page_items.length
58
103
  end
59
104
 
60
105
  def load_next_page
61
- @page = @page_iterator.next_page
106
+ @page = @item_pages.next_page
107
+ @item_index = 0
62
108
  end
63
109
  end
64
110
  end
@@ -12,11 +12,16 @@ module Schematic
12
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
13
  #
14
14
  # @return [Schematic::Internal::OffsetItemIterator]
15
- def initialize(initial_page:, item_field:, has_next_field:, step:, &)
15
+ def initialize(initial_page:, item_field:, has_next_field:, step:, &block)
16
16
  super()
17
17
  @item_field = item_field
18
- @page_iterator = OffsetPageIterator.new(initial_page:, item_field:, has_next_field:, step:, &)
18
+ @http_response = nil
19
+ get_page = track_http_responses(block)
20
+ @page_iterator = OffsetPageIterator.new(initial_page:, item_field:, has_next_field:, step:, &get_page)
21
+ # Items are read through their own page cursor so that reading `pages` never skips items.
22
+ @item_pages = OffsetPageIterator.new(initial_page:, item_field:, has_next_field:, step:, &get_page)
19
23
  @page = nil
24
+ @item_index = 0
20
25
  end
21
26
 
22
27
  # Returns the OffsetPageIterator that is mediating access to the underlying API.
@@ -19,36 +19,74 @@ module Schematic
19
19
  # The block should return a two-element array: [parsed_page, raw_http_response].
20
20
  # @return [Schematic::Internal::OffsetPageIterator]
21
21
  def initialize(initial_page:, item_field:, has_next_field:, step:, &block)
22
- @page_number = initial_page || (step ? 0 : 1)
22
+ @initial_page = initial_page || (step ? 0 : 1)
23
23
  @item_field = item_field
24
24
  @has_next_field = has_next_field
25
25
  @step = step
26
26
  @get_next_page = block
27
+ @http_response = nil
28
+ @first_page_requested = false
29
+ @first_page = nil
30
+ rewind
31
+ end
27
32
 
28
- # A cache of whether the API has another page, if it gives us that information...
29
- @next_page = nil
30
- # ...or the actual next page, preloaded, if it doesn't.
31
- @has_next_page = nil
33
+ # Sends the request for the first page now instead of on the first iteration, so an API error for that page
34
+ # is raised here. The page is kept, and every loop that starts from the first page reuses it instead of
35
+ # requesting it again (an empty first page ends those loops without a new request). Does nothing if the first
36
+ # page was already requested.
37
+ #
38
+ # @return [self]
39
+ def load_first_page
40
+ return self if @first_page_requested
32
41
 
33
- @http_response = nil
42
+ @first_page = fetch_page(@initial_page)
43
+ rewind
44
+ self
34
45
  end
35
46
 
36
- # Iterates over each page returned by the API.
47
+ # Uses the first page that `other` loaded with `load_first_page`, unless this iterator already sent a request.
37
48
  #
38
- # @param block [Proc] The block which each retrieved page is yielded to.
49
+ # @param other [Schematic::Internal::OffsetPageIterator]
39
50
  # @return [NilClass]
51
+ def reuse_first_page(other)
52
+ return if @first_page_requested || other.first_page.nil?
53
+
54
+ @first_page = other.first_page
55
+ rewind
56
+ end
57
+
58
+ # Iterates over each page returned by the API, starting again from the first page on every call.
59
+ #
60
+ # @param block [Proc] The block which each retrieved page is yielded to.
61
+ # @return [NilClass, Enumerator] An Enumerator when no block is given.
40
62
  def each(&block)
63
+ return enum_for(:each) unless block_given?
64
+
65
+ rewind
41
66
  while (page = next_page)
42
67
  block.call(page)
43
68
  end
44
69
  end
45
70
 
71
+ # Resets page-by-page iteration (`next_page` / `next?`) to the first page.
72
+ #
73
+ # @return [NilClass]
74
+ def rewind
75
+ @page_number = @initial_page
76
+ # A cache of whether the API has another page, if it gives us that information...
77
+ @has_next_page = nil
78
+ # ...or the actual next page, preloaded, if it doesn't. A first page kept by `load_first_page` is reused.
79
+ @next_page = @first_page
80
+ nil
81
+ end
82
+
46
83
  # Whether another page will be available from the API.
47
84
  #
48
85
  # @return [Boolean]
49
86
  def next?
50
87
  return @has_next_page unless @has_next_page.nil?
51
- return true if @next_page
88
+ return true if @next_page && page_has_items?(@next_page)
89
+ return @has_next_page = false if @next_page
52
90
 
53
91
  fetched_page = fetch_page(@page_number)
54
92
  fetched_page_items = fetched_page&.send(@item_field)
@@ -86,10 +124,20 @@ module Schematic
86
124
  this_page
87
125
  end
88
126
 
127
+ protected
128
+
129
+ attr_reader :first_page
130
+
89
131
  private
90
132
 
133
+ def page_has_items?(page)
134
+ items = page.send(@item_field)
135
+ !items.nil? && !items.empty?
136
+ end
137
+
91
138
  def fetch_page(page_number)
92
139
  result = @get_next_page.call(page_number)
140
+ @first_page_requested = true
93
141
  if result.is_a?(Array)
94
142
  fetched_page, raw_response = result
95
143
  @http_response = raw_response
@@ -31,9 +31,18 @@ module Schematic
31
31
  merge_additional_headers(sdk_headers, protected_keys:)
32
32
  end
33
33
 
34
- # @return [String, nil] The encoded HTTP request body.
34
+ # @return [String, nil] The encoded HTTP request body, including any additional_body_parameters
35
+ # from request_options.
35
36
  def encode_body
36
- @body.nil? ? nil : ::JSON.generate(@body)
37
+ body = request_body
38
+ body.nil? ? nil : ::JSON.generate(body)
39
+ end
40
+
41
+ private
42
+
43
+ # @return [Object, nil] The request body merged with additional_body_parameters.
44
+ def request_body
45
+ merge_additional_body_parameters(@body)
37
46
  end
38
47
  end
39
48
  end
@@ -31,6 +31,7 @@ module Schematic
31
31
  end
32
32
 
33
33
  # @return [String, nil] The encoded HTTP request body.
34
+ # Note: additional_body_parameters from request_options are not applied to multipart requests.
34
35
  def encode_body
35
36
  @body&.encode
36
37
  end
@@ -108,6 +108,7 @@ module Schematic
108
108
  method_name = :"#{name}="
109
109
 
110
110
  define_method(method_name) do |val|
111
+ @explicit_fields[name] = true
111
112
  @data[name] = val
112
113
  end
113
114
  end
@@ -137,6 +138,7 @@ module Schematic
137
138
  # @return [self]
138
139
  def initialize(values = {})
139
140
  @data = {}
141
+ @explicit_fields = {}
140
142
 
141
143
  values = Utils.symbolize_keys(values.dup)
142
144
 
@@ -145,6 +147,7 @@ module Schematic
145
147
  [field.api_name.to_sym, field_name].uniq.each do |key|
146
148
  next unless values.key?(key)
147
149
 
150
+ @explicit_fields[field_name] = true
148
151
  candidate = values.delete(key)
149
152
  value = candidate if value.nil?
150
153
  end
@@ -173,14 +176,15 @@ module Schematic
173
176
  # If there is a value present in the data, use that value
174
177
  # If there is a `nil` value present in the data, and it is optional but NOT nullable, exclude key altogether
175
178
  # If there is a `nil` value present in the data, and it is optional and nullable, use the nil value
179
+ # only when it was explicitly provided; an omitted optional field is excluded
176
180
 
177
181
  value = @data[name]
178
182
 
179
- next if value.nil? && field.optional && !field.nullable
183
+ next if value.nil? && field.optional && (!field.nullable || !@explicit_fields&.key?(name))
180
184
 
181
185
  if value.is_a?(::Array)
182
- value = value.map { |item| item.respond_to?(:to_h) ? item.to_h : item }
183
- elsif value.respond_to?(:to_h)
186
+ value = value.map { |item| !item.nil? && item.respond_to?(:to_h) ? item.to_h : item }
187
+ elsif !value.nil? && value.respond_to?(:to_h)
184
188
  value = value.to_h
185
189
  end
186
190
 
@@ -60,11 +60,18 @@ module Schematic
60
60
  case value
61
61
  when Numeric, String, Time
62
62
  return value.to_i
63
+ when Symbol
64
+ # JSON object keys are parsed as symbols, so numeric map keys arrive as e.g. :"1"
65
+ parsed = Integer(value.to_s, 10, exception: false)
66
+ return parsed unless parsed.nil?
63
67
  end
64
68
  in ->(t) { t <= Float }
65
69
  case value
66
70
  when Numeric, Time, String
67
71
  return value.to_f
72
+ when Symbol
73
+ parsed = Float(value.to_s, exception: false)
74
+ return parsed unless parsed.nil?
68
75
  end
69
76
  in ->(t) { t <= Model }
70
77
  case value
@@ -59,7 +59,15 @@ module Schematic
59
59
  (response.body.to_s.empty? ? nil : Schematic::Licenses::Types::ListLicensesResponse.load(response.body))
60
60
  else
61
61
  error_class = Schematic::Errors::ResponseError.subclass_for_code(code)
62
- raise error_class.new(response.body, code: code)
62
+ error_types = {
63
+ 400 => Schematic::Types::ApiError,
64
+ 401 => Schematic::Types::ApiError,
65
+ 403 => Schematic::Types::ApiError,
66
+ 404 => Schematic::Types::ApiError,
67
+ 500 => Schematic::Types::ApiError
68
+ }
69
+ error_body = Schematic::Errors::ResponseError.load_error_body(code, response.body, error_types)
70
+ raise error_class.new(response.body, code: code, body: error_body)
63
71
  end
64
72
  end
65
73
 
@@ -94,7 +102,14 @@ module Schematic
94
102
  (response.body.to_s.empty? ? nil : Schematic::Licenses::Types::GetSingleLicenseResponse.load(response.body))
95
103
  else
96
104
  error_class = Schematic::Errors::ResponseError.subclass_for_code(code)
97
- raise error_class.new(response.body, code: code)
105
+ error_types = {
106
+ 401 => Schematic::Types::ApiError,
107
+ 403 => Schematic::Types::ApiError,
108
+ 404 => Schematic::Types::ApiError,
109
+ 500 => Schematic::Types::ApiError
110
+ }
111
+ error_body = Schematic::Errors::ResponseError.load_error_body(code, response.body, error_types)
112
+ raise error_class.new(response.body, code: code, body: error_body)
98
113
  end
99
114
  end
100
115
 
@@ -147,7 +162,15 @@ module Schematic
147
162
  (response.body.to_s.empty? ? nil : Schematic::Licenses::Types::CountLicensesResponse.load(response.body))
148
163
  else
149
164
  error_class = Schematic::Errors::ResponseError.subclass_for_code(code)
150
- raise error_class.new(response.body, code: code)
165
+ error_types = {
166
+ 400 => Schematic::Types::ApiError,
167
+ 401 => Schematic::Types::ApiError,
168
+ 403 => Schematic::Types::ApiError,
169
+ 404 => Schematic::Types::ApiError,
170
+ 500 => Schematic::Types::ApiError
171
+ }
172
+ error_body = Schematic::Errors::ResponseError.load_error_body(code, response.body, error_types)
173
+ raise error_class.new(response.body, code: code, body: error_body)
151
174
  end
152
175
  end
153
176
  end
@@ -53,7 +53,15 @@ module Schematic
53
53
  (response.body.to_s.empty? ? nil : Schematic::Planbundle::Types::CreateCustomPlanBundleResponse.load(response.body))
54
54
  else
55
55
  error_class = Schematic::Errors::ResponseError.subclass_for_code(code)
56
- raise error_class.new(response.body, code: code)
56
+ error_types = {
57
+ 400 => Schematic::Types::ApiError,
58
+ 401 => Schematic::Types::ApiError,
59
+ 403 => Schematic::Types::ApiError,
60
+ 404 => Schematic::Types::ApiError,
61
+ 500 => Schematic::Types::ApiError
62
+ }
63
+ error_body = Schematic::Errors::ResponseError.load_error_body(code, response.body, error_types)
64
+ raise error_class.new(response.body, code: code, body: error_body)
57
65
  end
58
66
  end
59
67
 
@@ -96,7 +104,15 @@ module Schematic
96
104
  (response.body.to_s.empty? ? nil : Schematic::Planbundle::Types::CreatePlanBundleResponse.load(response.body))
97
105
  else
98
106
  error_class = Schematic::Errors::ResponseError.subclass_for_code(code)
99
- raise error_class.new(response.body, code: code)
107
+ error_types = {
108
+ 400 => Schematic::Types::ApiError,
109
+ 401 => Schematic::Types::ApiError,
110
+ 403 => Schematic::Types::ApiError,
111
+ 404 => Schematic::Types::ApiError,
112
+ 500 => Schematic::Types::ApiError
113
+ }
114
+ error_body = Schematic::Errors::ResponseError.load_error_body(code, response.body, error_types)
115
+ raise error_class.new(response.body, code: code, body: error_body)
100
116
  end
101
117
  end
102
118
 
@@ -144,7 +160,15 @@ module Schematic
144
160
  (response.body.to_s.empty? ? nil : Schematic::Planbundle::Types::UpdatePlanBundleResponse.load(response.body))
145
161
  else
146
162
  error_class = Schematic::Errors::ResponseError.subclass_for_code(code)
147
- raise error_class.new(response.body, code: code)
163
+ error_types = {
164
+ 400 => Schematic::Types::ApiError,
165
+ 401 => Schematic::Types::ApiError,
166
+ 403 => Schematic::Types::ApiError,
167
+ 404 => Schematic::Types::ApiError,
168
+ 500 => Schematic::Types::ApiError
169
+ }
170
+ error_body = Schematic::Errors::ResponseError.load_error_body(code, response.body, error_types)
171
+ raise error_class.new(response.body, code: code, body: error_body)
148
172
  end
149
173
  end
150
174
  end