action_webhook 1.0.0 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7077d23d4f22c68b2a33a9cc9ee6e6ad911d9c9ee23a2a861bb93837c69910e7
4
- data.tar.gz: 124749c8b109502308684d10e5a05791f99f35f26a0d01bdc1b3d11fd3ff43d3
3
+ metadata.gz: 5fd47bb2e72c2bd12feab3682031b00603f271279b435eeb56b5a4e7dba6bccb
4
+ data.tar.gz: a4e22834b739227926772b9b8d18f4dbc74c1d7f7fc06bcbc1174707a306897d
5
5
  SHA512:
6
- metadata.gz: c3be33f1a7a4528062882ea98e05caa9d61ef413bc72e4fba03788a7862e52b891f91897389e513f712fddaab7f7b5b68f4367b091f148df897a0548b25db556
7
- data.tar.gz: 41d39d74c5ce2854375b1397552b687d64d9f3ad991eade8b92627b2f5efed8744de3fa092fe72926a244e50244a4363174b1ba91ad537bc0f485c94552d8a5c
6
+ metadata.gz: dd8df71521ef095e6f52aad36daacb1e518ec400a0cb1aa8919568dbe240731097679f18933bd9d30406d17a02685ffd79570189067e84405c80968c834de015
7
+ data.tar.gz: f42e11c74f275f4206c992a9e96a6e3b37eb40ed4f53191dbbef2192c596a2f039c172bd9d888f33ae0c18b9dc504a25d40055f86fba847ba5188c4e578af672
@@ -4,6 +4,10 @@ module ActionWebhook
4
4
  # Subclass this and define webhook methods (e.g. `created`, `updated`) that
5
5
  # define instance variables and call deliver to send webhooks.
6
6
  #
7
+ # Headers can be provided in two formats:
8
+ # 1. Hash format: { 'Authorization' => 'Bearer token', 'Content-Type' => 'application/json' }
9
+ # 2. Array format: [{ 'key' => 'Authorization', 'value' => 'Bearer token' }, { 'key' => 'Content-Type', 'value' => 'application/json' }]
10
+ #
7
11
  # Example:
8
12
  #
9
13
  # class UserWebhook < ActionWebhook::Base
@@ -41,10 +45,26 @@ module ActionWebhook
41
45
  #
42
46
  # def created(user)
43
47
  # @user = user
44
- # endpoints = WebhookSubscription.where(event: 'user.created').map do |sub|
45
- # { url: sub.url, headers: { 'Authorization' => "Bearer #{sub.token}" } }
48
+ # # Headers can be provided as a hash
49
+ # endpoints_with_hash_headers = WebhookSubscription.where(event: 'user.created').map do |sub|
50
+ # {
51
+ # url: sub.url,
52
+ # headers: { 'Authorization' => "Bearer #{sub.token}", 'X-Custom-Header' => 'value' }
53
+ # }
46
54
  # end
47
- # deliver(endpoints)
55
+ #
56
+ # # Or headers can be provided as an array of key/value objects (useful for database storage)
57
+ # endpoints_with_array_headers = WebhookSubscription.where(event: 'user.created').map do |sub|
58
+ # {
59
+ # url: sub.url,
60
+ # headers: [
61
+ # { 'key' => 'Authorization', 'value' => "Bearer #{sub.token}" },
62
+ # { 'key' => 'X-Custom-Header', 'value' => 'value' }
63
+ # ]
64
+ # }
65
+ # end
66
+ #
67
+ # deliver(endpoints_with_hash_headers)
48
68
  # end
49
69
  # end
50
70
  #
@@ -81,12 +101,21 @@ module ActionWebhook
81
101
  @attempts += 1
82
102
  response = process_webhook
83
103
 
84
- if response.all? { |r| r[:success] }
85
- invoke_callback(self.class.after_deliver_callback, response)
86
- elsif @attempts < self.class.max_retries
87
- retry_with_backoff
88
- else
89
- invoke_callback(self.class.after_retries_exhausted_callback, response)
104
+ # Separate successful and failed responses
105
+ successful_responses = response.select { |r| r[:success] }
106
+ failed_responses = response.reject { |r| r[:success] }
107
+
108
+ # Invoke success callback for successful deliveries
109
+ invoke_callback(self.class.after_deliver_callback, successful_responses) if successful_responses.any?
110
+
111
+ # Handle failed responses
112
+ if failed_responses.any? && @attempts < self.class.max_retries
113
+ # Extract failed webhook details for retry
114
+ failed_webhook_details = failed_responses.map { |r| @webhook_details.find { |detail| detail[:url] == r[:url] } }.compact
115
+ retry_with_backoff(failed_webhook_details)
116
+ elsif failed_responses.any?
117
+ # All retries exhausted for failed URLs
118
+ invoke_callback(self.class.after_retries_exhausted_callback, failed_responses)
90
119
  end
