action_webhook 0.1.1 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54d0eddc17626872f1d795b8c25c4fda06719a1bfed4b185093c8803d442d2ad
4
- data.tar.gz: ec2b67113640e9b1d7e4ddb397858a4d38724d01024f781b492906ed629dfc62
3
+ metadata.gz: 2063e93683fe64ce519f241843f17e33da9f29d671b965af97c0f97540a98ecc
4
+ data.tar.gz: 1f591bf9cfb727d9ab6e750338b708e4e24df2e782ea3f1a5096277f4311474d
5
5
  SHA512:
6
- metadata.gz: 5635f2e1541ecf586c52a37df1fb03b6e11e71585255cdbef8a01be30323e1d7a53f7f0ab053ff98dd7a8f3bc6e4e5d95dfbd1b0c1b86b3238577aa13bde0459
7
- data.tar.gz: e97e0940c7bf16f20d4c6457d115a2054494af9c17fd9957c3f5537774a45374ee1c395a228de33689edac80ba9f3e3b367e82baa2a972b178b3ac9df5ac719e
6
+ metadata.gz: 14ed8242d705d62f573fd884e83bc70479ed4397c4bb3fffd75fd178bdc929fe5d981c395abb99ab7d647c7f1aec3aeaf1685784b3d521e3e62ed2e073ee8c1c
7
+ data.tar.gz: 45c7362968926262d79f6dedb16f2391d30a2b64980e3a5d0116558360ee63fe416b54f33a17012b82e5b50d8a465a58d5f91e1b6fa1f05e185959a2ce547230
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module ActionWebhook
4
2
  # Base class for defining and delivering webhooks
5
3
  #
@@ -24,11 +22,33 @@ module ActionWebhook
24
22
  # # Send immediately
25
23
  # UserWebhook.created(user).deliver_now
26
24
  #
27
- # # Send in background
25
+ # # Send in background (uses default queue)
28
26
  # UserWebhook.created(user).deliver_later
29
27
  #
28
+ # # Send in background with specific queue
29
+ # UserWebhook.created(user).deliver_later(queue: 'webhooks')
30
+ #
31
+ # # Send in background with delay
32
+ # UserWebhook.created(user).deliver_later(wait: 5.minutes)
33
+ #
34
+ # # Send in background with specific queue and delay
35
+ # UserWebhook.created(user).deliver_later(queue: 'webhooks', wait: 10.minutes)
36
+ #
37
+ # You can also configure the default queue at the class level:
38
+ #
39
+ # class UserWebhook < ActionWebhook::Base
40
+ # self.deliver_later_queue_name = 'webhooks'
41
+ #
42
+ # def created(user)
43
+ # @user = user
44
+ # endpoints = WebhookSubscription.where(event: 'user.created').map do |sub|
45
+ # { url: sub.url, headers: { 'Authorization' => "Bearer #{sub.token}" } }
46
+ # end
47
+ # deliver(endpoints)
48
+ # end
49
+ # end
50
+ #
30
51
  class Base
31
- # Add these lines near the top of your class
32
52
  include GlobalID::Identification if defined?(GlobalID)
33
53
  include ActiveJob::SerializationAdapter::ObjectSerializer if defined?(ActiveJob::SerializationAdapter)
34
54
 
@@ -42,249 +62,101 @@ module ActionWebhook
42
62
  # Retry configuration
43
63
  class_attribute :max_retries, instance_writer: false, default: 3
44
64
  class_attribute :retry_delay, instance_writer: false, default: 30.seconds
45
- class_attribute :retry_backoff, instance_writer: false, default: :exponential # :linear or :exponential
65
+ class_attribute :retry_backoff, instance_writer: false, default: :exponential
46
66
  class_attribute :retry_jitter, instance_writer: false, default: 5.seconds
47
67
 
48
68
  # Callbacks
49
69
  class_attribute :after_deliver_callback, instance_writer: false
50
70
  class_attribute :after_retries_exhausted_callback, instance_writer: false
51
71
 
52
- # The webhook action that will be performed
53
- attr_accessor :action_name
54
-
55
- # The webhook details (URLs and headers) that will be used
56
- attr_accessor :webhook_details
57
-
58
- # Stores the execution params for later delivery
59
- attr_accessor :params
60
-
61
- # Current attempt number for retries
62
- attr_accessor :attempts
72
+ attr_accessor :action_name, :webhook_details, :params, :attempts
63
73
 
