action_hooks 0.1.0 → 0.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: 064ad39bd578ac07aaf40f9bbc3551a7f8134a8543666533c7346782ec38a7d2
4
- data.tar.gz: f1f6e53ed1cd91f80ebbea439c765aa7a7ec9cae4a7ff315f3230b51fb92a388
3
+ metadata.gz: 284686e350568c9e92b85744b29ad6eb114657ae8872464292020edd97ed32ae
4
+ data.tar.gz: 3f226eff09c69f48a0430bcedd6d8d6583ae4bf0c4cb359de9f13f833d4ce8e2
5
5
  SHA512:
6
- metadata.gz: 29b792245c0630a7542e2189baaf89724090400baed65d26931e7d3fd513bf6f38092868ea9e0e7010b0940dcaa23058367ecde6387394c25c8519aa4990cb59
7
- data.tar.gz: b166f6b4be57655cbab8449eba6ed16d9dfe54f8c0eb2449a6362ff24492dc4c7770391fc07da69c832b008e0d477f7b3b8475e1958daf2ebc287dfbd611a557
6
+ metadata.gz: 11209c21b103ca1e42b2970dc2d9bed285ac0b9b032824303a418d02da6850c1db36319aaf6f4fa30ee3b6eec7d815a066bd1ae091d9d033ad44dafcefdf88ca
7
+ data.tar.gz: 1efbaf2b0533019d20e3d00f52b348bfa3f11a44b9880fd9676136575ebcaab2ce563a1363b92661552706a1cba88350d6feb14b7d4a11fe97fa5365de7f6bd7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-03-02
4
+
5
+ - Refactored ActionHooks to operate purely as middleware. It now automatically mounts `POST /webhooks/:source` without needing any generated configuration route handling.
6
+ - Re-designed `rails g action_hooks:webhook` generator to create only Jobs by default. A new `--controller` flag is available for customized business logic needs.
7
+
3
8
  ## [0.1.0] - 2026-03-01
4
9
 
5
10
  - Initial release
data/README.md CHANGED
@@ -36,15 +36,15 @@ $ rails db:migrate
36
36
 
37
37
  ### 1. Configuration
38
38
 
39
- Configure your webhook sources in the generated initializer (`config/initializers/action_hooks.rb`). Each source represents a third-party service sending webhooks to your application.
39
+ Configure your webhook sources in the generated initializer (`config/initializers/action_hooks.rb`). Each source represents a third-party service sending webhooks to your application. ActionHooks automatically mounts a catch-all route `POST /webhooks/:source`, so configuring a source is enough to start receiving requests.
40
40
 
41
41
  ```ruby
42
42
  # config/initializers/action_hooks.rb
43
43
  ActionHooks.configure do |config|
44
44
  config.add_source(:stripe) do |source|
45
45
  # The ActiveJob worker class that will process the webhook
46
- source.worker = "StripeWebhookWorker"
47
-
46
+ source.worker = "StripeWebhookJob"
47
+
48
48
  # Lambda to verify the signature of the incoming request
49
49
  source.verify_signature = ->(request) do
50
50
  payload = request.body.read
@@ -53,60 +53,80 @@ ActionHooks.configure do |config|
53
53
  # Stripe::Webhook::Signature.verify_header(payload, sig_header, ENV['STRIPE_WEBHOOK_SECRET'])
54
54
  true
55
55
  end
56
-
57
- # Optional: Restrict incoming requests to specific IP addresses
58
- # source.allowed_ips = ["127.0.0.1", "10.0.0.1"]
56
+
57
+ # Optional: Restrict incoming requests to specific IP addresses/hosts
58
+ # source.allowed_hosts = ["127.0.0.1", "10.0.0.1"] # Also aliased as `allowed_ips`
59
59
  end
60
60
  end
61
61
  ```
62
62
 
63
- ### 2. Generating a Webhook Controller
63
+ ### 2. Generating a Job
64
64
 
65
- To create an endpoint for a configured source, use the webhook generator. Pass the name of the source as an argument:
65
+ By default, an incoming webhook is authenticated, persisted, and handed over to a background job. You can easily generate a job template for your source:
66
66
 
67
67
  ```bash
68
68
  $ rails generate action_hooks:webhook stripe
69
69
  ```
70
70
 