91
120
 
92
121
  response
@@ -225,8 +254,51 @@ module ActionWebhook
225
254
  assigns
226
255
  end
227
256
 
257
+ # Builds HTTP headers for webhook requests
258
+ #
259
+ # Supports two input formats:
260
+ # 1. Hash format: { 'Authorization' => 'Bearer token', 'Content-Type' => 'application/json' }
261
+ # 2. Array format: [{ 'key' => 'Authorization', 'value' => 'Bearer token' }, { 'key' => 'Content-Type', 'value' => 'application/json' }]
262
+ #
263
+ # The array format is useful when storing headers in databases where you need
264
+ # structured data with separate key and value fields.
265
+ #
266
+ # @param detail_headers [Hash, Array, nil] Headers in hash or array format
267
+ # @return [Hash] Formatted headers hash ready for HTTP request
228
268
  def build_headers(detail_headers)
229
- headers = default_headers.merge(detail_headers)
269
+ # Handle both hash format and array format with key/value objects
270
+ processed_headers = case detail_headers
271
+ when Array
272
+ # Transform array of header hashes [{'key': 'value'}] into a single hash
273
+ detail_headers.each_with_object({}) do |header_item, acc|
274
+ next unless header_item.is_a?(Hash)
275
+
276
+ # Handle string keys
277
+ if header_item.key?('key') && header_item.key?('value')
278
+ key = header_item['key']
279
+ value = header_item['value']
280
+ acc[key.to_s] = value.to_s if key && value
281
+ # Handle symbol keys
282
+ elsif header_item.key?(:key) && header_item.key?(:value)
283
+ key = header_item[:key]
284
+ value = header_item[:value]
285
+ acc[key.to_s] = value.to_s if key && value
286
+ else
287
+ # Log warning for malformed header items
288
+ logger&.warn("Skipping malformed header item: #{header_item.inspect}")
289
+ end
290
+ end
291
+ when Hash
292
+ # Ensure all keys and values are strings for consistency
293
+ detail_headers.transform_keys(&:to_s).transform_values(&:to_s)
294
+ when NilClass
295
+ {}
296
+ else
297
+ logger&.warn("Unknown header format: #{detail_headers.class}. Expected Hash or Array.")
298
+ {}
299
+ end
300
+
301
+ headers = default_headers.merge(processed_headers)
230
302
  headers["Content-Type"] = "application/json" unless headers.key?("Content-Type")
231
303
  headers["X-Webhook-Attempt"] = @attempts.to_s if @attempts.positive?
232
304
  headers
@@ -318,13 +390,19 @@ module ActionWebhook
318
390
  end
319
391
  end
320
392
 
321
- def retry_with_backoff
393
+ def retry_with_backoff(failed_webhook_details = nil)
394
+ # Use failed webhook details if provided, otherwise retry all
395
+ retry_details = failed_webhook_details || @webhook_details
396
+
322
397
  delay = calculate_backoff_delay
323
- logger.info("Scheduling webhook retry #{@attempts + 1}/#{self.class.max_retries} in #{delay} seconds")
398
+ logger.info("Scheduling webhook retry #{@attempts + 1}/#{self.class.max_retries} for #{retry_details.size} URLs in #{delay} seconds")
324
399
 
325
400
  job_class = resolve_job_class
326
401
  serialized_webhook = serialize
327
402
 
403
+ # Update the webhook details to only include failed URLs
404
+ serialized_webhook["webhook_details"] = retry_details
405
+
328
406
  enqueue_retry_job(job_class, serialized_webhook, delay)
329
407
  end
330
408
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionWebhook
4
- VERSION = "1.0.0"
4
+ VERSION = "1.2.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_webhook
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vinay Uttam Vemparala
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-06-06 00:00:00.000000000 Z
10
+ date: 2025-07-30 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: httparty