munster 0.1.0 → 0.3.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: 11f34eb8fe918df292345851086c9190018d344bf73373125a1caec2e34984c8
4
- data.tar.gz: 51741866e336befe8d663cb27e225f8f9adff3552f3247437a11b3d0d966557d
3
+ metadata.gz: a01d139e5111555bba17ada9659ffcf0a42bbb9fc119ece2242e83ac12571f09
4
+ data.tar.gz: cc5892ac2ac534aa2112192a4c4e937a81d6d697bd87711b795d14a6aa19032f
5
5
  SHA512:
6
- metadata.gz: 565266df3f8b518248b20ba6e5d7f12e420ee3cce7755f1d775a69c8ff3f99ef22db9d016051f23670f2b2e3326e72d17b28f0152641b052bf0a25daa4d04c12
7
- data.tar.gz: cc689a7151f3dcf888efd826f87697c7cd552d6804b1e082242abffb6d83c2e4285e51feec3c97761ff1e6c6a30ff58d28d7762239af30ea6d6264c2934cb291
6
+ metadata.gz: 6abb1b3e7b2ca12fcbbdfcebf4cb4d806554a27e8faaac2166e78cfaf0c49ae5441934079d737457723c169a5c2c842f5c882cb67e38ae03e67952fbb782885f
7
+ data.tar.gz: f8640e4e671d9b5cbb2759ae1130b71f702651cdc7c4f85002a775777ef937d947b261a04000a9efa6c300af1091cbcaec311fa6d94acaf26b055cf8f978d3de
data/.rubocop.yml ADDED
@@ -0,0 +1,9 @@
1
+ require: standard
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 3.0
5
+
6
+ inherit_gem:
7
+ standard: config/base.yml
8
+ # Don't define any actual rubocop config here - this file is only used for
9
+ # proper editor support, and not used on CI, formatters, nor anywhere else.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ This format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+
6
+ ## 0.3.0
7
+
8
+ ### Changed
9
+ - state_machine_enum library was moved in it's own library/gem.
10
+
11
+ ### Fixed
12
+ - Provide handled: true attribute for Rails.error.report method, because it is required in Rails 7.0.
13
+
14
+ ## 0.2.0
15
+
16
+ ### Changed
17
+
18
+ - Handler methods are now defined as instance methods for simplicity.
19
+ - Define service_id in initializer with active_handlers, instead of handler class.
20
+ - Use ruby 3.0 as a base for standard/rubocop, format all code according to it.
21
+
22
+ ### Added
23
+
24
+ - Introduce Rails common error reporter ( https://guides.rubyonrails.org/error_reporting.html )
25
+
1
26
  ## 0.1.0
2
27
 
3
28
  - Initial release
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # Munster
2
+
2
3
  Munster is a Rails engine that provides a webhook endpoint for receiving and processing webhooks from various services. Engine stores received webhook first and later processes webhook in a separete async process.
3
4
 
4
5
  Source code is extracted from https://cheddar.me/ main service to be used in internal microservices. Code here could be a subject to change while we flesh out details.
@@ -16,6 +17,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
16
17
  $ gem install munster
17
18
 
18
19
  ## Usage
20
+
19
21
  Generate migrations and initializer file.
20
22
 
21
23
  `munster:install`
@@ -24,6 +26,24 @@ Mount munster engine in your routes.
24
26
 
25
27
  `mount Munster::Engine, at: "/webhooks"`
26
28
 
29
+ ## Requirements
30
+
31
+ This project depends on two dependencies:
32
+
33
+ - Ruby >= 3.0
34
+ - Rails >= 7.0
35
+
36
+ ## Error reporter
37
+
38
+ This gem uses [Rails common error reporter](https://guides.rubyonrails.org/error_reporting.html) to report any possible error to services like Honeybadger, Appsignal, Sentry and etc. Most of those services already support this common interface, if not - it's not that hard to add this support on your own.
39
+
40
+ It's possible to provide additional context for every error. e.g.
41
+ ```
42
+ Munster.configure do |config|
43
+ config.error_context = { appsignal: { namespace: "webhooks" } }
44
+ end
45
+ ```
46
+
27
47
  ## Development
28
48
 
29
49
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/example/Gemfile CHANGED
@@ -6,7 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
6
6
  ruby "3.2.2"
7
7
 
8
8
  # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
9
- gem "rails", "~> 7.0.8", ">= 7.0.8.1"
9
+ gem "rails", ">= 7.0.8.1"
10
10
 
11
11
  # The modern asset pipeline for Rails [https://github.com/rails/propshaft]
12
12
  gem "propshaft"
data/example/Gemfile.lock CHANGED
@@ -2,83 +2,96 @@ PATH
2
2
  remote: ..
3
3
  specs:
4
4
  munster (0.1.0)
5
- rails (~> 7.0)
5
+ rails (~> 7.1)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- actioncable (7.0.8.1)
11
- actionpack (= 7.0.8.1)
12
- activesupport (= 7.0.8.1)
10
+ actioncable (7.1.3.3)
11
+ actionpack (= 7.1.3.3)
12
+ activesupport (= 7.1.3.3)
13
13
  nio4r (~> 2.0)
14
14
  websocket-driver (>= 0.6.1)
15
- actionmailbox (7.0.8.1)
16
- actionpack (= 7.0.8.1)
17
- activejob (= 7.0.8.1)
18
- activerecord (= 7.0.8.1)
19
- activestorage (= 7.0.8.1)
20
- activesupport (= 7.0.8.1)
15
+ zeitwerk (~> 2.6)
16
+ actionmailbox (7.1.3.3)
17
+ actionpack (= 7.1.3.3)
18
+ activejob (= 7.1.3.3)
19
+ activerecord (= 7.1.3.3)
20
+ activestorage (= 7.1.3.3)
21
+ activesupport (= 7.1.3.3)
21
22
  mail (>= 2.7.1)
22
23
  net-imap
23
24
  net-pop
24
25
  net-smtp
25
- actionmailer (7.0.8.1)
26
- actionpack (= 7.0.8.1)
27
- actionview (= 7.0.8.1)
28
- activejob (= 7.0.8.1)
29
- activesupport (= 7.0.8.1)
26
+ actionmailer (7.1.3.3)
27
+ actionpack (= 7.1.3.3)
28
+ actionview (= 7.1.3.3)
29
+ activejob (= 7.1.3.3)
30
+ activesupport (= 7.1.3.3)
30
31
  mail (~> 2.5, >= 2.5.4)
31
32
  net-imap
32
33
  net-pop
33
34
  net-smtp
34
- rails-dom-testing (~> 2.0)
35
- actionpack (7.0.8.1)
36
- actionview (= 7.0.8.1)
37
- activesupport (= 7.0.8.1)
38
- rack (~> 2.0, >= 2.2.4)
35
+ rails-dom-testing (~> 2.2)
36
+ actionpack (7.1.3.3)
37
+ actionview (= 7.1.3.3)
38
+ activesupport (= 7.1.3.3)
39
+ nokogiri (>= 1.8.5)
40
+ racc
41
+ rack (>= 2.2.4)
42
+ rack-session (>= 1.0.1)
39
43
  rack-test (>= 0.6.3)
40
- rails-dom-testing (~> 2.0)
41
- rails-html-sanitizer (~> 1.0, >= 1.2.0)
42
- actiontext (7.0.8.1)
43
- actionpack (= 7.0.8.1)
44
- activerecord (= 7.0.8.1)
45
- activestorage (= 7.0.8.1)
46
- activesupport (= 7.0.8.1)
44
+ rails-dom-testing (~> 2.2)
45
+ rails-html-sanitizer (~> 1.6)
46
+ actiontext (7.1.3.3)
47
+ actionpack (= 7.1.3.3)
48
+ activerecord (= 7.1.3.3)
49
+ activestorage (= 7.1.3.3)
50
+ activesupport (= 7.1.3.3)
47
51
  globalid (>= 0.6.0)
48
52
  nokogiri (>= 1.8.5)
49
- actionview (7.0.8.1)
50
- activesupport (= 7.0.8.1)
53
+ actionview (7.1.3.3)
54
+ activesupport (= 7.1.3.3)
51
55
  builder (~> 3.1)
52
- erubi (~> 1.4)
53
- rails-dom-testing (~> 2.0)
54
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
55
- activejob (7.0.8.1)
56
- activesupport (= 7.0.8.1)
56
+ erubi (~> 1.11)
57
+ rails-dom-testing (~> 2.2)
58
+ rails-html-sanitizer (~> 1.6)
59
+ activejob (7.1.3.3)
60
+ activesupport (= 7.1.3.3)
57
61
  globalid (>= 0.3.6)
58
- activemodel (7.0.8.1)
59
- activesupport (= 7.0.8.1)
60
- activerecord (7.0.8.1)
61
- activemodel (= 7.0.8.1)
62
- activesupport (= 7.0.8.1)
63
- activestorage (7.0.8.1)
64
- actionpack (= 7.0.8.1)
65
- activejob (= 7.0.8.1)
66
- activerecord (= 7.0.8.1)
67
- activesupport (= 7.0.8.1)
62
+ activemodel (7.1.3.3)
63
+ activesupport (= 7.1.3.3)
64
+ activerecord (7.1.3.3)
65
+ activemodel (= 7.1.3.3)
66
+ activesupport (= 7.1.3.3)
67
+ timeout (>= 0.4.0)
68
+ activestorage (7.1.3.3)
69
+ actionpack (= 7.1.3.3)
70
+ activejob (= 7.1.3.3)
71
+ activerecord (= 7.1.3.3)
72
+ activesupport (= 7.1.3.3)
68
73
  marcel (~> 1.0)
69
- mini_mime (>= 1.1.0)
70
- activesupport (7.0.8.1)
74
+ activesupport (7.1.3.3)
75
+ base64
76
+ bigdecimal
71
77
  concurrent-ruby (~> 1.0, >= 1.0.2)
78
+ connection_pool (>= 2.2.5)
79
+ drb
72
80
  i18n (>= 1.6, < 2)
73
81
  minitest (>= 5.1)
82
+ mutex_m
74
83
  tzinfo (~> 2.0)
84
+ base64 (0.2.0)
85
+ bigdecimal (3.1.8)
75
86
  builder (3.2.4)
76
87
  concurrent-ruby (1.2.3)
88
+ connection_pool (2.4.1)
77
89
  crass (1.0.6)
78
90
  date (3.3.4)
79
91
  debug (1.9.2)
80
92
  irb (~> 1.10)
81
93
  reline (>= 0.3.8)
94
+ drb (2.2.1)
82
95
  erubi (1.12.0)
83
96
  globalid (1.2.1)
84
97
  activesupport (>= 6.1)
@@ -97,9 +110,9 @@ GEM
97
110
  net-pop
98
111
  net-smtp
99
112
  marcel (1.0.4)
100
- method_source (1.1.0)
101
113
  mini_mime (1.1.5)
102
114
  minitest (5.22.3)
115
+ mutex_m (0.2.0)
103
116
  net-imap (0.4.11)
104
117
  date
105
118
  net-protocol
@@ -133,22 +146,27 @@ GEM
133
146
  nio4r (~> 2.0)
134
147
  racc (1.7.3)
135
148
  rack (2.2.9)
149
+ rack-session (1.0.2)
150
+ rack (< 3)
136
151
  rack-test (2.1.0)
137
152
  rack (>= 1.3)
138
- rails (7.0.8.1)
139
- actioncable (= 7.0.8.1)
140
- actionmailbox (= 7.0.8.1)
141
- actionmailer (= 7.0.8.1)
142
- actionpack (= 7.0.8.1)
143
- actiontext (= 7.0.8.1)
144
- actionview (= 7.0.8.1)
145
- activejob (= 7.0.8.1)
146
- activemodel (= 7.0.8.1)
147
- activerecord (= 7.0.8.1)
148
- activestorage (= 7.0.8.1)
149
- activesupport (= 7.0.8.1)
153
+ rackup (1.0.0)
154
+ rack (< 3)
155
+ webrick
156
+ rails (7.1.3.3)
157
+ actioncable (= 7.1.3.3)
158
+ actionmailbox (= 7.1.3.3)
159
+ actionmailer (= 7.1.3.3)
160
+ actionpack (= 7.1.3.3)
161
+ actiontext (= 7.1.3.3)
162
+ actionview (= 7.1.3.3)
163
+ activejob (= 7.1.3.3)
164
+ activemodel (= 7.1.3.3)
165
+ activerecord (= 7.1.3.3)
166
+ activestorage (= 7.1.3.3)
167
+ activesupport (= 7.1.3.3)
150
168
  bundler (>= 1.15.0)
151
- railties (= 7.0.8.1)
169
+ railties (= 7.1.3.3)
152
170
  rails-dom-testing (2.2.0)
153
171
  activesupport (>= 5.0.0)
154
172
  minitest
@@ -156,13 +174,14 @@ GEM
156
174
  rails-html-sanitizer (1.6.0)
157
175
  loofah (~> 2.21)
158
176
  nokogiri (~> 1.14)
159
- railties (7.0.8.1)
160
- actionpack (= 7.0.8.1)
161
- activesupport (= 7.0.8.1)
162
- method_source
177
+ railties (7.1.3.3)
178
+ actionpack (= 7.1.3.3)
179
+ activesupport (= 7.1.3.3)
180
+ irb
181
+ rackup (>= 1.0.0)
163
182
  rake (>= 12.2)
164
- thor (~> 1.0)
165
- zeitwerk (~> 2.5)
183
+ thor (~> 1.0, >= 1.2.2)
184
+ zeitwerk (~> 2.6)
166
185
  rake (13.2.1)
167
186
  rdoc (6.6.3.1)
168
187
  psych (>= 4.0.0)
@@ -179,6 +198,7 @@ GEM
179
198
  timeout (0.4.1)
180
199
  tzinfo (2.0.6)
181
200
  concurrent-ruby (~> 1.0)
201
+ webrick (1.8.1)
182
202
  websocket-driver (0.7.6)
183
203
  websocket-extensions (>= 0.1.0)
184
204
  websocket-extensions (0.1.5)
@@ -197,7 +217,7 @@ DEPENDENCIES
197
217
  munster!
198
218
  propshaft
199
219
  puma (~> 5.0)
200
- rails (~> 7.0.8, >= 7.0.8.1)
220
+ rails (>= 7.0.8.1)
201
221
  sqlite3 (~> 1.4)
202
222
  tzinfo-data
203
223
 
@@ -4,25 +4,16 @@
4
4
  # if a banking provider test fails, indicating that the bank might be having an incident
5
5
 
6
6
  class WebhookTestHandler < Munster::BaseHandler
7
- def self.valid?(request)
8
- true
9
- end
7
+ def valid?(request) = true
10
8
 
11
- def self.process(webhook)
9
+ def process(webhook)
12
10
  return unless webhook.received?
13
11
  webhook.update!(status: "processing")
14
- wait(5)
15
12
  webhook.update!(status: "processed")
16
13
  rescue
17
- webhook.update!(status: "received")
14
+ webhook.update!(status: "error")
18
15
  raise
19
16
  end
20
17
 
21
- def self.service_id
22
- :test
23
- end
24
-
25
- def self.expose_errors_to_sender?
26
- true
27
- end
18
+ def expose_errors_to_sender? = true
28
19
  end
@@ -61,7 +61,7 @@ Rails.application.configure do
61
61
  # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
62
62
 
63
63
  if ENV["RAILS_LOG_TO_STDOUT"].present?
64
- logger = ActiveSupport::Logger.new(STDOUT)
64
+ logger = ActiveSupport::Logger.new($stdout)
65
65
  logger.formatter = config.log_formatter
66
66
  config.logger = ActiveSupport::TaggedLogging.new(logger)
67
67
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../app/webhooks/test_handler"
3
+ require_relative "../../app/webhooks/webhook_test_handler"
4
4
 
5
5
  Munster.configure do |config|
6
6
  config.active_handlers = [WebhookTestHandler]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Rails.application.routes.draw do
4
- mount Munster::Engine, at: "/webhooks"
4
+ mount Munster::Engine, at: "/webhooks", as: "webhooks"
5
5
  end
@@ -4,64 +4,59 @@ require_relative "jobs/processing_job"
4
4
 
5
5
  module Munster
6
6
  class BaseHandler
7
- class << self
8
- # Reimplement this method, it's being used in WebhooksController to store incoming webhook.
9
- # Also que for processing in the end.
10
- # @return [void]
11
- def handle(action_dispatch_request)
12
- binary_body_str = action_dispatch_request.body.read.force_encoding(Encoding::BINARY)
13
- attrs = {
14
- body: binary_body_str,
15
- handler_module_name: name,
16
- handler_event_id: extract_event_id_from_request(action_dispatch_request)
17
- }
18
- webhook = Munster::ReceivedWebhook.create!(**attrs)
19
-
20
- Munster.configuration.processing_job_class.perform_later(webhook)
21
- rescue ActiveRecord::RecordNotUnique # Deduplicated
22
- nil
23
- end
7
+ # Gets called from the background job
8
+ def self.process(...)
9
+ new.process(...)
10
+ end
24
11
 
25
- # This method will be used to process webhook by async worker.
26
- def process(received_webhook)
27
- end
12
+ # Reimplement this method, it's being used in WebhooksController to store incoming webhook.
13
+ # Also que for processing in the end.
14
+ # @return [void]
15
+ def handle(action_dispatch_request)
16
+ binary_body_str = action_dispatch_request.body.read.force_encoding(Encoding::BINARY)
17
+ attrs = {
18
+ body: binary_body_str,
19
+ handler_module_name: self.class.name,
20
+ handler_event_id: extract_event_id_from_request(action_dispatch_request)
21
+ }
22
+ webhook = Munster::ReceivedWebhook.create!(**attrs)
23
+
24
+ Munster.configuration.processing_job_class.perform_later(webhook)
25
+ rescue ActiveRecord::RecordNotUnique # Deduplicated
26
+ nil
27
+ end
28
28
 
29
- # This should be defined for each webhook handler and should be unique.
30
- # Otherwise controller will never pick up, that this handler exists.
31
- #
32
- # Please consider that this will be used in url, so don't use underscores or any other symbols that are not used in URL.
33
- def service_id
34
- :base
35
- end
29
+ # This method will be used to process webhook by async worker.
30
+ def process(received_webhook)
31
+ end
36
32
 
37
- # This method verifies that request actually comes from provider:
38
- # signature validation, HTTP authentication, IP whitelisting and the like
39
- def valid?(action_dispatch_request)
40
- true
41
- end
33
+ # This method verifies that request actually comes from provider:
34
+ # signature validation, HTTP authentication, IP whitelisting and the like
35
+ def valid?(action_dispatch_request)
36
+ true
37
+ end
42
38
 
43
- # Default implementation just generates UUID, but if the webhook sender sends us
44
- # an event ID we use it for deduplication.
45
- def extract_event_id_from_request(action_dispatch_request)
46
- SecureRandom.uuid
47
- end
39
+ # Default implementation just generates UUID, but if the webhook sender sends us
40
+ # an event ID we use it for deduplication.
41
+ def extract_event_id_from_request(action_dispatch_request)
42
+ SecureRandom.uuid
43
+ end
48
44
 
49
- # Webhook senders have varying retry behaviors, and often you want to "pretend"
50
- # everything is fine even though there is an error so that they keep sending you
51
- # data and do not disable your endpoint forcibly. We allow this to be configured
52
- # on a per-handler basis - a better webhooks sender will be able to make out
53
- # some sense of the errors.
54
- def expose_errors_to_sender?
55
- false
56
- end
45
+ # Webhook senders have varying retry behaviors, and often you want to "pretend"
46
+ # everything is fine even though there is an error so that they keep sending you
47
+ # data and do not disable your endpoint forcibly. We allow this to be configured
48
+ # on a per-handler basis - a better webhooks sender will be able to make out
49
+ # some sense of the errors.
50
+ def expose_errors_to_sender?
51
+ false
52
+ end
57
53
 
58
- # Tells the controller whether this handler is active or not. This can be used
59
- # to deactivate a particular handler via feature flags for example, or use other
60
- # logic to determine whether the handler may be used to create new received webhooks
61
- # in the system. This is primarily needed for load shedding.
62
- def active?
63
- true
64
- end
54
+ # Tells the controller whether this handler is active or not. This can be used
55
+ # to deactivate a particular handler via feature flags for example, or use other
56
+ # logic to determine whether the handler may be used to create new received webhooks
57
+ # in the system. This is primarily needed for load shedding.
58
+ def active?
59
+ true
65
60
  end
66
61
  end
67
62
  end
@@ -5,22 +5,24 @@ module Munster
5
5
  class HandlerRefused < StandardError
6
6
  end
7
7
 
8
+ class HandlerInactive < StandardError
9
+ end
10
+
8
11
  def create
9
- handler = lookup_handler(params[:service_id])
10
- return render_error("Webhook handler is inactive", :service_unavailable) unless handler.active?
12
+ handler = lookup_handler(params[:service_id]).new
11
13
 
14
+ raise HandlerInactive unless handler.active?
12
15
  raise HandlerRefused unless handler.valid?(request)
13
16
 
14
- # FIXME: Duplicated webhook will be overwritten here and processing job will be quite for second time.
15
- # This will generate a following error in this case:
16
- # Error performing Munster::ProcessingJob (Job ID: b40f3f28-81be-4c99-bce8-9ad879ec9754) from Async(default) in 9.95ms: ActiveRecord::RecordInvalid (Validation failed: Status Invalid transition from processing to received):
17
- #
18
- # This should be handled properly.
19
17
  handler.handle(request)
20
18
  head :ok
19
+ rescue KeyError # handler was not found, so we return generic 404 error.
20
+ render_error("Required parameters were not present in the request", :not_found)
21
21
  rescue => e
22
- # TODO: add exception handler here
23
- # Appsignal.add_exception(e)
22
+ Rails.error.set_context(**Munster.configuration.error_context)
23
+ # Rails 7.1 only requires `error` attribute for .report method, but Rails 7.0 requires `handled:` attribute additionally.
24
+ # We're setting `handled:` and `severity:` attributes to maintain compatibility with all versions of > rails 7.
25
+ Rails.error.report(e, handled: true, severity: :error)
24
26
 
25
27
  if handler&.expose_errors_to_sender?
26
28
  error_for_sender_from_exception(e)
@@ -33,8 +35,10 @@ module Munster
33
35
  case e
34
36
  when HandlerRefused
35
37
  render_error("Webhook handler did not validate the request (signature or authentication may be invalid)", :forbidden)
36
- when JSON::ParserError, KeyError
37
- render_error("Required parameters were not present in the request or the request body was not valid JSON", :bad_request)
38
+ when HandlerInactive
39
+ render_error("Webhook handler is inactive", :service_unavailable)
40
+ when JSON::ParserError
41
+ render_error("Request body is not a valid JSON", :bad_request)
38
42
  else
39
43
  render_error("Internal error", :internal_server_error)
40
44
  end
@@ -42,11 +46,11 @@ module Munster
42
46
 
43
47
  def render_error(message_str, status_sym)
44
48
  json = {error: message_str}.to_json
45
- render(json:, status: status_sym)
49
+ render(json: json, status: status_sym)
46
50
  end
47
51
 
48
52
  def lookup_handler(service_id_str)
49
- Munster.configuration.active_handlers.index_by(&:service_id).fetch(service_id_str.to_sym)
53
+ Munster.configuration.active_handlers.with_indifferent_access.fetch(service_id_str)
50
54
  end
51
55
  end
52
56
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../state_machine_enum"
3
+ require 'state_machine_enum'
4
4
 
5
5
  module Munster
6
6
  class ReceivedWebhook < ActiveRecord::Base
7
7
  self.implicit_order_column = "created_at"
8
8
  self.table_name = "received_webhooks"
9
9
 
10
- include Munster::StateMachineEnum
10
+ include StateMachineEnum
11
11
 
12
12
  state_machine_enum :status do |s|
13
13
  s.permit_transition(:received, :processing)
@@ -1,4 +1,28 @@
1
1
  Munster.configure do |config|
2
- config.active_handlers = []
3
- config.processing_job_class = Munster::ProcessingJob
2
+ # Active Handlers are defined as hash with key as a service_id and handler class that would handle webhook request.
3
+ # Example:
4
+ # {:test => TestHandler, :inactive => InactiveHandler}
5
+ config.active_handlers = {}
6
+
7
+ # It's possible to overwrite default processing job to enahance it. As example if you want to add proper locking or retry mechanism.
8
+ #
9
+ # Example:
10
+ #
11
+ # class WebhookProcessingJob < Munster::ProcessingJob
12
+ # def perform(webhook)
13
+ # TokenLock.with(name: "webhook-processing-#{webhook.id}") do
14
+ # super(webhook)
15
+ # end
16
+ # end
17
+ #
18
+ # This is how you can change processing job:
19
+ #
20
+ # config.processing_job_class = WebhookProcessingJob
21
+
22
+ # We're using a common interface for error reporting provided by Rails, e.g Rails.error.report. In some cases
23
+ # you want to enhance those errors with additional context. As example to provide a namespace:
24
+ #
25
+ # { appsignal: { namespace: "webhooks" } }
26
+ #
27
+ # config.error_context = { appsignal: { namespace: "webhooks" } }
4
28
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Munster
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/munster.rb CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  require_relative "munster/version"
4
4
  require_relative "munster/engine"
5
- require "active_support/configurable"
6
5
  require_relative "munster/jobs/processing_job"
6
+ require "active_support/configurable"
7
7
 
8
8
  module Munster
9
9
  def self.configuration
@@ -20,4 +20,5 @@ class Munster::Configuration
20
20
 
21
21
  config_accessor(:processing_job_class) { Munster::ProcessingJob }
22
22
  config_accessor(:active_handlers) { [] }
23
+ config_accessor(:error_context) { {} }
23
24
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: munster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stanislav Katkov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-06-03 00:00:00.000000000 Z
11
+ date: 2024-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: state_machine_enum
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rake
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -88,6 +102,7 @@ extensions: []
88
102
  extra_rdoc_files: []
89
103
  files:
90
104
  - ".editorconfig"
105
+ - ".rubocop.yml"
91
106
  - ".standard.yml"
92
107
  - CHANGELOG.md
93
108
  - LICENSE
@@ -110,7 +125,7 @@ files:
110
125
  - example/app/models/application_record.rb
111
126
  - example/app/models/concerns/.keep
112
127
  - example/app/views/layouts/application.html.erb
113
- - example/app/webhooks/test_handler.rb
128
+ - example/app/webhooks/webhook_test_handler.rb
114
129
  - example/bin/bundle
115
130
  - example/bin/rails
116
131
  - example/bin/rake
@@ -163,7 +178,6 @@ files:
163
178
  - lib/munster/install_generator.rb
164
179
  - lib/munster/jobs/processing_job.rb
165
180
  - lib/munster/models/received_webhook.rb
166
- - lib/munster/state_machine_enum.rb
167
181
  - lib/munster/templates/create_munster_tables.rb.erb
168
182
  - lib/munster/templates/munster.rb
169
183
  - lib/munster/version.rb
@@ -1,125 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This concern adds a method called "state_enum" useful for defining an enum using
4
- # string values along with valid state transitions. Validations will be added for the
5
- # state transitions and a proper enum is going to be defined. For example:
6
- #
7
- # state_machine_enum :state do |states|
8
- # states.permit_transition(:created, :approved_pending_settlement)
9
- # states.permit_transition(:approved_pending_settlement, :rejected)
10
- # states.permit_transition(:created, :rejected)
11
- # states.permit_transition(:approved_pending_settlement, :settled)
12
- # end
13
- module Munster
14
- module StateMachineEnum
15
- extend ActiveSupport::Concern
16
-
17
- class StatesCollector
18
- attr_reader :states
19
- attr_reader :after_commit_hooks
20
- attr_reader :common_after_commit_hooks
21
- attr_reader :after_attribute_write_hooks
22
- attr_reader :common_after_write_hooks
23
-
24
- def initialize
25
- @transitions = Set.new
26
- @states = Set.new
27
- @after_commit_hooks = {}
28
- @common_after_commit_hooks = []
29
- @after_attribute_write_hooks = {}
30
- @common_after_write_hooks = []
31
- end
32
-
33
- def permit_transition(from, to)
34
- @states << from.to_s << to.to_s
35
- @transitions << [from.to_s, to.to_s]
36
- end
37
-
38
- def may_transition?(from, to)
39
- @transitions.include?([from.to_s, to.to_s])
40
- end
41
-
42
- def after_inline_transition_to(target_state, &blk)
43
- @after_attribute_write_hooks[target_state.to_s] ||= []
44
- @after_attribute_write_hooks[target_state.to_s] << blk.to_proc
45
- end
46
-
47
- def after_committed_transition_to(target_state, &blk)
48
- @after_commit_hooks[target_state.to_s] ||= []
49
- @after_commit_hooks[target_state.to_s] << blk.to_proc
50
- end
51
-
52
- def after_any_committed_transition(&blk)
53
- @common_after_commit_hooks << blk.to_proc
54
- end
55
-
56
- def validate(model, attribute_name)
57
- return unless model.persisted?
58
-
59
- was = model.attribute_was(attribute_name)
60
- is = model[attribute_name]
61
-
62
- unless was == is || @transitions.include?([was, is])
63
- model.errors.add(attribute_name, "Invalid transition from #{was} to #{is}")
64
- end
65
- end
66
- end
67
-
68
- class InvalidState < StandardError
69
- end
70
-
71
- class_methods do
72
- def state_machine_enum(attribute_name, **options_for_enum)
73
- # Collect the states
74
- collector = StatesCollector.new
75
- yield(collector).tap do
76
- # Define the enum using labels, with string values
77
- enum_map = collector.states.map(&:to_sym).zip(collector.states.to_a).to_h
78
- enum(attribute_name, enum_map, **options_for_enum)
79
-
80
- # Define validations for transitions
81
- validates attribute_name, presence: true
82
- validate { |model| collector.validate(model, attribute_name) }
83
-
84
- # Define inline hooks
85
- before_save do |model|
86
- _value_was, value_has_become = model.changes[attribute_name]
87
- next unless value_has_become
88
- hook_procs = collector.after_attribute_write_hooks[value_has_become].to_a + collector.common_after_write_hooks.to_a
89
- hook_procs.each do |hook_proc|
90
- hook_proc.call(model)
91
- end
92
- end
93
-
94
- # Define after commit hooks
95
- after_commit do |model|
96
- _value_was, value_has_become = model.previous_changes[attribute_name]
97
- next unless value_has_become
98
- hook_procs = collector.after_commit_hooks[value_has_become].to_a + collector.common_after_commit_hooks.to_a
99
- hook_procs.each do |hook_proc|
100
- hook_proc.call(model)
101
- end
102
- end
103
-
104
- # Define the check methods
105
- define_method(:"ensure_#{attribute_name}_one_of!") do |*allowed_states|
106
- val = self[attribute_name]
107
- return if Set.new(allowed_states.map(&:to_s)).include?(val)
108
- raise InvalidState, "#{attribute_name} must be one of #{allowed_states.inspect} but was #{val.inspect}"
109
- end
110
-
111
- define_method(:"ensure_#{attribute_name}_may_transition_to!") do |next_state|
112
- val = self[attribute_name]
113
- raise InvalidState, "#{attribute_name} already is #{val.inspect}" if next_state.to_s == val
114
- end
115
-
116
- define_method(:"#{attribute_name}_may_transition_to?") do |next_state|
117
- val = self[attribute_name]
118
- return false if val == next_state.to_s
119
- collector.may_transition?(val, next_state)
120
- end
121
- end
122
- end
123
- end
124
- end
125
- end