71
- This will:
72
- 1. Create a controller at `app/controllers/stripe_webhooks_controller.rb`.
73
- 2. Add a route to `config/routes.rb` (e.g., `post "webhooks/stripe", to: "stripe_webhooks#create"`).
74
-
75
- The generated controller includes `ActionHooks::WebhookControllerBehavior`, which handles everything from skipping CSRF verification, verifying the IP and signature, saving the request to the database, and enqueueing your worker.
76
-
77
- ### 3. Processing the Webhook
78
-
79
- Create the worker class that you specified in your configuration. The worker will receive the ID of the `ActionHooks::WebhookRequest` record.
71
+ This will create `app/jobs/stripe_webhook_job.rb`. The background job will receive the ID of the saved `ActionHooks::WebhookRequest` record:
80
72
 
81
73
  ```ruby
82
- # app/jobs/stripe_webhook_worker.rb
83
- class StripeWebhookWorker < ApplicationJob
74
+ # app/jobs/stripe_webhook_job.rb
75
+ class StripeWebhookJob < ApplicationJob
84
76
  queue_as :default
85
77
 
86
78
  def perform(webhook_request_id)
87
79
  webhook_request = ActionHooks::WebhookRequest.find(webhook_request_id)
88
-
89
- # Access the parsed JSON payload
90
80
  payload = webhook_request.payload
91
-
92
- # Process the payload...
93
- if payload['type'] == 'payment_intent.succeeded'
94
- # Do something
81
+
82
+ case payload["type"]
83
+ when "payment_intent.succeeded"
84
+ # handle payment
95
85
  end
96
-
97
- # Update the state of the webhook request when done
86
+
98
87
  webhook_request.processed!
99
88
  rescue => e
100
- # Mark as failed if something goes wrong
101
89
  webhook_request.failed!
102
90
  raise e
103
91
  end
104
92
  end
105
93
  ```
106
94
 
95
+ ### 3. Custom Controller (Optional)
96
+
97
+ If your webhook processing requires complex synchronous logic before placing the job into the queue, you can generate a custom controller using the `--controller` flag:
98
+
99
+ ```bash
100
+ $ rails generate action_hooks:webhook stripe --controller
101
+ ```
102
+
103
+ This generates:
104
+
105
+ - A job: `app/jobs/stripe_webhook_job.rb` (unless `--skip-job` is provided)
106
+ - A controller: `app/controllers/webhooks/stripe_controller.rb`
107
+ - A specific route mapping in `config/routes.rb`
108
+
109
+ Your custom controller will inherit from `ActionHooks::WebhookController`. The parent controller handles persistence, IP checks, and signature verification, while your controller can focus just on the business logic inside the `process_webhook` hook:
110
+
111
+ ```ruby
112
+ # app/controllers/webhooks/stripe_controller.rb
113
+ class Webhooks::StripeController < ActionHooks::WebhookController
114
+ private
115
+
116
+ def webhook_source_name
117
+ :stripe
118
+ end
119
+
120
+ def process_webhook(webhook_request)
121
+ # Business logic here.
122
+ # The default behavior inside `process_webhook` is to enqueue the configured background job.
123
+ end
124
+ end
125
+ ```
126
+
107
127
  ## Development
108
128
 
109
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
129
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
110
130
 
111
131
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
112
132
 
@@ -5,6 +5,8 @@ module ActionHooks
5
5
 
6
6
  class Source
7
7
  attr_accessor :name, :worker, :verify_signature, :allowed_ips
8
+ alias_method :allowed_hosts, :allowed_ips
9
+ alias_method :allowed_hosts=, :allowed_ips=
8
10
 
9
11
  def initialize(name)
10
12
  @name = name
@@ -5,5 +5,13 @@ require "rails/engine"
5
5
  module ActionHooks
6
6
  class Engine < ::Rails::Engine
7
7
  isolate_namespace ActionHooks
8
+
9
+ initializer "action_hooks.routes" do |app|
10
+ app.routes.append do
11
+ post "webhooks/:source",
12
+ to: "action_hooks/webhooks#create",
13
+ constraints: {source: /[a-z0-9_]+/}
14
+ end
15
+ end
8
16
  end
9
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionHooks
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -10,28 +10,21 @@ module ActionHooks
10
10
 
11
11
  before_action :verify_webhook_ip!
12
12
  before_action :verify_webhook_signature!
13
+ before_action :persist_webhook_request!
13
14
  end
14
15
 
15
- def create
16
- payload = parse_webhook_payload
16
+ private
17
17
 