64
- # Creates a new webhook and initializes its settings
65
74
  def initialize
66
75
  @_webhook_message = {}
67
76
  @_webhook_defaults = {}
68
77
  @attempts = 0
69
78
  end
70
79
 
71
- # Synchronously delivers the webhook
72
80
  def deliver_now
73
81
  @attempts += 1
74
82
  response = process_webhook
75
83
 
76
- # Call success callback if defined
77
- if response.all? { |r| r[:success] }
78
- invoke_callback(self.class.after_deliver_callback, response)
79
- elsif @attempts < self.class.max_retries
80
- # Schedule a retry with backoff
81
- retry_with_backoff
82
- else
83
- # We've exhausted all retries
84
- invoke_callback(self.class.after_retries_exhausted_callback, response)
84
+ # Separate successful and failed responses
85
+ successful_responses = response.select { |r| r[:success] }
86
+ failed_responses = response.reject { |r| r[:success] }
87
+
88
+ # Invoke success callback for successful deliveries
89
+ invoke_callback(self.class.after_deliver_callback, successful_responses) if successful_responses.any?
90
+
91
+ # Handle failed responses
92
+ if failed_responses.any? && @attempts < self.class.max_retries
93
+ # Extract failed webhook details for retry
94
+ failed_webhook_details = failed_responses.map { |r| @webhook_details.find { |detail| detail[:url] == r[:url] } }.compact
95
+ retry_with_backoff(failed_webhook_details)
96
+ elsif failed_responses.any?
97
+ # All retries exhausted for failed URLs
98
+ invoke_callback(self.class.after_retries_exhausted_callback, failed_responses)
85
99
  end
86
100
 
87
101
  response
88
102
  end
89
103
 
90
- # Helper method to invoke a callback that might be a symbol or a proc
91
- def invoke_callback(callback, response)
92
- return unless callback
93
-
94
- case callback
95
- when Symbol
96
- send(callback, response)
97
- when Proc
98
- callback.call(self, response)
99
- end
100
- end
101
-
102
- # Enqueues the webhook delivery via ActiveJob
103
104
  def deliver_later(options = {})
104
105
  enqueue_delivery(:deliver_now, options)
105
106
  end
106
107
 
107
- # Prepares the webhook for delivery using the current method as template name
108
- # and instance variables as template data
109
- #
110
- # @param webhook_details [Array<Hash>] Array of hashes with :url and :headers keys
111
- # @param params [Hash] Optional parameters to store for later delivery
112
- # @return [DeliveryMessenger] A messenger object for further delivery options
113
108
  def deliver(webhook_details, params = {})
114
- # Determine action name from the caller
115
109
  @action_name = caller_locations(1, 1)[0].label.to_sym
116
110
  @webhook_details = webhook_details
117
111
  @params = params
118
112
 
119
- # Return self for chaining with delivery methods
120
113
  DeliveryMessenger.new(self)
121
114
  end
122
115
 
123
- # Renders a template based on the action name
124
- #
125
- # @param variables [Hash] Optional variables to add to template context
126
- # @return [Hash] The JSON payload
127
116
  def build_payload(variables = {})
128
- # Combine instance variables and passed variables
129
- assigns = {}
130
-
131
- # Extract instance variables
132
- instance_variables.each do |ivar|
133
- # Skip internal instance variables
134
- next if ivar.to_s.start_with?("@_")
135
- next if %i[@action_name @webhook_details @params @attempts].include?(ivar)
136
-
137
- # Add to assigns with symbol key (without @)
138
- assigns[ivar.to_s[1..].to_sym] = instance_variable_get(ivar)
139
- end
140
-
141
- # Add passed variables
117
+ assigns = extract_instance_variables
142
118
  assigns.merge!(variables)
143
-
144
- # Render the template
145
119
  generate_json_from_template(@action_name, assigns)
146
120
  end
147
121
 
148
- # Posts payload(s) to the given webhook endpoints
149
- #
150
- # @param webhook_details [Array<Hash>] An array of hashes containing `url` and `headers`
151
- # @param payloads [Array<Hash>] One payload for each webhook
152
- # @return [Array<Hash>] Array of response objects with status and body
153
- def post_webhook(webhook_details, payloads)
122
+ def post_webhook(webhook_details, payload)
154
123
  responses = []
155
124
 
