munster 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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