hooksmith 0.1.2 → 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: c80476ba099d00e9dfefb14d1b05d696a4b5cef3eb655e6f145c334e86fcf1ac
4
- data.tar.gz: 4c1e6ae0d6cdfe4e1a013e2028b446912a636b295d949d499801e83ce4a640c8
3
+ metadata.gz: 82cacda1d785736b14476d699c2d18e90d65036f2b4e291d03dd2b393a907d14
4
+ data.tar.gz: 65407cf717e2e3ac6edede0392ed319250b3e0c06fc61dc0d730fd82fafead3c
5
5
  SHA512:
6
- metadata.gz: 0c004376be79241af2a779aa9eeafe4bd4075d09ab45da029d8c9d88ed6faa2d1248652b160ac0761a107c7e0ddddec81a41cec8ed1f5e147f1e6f43d8dd9ca0
7
- data.tar.gz: c555b8226888a88774fb0fd97bd3b57a1741a35bdd615d462bbd479786ab6a1aede0875a070cb9fcbc0d76f4b3ad6ac85256ff023dd2d9e9ccd552fcd8d928fe
6
+ metadata.gz: bbd5f49b4248a597997816a03c0e9624c93ec3fe8185837398564360a16663defe758463ba2528f1af38e53e0816fadf1b61d5243591835f8b84cd9c822a7f40
7
+ data.tar.gz: b13ef16ba8d73c011c410ca838575f9a4eb8b3bdc58560b392cf88350f8520d0752a7081e556dc649fa5e6ad89441e975bde9e57085717b98356516a6e801f81
data/README.md CHANGED
@@ -48,6 +48,52 @@ Hooksmith.configure do |config|
48
48
  end