156
- webhook_details.each_with_index do |detail, idx|
157
- # Ensure headers exists
125
+ webhook_details.each do |detail|
158
126
  detail[:headers] ||= {}
127
+ headers = build_headers(detail[:headers])
159
128
 
160
- # Merge default headers
161
- headers = default_headers.merge(detail[:headers])
162
-
163
- # Add content type if not present
164
- headers["Content-Type"] = "application/json" unless headers.key?("Content-Type")
165
-
166
- # Add attempt tracking in headers
167
- headers["X-Webhook-Attempt"] = @attempts.to_s if @attempts.positive?
168
-
169
- response = HTTParty.post(
170
- detail[:url],
171
- body: payloads[idx].to_json,
172
- headers: headers,
173
- timeout: 10 # Add reasonable timeout
174
- )
175
-
176
- responses << {
177
- success: response.success?,
178
- status: response.code,
179
- body: response.body,
180
- url: detail[:url],
181
- attempt: @attempts
182
- }
129
+ response = send_webhook_request(detail[:url], payload, headers)
130
+ responses << build_response_hash(response, detail[:url])
131
+ log_webhook_result(response, detail[:url])
183
132
  rescue StandardError => e
184
- responses << {
185
- success: false,
186
- error: e.message,
187
- url: detail[:url],
188
- attempt: @attempts
189
- }
190
- logger.error("Webhook delivery failed: #{e.message} for URL: #{detail[:url]} (Attempt #{@attempts})")
133
+ responses << build_error_response_hash(e, detail[:url])
134
+ log_webhook_error(e, detail[:url])
191
135
  end
192
136
 
193
137
  responses
194
138
  end
195
139
 
196
- # Renders a JSON payload from a `.json.erb` template
197
- #
198
- # @param event_name [Symbol] the name of the webhook method (e.g. `:created`)
199
- # @param assigns [Hash] local variables to pass into the template
200
- # @return [Hash] the parsed JSON payload
201
140
  def generate_json_from_template(input_event_name, assigns = {})
202
141
  event_name = extract_method_name(input_event_name.to_s)
