mailhub-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. checksums.yaml +7 -0
  2. data/.fern/metadata.json +11 -0
  3. data/.fernignore +3 -0
  4. data/.rubocop.yml +103 -0
  5. data/CONTRIBUTING.md +120 -0
  6. data/README.md +159 -0
  7. data/Rakefile +20 -0
  8. data/custom.gemspec.rb +18 -0
  9. data/lib/Mailhub/campaign/client.rb +730 -0
  10. data/lib/Mailhub/campaign/types/add_campaign_media_by_code_request.rb +13 -0
  11. data/lib/Mailhub/campaign/types/cancel_campaign_request.rb +11 -0
  12. data/lib/Mailhub/campaign/types/catch_up_campaign_request.rb +11 -0
  13. data/lib/Mailhub/campaign/types/create_campaign_by_code_request.rb +27 -0
  14. data/lib/Mailhub/campaign/types/delete_campaign_request.rb +11 -0
  15. data/lib/Mailhub/campaign/types/get_campaign_activity_request.rb +11 -0
  16. data/lib/Mailhub/campaign/types/get_campaign_analytics_request.rb +11 -0
  17. data/lib/Mailhub/campaign/types/get_campaign_link_stats_request.rb +11 -0
  18. data/lib/Mailhub/campaign/types/get_campaign_media_request.rb +11 -0
  19. data/lib/Mailhub/campaign/types/get_campaign_rendered_preview_by_code_request.rb +21 -0
  20. data/lib/Mailhub/campaign/types/get_campaign_request.rb +11 -0
  21. data/lib/Mailhub/campaign/types/get_campaigns_request.rb +19 -0
  22. data/lib/Mailhub/campaign/types/pause_campaign_request.rb +11 -0
  23. data/lib/Mailhub/campaign/types/preview_campaign_request.rb +11 -0
  24. data/lib/Mailhub/campaign/types/remove_campaign_media_request.rb +13 -0
  25. data/lib/Mailhub/campaign/types/schedule_campaign_request.rb +11 -0
  26. data/lib/Mailhub/campaign/types/send_campaign_request.rb +11 -0
  27. data/lib/Mailhub/campaign/types/test_campaign_by_code_request.rb +23 -0
  28. data/lib/Mailhub/campaign/types/update_campaign_by_code_request.rb +29 -0
  29. data/lib/Mailhub/client.rb +55 -0
  30. data/lib/Mailhub/contact/client.rb +385 -0
  31. data/lib/Mailhub/contact/types/block_contact_request.rb +11 -0
  32. data/lib/Mailhub/contact/types/block_contacts_by_guid_request.rb +11 -0
  33. data/lib/Mailhub/contact/types/create_contact_request.rb +15 -0
  34. data/lib/Mailhub/contact/types/delete_contact_request.rb +11 -0
  35. data/lib/Mailhub/contact/types/delete_contacts_by_guid_request.rb +11 -0
  36. data/lib/Mailhub/contact/types/get_contact_request.rb +11 -0
  37. data/lib/Mailhub/contact/types/get_contacts_request.rb +21 -0
  38. data/lib/Mailhub/contact/types/get_import_job_request.rb +11 -0
  39. data/lib/Mailhub/contact/types/import_contacts_request.rb +9 -0
  40. data/lib/Mailhub/contact/types/update_contact_request.rb +17 -0
  41. data/lib/Mailhub/custom_field/client.rb +188 -0
  42. data/lib/Mailhub/custom_field/types/create_custom_field_request.rb +15 -0
  43. data/lib/Mailhub/custom_field/types/delete_custom_field_request.rb +11 -0
  44. data/lib/Mailhub/custom_field/types/get_custom_field_request.rb +11 -0
  45. data/lib/Mailhub/custom_field/types/update_custom_field_request.rb +15 -0
  46. data/lib/Mailhub/errors/api_error.rb +8 -0
  47. data/lib/Mailhub/errors/client_error.rb +17 -0
  48. data/lib/Mailhub/errors/redirect_error.rb +8 -0
  49. data/lib/Mailhub/errors/response_error.rb +42 -0
  50. data/lib/Mailhub/errors/server_error.rb +11 -0
  51. data/lib/Mailhub/errors/timeout_error.rb +8 -0
  52. data/lib/Mailhub/internal/errors/constraint_error.rb +10 -0
  53. data/lib/Mailhub/internal/errors/type_error.rb +10 -0
  54. data/lib/Mailhub/internal/http/base_request.rb +51 -0
  55. data/lib/Mailhub/internal/http/raw_client.rb +248 -0
  56. data/lib/Mailhub/internal/iterators/cursor_item_iterator.rb +28 -0
  57. data/lib/Mailhub/internal/iterators/cursor_page_iterator.rb +63 -0
  58. data/lib/Mailhub/internal/iterators/item_iterator.rb +65 -0
  59. data/lib/Mailhub/internal/iterators/offset_item_iterator.rb +30 -0
  60. data/lib/Mailhub/internal/iterators/offset_page_iterator.rb +103 -0
  61. data/lib/Mailhub/internal/json/request.rb +41 -0
  62. data/lib/Mailhub/internal/json/serializable.rb +25 -0
  63. data/lib/Mailhub/internal/multipart/multipart_encoder.rb +141 -0
  64. data/lib/Mailhub/internal/multipart/multipart_form_data.rb +78 -0
  65. data/lib/Mailhub/internal/multipart/multipart_form_data_part.rb +51 -0
  66. data/lib/Mailhub/internal/multipart/multipart_request.rb +40 -0
  67. data/lib/Mailhub/internal/types/array.rb +47 -0
  68. data/lib/Mailhub/internal/types/boolean.rb +34 -0
  69. data/lib/Mailhub/internal/types/enum.rb +56 -0
  70. data/lib/Mailhub/internal/types/hash.rb +36 -0
  71. data/lib/Mailhub/internal/types/model/field.rb +38 -0
  72. data/lib/Mailhub/internal/types/model.rb +208 -0
  73. data/lib/Mailhub/internal/types/type.rb +35 -0
  74. data/lib/Mailhub/internal/types/union.rb +161 -0
  75. data/lib/Mailhub/internal/types/unknown.rb +15 -0
  76. data/lib/Mailhub/internal/types/utils.rb +116 -0
  77. data/lib/Mailhub/media/client.rb +241 -0
  78. data/lib/Mailhub/media/types/delete_media_request.rb +11 -0
  79. data/lib/Mailhub/media/types/download_media_request.rb +11 -0
  80. data/lib/Mailhub/media/types/get_media_request.rb +11 -0
  81. data/lib/Mailhub/media/types/get_medias_request.rb +19 -0
  82. data/lib/Mailhub/media/types/update_media_request.rb +13 -0
  83. data/lib/Mailhub/media/types/upload_media_request.rb +9 -0
  84. data/lib/Mailhub/message/client.rb +201 -0
  85. data/lib/Mailhub/message/types/cancel_message_request.rb +11 -0
  86. data/lib/Mailhub/message/types/get_message_events_request.rb +11 -0
  87. data/lib/Mailhub/message/types/get_message_request.rb +11 -0
  88. data/lib/Mailhub/message/types/get_messages_request.rb +21 -0
  89. data/lib/Mailhub/message/types/send_message_request.rb +23 -0
  90. data/lib/Mailhub/segment/client.rb +377 -0
  91. data/lib/Mailhub/segment/types/create_segment_request.rb +13 -0
  92. data/lib/Mailhub/segment/types/delete_segment_request.rb +11 -0
  93. data/lib/Mailhub/segment/types/delete_segments_by_code_request.rb +13 -0
  94. data/lib/Mailhub/segment/types/get_segment_contacts_request.rb +15 -0
  95. data/lib/Mailhub/segment/types/get_segment_preview_request.rb +11 -0
  96. data/lib/Mailhub/segment/types/get_segment_request.rb +11 -0
  97. data/lib/Mailhub/segment/types/get_segments_request.rb +19 -0
  98. data/lib/Mailhub/segment/types/get_segments_union_preview_by_code_request.rb +11 -0
  99. data/lib/Mailhub/segment/types/sync_segment_request.rb +11 -0
  100. data/lib/Mailhub/segment/types/update_segment_request.rb +15 -0
  101. data/lib/Mailhub/template/client.rb +301 -0
  102. data/lib/Mailhub/template/types/create_template_request.rb +13 -0
  103. data/lib/Mailhub/template/types/delete_template_request.rb +11 -0
  104. data/lib/Mailhub/template/types/get_template_rendered_preview_request.rb +11 -0
  105. data/lib/Mailhub/template/types/get_template_request.rb +11 -0
  106. data/lib/Mailhub/template/types/get_templates_request.rb +19 -0
  107. data/lib/Mailhub/template/types/preview_template_request.rb +11 -0
  108. data/lib/Mailhub/template/types/update_template_request.rb +15 -0
  109. data/lib/Mailhub/types/api_error.rb +16 -0
  110. data/lib/Mailhub/types/api_error_detail.rb +12 -0
  111. data/lib/Mailhub/types/campaign_activity_point_model.rb +18 -0
  112. data/lib/Mailhub/types/campaign_analytics_model.rb +30 -0
  113. data/lib/Mailhub/types/campaign_link_stat_model.rb +14 -0
  114. data/lib/Mailhub/types/campaign_media_model.rb +24 -0
  115. data/lib/Mailhub/types/campaign_model.rb +52 -0
  116. data/lib/Mailhub/types/campaign_model_paged_result.rb +16 -0
  117. data/lib/Mailhub/types/campaign_segment_model.rb +14 -0
  118. data/lib/Mailhub/types/campaign_status.rb +17 -0
  119. data/lib/Mailhub/types/campaign_type.rb +12 -0
  120. data/lib/Mailhub/types/contact_import_job_model.rb +22 -0
  121. data/lib/Mailhub/types/contact_import_model.rb +10 -0
  122. data/lib/Mailhub/types/contact_import_status.rb +14 -0
  123. data/lib/Mailhub/types/contact_model.rb +26 -0
  124. data/lib/Mailhub/types/contact_model_cursor_paged_result.rb +18 -0
  125. data/lib/Mailhub/types/contact_model_paged_result.rb +16 -0
  126. data/lib/Mailhub/types/contact_segment_model.rb +14 -0
  127. data/lib/Mailhub/types/contact_status.rb +13 -0
  128. data/lib/Mailhub/types/custom_field_model.rb +20 -0
  129. data/lib/Mailhub/types/custom_field_type.rb +14 -0
  130. data/lib/Mailhub/types/import_contact_error_model.rb +12 -0
  131. data/lib/Mailhub/types/media_model.rb +28 -0
  132. data/lib/Mailhub/types/media_model_paged_result.rb +16 -0
  133. data/lib/Mailhub/types/message_event_model.rb +16 -0
  134. data/lib/Mailhub/types/message_event_type.rb +18 -0
  135. data/lib/Mailhub/types/message_model.rb +36 -0
  136. data/lib/Mailhub/types/message_model_cursor_paged_result.rb +18 -0
  137. data/lib/Mailhub/types/preview_segment_model.rb +10 -0
  138. data/lib/Mailhub/types/running_campaign_stats_model.rb +32 -0
  139. data/lib/Mailhub/types/segment_filter_group_model.rb +12 -0
  140. data/lib/Mailhub/types/segment_filter_model.rb +10 -0
  141. data/lib/Mailhub/types/segment_filter_rule_model.rb +14 -0
  142. data/lib/Mailhub/types/segment_model.rb +32 -0
  143. data/lib/Mailhub/types/segment_model_paged_result.rb +16 -0
  144. data/lib/Mailhub/types/segment_sync_status.rb +14 -0
  145. data/lib/Mailhub/types/template_expression_model.rb +14 -0
  146. data/lib/Mailhub/types/template_model.rb +20 -0
  147. data/lib/Mailhub/types/template_model_paged_result.rb +16 -0
  148. data/lib/Mailhub/version.rb +5 -0
  149. data/lib/Mailhub.rb +145 -0
  150. data/reference.md +3691 -0
  151. metadata +193 -0
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module CustomField
5
+ class Client
6
+ # @param client [Mailhub::Internal::Http::RawClient]
7
+ #
8
+ # @return [void]
9
+ def initialize(client:)
10
+ @client = client
11
+ end
12
+
13
+ # @param request_options [Hash]
14
+ # @param _params [Hash]
15
+ # @option request_options [String] :base_url
16
+ # @option request_options [Hash{String => Object}] :additional_headers
17
+ # @option request_options [Hash{String => Object}] :additional_query_parameters
18
+ # @option request_options [Hash{String => Object}] :additional_body_parameters
19
+ # @option request_options [Integer] :timeout_in_seconds
20
+ #
21
+ # @example
22
+ # client.custom_field.get_custom_fields
23
+ #
24
+ # @return [Array[Mailhub::Types::CustomFieldModel]]
25
+ def get_custom_fields(request_options: {}, **_params)
26
+ request = Mailhub::Internal::JSON::Request.new(
27
+ base_url: request_options[:base_url],
28
+ method: "GET",
29
+ path: "v1/custom-fields",
30
+ request_options: request_options
31
+ )
32
+ begin
33
+ response = @client.send(request)
34
+ rescue Net::HTTPRequestTimeout
35
+ raise Mailhub::Errors::TimeoutError
36
+ end
37
+ code = response.code.to_i
38
+ return if code.between?(200, 299)
39
+
40
+ error_class = Mailhub::Errors::ResponseError.subclass_for_code(code)
41
+ raise error_class.new(response.body, code: code)
42
+ end
43
+
44
+ # @param request_options [Hash]
45
+ # @param params [Mailhub::CustomField::Types::CreateCustomFieldRequest]
46
+ # @option request_options [String] :base_url
47
+ # @option request_options [Hash{String => Object}] :additional_headers
48
+ # @option request_options [Hash{String => Object}] :additional_query_parameters
49
+ # @option request_options [Hash{String => Object}] :additional_body_parameters
50
+ # @option request_options [Integer] :timeout_in_seconds
51
+ #
52
+ # @example
53
+ # client.custom_field.create_custom_field
54
+ #
55
+ # @return [Mailhub::Types::CustomFieldModel]
56
+ def create_custom_field(request_options: {}, **params)
57
+ params = Mailhub::Internal::Types::Utils.normalize_keys(params)
58
+ request = Mailhub::Internal::JSON::Request.new(
59
+ base_url: request_options[:base_url],
60
+ method: "POST",
61
+ path: "v1/custom-fields",
62
+ body: Mailhub::CustomField::Types::CreateCustomFieldRequest.new(params).to_h,
63
+ request_options: request_options
64
+ )
65
+ begin
66
+ response = @client.send(request)
67
+ rescue Net::HTTPRequestTimeout
68
+ raise Mailhub::Errors::TimeoutError
69
+ end
70
+ code = response.code.to_i
71
+ if code.between?(200, 299)
72
+ Mailhub::Types::CustomFieldModel.load(response.body)
73
+ else
74
+ error_class = Mailhub::Errors::ResponseError.subclass_for_code(code)
75
+ raise error_class.new(response.body, code: code)
76
+ end
77
+ end
78
+
79
+ # @param request_options [Hash]
80
+ # @param params [Hash]
81
+ # @option request_options [String] :base_url
82
+ # @option request_options [Hash{String => Object}] :additional_headers
83
+ # @option request_options [Hash{String => Object}] :additional_query_parameters
84
+ # @option request_options [Hash{String => Object}] :additional_body_parameters
85
+ # @option request_options [Integer] :timeout_in_seconds
86
+ # @option params [String] :key
87
+ #
88
+ # @example
89
+ # client.custom_field.get_custom_field(key: "key")
90
+ #
91
+ # @return [Mailhub::Types::CustomFieldModel]
92
+ def get_custom_field(request_options: {}, **params)
93
+ params = Mailhub::Internal::Types::Utils.normalize_keys(params)
94
+ request = Mailhub::Internal::JSON::Request.new(
95
+ base_url: request_options[:base_url],
96
+ method: "GET",
97
+ path: "v1/custom-fields/#{URI.encode_uri_component(params[:key].to_s)}",
98
+ request_options: request_options
99
+ )
100
+ begin
101
+ response = @client.send(request)
102
+ rescue Net::HTTPRequestTimeout
103
+ raise Mailhub::Errors::TimeoutError
104
+ end
105
+ code = response.code.to_i
106
+ if code.between?(200, 299)
107
+ Mailhub::Types::CustomFieldModel.load(response.body)
108
+ else
109
+ error_class = Mailhub::Errors::ResponseError.subclass_for_code(code)
110
+ raise error_class.new(response.body, code: code)
111
+ end
112
+ end
113
+
114
+ # @param request_options [Hash]
115
+ # @param params [Mailhub::CustomField::Types::UpdateCustomFieldRequest]
116
+ # @option request_options [String] :base_url
117
+ # @option request_options [Hash{String => Object}] :additional_headers
118
+ # @option request_options [Hash{String => Object}] :additional_query_parameters
119
+ # @option request_options [Hash{String => Object}] :additional_body_parameters
120
+ # @option request_options [Integer] :timeout_in_seconds
121
+ # @option params [String] :key
122
+ #
123
+ # @example
124
+ # client.custom_field.update_custom_field(key: "key")
125
+ #
126
+ # @return [Mailhub::Types::CustomFieldModel]
127
+ def update_custom_field(request_options: {}, **params)
128
+ params = Mailhub::Internal::Types::Utils.normalize_keys(params)
129
+ request_data = Mailhub::CustomField::Types::UpdateCustomFieldRequest.new(params).to_h
130
+ non_body_param_names = %w[key]
131
+ body = request_data.except(*non_body_param_names)
132
+
133
+ request = Mailhub::Internal::JSON::Request.new(
134
+ base_url: request_options[:base_url],
135
+ method: "PUT",
136
+ path: "v1/custom-fields/#{URI.encode_uri_component(params[:key].to_s)}",
137
+ body: body,
138
+ request_options: request_options
139
+ )
140
+ begin
141
+ response = @client.send(request)
142
+ rescue Net::HTTPRequestTimeout
143
+ raise Mailhub::Errors::TimeoutError
144
+ end
145
+ code = response.code.to_i
146
+ if code.between?(200, 299)
147
+ Mailhub::Types::CustomFieldModel.load(response.body)
148
+ else
149
+ error_class = Mailhub::Errors::ResponseError.subclass_for_code(code)
150
+ raise error_class.new(response.body, code: code)
151
+ end
152
+ end
153
+
154
+ # @param request_options [Hash]
155
+ # @param params [Hash]
156
+ # @option request_options [String] :base_url
157
+ # @option request_options [Hash{String => Object}] :additional_headers
158
+ # @option request_options [Hash{String => Object}] :additional_query_parameters
159
+ # @option request_options [Hash{String => Object}] :additional_body_parameters
160
+ # @option request_options [Integer] :timeout_in_seconds
161
+ # @option params [String] :key
162
+ #
163
+ # @example
164
+ # client.custom_field.delete_custom_field(key: "key")
165
+ #
166
+ # @return [untyped]
167
+ def delete_custom_field(request_options: {}, **params)
168
+ params = Mailhub::Internal::Types::Utils.normalize_keys(params)
169
+ request = Mailhub::Internal::JSON::Request.new(
170
+ base_url: request_options[:base_url],
171
+ method: "DELETE",
172
+ path: "v1/custom-fields/#{URI.encode_uri_component(params[:key].to_s)}",
173
+ request_options: request_options
174
+ )
175
+ begin
176
+ response = @client.send(request)
177
+ rescue Net::HTTPRequestTimeout
178
+ raise Mailhub::Errors::TimeoutError
179
+ end
180
+ code = response.code.to_i
181
+ return if code.between?(200, 299)
182
+
183
+ error_class = Mailhub::Errors::ResponseError.subclass_for_code(code)
184
+ raise error_class.new(response.body, code: code)
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module CustomField
5
+ module Types
6
+ class CreateCustomFieldRequest < Internal::Types::Model
7
+ field :key, -> { String }, optional: true, nullable: false
8
+
9
+ field :name, -> { String }, optional: true, nullable: false
10
+
11
+ field :type, -> { Mailhub::Types::CustomFieldType }, optional: true, nullable: false
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module CustomField
5
+ module Types
6
+ class DeleteCustomFieldRequest < Internal::Types::Model
7
+ field :key, -> { String }, optional: false, nullable: false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module CustomField
5
+ module Types
6
+ class GetCustomFieldRequest < Internal::Types::Model
7
+ field :key, -> { String }, optional: false, nullable: false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module CustomField
5
+ module Types
6
+ class UpdateCustomFieldRequest < Internal::Types::Model
7
+ field :key, -> { String }, optional: false, nullable: false
8
+
9
+ field :name, -> { String }, optional: true, nullable: false
10
+
11
+ field :type, -> { Mailhub::Types::CustomFieldType }, optional: true, nullable: false
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module Errors
5
+ class ApiError < StandardError
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module Errors
5
+ class ClientError < ResponseError
6
+ end
7
+
8
+ class UnauthorizedError < ClientError
9
+ end
10
+
11
+ class ForbiddenError < ClientError
12
+ end
13
+
14
+ class NotFoundError < ClientError
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module Errors
5
+ class RedirectError < ResponseError
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module Errors
5
+ class ResponseError < ApiError
6
+ attr_reader :code
7
+
8
+ def initialize(msg, code:)
9
+ @code = code
10
+ super(msg)
11
+ end
12
+
13
+ def inspect
14
+ "#<#{self.class.name} @code=#{code} @body=#{message}>"
15
+ end
16
+
17
+ # Returns the most appropriate error class for the given code.
18
+ #
19
+ # @return [Class]
20
+ def self.subclass_for_code(code)
21
+ case code
22
+ when 300..399
23
+ RedirectError
24
+ when 401
25
+ UnauthorizedError
26
+ when 403
27
+ ForbiddenError
28
+ when 404
29
+ NotFoundError
30
+ when 400..499
31
+ ClientError
32
+ when 503
33
+ ServiceUnavailableError
34
+ when 500..599
35
+ ServerError
36
+ else
37
+ ResponseError
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module Errors
5
+ class ServerError < ResponseError
6
+ end
7
+
8
+ class ServiceUnavailableError < ApiError
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module Errors
5
+ class TimeoutError < ApiError
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module Internal
5
+ module Errors
6
+ class ConstraintError < StandardError
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module Internal
5
+ module Errors
6
+ class TypeError < StandardError
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
4
+ module Internal
5
+ module Http
6
+ # @api private
7
+ class BaseRequest
8
+ attr_reader :base_url, :path, :method, :headers, :query, :request_options
9
+
10
+ # @param base_url [String] The base URL for the request
11
+ # @param path [String] The path for the request
12
+ # @param method [String] 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 request_options [Mailhub::RequestOptions, Hash{Symbol=>Object}, nil]
16
+ def initialize(base_url:, path:, method:, headers: {}, query: {}, request_options: {})
17
+ @base_url = base_url
18
+ @path = path
19
+ @method = method
20
+ @headers = headers
21
+ @query = query
22
+ @request_options = request_options
23
+ end
24
+
25
+ # @return [Hash] The query parameters merged with additional query parameters from request options.
26
+ def encode_query
27
+ additional_query = @request_options&.dig(:additional_query_parameters) || @request_options&.dig("additional_query_parameters") || {}
28
+ @query.merge(additional_query)
29
+ end
30
+
31
+ # Child classes should implement:
32
+ # - encode_headers: Returns the encoded HTTP request headers.
33
+ # - encode_body: Returns the encoded HTTP request body.
34
+
35
+ private
36
+
37
+ # Merges additional_headers from request_options into sdk_headers, filtering out
38
+ # any keys that collide with SDK-set or client-protected headers (case-insensitive).
39
+ # @param sdk_headers [Hash] Headers set by the SDK for this request type.
40
+ # @param protected_keys [Array<String>] Additional header keys that must not be overridden.
41
+ # @return [Hash] The merged headers.
42
+ def merge_additional_headers(sdk_headers, protected_keys: [])
43
+ additional_headers = @request_options&.dig(:additional_headers) || @request_options&.dig("additional_headers") || {}
44
+ all_protected = (sdk_headers.keys + protected_keys).to_set { |k| k.to_s.downcase }
45
+ filtered = additional_headers.reject { |key, _| all_protected.include?(key.to_s.downcase) }
46
+ sdk_headers.merge(filtered)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailhub
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
+ # @param auth_provider [Object, nil] An optional auth provider responding to
25
+ # `auth_headers`. When present its headers are resolved on every request so
26
+ # token-based schemes (e.g. OAuth) can refresh an expired token mid-session.
27
+ def initialize(base_url:, max_retries: 2, timeout: 60.0, headers: {}, auth_provider: nil)
28
+ @base_url = base_url
29
+ @max_retries = max_retries
30
+ @timeout = timeout
31
+ @auth_provider = auth_provider
32
+ @default_headers = headers
33
+ end
34
+
35
+ # @param request [Mailhub::Internal::Http::BaseRequest] The HTTP request.
36
+ # @return [HTTP::Response] The HTTP response.
37
+ def send(request)
38
+ url = build_url(request)
39
+ # Resolve auth headers once per request (not per retry) so token-based
40
+ # providers refresh at most once here; static providers are cheap.
41
+ auth_headers = resolve_auth_headers
42
+ attempt = 0
43
+ response = nil
44
+
45
+ loop do
46
+ http_request = build_http_request(
47
+ url:,
48
+ method: request.method,
49
+ headers: request.encode_headers(protected_keys: @default_headers.keys + auth_headers.keys),
50
+ body: request.encode_body,
51
+ auth_headers: auth_headers
52
+ )
53
+
54
+ conn = connect(url)
55
+ conn.open_timeout = @timeout
56
+ conn.read_timeout = @timeout
57
+ conn.write_timeout = @timeout
58
+ conn.continue_timeout = @timeout
59
+
60
+ response = conn.request(http_request)
61
+
62
+ break unless should_retry?(response, attempt)
63
+
64
+ delay = retry_delay(response, attempt)
65
+ sleep(delay)
66
+ attempt += 1
67
+ end
68
+
69
+ response
70
+ end
71
+
72
+ # Determines if a request should be retried based on the response status code.
73
+ # @param response [Net::HTTPResponse] The HTTP response.
74
+ # @param attempt [Integer] The current retry attempt (0-indexed).
75
+ # @return [Boolean] Whether the request should be retried.
76
+ def should_retry?(response, attempt)
77
+ return false if attempt >= @max_retries
78
+
79
+ status = response.code.to_i
80
+ RETRYABLE_STATUSES.include?(status)
81
+ end
82
+
83
+ # Calculates the delay before the next retry attempt using exponential backoff with jitter.
84
+ # Respects Retry-After header if present.
85
+ # @param response [Net::HTTPResponse] The HTTP response.
86
+ # @param attempt [Integer] The current retry attempt (0-indexed).
87
+ # @return [Float] The delay in seconds before the next retry.
88
+ def retry_delay(response, attempt)
89
+ # Check for Retry-After header (can be seconds or HTTP date)
90
+ retry_after = response["Retry-After"]
91
+ if retry_after
92
+ delay = parse_retry_after(retry_after)
93
+ return [delay, MAX_RETRY_DELAY].min if delay&.positive?
94
+ end
95
+
96
+ # Exponential backoff with jitter: base_delay * 2^attempt
97
+ base_delay = INITIAL_RETRY_DELAY * (2**attempt)
98
+ add_jitter([base_delay, MAX_RETRY_DELAY].min)
99
+ end
100
+
101
+ # Parses the Retry-After header value.
102
+ # @param value [String] The Retry-After header value (seconds or HTTP date).
103
+ # @return [Float, nil] The delay in seconds, or nil if parsing fails.
104
+ def parse_retry_after(value)
105
+ # Try parsing as integer (seconds)
106
+ seconds = Integer(value, exception: false)
107
+ return seconds.to_f if seconds
108
+
109
+ # Try parsing as HTTP date
110
+ begin
111
+ retry_time = Time.httpdate(value)
112
+ delay = retry_time - Time.now
113
+ delay.positive? ? delay : nil
114
+ rescue ArgumentError
115
+ nil
116
+ end
117
+ end
118
+
119
+ # Adds random jitter to a delay value.
120
+ # @param delay [Float] The base delay in seconds.
121
+ # @return [Float] The delay with jitter applied.
122
+ def add_jitter(delay)
123
+ jitter = delay * JITTER_FACTOR * (rand - 0.5) * 2
124
+ [delay + jitter, 0].max
125
+ end
126
+
127
+ LOCALHOST_HOSTS = %w[localhost 127.0.0.1 [::1]].freeze
128
+
129
+ # @param request [Mailhub::Internal::Http::BaseRequest] The HTTP request.
130
+ # @return [URI::Generic] The URL.
131
+ def build_url(request)
132
+ encoded_query = request.encode_query
133
+
134
+ # If the path is already an absolute URL, use it directly
135
+ if request.path.start_with?("http://", "https://")
136
+ url = request.path
137
+ url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
138
+ parsed = URI.parse(url)
139
+ validate_https!(parsed)
140
+ return parsed
141
+ end
142
+
143
+ path = request.path.start_with?("/") ? request.path[1..] : request.path
144
+ base = request.base_url || @base_url
145
+ url = "#{base.chomp("/")}/#{path}"
146
+ url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
147
+ parsed = URI.parse(url)
148
+ validate_https!(parsed)
149
+ parsed
150
+ end
151
+
152
+ # Raises if the URL uses http:// for a non-localhost host, which would
153
+ # send authentication credentials in plaintext.
154
+ # @param url [URI::Generic] The parsed URL.
155
+ def validate_https!(url)
156
+ return if url.scheme != "http"
157
+ return if LOCALHOST_HOSTS.include?(url.host)
158
+
159
+ raise ArgumentError,
160
+ "Refusing to send request to non-HTTPS URL: #{url}. " \
161
+ "HTTP is only allowed for localhost. Use HTTPS or pass a localhost URL."
162
+ end
163
+
164
+ # Resolves the auth headers to send with the next request. Delegates to the
165
+ # configured auth provider (if any) on every call so that token-based
166
+ # providers (e.g. OAuth client-credentials) can refresh an expired token
167
+ # before the request is sent. Returns an empty hash when no provider is set,
168
+ # which keeps the api-key / basic / bearer / no-auth paths unchanged.
169
+ # @return [Hash] The auth headers for the current request.
170
+ def resolve_auth_headers
171
+ return {} if @auth_provider.nil?
172
+
173
+ @auth_provider.auth_headers
174
+ end
175
+
176
+ # @param url [URI::Generic] The url to the resource.
177
+ # @param method [String] The HTTP method to use.
178
+ # @param headers [Hash] The headers for the request.
179
+ # @param body [String, nil] The body for the request.
180
+ # @param auth_headers [Hash] The auth headers resolved for this request. These
181
+ # take precedence over the static default headers but not over per-request
182
+ # headers, mirroring the previous baked-header precedence.
183
+ # @return [HTTP::Request] The HTTP request.
184
+ def build_http_request(url:, method:, headers: {}, body: nil, auth_headers: {})
185
+ request = Net::HTTPGenericRequest.new(
186
+ method,
187
+ !body.nil?,
188
+ method != "HEAD",
189
+ url
190
+ )
191
+
192
+ request_headers = @default_headers.merge(auth_headers).merge(headers)
193
+ request_headers.each { |name, value| request[name] = value }
194
+ request.body = body if body
195
+
196
+ # Net::HTTP disables its transparent gzip/deflate decoding as soon as an
197
+ # Accept-Encoding header is set explicitly on the request. Re-enable it so
198
+ # that compressed response bodies are still inflated.
199
+ request.extend(DecodeContent) if request_headers.keys.any? { |name| name.to_s.casecmp("accept-encoding").zero? }
200
+
201
+ request
202
+ end
203
+
204
+ # Keeps Net::HTTP's transparent gzip/deflate response decoding enabled
205
+ # even when an Accept-Encoding header is set explicitly on the request.
206
+ # @api private
207
+ module DecodeContent
208
+ def decode_content # rubocop:disable Naming/PredicateMethod
209
+ true
210
+ end
211
+ end
212
+
213
+ # @param query [Hash] The query for the request.
214
+ # @return [String, nil] The encoded query.
215
+ def encode_query(query)
216
+ query.to_h.empty? ? nil : URI.encode_www_form(query)
217
+ end
218
+
219
+ # @param url [URI::Generic] The url to connect to.
220
+ # @return [Net::HTTP] The HTTP connection.
221
+ def connect(url)
222
+ is_https = (url.scheme == "https")
223
+
224
+ port = if url.port
225
+ url.port
226
+ elsif is_https
227
+ Net::HTTP.https_default_port
228
+ else
229
+ Net::HTTP.http_default_port
230
+ end
231
+
232
+ http = Net::HTTP.new(url.host, port)
233
+ http.use_ssl = is_https
234
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER if is_https
235
+ # NOTE: We handle retries at the application level with HTTP status code awareness,
236
+ # so we set max_retries to 0 to disable Net::HTTP's built-in network-level retries.
237
+ http.max_retries = 0
238
+ http
239
+ end
240
+
241
+ # @return [String]
242
+ def inspect
243
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} @base_url=#{@base_url.inspect}>"
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end