49
49
  ```
50
50
 
51
+ ### Persisting Webhook Events (Optional)
52
+
53
+ Hooksmith can optionally persist incoming webhook events to your database. Configure it with your own ActiveRecord model and mapping logic.
54
+
55
+ 1) Create a model in your app (example):
56
+
57
+ ```ruby
58
+ class WebhookEvent < ApplicationRecord
59
+ self.table_name = 'webhook_events'
60
+ end
61
+ ```
62
+
63
+ 2) Example migration (customize fields as needed):
64
+
65
+ ```ruby
66
+ create_table :webhook_events do |t|
67
+ t.string :provider
68
+ t.string :event
69
+ t.jsonb :payload
70
+ t.datetime :received_at
71
+ t.timestamps
72
+ t.index :event
73
+ t.index :received_at
74
+ end
75
+ ```
76
+
77
+ 3) Configure Hooksmith to record events:
78
+
79
+ ```ruby
80
+ Hooksmith.configure do |config|
81
+ config.event_store do |store|
82
+ store.enabled = true
83
+ store.model_class_name = 'WebhookEvent' # your model class
84
+ store.record_timing = :before # :before, :after, or :both
85
+ store.mapper = ->(provider:, event:, payload:) {
86
+ {
87
+ provider: provider.to_s,
88
+ event: event.to_s,
89
+ payload:,
90
+ received_at: (Time.respond_to?(:current) ? Time.current : Time.now)
91
+ }
92
+ }
93
+ end
94
+ end
95
+ ```
96
+
51
97
  ## Implementing a Processor
52
98
  Create a processor by inheriting from `Hooksmith::Processor::Base`:
53
99
 
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hooksmith
4
+ module Config
5
+ # EventStore holds settings for optional event persistence.
6
+ class EventStore
7
+ # Whether persistence is enabled.
8
+ attr_accessor :enabled
9
+ # Class name of the model used to persist events. Must respond to .create!(attrs)
10
+ attr_accessor :model_class_name
11
+ # Proc to map provider/event/payload to attributes persisted
12
+ attr_accessor :mapper
13
+ # When to record: :before, :after, or :both
14
+ attr_accessor :record_timing
15
+
16
+ def initialize
17
+ @enabled = false
18
+ # No default model in the gem; applications should provide their own model
19
+ @model_class_name = nil
20
+ @record_timing = :before
21
+ @mapper = default_mapper
22
+ end
23
+
24
+ def model_class
25
+ return nil if model_class_name.nil?
26
+
27
+ Object.const_get(model_class_name)
28
+ rescue NameError
29
+ nil
30
+ end
31
+
32
+ private
33
+
34
+ def default_mapper
35
+ lambda do |provider:, event:, payload:|
36
+ now = Time.respond_to?(:current) ? Time.current : Time.now
37
+ {
38
+ provider: provider.to_s,
39
+ event: event.to_s,
40
+ payload:,
41
+ received_at: now
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hooksmith
4
+ module Config
5
+ # Provider is used internally by the DSL to collect processor registrations.
6
+ class Provider
7
+ # @return [Symbol, String] the provider name.
8
+ attr_reader :provider
9
+ # @return [Array<Hash>] list of entries registered.
10
+ attr_reader :entries
11
+
12
+ def initialize(provider)
13
+ @provider = provider
14
+ @entries = []
15
+ end
16
+
17
+ # Registers a processor for a specific event.
18
+ #
19
+ # @param event [Symbol, String] the event name.
20
+ # @param processor_class_name [String] the processor class name.
21
+ def register(event, processor_class_name)
22
+ entries << { event: event.to_sym, processor: processor_class_name }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -7,18 +7,21 @@ module Hooksmith
7
7
  class Configuration
8
8
  # @return [Hash] a registry mapping provider symbols to arrays of processor entries.
9
9
  attr_reader :registry
10
+ # @return [Hooksmith::Config::EventStore] configuration for event persistence
11
+ attr_reader :event_store_config
10
12
 
11
13
  def initialize
12
14
  # Registry structure: { provider_symbol => [{ event: event_symbol, processor: ProcessorClass }, ...] }
13
15
  @registry = Hash.new { |hash, key| hash[key] = [] }
16
+ @event_store_config = Hooksmith::Config::EventStore.new
14
17
  end
15
18
 
16
19
  # Groups registrations under a specific provider.
17
20
  #
18
21
  # @param provider_name [Symbol, String] the provider name (e.g., :stripe)
19
- # @yield [ProviderConfig] a block yielding a ProviderConfig object
22
+ # @yield [Hooksmith::Config::Provider] a block yielding a Provider object
20
23
  def provider(provider_name)
21
- provider_config = ProviderConfig.new(provider_name)
24
+ provider_config = Hooksmith::Config::Provider.new(provider_name)
22
25
  yield(provider_config)
23
26
  registry[provider_name.to_sym].concat(provider_config.entries)
24
27
  end
@@ -40,26 +43,23 @@ module Hooksmith
40
43
  def processors_for(provider, event)
41
44
  registry[provider.to_sym].select { |entry| entry[:event] == event.to_sym }
42
45
  end
43
- end
44
-
45
- # ProviderConfig is used internally by the DSL to collect processor registrations.
46
- class ProviderConfig
47
- # @return [Symbol, String] the provider name.
48
- attr_reader :provider
49
- # @return [Array<Hash>] list of entries registered.
50
- attr_reader :entries
51
-
52
- def initialize(provider)
53
- @provider = provider
54
- @entries = []
55
- end
56
46
 
57
- # Registers a processor for a specific event.
47
+ # Configure event store persistence.
58
48
  #
59
- # @param event [Symbol, String] the event name.
60
- # @param processor_class_name [String] the processor class name.
61
- def register(event, processor_class_name)
62
- entries << { event: event.to_sym, processor: processor_class_name }
49
+ # @yield [Hooksmith::Config::EventStore] a block yielding an EventStore object
50
+ # @example
51
+ # Hooksmith.configure do |config|
52
+ # config.event_store do |store|
53
+ # store.enabled = true
54
+ # store.model_class_name = 'MyApp::WebhookEvent'
55
+ # store.record_timing = :before # or :after, or :both
56
+ # store.mapper = ->(provider:, event:, payload:) {
57
+ # { provider:, event: event.to_s, payload:, received_at: (Time.respond_to?(:current) ? Time.current : Time.now) }
58
+ # }
59
+ # end
60
+ # end
61
+ def event_store
62
+ yield(@event_store_config)
63
63
  end
64
64
  end
65
65
  end
@@ -28,6 +28,9 @@ module Hooksmith
28
28
  #
29
29
  # @raise [MultipleProcessorsError] if multiple processors qualify.
30
30
  def run!
31
+ # Optionally record the incoming event before processing.
32
+ Hooksmith::EventRecorder.record!(provider: @provider, event: @event, payload: @payload, timing: :before)
33
+
31
34
  # Fetch all processors registered for this provider and event.
32
35
  entries = Hooksmith.configuration.processors_for(@provider, @event)
33
36
 
@@ -46,7 +49,12 @@ module Hooksmith
46
49
  raise MultipleProcessorsError.new(@provider, @event, @payload) if matching_processors.size > 1
47
50
 
48
51
  # Exactly one matching processor.
49
- matching_processors.first.process!
52
+ result = matching_processors.first.process!
53
+
54
+ # Optionally record the event after successful processing.
55
+ Hooksmith::EventRecorder.record!(provider: @provider, event: @event, payload: @payload, timing: :after)
56
+
57
+ result
50
58
  rescue StandardError => e
51
59
  Hooksmith.logger.error("Error processing #{@provider} event #{@event}: #{e.message}")
52
60
  raise e
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hooksmith
4
+ # Records webhook events to a configurable persistence model.
5
+ #
6
+ # This recorder is resilient: failures to persist are logged and do not
7
+ # impact the main processing flow.
8
+ module EventRecorder
9
+ module_function
10
+
11
+ # Record an event if the event store is enabled.
12
+ #
13
+ # @param provider [Symbol, String]
14
+ # @param event [Symbol, String]
15
+ # @param payload [Hash]
16
+ # @param timing [Symbol] one of :before or :after
17
+ def record!(provider:, event:, payload:, timing: :before)
18
+ config = Hooksmith.configuration.event_store_config
19
+ return unless config.enabled
20
+ return unless record_for_timing?(config, timing)
21
+
22
+ model_class = config.model_class
23
+ unless model_class
24
+ Hooksmith.logger.warn("Event store enabled but model '#{config.model_class_name}' not found")
25
+ return
26
+ end
27
+
28
+ attributes = safe_map(config, provider:, event:, payload:)
29
+ model_class.create!(attributes)
30
+ rescue StandardError => e
31
+ Hooksmith.logger.error("Failed to record webhook event: #{e.message}")
32
+ end
33
+
34
+ # Determine whether to record depending on the configured timing.
35
+ def record_for_timing?(config, timing)
36
+ case config.record_timing
37
+ when :both then true
38
+ when :before then timing == :before
39
+ when :after then timing == :after
40
+ end
41
+ end
42
+ private_class_method :record_for_timing?
43
+
44
+ # Safely map attributes using the configured mapper.
45
+ def safe_map(config, provider:, event:, payload:)
46
+ mapper = config.mapper
47
+ mapper.call(provider:, event:, payload:)
48
+ rescue StandardError => e
49
+ Hooksmith.logger.error("Event mapper raised: #{e.message}")
50
+ {}
51
+ end
52
+ private_class_method :safe_map
53
+ end
54
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hooksmith
4
- VERSION = '0.1.2'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/hooksmith.rb CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  require 'hooksmith/version'
4
4
  require 'hooksmith/configuration'
5
+ require 'hooksmith/config/provider'
6
+ require 'hooksmith/config/event_store'
5
7
  require 'hooksmith/dispatcher'
6
8
  require 'hooksmith/logger'
9
+ require 'hooksmith/event_recorder'
7
10
  require 'hooksmith/processor/base'
8
11
  require 'hooksmith/railtie' if defined?(Rails)
9
12
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hooksmith
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gregoryrivage
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-13 00:00:00.000000000 Z
10
+ date: 2025-09-05 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Hooksmith is a gem that allows you to handle webhooks in your Rails application.
13
13
  It provides a simple and flexible way to receive, validate, and process webhooks
@@ -27,10 +27,12 @@ files:
27
27
  - LICENSE.txt
28
28
  - README.md
29
29
  - Rakefile
30
- - hooksmith-0.1.0.gem
31
30
  - lib/hooksmith.rb
31
+ - lib/hooksmith/config/event_store.rb
32
+ - lib/hooksmith/config/provider.rb
32
33
  - lib/hooksmith/configuration.rb
33
34
  - lib/hooksmith/dispatcher.rb
35
+ - lib/hooksmith/event_recorder.rb
34
36
  - lib/hooksmith/logger.rb
35
37
  - lib/hooksmith/processor/base.rb
36
38
  - lib/hooksmith/railtie.rb
data/hooksmith-0.1.0.gem DELETED
Binary file