203
- webhook_class_name = self.class.name.underscore
204
-
205
- # Possible template locations
206
- possible_paths = [
207
- # Main app templates
208
- File.join(Rails.root.to_s, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb"),
209
-
210
- # Engine templates
211
- engine_root && File.join(engine_root, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb"),
212
-
213
- # Namespaced templates in engine
214
- engine_root && self.class.module_parent != Object &&
215
- File.join(engine_root,
216
- "app/webhooks/#{self.class.module_parent.name.underscore}/#{webhook_class_name.split("/").last}/#{event_name}.json.erb")
217
- ].compact
218
-
219
- # Find the first template that exists
220
- template_path = possible_paths.find { |path| File.exist?(path) }
142
+ template_path = find_template_path(event_name)
221
143
 
222
- unless template_path
223
- raise ArgumentError, "Template not found for #{event_name} in paths:\n#{possible_paths.join("\n")}"
224
- end
144
+ raise ArgumentError, "Template not found for #{event_name}" unless template_path
225
145
 
226
- template = ERB.new(File.read(template_path))
227
- json = template.result_with_hash(assigns)
228
- JSON.parse(json)
146
+ render_json_template(template_path, assigns)
229
147
  rescue JSON::ParserError => e
230
- raise "Invalid JSON in template #{event_name}: #{e.message}"
148
+ raise ArgumentError, "Invalid JSON in template #{event_name}: #{e.message}"
231
149
  end
232
150
 
233
151
  def extract_method_name(path)
234
152
  path.include?("#") ? path.split("#").last : path
235
153
  end
236
154
 
237
- # Process the webhook to generate and send the payload
238
155
  def process_webhook
239
- # Render the message
240
- payloads = [build_payload]
241
-
242
- # Post the webhook
243
- post_webhook(webhook_details, payloads)
156
+ payload = build_payload
157
+ post_webhook(webhook_details, payload)
244
158
  end
245
159
 
246
- # Schedule a retry with appropriate backoff delay
247
- # Modify the retry_with_backoff method:
248
- def retry_with_backoff
249
- delay = calculate_backoff_delay
250
-
251
- logger.info("Scheduling webhook retry #{@attempts + 1}/#{self.class.max_retries} in #{delay} seconds")
252
-
253
- # Get the actual job class by evaluating the proc
254
- job_class = self.class.delivery_job.is_a?(Proc) ? self.class.delivery_job.call : self.class.delivery_job
255
-
256
- # Serialize the webhook and pass the serialized data instead of the object
257
- serialized_webhook = serialize
258
-
259
- # Re-enqueue with the calculated delay
260
- if deliver_later_queue_name
261
- job_class.set(queue: deliver_later_queue_name, wait: delay).perform_later("deliver_now", serialized_webhook)
262
- else
263
- job_class.set(wait: delay).perform_later("deliver_now", serialized_webhook)
264
- end
265
- end
266
-
267
- # Calculate delay based on retry strategy
268
- def calculate_backoff_delay
269
- base_delay = self.class.retry_delay
270
-
271
- delay = case self.class.retry_backoff
272
- when :exponential
273
- # 30s, 60s, 120s, etc.
274
- base_delay * (2**(@attempts - 1))
275
- when :linear
276
- # 30s, 60s, 90s, etc.
277
- base_delay * @attempts
278
- else
279
- base_delay
280
- end
281
-
282
- # Add jitter to prevent thundering herd problem
283
- jitter = rand(self.class.retry_jitter)
284
- delay + jitter
285
- end
286
-
287
- # For ActiveJob serialization
288
160
  def serialize
289
161
  {
290
162
  "action_name" => @action_name.to_s,
@@ -296,143 +168,283 @@ module ActionWebhook
296
168
  }
297
169
  end
298
170
 
299
- # Restores state from serialized data
300
171
  def deserialize(data)
301
172
  @action_name = data["action_name"].to_sym
302
173
  @webhook_details = data["webhook_details"]
303
174
  @params = data["params"]
304
175
  @attempts = data["attempts"] || 0
305
176
 
306
- # Restore instance variables
307
- data["instance_variables"].each do |name, value|
308
- instance_variable_set("@#{name}", value)
177
+ restore_instance_variables(data["instance_variables"])
178
+ end
179
+
180
+ class << self
181
+ def method_missing(method_name, *args, &block)
182
+ if public_instance_methods(false).include?(method_name)
183
+ webhook = new
184
+ webhook.send(method_name, *args, &block)
185
+ else
186
+ super
187
+ end
188
+ end
189
+
190
+ def respond_to_missing?(method_name, include_private = false)
191
+ public_instance_methods(false).include?(method_name) || super
192
+ end
193
+
194
+ def deliveries
195
+ @deliveries ||= []
196
+ end
197
+
198
+ def clear_deliveries
199
+ @deliveries = []
200
+ end
201
+
202
+ def after_deliver(method_name = nil, &block)
203
+ self.after_deliver_callback = block_given? ? block : method_name
204
+ end
205
+
206
+ def after_retries_exhausted(method_name = nil, &block)
207
+ self.after_retries_exhausted_callback = block_given? ? block : method_name
309
208
  end
310
209
  end
311
210
 
312
211
  private
313
212
 
314
- # Collects all non-system instance variables for serialization
315
- def collect_instance_variables
316
- result = {}
213
+ EXCLUDED_INSTANCE_VARIABLES = %w[@action_name @webhook_details @params @attempts].freeze
214
+
215
+ def invoke_callback(callback, response)
216
+ return unless callback
217
+
218
+ case callback
219
+ when Symbol
220
+ send(callback, response)
221
+ when Proc
222
+ callback.call(self, response)
223
+ end
224
+ end
225
+
226
+ def extract_instance_variables
227
+ assigns = {}
317
228
  instance_variables.each do |ivar|
318
- # Skip internal instance variables
319
229
  next if ivar.to_s.start_with?("@_")
320
- next if %i[@action_name @webhook_details @params @attempts].include?(ivar)
230
+ next if EXCLUDED_INSTANCE_VARIABLES.include?(ivar.to_s)
321
231
 
322
- # Add to result without @
323
- result[ivar.to_s[1..]] = instance_variable_get(ivar)
232
+ assigns[ivar.to_s[1..].to_sym] = instance_variable_get(ivar)
324
233
  end
325
- result
234
+ assigns
326
235
  end
327
236
 
328
- # Find the engine root path
329
- def engine_root
330
- return nil unless defined?(Rails::Engine)
237
+ def build_headers(detail_headers)
238
+ headers = default_headers.merge(detail_headers)
239
+ headers["Content-Type"] = "application/json" unless headers.key?("Content-Type")
240
+ headers["X-Webhook-Attempt"] = @attempts.to_s if @attempts.positive?
241
+ headers
242
+ end
331
243
 
332
- mod = self.class.module_parent
333
- while mod != Object
334
- constants = mod.constants.select do |c|
335
- const = mod.const_get(c)
336
- const.is_a?(Class) && const < Rails::Engine
337
- end
244
+ def send_webhook_request(url, payload, headers)
245
+ HTTParty.post(url, body: payload.to_json, headers: headers, timeout: 10)
246
+ end
338
247
 
339
- return mod.const_get(constants.first).root.to_s if constants.any?
248
+ def build_response_hash(response, url)
249
+ {
250
+ success: response.success?,
251
+ status: response.code,
252
+ body: response.body,
253
+ url: url,
254
+ attempt: @attempts
255
+ }
256
+ end
340
257
 
341
- mod = mod.module_parent
258
+ def build_error_response_hash(error, url)
259
+ {
260
+ success: false,
261
+ error: error.message,
262
+ url: url,
263
+ attempt: @attempts
264
+ }
265
+ end
266
+
267
+ def log_webhook_result(response, url)
268
+ if response.success?
269
+ logger.info("Webhook delivered successfully: #{url} (Status: #{response.code}, Attempt: #{@attempts})")
270
+ else
271
+ logger.warn("Webhook delivery failed with HTTP error: #{url} (Status: #{response.code}, Attempt: #{@attempts})")
342
272
  end
273
+ end
343
274
 
344
- nil
275
+ def log_webhook_error(error, url)
276
+ logger.error("Webhook delivery failed: #{error.message} for URL: #{url} (Attempt: #{@attempts})")
345
277
  end
346
278
 
347
- # Similarly update enqueue_delivery:
348
- def enqueue_delivery(delivery_method, options = {})
349
- options = options.dup
350
- queue = options.delete(:queue) || self.class.deliver_later_queue_name
279
+ def find_template_path(event_name)
280
+ webhook_class_name = self.class.name.underscore
281
+ possible_paths = build_template_paths(webhook_class_name, event_name)
282
+ possible_paths.find { |path| File.exist?(path) }
283
+ end
351
284
 
352
- args = [delivery_method.to_s]
285
+ def build_template_paths(webhook_class_name, event_name)
286
+ [
287
+ File.join(Rails.root.to_s, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb"),
288
+ engine_template_path(webhook_class_name, event_name),
289
+ namespaced_engine_template_path(webhook_class_name, event_name)
290
+ ].compact
291
+ end
353
292
 
354
- # Get the actual job class by evaluating the proc
355
- job_class = self.class.delivery_job.is_a?(Proc) ? self.class.delivery_job.call : self.class.delivery_job
293
+ def engine_template_path(webhook_class_name, event_name)
294
+ return unless engine_root
356
295
 
357
- # Serialize the webhook
358
- serialized_webhook = serialize
296
+ File.join(engine_root, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb")
297
+ end
359
298
 
360
- # Use the delivery job to perform the delivery with serialized data
361
- if queue
362
- job_class.set(queue: queue).perform_later(*args, serialized_webhook)
363
- else
364
- job_class.set(wait: options[:wait]).perform_later(*args, serialized_webhook) if options[:wait]
365
- job_class.perform_later(*args, serialized_webhook) unless options[:wait]
366
- end
299
+ def namespaced_engine_template_path(webhook_class_name, event_name)
300
+ return unless engine_root && self.class.module_parent != Object
301
+
302
+ parent_name = self.class.module_parent.name.underscore
303
+ class_name = webhook_class_name.split("/").last
304
+ File.join(engine_root, "app/webhooks/#{parent_name}/#{class_name}/#{event_name}.json.erb")
367
305
  end
368
306
 
369
- # Logger for webhook errors
370
- def logger
371
- Rails.logger
307
+ def render_json_template(template_path, assigns)
308
+ template = ERB.new(File.read(template_path))
309
+ json = template.result_with_hash(assigns)
310
+ JSON.parse(json)
372
311
  end
373
312
 
374
- # Class methods
375
- class << self
376
- # Handle method calls on the class
377
- def method_missing(method_name, *args, &block)
378
- if public_instance_methods(false).include?(method_name)
379
- # Create a new instance
380
- webhook = new
313
+ def collect_instance_variables
314
+ result = {}
315
+ instance_variables.each do |ivar|
316
+ next if ivar.to_s.start_with?("@_")
317
+ next if EXCLUDED_INSTANCE_VARIABLES.include?(ivar.to_s)
381
318
 
382
- # Call the instance method
383
- webhook.send(method_name, *args, &block)
384
- else
385
- super
386
- end
319
+ result[ivar.to_s[1..]] = instance_variable_get(ivar)
387
320
  end
321
+ result
322
+ end
388
323
 
389
- def respond_to_missing?(method_name, include_private = false)
390
- public_instance_methods(false).include?(method_name) || super
324
+ def restore_instance_variables(variables)
325
+ variables.each do |name, value|
326
+ instance_variable_set("@#{name}", value)
391
327
  end
328
+ end
392
329
 
393
- # Test delivery collection
394
- def deliveries
395
- @deliveries ||= []
330
+ def retry_with_backoff(failed_webhook_details = nil)
331
+ # Use failed webhook details if provided, otherwise retry all
332
+ retry_details = failed_webhook_details || @webhook_details
333
+
334
+ delay = calculate_backoff_delay
335
+ logger.info("Scheduling webhook retry #{@attempts + 1}/#{self.class.max_retries} for #{retry_details.size} URLs in #{delay} seconds")
336
+
337
+ job_class = resolve_job_class
338
+ serialized_webhook = serialize
339
+
340
+ # Update the webhook details to only include failed URLs
341
+ serialized_webhook["webhook_details"] = retry_details
342
+
343
+ enqueue_retry_job(job_class, serialized_webhook, delay)
344
+ end
345
+
346
+ def calculate_backoff_delay
347
+ base_delay = self.class.retry_delay
348
+ multiplier = case self.class.retry_backoff
349
+ when :exponential
350
+ 2**(@attempts - 1)
351
+ when :linear
352
+ @attempts
353
+ else
354
+ 1
355
+ end
356
+
357
+ base_delay * multiplier + rand(self.class.retry_jitter)
358
+ end
359
+
360
+ def resolve_job_class
361
+ self.class.delivery_job.is_a?(Proc) ? self.class.delivery_job.call : self.class.delivery_job
362
+ end
363
+
364
+ def enqueue_retry_job(job_class, serialized_webhook, delay)
365
+ if deliver_later_queue_name
366
+ job_class.set(queue: deliver_later_queue_name, wait: delay).perform_later("deliver_now", serialized_webhook)
367
+ else
368
+ job_class.set(wait: delay).perform_later("deliver_now", serialized_webhook)
396
369
  end
370
+ end
397
371
 
398
- # Reset the test delivery collection
399
- def clear_deliveries
400
- @deliveries = []
372
+ def engine_root
373
+ return nil unless defined?(Rails::Engine)
374
+
375
+ find_engine_root_for_module(self.class.module_parent)
376
+ end
377
+
378
+ def find_engine_root_for_module(mod)
379
+ while mod != Object
380
+ engine_constant = find_engine_constant(mod)
381
+ return mod.const_get(engine_constant).root.to_s if engine_constant
382
+
383
+ mod = mod.module_parent
401
384
  end
402
385
 
403
- # Register callback for successful delivery
404
- def after_deliver(method_name = nil, &block)
405
- self.after_deliver_callback = block_given? ? block : method_name
386
+ nil
387
+ end
388
+
389
+ def find_engine_constant(mod)
390
+ mod.constants.find do |c|
391
+ const = mod.const_get(c)
392
+ const.is_a?(Class) && const < Rails::Engine
406
393
  end
394
+ end
407
395
 
408
- # Register callback for when retries are exhausted
409
- def after_retries_exhausted(method_name = nil, &block)
410
- self.after_retries_exhausted_callback = block_given? ? block : method_name
396
+ def enqueue_delivery(delivery_method, options = {})
397
+ options = options.dup
398
+ queue = options.delete(:queue) || self.class.deliver_later_queue_name
399
+ job_class = resolve_job_class
400
+ serialized_webhook = serialize
401
+
402
+ enqueue_job(job_class, delivery_method.to_s, serialized_webhook, queue, options)
403
+ end
404
+
405
+ def enqueue_job(job_class, delivery_method, serialized_webhook, queue, options)
406
+ if queue
407
+ job_class.set(queue: queue).perform_later(delivery_method, serialized_webhook)
408
+ elsif options[:wait]
409
+ job_class.set(wait: options[:wait]).perform_later(delivery_method, serialized_webhook)
410
+ else
411
+ job_class.perform_later(delivery_method, serialized_webhook)
411
412
  end
412
413
  end
414
+
415
+ def logger
416
+ Rails.logger
417
+ end
413
418
  end
414
419
 
415
- # Delivery messenger for ActionMailer-like API
416
420
  class DeliveryMessenger
417
421
  def initialize(webhook)
418
422
  @webhook = webhook
419
423
  end
420
424
 
421
425
  def deliver_now
422
- if @webhook.respond_to?(:perform_deliveries) && !@webhook.perform_deliveries
423
- # Skip delivery
424
- nil
425
- elsif @webhook.class.delivery_method == :test
426
- # Test delivery
427
- ActionWebhook::Base.deliveries << @webhook
428
- else
429
- # Normal delivery
430
- @webhook.deliver_now
431
- end
426
+ return nil if skip_delivery?
427
+ return test_delivery if test_mode?
428
+
429
+ @webhook.deliver_now
432
430
  end
433
431
 
434
432
  def deliver_later(options = {})
435
433
  @webhook.deliver_later(options)
436
434
  end
435
+
436
+ private
437
+
438
+ def skip_delivery?
439
+ @webhook.respond_to?(:perform_deliveries) && !@webhook.perform_deliveries
440
+ end
441
+
442
+ def test_mode?
443
+ @webhook.class.delivery_method == :test
444
+ end
445
+
446
+ def test_delivery
447
+ ActionWebhook::Base.deliveries << @webhook
448
+ end
437
449
  end
438
450
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionWebhook
4
- VERSION = "0.1.1"
4
+ VERSION = "1.1.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: 0.1.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vinay Uttam Vemparala
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-27 00:00:00.000000000 Z
10
+ date: 2025-06-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: httparty
@@ -24,33 +24,53 @@ dependencies:
24
24
  - !ruby/object:Gem::Version
25
25
  version: 0.18.1
26
26
  - !ruby/object:Gem::Dependency
27
- name: rails
27
+ name: activejob
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '6.0'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '8.0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '6.0'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '8.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: globalid
28
48
  requirement: !ruby/object:Gem::Requirement
29
49
  requirements:
30
50
  - - "~>"
31
51
  - !ruby/object:Gem::Version
32
- version: '7'
52
+ version: '1.0'
33
53
  type: :runtime
34
54
  prerelease: false
35
55
  version_requirements: !ruby/object:Gem::Requirement
36
56
  requirements:
37
57
  - - "~>"
38
58
  - !ruby/object:Gem::Version
39
- version: '7'
59
+ version: '1.0'
40
60
  - !ruby/object:Gem::Dependency
41
61
  name: yard
42
62
  requirement: !ruby/object:Gem::Requirement
43
63
  requirements:
44
- - - ">="
64
+ - - "~>"
45
65
  - !ruby/object:Gem::Version
46
- version: '0'
66
+ version: '0.9'
47
67
  type: :development
48
68
  prerelease: false
49
69
  version_requirements: !ruby/object:Gem::Requirement
50
70
  requirements:
51
- - - ">="
71
+ - - "~>"
52
72
  - !ruby/object:Gem::Version
53
- version: '0'
73
+ version: '0.9'
54
74
  description: A Rails library for triggering webhooks. Inspired by ActionMailer from
55
75
  Rails
56
76
  email:
@@ -63,13 +83,13 @@ files:
63
83
  - lib/action_webhook/base.rb
64
84
  - lib/action_webhook/delivery_job.rb
65
85
  - lib/action_webhook/version.rb
66
- homepage: https://github.com/vinayuttam/action_webhook.git
86
+ homepage: https://github.com/vinayuttam/action_webhook
67
87
  licenses:
68
88
  - MIT
69
89
  metadata:
70
- homepage_uri: https://github.com/vinayuttam/action_webhook.git
71
- source_code_uri: https://github.com/vinayuttam/action_webhook.git
72
- changelog_uri: https://github.com/vinayuttam/action_webhook.git/blob/main/CHANGELOG.md
90
+ homepage_uri: https://github.com/vinayuttam/action_webhook
91
+ source_code_uri: https://github.com/vinayuttam/action_webhook/tree/main
92
+ changelog_uri: https://github.com/vinayuttam/action_webhook/blob/main/CHANGELOG.md
73
93
  rdoc_options: []
74
94
  require_paths:
75
95
  - lib