munster 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +14 -0
  3. data/.standard.yml +3 -0
  4. data/CHANGELOG.md +3 -0
  5. data/LICENSE +21 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +39 -0
  8. data/Rakefile +18 -0
  9. data/config/routes.rb +3 -0
  10. data/example/.gitattributes +7 -0
  11. data/example/.gitignore +29 -0
  12. data/example/.ruby-version +1 -0
  13. data/example/Gemfile +32 -0
  14. data/example/Gemfile.lock +208 -0
  15. data/example/README.md +24 -0
  16. data/example/Rakefile +8 -0
  17. data/example/app/assets/images/.keep +0 -0
  18. data/example/app/assets/stylesheets/application.css +1 -0
  19. data/example/app/controllers/application_controller.rb +4 -0
  20. data/example/app/controllers/concerns/.keep +0 -0
  21. data/example/app/helpers/application_helper.rb +4 -0
  22. data/example/app/models/application_record.rb +5 -0
  23. data/example/app/models/concerns/.keep +0 -0
  24. data/example/app/views/layouts/application.html.erb +15 -0
  25. data/example/app/webhooks/test_handler.rb +28 -0
  26. data/example/bin/bundle +109 -0
  27. data/example/bin/rails +4 -0
  28. data/example/bin/rake +4 -0
  29. data/example/bin/setup +33 -0
  30. data/example/config/application.rb +39 -0
  31. data/example/config/boot.rb +5 -0
  32. data/example/config/credentials.yml.enc +1 -0
  33. data/example/config/database.yml +25 -0
  34. data/example/config/environment.rb +7 -0
  35. data/example/config/environments/development.rb +61 -0
  36. data/example/config/environments/production.rb +71 -0
  37. data/example/config/environments/test.rb +52 -0
  38. data/example/config/initializers/assets.rb +14 -0
  39. data/example/config/initializers/content_security_policy.rb +27 -0
  40. data/example/config/initializers/filter_parameter_logging.rb +10 -0
  41. data/example/config/initializers/generators.rb +7 -0
  42. data/example/config/initializers/inflections.rb +18 -0
  43. data/example/config/initializers/munster.rb +7 -0
  44. data/example/config/initializers/permissions_policy.rb +13 -0
  45. data/example/config/locales/en.yml +33 -0
  46. data/example/config/puma.rb +45 -0
  47. data/example/config/routes.rb +5 -0
  48. data/example/config.ru +8 -0
  49. data/example/db/migrate/20240523125859_create_munster_tables.rb +22 -0
  50. data/example/db/schema.rb +24 -0
  51. data/example/db/seeds.rb +9 -0
  52. data/example/lib/assets/.keep +0 -0
  53. data/example/lib/tasks/.keep +0 -0
  54. data/example/log/.keep +0 -0
  55. data/example/public/404.html +67 -0
  56. data/example/public/422.html +67 -0
  57. data/example/public/500.html +66 -0
  58. data/example/public/apple-touch-icon-precomposed.png +0 -0
  59. data/example/public/apple-touch-icon.png +0 -0
  60. data/example/public/favicon.ico +0 -0
  61. data/example/public/robots.txt +1 -0
  62. data/example/test/controllers/.keep +0 -0
  63. data/example/test/fixtures/files/.keep +0 -0
  64. data/example/test/helpers/.keep +0 -0
  65. data/example/test/integration/.keep +0 -0
  66. data/example/test/models/.keep +0 -0
  67. data/example/test/test_helper.rb +15 -0
  68. data/example/tmp/.keep +0 -0
  69. data/example/tmp/pids/.keep +0 -0
  70. data/example/vendor/.keep +0 -0
  71. data/lib/munster/base_handler.rb +67 -0
  72. data/lib/munster/controllers/receive_webhooks_controller.rb +52 -0
  73. data/lib/munster/engine.rb +22 -0
  74. data/lib/munster/install_generator.rb +30 -0
  75. data/lib/munster/jobs/processing_job.rb +14 -0
  76. data/lib/munster/models/received_webhook.rb +23 -0
  77. data/lib/munster/state_machine_enum.rb +125 -0
  78. data/lib/munster/templates/create_munster_tables.rb.erb +23 -0
  79. data/lib/munster/templates/munster.rb +4 -0
  80. data/lib/munster/version.rb +5 -0
  81. data/lib/munster.rb +23 -0
  82. data/lib/tasks/munster_tasks.rake +4 -0
  83. data/sig/munster.rbs +4 -0
  84. metadata +197 -0
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ .rails-default-error-page {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ .rails-default-error-page div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ .rails-default-error-page div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ .rails-default-error-page h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ .rails-default-error-page div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body class="rails-default-error-page">
58
+ <!-- This file lives in public/422.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The change you wanted was rejected.</h1>
62
+ <p>Maybe you tried to change something you didn't have access to.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ .rails-default-error-page {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ .rails-default-error-page div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ .rails-default-error-page div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ .rails-default-error-page h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ .rails-default-error-page div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body class="rails-default-error-page">
58
+ <!-- This file lives in public/500.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>We're sorry, but something went wrong.</h1>
62
+ </div>
63
+ <p>If you are the application owner check the logs for more information.</p>
64
+ </div>
65
+ </body>
66
+ </html>
File without changes
File without changes
File without changes
@@ -0,0 +1 @@
1
+ # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV["RAILS_ENV"] ||= "test"
4
+ require_relative "../config/environment"
5
+ require "rails/test_help"
6
+
7
+ class ActiveSupport::TestCase
8
+ # Run tests in parallel with specified workers
9
+ parallelize(workers: :number_of_processors)
10
+
11
+ # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
12
+ fixtures :all
13
+
14
+ # Add more helper methods to be used by all tests here...
15
+ end
data/example/tmp/.keep ADDED
File without changes
File without changes
File without changes
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "jobs/processing_job"
4
+
5
+ module Munster
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
24
+
25
+ # This method will be used to process webhook by async worker.
26
+ def process(received_webhook)
27
+ end
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
36
+
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
42
+
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
48
+
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
57
+
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
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Munster
4
+ class ReceiveWebhooksController < ActionController::API
5
+ class HandlerRefused < StandardError
6
+ end
7
+
8
+ def create
9
+ handler = lookup_handler(params[:service_id])
10
+ return render_error("Webhook handler is inactive", :service_unavailable) unless handler.active?
11
+
12
+ raise HandlerRefused unless handler.valid?(request)
13
+
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
+ handler.handle(request)
20
+ head :ok
21
+ rescue => e
22
+ # TODO: add exception handler here
23
+ # Appsignal.add_exception(e)
24
+
25
+ if handler&.expose_errors_to_sender?
26
+ error_for_sender_from_exception(e)
27
+ else
28
+ head :ok
29
+ end
30
+ end
31
+
32
+ def error_for_sender_from_exception(e)
33
+ case e
34
+ when HandlerRefused
35
+ 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
+ else
39
+ render_error("Internal error", :internal_server_error)
40
+ end
41
+ end
42
+
43
+ def render_error(message_str, status_sym)
44
+ json = {error: message_str}.to_json
45
+ render(json:, status: status_sym)
46
+ end
47
+
48
+ def lookup_handler(service_id_str)
49
+ Munster.configuration.active_handlers.index_by(&:service_id).fetch(service_id_str.to_sym)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../munster"
4
+ require_relative "controllers/receive_webhooks_controller"
5
+ require_relative "jobs/processing_job"
6
+ require_relative "models/received_webhook"
7
+ require_relative "base_handler"
8
+
9
+ module Munster
10
+ class Engine < ::Rails::Engine
11
+ isolate_namespace Munster
12
+
13
+ autoload :Munster, "munster"
14
+ autoload :ReceiveWebhooksController, "munster/controllers/receive_webhooks_controller"
15
+ autoload :ProcessingJob, "munster/jobs/processing_job"
16
+ autoload :BaseHandler, "munster/base_handler"
17
+
18
+ generators do
19
+ require_relative "install_generator"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Munster
7
+ #
8
+ # Rails generator used for setting up Munster in a Rails application.
9
+ # Run it with +bin/rails g munster:install+ in your console.
10
+ #
11
+ class InstallGenerator < Rails::Generators::Base
12
+ include ActiveRecord::Generators::Migration
13
+
14
+ source_root File.expand_path("../templates", __FILE__)
15
+
16
+ def create_migration_file
17
+ migration_template "create_munster_tables.rb.erb", File.join(db_migrate_path, "create_munster_tables.rb")
18
+ end
19
+
20
+ def copy_files
21
+ template "munster.rb", File.join("config", "initializers", "munster.rb")
22
+ end
23
+
24
+ private
25
+
26
+ def migration_version
27
+ "[#{ActiveRecord::VERSION::STRING.to_f}]"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job" if defined?(Rails)
4
+
5
+ module Munster
6
+ class ProcessingJob < ActiveJob::Base
7
+ def perform(webhook)
8
+ # TODO: there should be some sort of locking or concurrency control here, but it's outside of
9
+ # Munsters scope of responsibility. Developer implementing this should decide how this should be handled.
10
+ webhook.handler.process(webhook)
11
+ # TODO: remove process attribute
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../state_machine_enum"
4
+
5
+ module Munster
6
+ class ReceivedWebhook < ActiveRecord::Base
7
+ self.implicit_order_column = "created_at"
8
+ self.table_name = "received_webhooks"
9
+
10
+ include Munster::StateMachineEnum
11
+
12
+ state_machine_enum :status do |s|
13
+ s.permit_transition(:received, :processing)
14
+ s.permit_transition(:processing, :skipped)
15
+ s.permit_transition(:processing, :processed)
16
+ s.permit_transition(:processing, :error)
17
+ end
18
+
19
+ def handler
20
+ handler_module_name.constantize
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,125 @@
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
@@ -0,0 +1,23 @@
1
+ class CreateMunsterTables < ActiveRecord::Migration<%= migration_version %>
2
+ <% id_type = Rails.application.config.generators.options[:active_record][:primary_key_type] rescue nil %>
3
+ def change
4
+ create_table :received_webhooks <%= ", id: :#{id_type}" if id_type %> do |t|
5
+ t.string :handler_event_id, null: false
6
+ t.string :handler_module_name, null: false
7
+ t.string :status, default: "received", null: false
8
+
9
+ # We don't assume, that we can always parse received body as JSON. Body could be invalid or partly missing,
10
+ # we can argue how we can handle that for different integrations, but we still should be able to save this data
11
+ # if it's required. Hence, we don't use :jsonb, but :binary type column here.
12
+ t.binary :body, null: false
13
+
14
+ t.timestamps
15
+ end
16
+
17
+ # For deduplication at ingress. UNIQUE indices in Postgres are case-sensitive
18
+ # which is what we want, as these are externally-generated IDs
19
+ add_index :received_webhooks, [:handler_module_name, :handler_event_id], unique: true, name: "webhook_dedup_idx"
20
+ # For backfill processing (to know how many skipped etc. payloads we have)
21
+ add_index :received_webhooks, [:status]
22
+ end
23
+ end
@@ -0,0 +1,4 @@
1
+ Munster.configure do |config|
2
+ config.active_handlers = []
3
+ config.processing_job_class = Munster::ProcessingJob
4
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Munster
4
+ VERSION = "0.1.0"
5
+ end
data/lib/munster.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "munster/version"
4
+ require_relative "munster/engine"
5
+ require "active_support/configurable"
6
+ require_relative "munster/jobs/processing_job"
7
+
8
+ module Munster
9
+ def self.configuration
10
+ @configuration ||= Configuration.new
11
+ end
12
+
13
+ def self.configure
14
+ yield configuration
15
+ end
16
+ end
17
+
18
+ class Munster::Configuration
19
+ include ActiveSupport::Configurable
20
+
21
+ config_accessor(:processing_job_class) { Munster::ProcessingJob }
22
+ config_accessor(:active_handlers) { [] }
23
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :munster do
3
+ # # Task goes here
4
+ # end
data/sig/munster.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Munster
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end