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