18
- webhook_request = ActionHooks::WebhookRequest.create!(
18
+ def persist_webhook_request!
19
+ @webhook_request = ActionHooks::WebhookRequest.create!(
19
20
  source: webhook_source_name.to_s,
20
- payload: payload,
21
+ payload: parse_webhook_payload,
21
22
  state: :pending
22
23
  )
23
-
24
- worker_class = webhook_source_config.worker
25
- worker_class&.constantize&.perform_later(webhook_request.id)
26
-
27
- head :ok
28
24
  end
29
25
 
30
- private
31
-
32
26
  def webhook_source_name
33
- # To be overridden or inferred by the controller
34
- self.class.name.sub(/WebhooksController$/, "").underscore
27
+ raise NotImplementedError, "You must define `#webhook_source_name` in your webhook controller."
35
28
  end
36
29
 
37
30
  def webhook_source_config
@@ -1,7 +1,16 @@
1
1
  class CreateWebhookRequests < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
- create_table :webhook_requests, id: :uuid do |t|
4
- t.jsonb :payload, default: {}, null: false
3
+ <%-
4
+ json_type = (ActiveRecord::Base.connection.adapter_name.downcase == "postgresql") ? :jsonb : :json
5
+
6
+ supports_uuid = if ActiveRecord::Base.connection.respond_to?(:supports_uuid?)
7
+ ActiveRecord::Base.connection.supports_uuid? # PostgreSQL
8
+ else
9
+ false # Defaults for others like SQLite, typical MySQL setups unless explicitly configured
10
+ end
11
+ -%>
12
+ create_table :webhook_requests<%= supports_uuid ? ", id: :uuid" : "" %> do |t|
13
+ t.<%= json_type %> :payload, default: {}, null: false
5
14
  t.string :source, null: false
6
15
  t.integer :state, null: false, default: 0
7
16
 
@@ -1,9 +1,12 @@
1
- class <%= class_name %>WebhooksController < ApplicationController
2
- include ActionHooks::WebhookControllerBehavior
3
-
1
+ class Webhooks::<%= class_name %>Controller < ActionHooks::WebhookController
4
2
  private
5
3
 
6
4
  def webhook_source_name
7
5
  :<%= file_name %>
8
6
  end
7
+
8
+ def process_webhook(webhook_request)
9
+ # Business logic here.
10
+ # @webhook_request is also available as an instance variable.
11
+ end
9
12
  end
@@ -0,0 +1,19 @@
1
+ class <%= class_name %>WebhookJob < ApplicationJob
2
+ queue_as :default
3
+
4
+ def perform(webhook_request_id)
5
+ webhook_request = ActionHooks::WebhookRequest.find(webhook_request_id)
6
+ payload = webhook_request.payload
7
+
8
+ # TODO: handle payload
9
+ # case payload["type"]
10
+ # when "example.event"
11
+ # # handle event
12
+ # end
13
+
14
+ webhook_request.processed!
15
+ rescue => e
16
+ webhook_request.failed!
17
+ raise e
18
+ end
19
+ end
@@ -1,17 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails/generators"
2
4
 
3
5
  module ActionHooks
4
6
  module Generators
5
7
  class WebhookGenerator < ::Rails::Generators::NamedBase
6
8
  source_root File.expand_path("templates", __dir__)
7
- desc "Creates a webhook controller for a given source and adds its route."
9
+ desc "Creates a webhook job (and optionally a controller) for a given source."
10
+
11
+ class_option :controller, type: :boolean, default: false,
12
+ desc: "Also generate a custom controller for business logic"
13
+ class_option :skip_job, type: :boolean, default: false,
14
+ desc: "Skip job generation"
15
+
16
+ def create_job_file
17
+ return if options[:skip_job]
18
+ template "job.rb.erb", "app/jobs/#{file_name}_webhook_job.rb"
19
+ end
8
20
 
9
21
  def create_controller_file
10
- template "controller.rb.erb", "app/controllers/#{file_name}_webhooks_controller.rb"
22
+ return unless options[:controller]
23
+ template "controller.rb.erb", "app/controllers/webhooks/#{file_name}_controller.rb"
11
24
  end
12
25
 
13
26
  def add_route
14
- route %(post "webhooks/#{file_name}", to: "#{file_name}_webhooks#create")
27
+ return unless options[:controller]
28
+ route %(post "webhooks/#{file_name}", to: "webhooks/#{file_name}#create")
15
29
  end
16
30
  end
17
31
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_hooks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Poimtsev
@@ -127,6 +127,7 @@ files:
127
127
  - lib/generators/action_hooks/install/templates/action_hooks.rb
128
128
  - lib/generators/action_hooks/install/templates/create_webhook_requests.rb.erb
129
129
  - lib/generators/action_hooks/webhook/templates/controller.rb.erb
130
+ - lib/generators/action_hooks/webhook/templates/job.rb.erb
130
131
  - lib/generators/action_hooks/webhook/webhook_generator.rb
131
132
  homepage: https://github.com/alec-c4/action_hooks
132
133
  licenses: