helo-email-sdk 1.0.0.pre.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. checksums.yaml +7 -0
  2. data/lib/helo/api_error.rb +23 -0
  3. data/lib/helo/apis/activity.rb +24 -0
  4. data/lib/helo/apis/broadcasts.rb +30 -0
  5. data/lib/helo/apis/channels.rb +35 -0
  6. data/lib/helo/apis/domains.rb +45 -0
  7. data/lib/helo/apis/sending.rb +43 -0
  8. data/lib/helo/apis/statistics.rb +25 -0
  9. data/lib/helo/apis/suppressions.rb +25 -0
  10. data/lib/helo/apis/webhooks.rb +40 -0
  11. data/lib/helo/client.rb +20 -0
  12. data/lib/helo/configuration.rb +9 -0
  13. data/lib/helo/core/api.rb +22 -0
  14. data/lib/helo/core/api_error.rb +23 -0
  15. data/lib/helo/core/array_type.rb +32 -0
  16. data/lib/helo/core/client.rb +75 -0
  17. data/lib/helo/core/configuration.rb +18 -0
  18. data/lib/helo/core/model.rb +129 -0
  19. data/lib/helo/core/model_type.rb +32 -0
  20. data/lib/helo/enums/attachment_disposition.rb +12 -0
  21. data/lib/helo/enums/broadcast_status.rb +14 -0
  22. data/lib/helo/enums/delivery_type.rb +12 -0
  23. data/lib/helo/enums/dns_record_status.rb +14 -0
  24. data/lib/helo/enums/dns_record_type.rb +12 -0
  25. data/lib/helo/enums/event_type.rb +19 -0
  26. data/lib/helo/enums/mail_type.rb +12 -0
  27. data/lib/helo/enums/suppression_reason.rb +14 -0
  28. data/lib/helo/enums/webhook_event.rb +19 -0
  29. data/lib/helo/models/accepted_webhook_payload.rb +17 -0
  30. data/lib/helo/models/activity_event.rb +19 -0
  31. data/lib/helo/models/activity_list_events_request.rb +20 -0
  32. data/lib/helo/models/activity_list_messages_request.rb +19 -0
  33. data/lib/helo/models/activity_mail_address.rb +10 -0
  34. data/lib/helo/models/attachment.rb +13 -0
  35. data/lib/helo/models/bounced_webhook_payload.rb +18 -0
  36. data/lib/helo/models/broadcast_content.rb +15 -0
  37. data/lib/helo/models/broadcast_content_attachment.rb +11 -0
  38. data/lib/helo/models/broadcast_content_template.rb +11 -0
  39. data/lib/helo/models/broadcast_details_response.rb +19 -0
  40. data/lib/helo/models/broadcast_failure_response.rb +12 -0
  41. data/lib/helo/models/broadcast_response.rb +14 -0
  42. data/lib/helo/models/broadcast_statistics.rb +15 -0
  43. data/lib/helo/models/broadcast_tracking.rb +10 -0
  44. data/lib/helo/models/broadcasts_list_failures_request.rb +11 -0
  45. data/lib/helo/models/broadcasts_list_request.rb +14 -0
  46. data/lib/helo/models/broadcasts_list_suppressions_request.rb +11 -0
  47. data/lib/helo/models/channel_basic_response.rb +12 -0
  48. data/lib/helo/models/channel_details_response.rb +14 -0
  49. data/lib/helo/models/channel_tracking.rb +10 -0
  50. data/lib/helo/models/channels_list_request.rb +14 -0
  51. data/lib/helo/models/clicked_webhook_payload.rb +18 -0
  52. data/lib/helo/models/complained_webhook_payload.rb +18 -0
  53. data/lib/helo/models/create_channel_request.rb +11 -0
  54. data/lib/helo/models/create_channel_tracking.rb +10 -0
  55. data/lib/helo/models/create_domain_request.rb +10 -0
  56. data/lib/helo/models/create_suppressions_request.rb +11 -0
  57. data/lib/helo/models/create_suppressions_response.rb +9 -0
  58. data/lib/helo/models/create_webhook_request.rb +13 -0
  59. data/lib/helo/models/delivered_webhook_payload.rb +18 -0
  60. data/lib/helo/models/delivery_stats.rb +15 -0
  61. data/lib/helo/models/dns_record_response.rb +13 -0
  62. data/lib/helo/models/dns_records_response.rb +13 -0
  63. data/lib/helo/models/domain_channel_response.rb +12 -0
  64. data/lib/helo/models/domain_response.rb +13 -0
  65. data/lib/helo/models/domain_with_dns_response.rb +14 -0
  66. data/lib/helo/models/domains_list_request.rb +13 -0
  67. data/lib/helo/models/error_response.rb +16 -0
  68. data/lib/helo/models/mail_address.rb +10 -0
  69. data/lib/helo/models/message.rb +17 -0
  70. data/lib/helo/models/message_details_response.rb +30 -0
  71. data/lib/helo/models/message_details_response_attachment.rb +11 -0
  72. data/lib/helo/models/message_details_response_event.rb +12 -0
  73. data/lib/helo/models/message_details_response_tracking.rb +10 -0
  74. data/lib/helo/models/opened_webhook_payload.rb +18 -0
  75. data/lib/helo/models/paginated_events_response.rb +11 -0
  76. data/lib/helo/models/paginated_messages_response.rb +11 -0
  77. data/lib/helo/models/paginated_response_of_broadcast.rb +10 -0
  78. data/lib/helo/models/paginated_response_of_broadcast_failure.rb +10 -0
  79. data/lib/helo/models/paginated_response_of_broadcast_suppression.rb +10 -0
  80. data/lib/helo/models/paginated_response_of_domain_response.rb +10 -0
  81. data/lib/helo/models/paginated_response_of_suppression_response.rb +10 -0
  82. data/lib/helo/models/pagination_result_of_channel_basic_response.rb +10 -0
  83. data/lib/helo/models/pagination_result_of_webhook_response.rb +10 -0
  84. data/lib/helo/models/processed_webhook_payload.rb +17 -0
  85. data/lib/helo/models/recipient_event_fields.rb +10 -0
  86. data/lib/helo/models/recipient_headers.rb +11 -0
  87. data/lib/helo/models/remove_suppression_result.rb +11 -0
  88. data/lib/helo/models/remove_suppressions_request.rb +11 -0
  89. data/lib/helo/models/remove_suppressions_response.rb +9 -0
  90. data/lib/helo/models/resubscribed_webhook_payload.rb +18 -0
  91. data/lib/helo/models/send_broadcast_request.rb +17 -0
  92. data/lib/helo/models/send_broadcast_request_message.rb +15 -0
  93. data/lib/helo/models/send_broadcast_request_template.rb +13 -0
  94. data/lib/helo/models/send_broadcast_request_tracking.rb +10 -0
  95. data/lib/helo/models/send_broadcast_response.rb +10 -0
  96. data/lib/helo/models/send_message_accepted_response.rb +11 -0
  97. data/lib/helo/models/send_message_batch_request.rb +9 -0
  98. data/lib/helo/models/send_message_batch_response.rb +9 -0
  99. data/lib/helo/models/send_message_failed_response.rb +11 -0
  100. data/lib/helo/models/send_message_request.rb +22 -0
  101. data/lib/helo/models/send_message_request_template.rb +13 -0
  102. data/lib/helo/models/send_message_request_tracking.rb +10 -0
  103. data/lib/helo/models/send_message_response.rb +8 -0
  104. data/lib/helo/models/statistics_daily_response.rb +9 -0
  105. data/lib/helo/models/statistics_daily_response_result.rb +11 -0
  106. data/lib/helo/models/statistics_hourly_response.rb +9 -0
  107. data/lib/helo/models/statistics_hourly_response_result.rb +11 -0
  108. data/lib/helo/models/statistics_retrieve_daily_request.rb +14 -0
  109. data/lib/helo/models/statistics_retrieve_hourly_request.rb +13 -0
  110. data/lib/helo/models/statistics_retrieve_totals_request.rb +13 -0
  111. data/lib/helo/models/statistics_totals_response.rb +10 -0
  112. data/lib/helo/models/suppression_response.rb +11 -0
  113. data/lib/helo/models/suppression_result.rb +11 -0
  114. data/lib/helo/models/suppressions_list_request.rb +15 -0
  115. data/lib/helo/models/unsubscribed_webhook_payload.rb +18 -0
  116. data/lib/helo/models/update_channel_request.rb +11 -0
  117. data/lib/helo/models/update_channel_tracking.rb +10 -0
  118. data/lib/helo/models/update_domain_request.rb +9 -0
  119. data/lib/helo/models/update_webhook_request.rb +13 -0
  120. data/lib/helo/models/validation_error.rb +9 -0
  121. data/lib/helo/models/webhook_header.rb +10 -0
  122. data/lib/helo/models/webhook_last_response.rb +11 -0
  123. data/lib/helo/models/webhook_payload_common.rb +15 -0
  124. data/lib/helo/models/webhook_response.rb +16 -0
  125. data/lib/helo/models/webhooks_list_request.rb +12 -0
  126. data/lib/helo/version.rb +5 -0
  127. data/lib/helo-email-sdk.rb +3 -0
  128. data/lib/helo.rb +35 -0
  129. metadata +239 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 298571a7c53335cc539139041493cb71bdb10a8c94cb01bf88bf64027b59bfec
4
+ data.tar.gz: eeadd57916455673769b3918e151651b47f677d6b918a8a1fb43bab81caa51bd
5
+ SHA512:
6
+ metadata.gz: 30bf674dca548f389a28f107da2f878ac9b4b7bb9e27b2ca25964421ddfd4f5e9f519b81a6c1439fa9b013e1cd98181ea9adf2e52ecffd5d00305914c766e0ce
7
+ data.tar.gz: ec8aa5f075af43c849b1ec6d3c37d650e6fbdf1a473bc597729acc381467e3f8762f80424e468c1b5d6090e11bf64c53eda7d4a3578c1dbbf1863c3063b74c79
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Helo
6
+ class APIError < Helo::Core::APIError
7
+ def detail
8
+ json_response.fetch(:detail, ["API Error", code, @message].compact.join(" - "))
9
+ end
10
+
11
+ def errors
12
+ json_response
13
+ .fetch(:errors, {})
14
+ .transform_keys(&:to_s)
15
+ end
16
+
17
+ private
18
+
19
+ def json_response
20
+ @_json_response ||= response_body.try(:deep_symbolize_keys) || {}
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class Activity < API
5
+ def list_events(request_data = {})
6
+ request = ActivityListEventsRequest.new(request_data)
7
+ response = @client.request(:get, "/activity/events", params: request.to_params)
8
+ PaginatedEventsResponse.from_hash(response.body)
9
+ end
10
+
11
+ def list_messages(request_data = {})
12
+ request = ActivityListMessagesRequest.new(request_data)
13
+ response = @client.request(:get, "/activity/messages", params: request.to_params)
14
+ PaginatedMessagesResponse.from_hash(response.body)
15
+ end
16
+
17
+ def retrieve_message(id)
18
+ response = @client.request(:get, "/activity/messages/#{id}")
19
+ MessageDetailsResponse.from_hash(response.body)
20
+ end
21
+
22
+ class_methods :list_events, :list_messages, :retrieve_message
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class Broadcasts < API
5
+ def list(request_data = {})
6
+ request = BroadcastsListRequest.new(request_data)
7
+ response = @client.request(:get, "/broadcasts", params: request.to_params)
8
+ PaginatedResponseOfBroadcast.from_hash(response.body)
9
+ end
10
+
11
+ def retrieve(id)
12
+ response = @client.request(:get, "/broadcasts/#{id}")
13
+ BroadcastDetailsResponse.from_hash(response.body)
14
+ end
15
+
16
+ def list_failures(id, request_data = {})
17
+ request = BroadcastsListFailuresRequest.new(request_data)
18
+ response = @client.request(:get, "/broadcasts/#{id}/failures", params: request.to_params)
19
+ PaginatedResponseOfBroadcastFailure.from_hash(response.body)
20
+ end
21
+
22
+ def list_suppressions(id, request_data = {})
23
+ request = BroadcastsListSuppressionsRequest.new(request_data)
24
+ response = @client.request(:get, "/broadcasts/#{id}/suppressions", params: request.to_params)
25
+ PaginatedResponseOfBroadcastSuppression.from_hash(response.body)
26
+ end
27
+
28
+ class_methods :list, :retrieve, :list_failures, :list_suppressions
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class Channels < API
5
+ def list(request_data = {})
6
+ request = ChannelsListRequest.new(request_data)
7
+ response = @client.request(:get, "/channels", params: request.to_params)
8
+ PaginationResultOfChannelBasicResponse.from_hash(response.body)
9
+ end
10
+
11
+ def create(request_data)
12
+ request = CreateChannelRequest.new(request_data)
13
+ response = @client.request(:post, "/channels", body: request.to_params)
14
+ ChannelDetailsResponse.from_hash(response.body)
15
+ end
16
+
17
+ def retrieve(id)
18
+ response = @client.request(:get, "/channels/#{id}")
19
+ ChannelDetailsResponse.from_hash(response.body)
20
+ end
21
+
22
+ def update(id, request_data)
23
+ request = UpdateChannelRequest.new(request_data)
24
+ response = @client.request(:patch, "/channels/#{id}", body: request.to_params)
25
+ ChannelDetailsResponse.from_hash(response.body)
26
+ end
27
+
28
+ def delete(id)
29
+ response = @client.request(:delete, "/channels/#{id}")
30
+ nil
31
+ end
32
+
33
+ class_methods :list, :create, :retrieve, :update, :delete
34
+ end
35
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class Domains < API
5
+ def list(request_data = {})
6
+ request = DomainsListRequest.new(request_data)
7
+ response = @client.request(:get, "/domains", params: request.to_params)
8
+ PaginatedResponseOfDomainResponse.from_hash(response.body)
9
+ end
10
+
11
+ def create(request_data)
12
+ request = CreateDomainRequest.new(request_data)
13
+ response = @client.request(:post, "/domains", body: request.to_params)
14
+ DomainWithDnsResponse.from_hash(response.body)
15
+ end
16
+
17
+ def retrieve(id)
18
+ response = @client.request(:get, "/domains/#{id}")
19
+ DomainWithDnsResponse.from_hash(response.body)
20
+ end
21
+
22
+ def update(id, request_data)
23
+ request = UpdateDomainRequest.new(request_data)
24
+ response = @client.request(:patch, "/domains/#{id}", body: request.to_params)
25
+ DomainResponse.from_hash(response.body)
26
+ end
27
+
28
+ def delete(id)
29
+ response = @client.request(:delete, "/domains/#{id}")
30
+ nil
31
+ end
32
+
33
+ def verify(id)
34
+ response = @client.request(:post, "/domains/#{id}/verify")
35
+ DnsRecordsResponse.from_hash(response.body)
36
+ end
37
+
38
+ def rotate_key(id)
39
+ response = @client.request(:post, "/domains/#{id}/rotate-key")
40
+ DnsRecordResponse.from_hash(response.body)
41
+ end
42
+
43
+ class_methods :list, :create, :retrieve, :update, :delete, :verify, :rotate_key
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class Sending < API
5
+ def transactional(request_data, channel_id: nil, idempotency_key: nil)
6
+ request = SendMessageRequest.new(request_data)
7
+ headers = {}
8
+ headers["X-Helo-Channel-Id"] = channel_id unless channel_id.nil?
9
+ headers["X-Helo-Idempotency-Key"] = idempotency_key unless idempotency_key.nil?
10
+ response = @client.request(:post, "/send/transactional", body: request.to_params, headers: headers)
11
+ SendMessageAcceptedResponse.from_hash(response.body)
12
+ end
13
+
14
+ def transactional_batch(request_data, channel_id: nil, idempotency_key: nil)
15
+ request = SendMessageBatchRequest.new(request_data)
16
+ headers = {}
17
+ headers["X-Helo-Channel-Id"] = channel_id unless channel_id.nil?
18
+ headers["X-Helo-Idempotency-Key"] = idempotency_key unless idempotency_key.nil?
19
+ response = @client.request(:post, "/send/transactional/batch", body: request.to_params, headers: headers)
20
+ SendMessageBatchResponse.from_hash(response.body)
21
+ end
22
+
23
+ def broadcast(request_data, channel_id: nil, idempotency_key: nil)
24
+ request = SendBroadcastRequest.new(request_data)
25
+ headers = {}
26
+ headers["X-Helo-Channel-Id"] = channel_id unless channel_id.nil?
27
+ headers["X-Helo-Idempotency-Key"] = idempotency_key unless idempotency_key.nil?
28
+ response = @client.request(:post, "/send/broadcast", body: request.to_params, headers: headers)
29
+ SendBroadcastResponse.from_hash(response.body)
30
+ end
31
+
32
+ def broadcast_message(request_data, channel_id: nil, idempotency_key: nil)
33
+ request = SendMessageRequest.new(request_data)
34
+ headers = {}
35
+ headers["X-Helo-Channel-Id"] = channel_id unless channel_id.nil?
36
+ headers["X-Helo-Idempotency-Key"] = idempotency_key unless idempotency_key.nil?
37
+ response = @client.request(:post, "/send/broadcast/message", body: request.to_params, headers: headers)
38
+ SendMessageAcceptedResponse.from_hash(response.body)
39
+ end
40
+
41
+ class_methods :transactional, :transactional_batch, :broadcast, :broadcast_message
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class Statistics < API
5
+ def retrieve_hourly(request_data = {})
6
+ request = StatisticsRetrieveHourlyRequest.new(request_data)
7
+ response = @client.request(:get, "/activity/statistics/hourly", params: request.to_params)
8
+ StatisticsHourlyResponse.from_hash(response.body)
9
+ end
10
+
11
+ def retrieve_daily(request_data = {})
12
+ request = StatisticsRetrieveDailyRequest.new(request_data)
13
+ response = @client.request(:get, "/activity/statistics/daily", params: request.to_params)
14
+ StatisticsDailyResponse.from_hash(response.body)
15
+ end
16
+
17
+ def retrieve_totals(request_data = {})
18
+ request = StatisticsRetrieveTotalsRequest.new(request_data)
19
+ response = @client.request(:get, "/activity/statistics/totals", params: request.to_params)
20
+ StatisticsTotalsResponse.from_hash(response.body)
21
+ end
22
+
23
+ class_methods :retrieve_hourly, :retrieve_daily, :retrieve_totals
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class Suppressions < API
5
+ def list(request_data = {})
6
+ request = SuppressionsListRequest.new(request_data)
7
+ response = @client.request(:get, "/suppressions", params: request.to_params)
8
+ PaginatedResponseOfSuppressionResponse.from_hash(response.body)
9
+ end
10
+
11
+ def create(request_data)
12
+ request = CreateSuppressionsRequest.new(request_data)
13
+ response = @client.request(:post, "/suppressions", body: request.to_params)
14
+ CreateSuppressionsResponse.from_hash(response.body)
15
+ end
16
+
17
+ def remove(request_data)
18
+ request = RemoveSuppressionsRequest.new(request_data)
19
+ response = @client.request(:post, "/suppressions/remove", body: request.to_params)
20
+ RemoveSuppressionsResponse.from_hash(response.body)
21
+ end
22
+
23
+ class_methods :list, :create, :remove
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class Webhooks < API
5
+ def list(request_data = {})
6
+ request = WebhooksListRequest.new(request_data)
7
+ response = @client.request(:get, "/webhooks", params: request.to_params)
8
+ PaginationResultOfWebhookResponse.from_hash(response.body)
9
+ end
10
+
11
+ def create(request_data)
12
+ request = CreateWebhookRequest.new(request_data)
13
+ response = @client.request(:post, "/webhooks", body: request.to_params)
14
+ WebhookResponse.from_hash(response.body)
15
+ end
16
+
17
+ def retrieve(id)
18
+ response = @client.request(:get, "/webhooks/#{id}")
19
+ WebhookResponse.from_hash(response.body)
20
+ end
21
+
22
+ def update(id, request_data)
23
+ request = UpdateWebhookRequest.new(request_data)
24
+ response = @client.request(:patch, "/webhooks/#{id}", body: request.to_params)
25
+ WebhookResponse.from_hash(response.body)
26
+ end
27
+
28
+ def delete(id)
29
+ response = @client.request(:delete, "/webhooks/#{id}")
30
+ nil
31
+ end
32
+
33
+ def regenerate_signing_key(id)
34
+ response = @client.request(:post, "/webhooks/#{id}/regenerate-signing-key")
35
+ WebhookResponse.from_hash(response.body)
36
+ end
37
+
38
+ class_methods :list, :create, :retrieve, :update, :delete, :regenerate_signing_key
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class Client < Helo::Core::Client
5
+ private
6
+
7
+ def handle_error_response(response)
8
+ raise APIError.new(
9
+ "Request failed",
10
+ code: response.status,
11
+ response_body: response.body,
12
+ response_headers: response.headers
13
+ )
14
+ end
15
+
16
+ def handle_connection_error(exception)
17
+ raise APIError.new("Connection failed: #{exception.message}")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class Configuration < Helo::Core::Configuration
5
+ def initialize(base_url: "https://api.helohq.com", api_key: nil)
6
+ super
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/class/attribute"
4
+
5
+ module Helo::Core
6
+ # Base class for all API resources
7
+ class API
8
+ class_attribute :default_client
9
+
10
+ def self.class_methods(*methods)
11
+ methods.each do |method|
12
+ define_singleton_method(method) do |*args, **kwargs, &block|
13
+ new(default_client).public_send(method, *args, **kwargs, &block)
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(client)
19
+ @client = client
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo::Core
4
+ class APIError < StandardError
5
+ attr_reader :code, :response_body, :response_headers
6
+
7
+ def initialize(message = nil, code: nil, response_body: nil, response_headers: nil)
8
+ @message = message
9
+ @code = code
10
+ @response_body = response_body
11
+ @response_headers = response_headers
12
+ super(message)
13
+ end
14
+
15
+ def message
16
+ msg = @message
17
+ msg += "\nHTTP status code: #{code}" if code
18
+ msg += "\nResponse headers: #{response_headers}" if response_headers
19
+ msg += "\nResponse body: #{response_body}" if response_body
20
+ msg
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module Helo::Core
6
+ # Custom ActiveModel type for arrays of model objects
7
+ class ArrayType < ActiveModel::Type::Value
8
+ def initialize(model_class_name)
9
+ @model_class_name = model_class_name
10
+ super()
11
+ end
12
+
13
+ def cast(value)
14
+ return if value.nil?
15
+
16
+ value.map do |item|
17
+ case item
18
+ when model_class
19
+ item
20
+ when Hash
21
+ model_class.from_hash(item)
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def model_class
29
+ @_model_class ||= @model_class_name.constantize
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ module Helo::Core
6
+ class Client
7
+ attr_reader :configuration
8
+
9
+ def initialize(configuration)
10
+ raise ArgumentError, "base_url is not configured" if configuration.base_url.to_s.empty?
11
+
12
+ @configuration = configuration
13
+ @connection = Faraday.new(url: configuration.base_url) do |f|
14
+ f.request :json
15
+ f.response :json
16
+ f.adapter Faraday.default_adapter
17
+ f.options.params_encoder = Faraday::FlatParamsEncoder
18
+
19
+ # Allow custom Faraday configuration
20
+ configuration.faraday_configuration_block&.call(f)
21
+ end
22
+ end
23
+
24
+ def request(method, path, params: {}, body: nil, headers: {})
25
+ response = connection.public_send(method, path) do |req|
26
+ req.params = params
27
+ req.body = body
28
+
29
+ # Set Authorization header with dynamic or static token
30
+ token = current_api_key
31
+ req.headers["Authorization"] = "Bearer #{token}" if token
32
+
33
+ # Apply per-request headers verbatim (already in wire form)
34
+ headers.each do |key, value|
35
+ req.headers[key.to_s] = value.to_s
36
+ end
37
+ end
38
+
39
+ handle_response(response)
40
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
41
+ handle_connection_error(e)
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :connection
47
+
48
+ def current_api_key
49
+ token = configuration.api_key
50
+ token.respond_to?(:call) ? token.call : token
51
+ end
52
+
53
+ def handle_response(response)
54
+ case response.status
55
+ when 200..299
56
+ response
57
+ else
58
+ handle_error_response(response)
59
+ end
60
+ end
61
+
62
+ def handle_error_response(response)
63
+ raise APIError.new(
64
+ "Request failed",
65
+ code: response.status,
66
+ response_body: response.body,
67
+ response_headers: response.headers
68
+ )
69
+ end
70
+
71
+ def handle_connection_error(exception)
72
+ raise APIError.new("Connection failed: #{exception.message}")
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo::Core
4
+ class Configuration
5
+ attr_accessor :base_url, :api_key
6
+ attr_reader :faraday_configuration_block
7
+
8
+ def initialize(base_url: nil, api_key: nil)
9
+ @base_url = base_url
10
+ @api_key = api_key
11
+ @faraday_configuration_block = nil
12
+ end
13
+
14
+ def configure_faraday_connection(&block)
15
+ @faraday_configuration_block = block
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "active_support/concern"
5
+ require "date"
6
+
7
+ module Helo::Core
8
+ module Model
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ include ActiveModel::AttributeAssignment
13
+ include ActiveModel::Attributes
14
+ include ActiveModel::Serializers::JSON
15
+
16
+ class_attribute :_api_attributes, default: {}
17
+
18
+ def initialize(attributes = {})
19
+ super()
20
+ assign_attributes(attributes) if attributes
21
+ end
22
+
23
+ # Serialize to hash for API requests (camelCase keys)
24
+ def to_params
25
+ result = {}
26
+ self.class._api_attributes.each do |attr_name, meta|
27
+ value = attributes[attr_name.to_s]
28
+
29
+ if value.nil?
30
+ next unless meta[:nullable] && @_explicitly_set&.include?(attr_name)
31
+ end
32
+
33
+ result[meta[:json_key]] = serialize_for_api(value, meta)
34
+ end
35
+ result
36
+ end
37
+
38
+ def to_h
39
+ attributes.to_h do |key, value|
40
+ [ key.to_sym, deep_to_h(value) ]
41
+ end
42
+ end
43
+
44
+ def slice(*keys)
45
+ attributes.symbolize_keys.slice(*keys.map(&:to_sym))
46
+ end
47
+
48
+ def [](key)
49
+ attributes[key.to_s]
50
+ end
51
+
52
+ def []=(key, value)
53
+ assign_attributes(key => value)
54
+ end
55
+
56
+ def inspect
57
+ attrs = attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
58
+ "#<#{self.class.name} #{attrs}>"
59
+ end
60
+
61
+ private
62
+
63
+ def _write_attribute(attr_name, value)
64
+ @_explicitly_set ||= Set.new
65
+ @_explicitly_set << attr_name.to_sym
66
+ super
67
+ end
68
+
69
+ def deep_to_h(value)
70
+ if value.nil?
71
+ nil
72
+ elsif value.is_a?(Array)
73
+ value.map { |item| deep_to_h(item) }
74
+ elsif value.respond_to?(:to_h)
75
+ value.to_h
76
+ else
77
+ value
78
+ end
79
+ end
80
+
81
+ def serialize_for_api(value, meta = {})
82
+ return value if value.nil?
83
+
84
+ case value
85
+ when Array
86
+ serialized = value.map { |item| serialize_for_api(item) }
87
+ meta[:explode] == false ? serialized.join(",") : serialized
88
+ when Time, DateTime
89
+ value.utc.iso8601
90
+ when Date
91
+ value.to_s
92
+ else
93
+ value.respond_to?(:to_params) ? value.to_params : value
94
+ end
95
+ end
96
+ end
97
+
98
+ class_methods do
99
+ def new(input = {})
100
+ return input if input.is_a?(self)
101
+ super(input)
102
+ end
103
+
104
+ def api_attribute(name, type = nil, key:, nullable: false, explode: true)
105
+ attribute(name, type)
106
+
107
+ self._api_attributes[name] = {
108
+ json_key: key,
109
+ nullable: nullable,
110
+ explode: explode
111
+ }
112
+ end
113
+
114
+ def from_hash(hash)
115
+ return if hash.nil?
116
+
117
+ hash = hash.transform_keys(&:to_sym)
118
+
119
+ attributes = _api_attributes.to_h do |attr_name, meta|
120
+ json_key = meta.fetch(:json_key).to_sym
121
+ value = hash[json_key] || hash[attr_name]
122
+ [ attr_name, value ]
123
+ end
124
+
125
+ new(attributes)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module Helo::Core
6
+ # Custom ActiveModel type for nested model objects
7
+ class ModelType < ActiveModel::Type::Value
8
+ def initialize(model_class_name)
9
+ @model_class_name = model_class_name
10
+ super()
11
+ end
12
+
13
+ def cast(value)
14
+ return if value.nil?
15
+
16
+ case value
17
+ when model_class
18
+ value
19
+ when Hash
20
+ model_class.from_hash(value)
21
+ else
22
+ raise ArgumentError, "Expected #{@model_class_name}, got #{value.class}"
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def model_class
29
+ @_model_class ||= @model_class_name.constantize
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helo
4
+ class AttachmentDisposition
5
+ ATTACHMENT = "attachment"
6
+ INLINE = "inline"
7
+
8
+ def self.all
9
+ [ATTACHMENT, INLINE].freeze
10
+ end
11
+ end
12
+ end