event_trigger 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +142 -0
- data/Rakefile +9 -0
- data/lib/event_trigger/configuration.rb +123 -0
- data/lib/event_trigger/defaults.rb +40 -0
- data/lib/event_trigger/dispatcher.rb +70 -0
- data/lib/event_trigger/event.rb +34 -0
- data/lib/event_trigger/provider.rb +49 -0
- data/lib/event_trigger/providers/email_provider.rb +115 -0
- data/lib/event_trigger/providers/slack_provider.rb +123 -0
- data/lib/event_trigger/providers/webhook_provider.rb +65 -0
- data/lib/event_trigger/railtie.rb +59 -0
- data/lib/event_trigger/registry.rb +41 -0
- data/lib/event_trigger/version.rb +5 -0
- data/lib/event_trigger.rb +103 -0
- data/lib/generators/event_trigger/install_generator.rb +25 -0
- data/lib/generators/event_trigger/templates/event_trigger.rb +29 -0
- data/sig/event_trigger.rbs +4 -0
- metadata +109 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e2c3d6d3c6ba705815ec39d822e4a83d8d832a395b63eb85fd9b149e9cdc4a5e
|
|
4
|
+
data.tar.gz: 0e5339bc3d82788cdd942d0b7c69f979855379ac5f9c5b7ccde950617b46a9b8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 399d0f668e2781a691c53bb6bc172ffbfc984656c50dc72e3eeda8831869209f1ac388c42faf6f5c3542bfa6aab9672c02d9c1a5730d7d85a0df4cdbe57f2930
|
|
7
|
+
data.tar.gz: 24811741cb39d9d792b138445754da696bdb4b514a4f194c4fbbbaed0b2b5854ab9c8a3285bc5ea40415a92182d3f1bf8269ef47d0c0d6d3e319cd65b7803ea6
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kamlesh Parmar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# EventTrigger
|
|
2
|
+
|
|
3
|
+
Configurable event-based notification system for Ruby and Rails.
|
|
4
|
+
|
|
5
|
+
Configure notification providers **once**, then simply call:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
EventTrigger.trigger("loan.activated", data: { loan_id: 123, customer_name: "Kamlesh" })
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The gem determines automatically which **enabled** providers are
|
|
12
|
+
configured for that event and executes only those providers.
|
|
13
|
+
Anything can be sent in `data:` -- passed through to providers/handlers.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- Providers: **Slack**, **Email**, **Webhook**, plus custom providers
|
|
18
|
+
- Central `EventTrigger.configure` block (plain Ruby and Rails)
|
|
19
|
+
- Per-provider `enabled` flag + global `config.enabled` kill-switch
|
|
20
|
+
- Per-provider `events` allow-list (`nil`/empty = all events)
|
|
21
|
+
- Simple API: `trigger(name, data:)` + `on(name) { |event| }`
|
|
22
|
+
- Adapter architecture: every provider implements `#deliver(event)`
|
|
23
|
+
- Failure isolation: one provider failing never blocks others
|
|
24
|
+
- Optional logging: triggered / executed / skipped / failed
|
|
25
|
+
- No app-specific business logic -- just events + providers
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
gem "event_trigger"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bundle install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires Ruby >= 2.6. No runtime dependencies (stdlib only).
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
EventTrigger.configure do |config|
|
|
43
|
+
config.enabled = true
|
|
44
|
+
config.logger = Logger.new($stdout) # optional; nil = silent
|
|
45
|
+
|
|
46
|
+
config.slack.enabled = true
|
|
47
|
+
config.slack.webhook_url = ENV["SLACK_WEBHOOK_URL"]
|
|
48
|
+
config.slack.events = ["loan.activated", "loan.overdue", "payment.received"]
|
|
49
|
+
|
|
50
|
+
config.email.enabled = false
|
|
51
|
+
config.email.from = ENV["EVENT_TRIGGER_EMAIL_FROM"]
|
|
52
|
+
config.email.to = "ops@example.com"
|
|
53
|
+
config.email.events = ["loan.activated", "loan.foreclosed"]
|
|
54
|
+
|
|
55
|
+
config.webhook.enabled = true
|
|
56
|
+
config.webhook.url = ENV["EVENT_TRIGGER_WEBHOOK_URL"]
|
|
57
|
+
config.webhook.events = ["loan.activated"]
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Shortcuts `config.slack_webhook_url=` and `config.email_from=` delegate
|
|
62
|
+
to the per-provider configs.
|
|
63
|
+
|
|
64
|
+
`events` is an allow-list per provider. `nil` (default) or `[]` means
|
|
65
|
+
**ALL events** — the provider runs for every trigger (as long as it is
|
|
66
|
+
enabled):
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
config.slack.events = nil # or [] → all events
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Rails initializer
|
|
73
|
+
|
|
74
|
+
Copy `examples/initializer.rb` to `config/initializers/event_trigger.rb`.
|
|
75
|
+
A Railtie is shipped so the gem loads cleanly inside Rails.
|
|
76
|
+
|
|
77
|
+
## Triggering events
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
EventTrigger.trigger("loan.activated", data: { loan_id: 123, customer_name: "Kamlesh" })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Rules: `loan.activated` with Slack+Email enabled and subscribed runs both;
|
|
84
|
+
`loan.overdue` with only Slack subscribed runs Slack only; with
|
|
85
|
+
`config.enabled = false` nothing runs.
|
|
86
|
+
|
|
87
|
+
## Handlers
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
EventTrigger.on("loan.activated") do |event|
|
|
91
|
+
puts "Loan #{event.payload[:loan_id]} activated"
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`event` exposes `#name`, `#payload`, `#timestamp`. Use `off(name)` to
|
|
96
|
+
remove handlers. Handler errors are logged, never raised.
|
|
97
|
+
|
|
98
|
+
## Providers
|
|
99
|
+
|
|
100
|
+
Slack posts `{ text: }` to `hooks.slack.com` URLs (error payloads render as
|
|
101
|
+
error plus backtrace lines) and a rich embed (`username: RailsErrorNotifier`,
|
|
102
|
+
Backtrace/Context fields) to other webhook URLs. Email sends via Net::SMTP
|
|
103
|
+
in plain Ruby (or ActionMailer when configured with `mailer`). Webhook POSTs
|
|
104
|
+
`{ event:, payload:, timestamp: }` as JSON.
|
|
105
|
+
|
|
106
|
+
Custom provider:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class SmsProvider < EventTrigger::Provider
|
|
110
|
+
def deliver(event)
|
|
111
|
+
# use config + global_config
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
EventTrigger.register_provider(:sms, SmsProvider)
|
|
115
|
+
EventTrigger.configure { |c| c.for(:sms).enabled = true }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Architecture: EventTrigger -> Event -> Enabled Providers (Slack/Email/Webhook).
|
|
119
|
+
|
|
120
|
+
## Logging
|
|
121
|
+
|
|
122
|
+
Set `config.logger`. Logs: event triggered, provider executed, provider
|
|
123
|
+
skipped (disabled / not subscribed), provider failed, handler failed.
|
|
124
|
+
|
|
125
|
+
## Public API
|
|
126
|
+
|
|
127
|
+
`configure`, `configuration`, `reset!`, `trigger(name, data:)`, `on`, `off`,
|
|
128
|
+
`register_provider`, `Provider#deliver`, `Registry`, `Dispatcher`.
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
bundle install
|
|
134
|
+
bundle exec rspec
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
See `examples/plain_ruby.rb` for a runnable demo.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT -- see LICENSE.txt.
|
|
142
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module EventTrigger
|
|
6
|
+
# Per-provider configuration. Every provider (built-in or custom) gets
|
|
7
|
+
# an instance of this class with its own +enabled+, +events+ and
|
|
8
|
+
# provider-specific settings stored in +options+.
|
|
9
|
+
class ProviderConfig
|
|
10
|
+
attr_accessor :enabled
|
|
11
|
+
attr_writer :events
|
|
12
|
+
|
|
13
|
+
def initialize(enabled: false, events: nil)
|
|
14
|
+
@enabled = enabled
|
|
15
|
+
@events = events # nil => subscribed to ALL events
|
|
16
|
+
@options = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# List of event names this provider cares about, or nil for "all events".
|
|
20
|
+
# Always returns strings (or nil).
|
|
21
|
+
def events
|
|
22
|
+
return nil if @events.nil?
|
|
23
|
+
|
|
24
|
+
Array(@events).map(&:to_s)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# True when this provider should run for +event_name+.
|
|
28
|
+
# nil/empty events list means "all events".
|
|
29
|
+
def subscribed_to?(event_name)
|
|
30
|
+
list = events
|
|
31
|
+
return true if list.nil? || list.empty?
|
|
32
|
+
|
|
33
|
+
list.include?(event_name.to_s)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Generic option bag for provider-specific settings
|
|
37
|
+
# (webhook_url, from, url, ...). Allows:
|
|
38
|
+
# config.slack.webhook_url = "..."
|
|
39
|
+
def method_missing(name, *args)
|
|
40
|
+
name_str = name.to_s
|
|
41
|
+
if name_str.end_with?("=")
|
|
42
|
+
@options[name_str.chomp("=").to_sym] = args.first
|
|
43
|
+
elsif @options.key?(name)
|
|
44
|
+
@options[name]
|
|
45
|
+
elsif args.empty?
|
|
46
|
+
nil
|
|
47
|
+
else
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def respond_to_missing?(name, include_private = false)
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def options
|
|
57
|
+
@options.dup
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_h
|
|
61
|
+
{ enabled: enabled, events: events }.merge(@options)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Global configuration object yielded by EventTrigger.configure.
|
|
66
|
+
class Configuration
|
|
67
|
+
attr_accessor :enabled, :logger
|
|
68
|
+
attr_reader :slack, :email, :webhook
|
|
69
|
+
|
|
70
|
+
# Backwards-compatible top-level shortcuts required by the spec:
|
|
71
|
+
# config.slack_webhook_url = ENV["SLACK_WEBHOOK_URL"]
|
|
72
|
+
# config.email_from = ENV["EVENT_TRIGGER_EMAIL_FROM"]
|
|
73
|
+
# They delegate to the per-provider configs.
|
|
74
|
+
def initialize
|
|
75
|
+
@enabled = true
|
|
76
|
+
@logger = nil
|
|
77
|
+
@slack = ProviderConfig.new
|
|
78
|
+
@email = ProviderConfig.new
|
|
79
|
+
@webhook = ProviderConfig.new
|
|
80
|
+
@custom = {}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def slack_webhook_url
|
|
84
|
+
slack.webhook_url
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def slack_webhook_url=(value)
|
|
88
|
+
slack.webhook_url = value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def email_from
|
|
92
|
+
email.from
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def email_from=(value)
|
|
96
|
+
email.from = value
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Access (or lazily create) the config for any provider name,
|
|
100
|
+
# including future/custom providers:
|
|
101
|
+
# config.for(:sms).enabled = true
|
|
102
|
+
def for(name)
|
|
103
|
+
key = name.to_sym
|
|
104
|
+
case key
|
|
105
|
+
when :slack then slack
|
|
106
|
+
when :email then email
|
|
107
|
+
when :webhook then webhook
|
|
108
|
+
else
|
|
109
|
+
@custom[key] ||= ProviderConfig.new
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
alias provider for
|
|
113
|
+
|
|
114
|
+
# All provider configs keyed by name, including custom ones.
|
|
115
|
+
def providers
|
|
116
|
+
{ slack: slack, email: email, webhook: webhook }.merge(@custom)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def enabled?
|
|
120
|
+
!!@enabled
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventTrigger
|
|
4
|
+
# Default configuration applied when the gem is bundled into an app.
|
|
5
|
+
#
|
|
6
|
+
# Runs via the Railtie BEFORE the host app's initializers, so a
|
|
7
|
+
# user-provided config/initializers/event_trigger.rb can override
|
|
8
|
+
# anything set here. It also means the gem works out-of-the-box with
|
|
9
|
+
# zero configuration: this exact block is the default.
|
|
10
|
+
#
|
|
11
|
+
# EventTrigger.configure do |config|
|
|
12
|
+
# config.enabled = true
|
|
13
|
+
# config.slack.enabled = true
|
|
14
|
+
# config.slack.webhook_url = ENV["SLACK_WEBHOOK_URL"]
|
|
15
|
+
# config.slack.events = ["loan.activated", "loan.overdue", "payment.received"]
|
|
16
|
+
# config.email.enabled = false
|
|
17
|
+
# config.email.from = ENV["EVENT_TRIGGER_EMAIL_FROM"]
|
|
18
|
+
# ...
|
|
19
|
+
# end
|
|
20
|
+
def self.apply_defaults!(logger: nil)
|
|
21
|
+
configure do |config|
|
|
22
|
+
config.enabled = true
|
|
23
|
+
config.logger ||= logger
|
|
24
|
+
|
|
25
|
+
config.slack.enabled = true
|
|
26
|
+
config.slack.webhook_url ||= ENV["SLACK_WEBHOOK_URL"]
|
|
27
|
+
config.slack.events = ["loan.activated", "loan.overdue", "payment.received"]
|
|
28
|
+
|
|
29
|
+
config.email.enabled = false
|
|
30
|
+
config.email.from ||= ENV["EVENT_TRIGGER_EMAIL_FROM"]
|
|
31
|
+
config.email.to ||= ENV["EVENT_TRIGGER_EMAIL_TO"] || "ops@example.com"
|
|
32
|
+
config.email.events = ["loan.activated", "loan.foreclosed"]
|
|
33
|
+
|
|
34
|
+
config.webhook.enabled = false
|
|
35
|
+
config.webhook.url ||= ENV["EVENT_TRIGGER_WEBHOOK_URL"]
|
|
36
|
+
# Leave webhook subscribed to nothing until the app opts in.
|
|
37
|
+
config.webhook.events ||= []
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventTrigger
|
|
4
|
+
# Fans an Event out to every enabled provider subscribed to that event,
|
|
5
|
+
# then runs any user-registered handlers (EventTrigger.on).
|
|
6
|
+
#
|
|
7
|
+
# Guarantees:
|
|
8
|
+
# - Global disable (config.enabled == false) => nothing runs.
|
|
9
|
+
# - Only enabled providers subscribed to the event run.
|
|
10
|
+
# - Unknown provider names are skipped (with a log line) instead of crashing.
|
|
11
|
+
# - One provider raising never prevents the others from running;
|
|
12
|
+
# failures are logged and swallowed so all providers always run.
|
|
13
|
+
# - Handler blocks are likewise isolated: a failing handler is logged
|
|
14
|
+
# and the rest still run.
|
|
15
|
+
class Dispatcher
|
|
16
|
+
def initialize(configuration)
|
|
17
|
+
@configuration = configuration
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the list of provider instances that were executed.
|
|
21
|
+
def dispatch(event, handlers = [])
|
|
22
|
+
log("event triggered: #{event.name} payload=#{event.payload.inspect}")
|
|
23
|
+
return [] unless @configuration.enabled?
|
|
24
|
+
|
|
25
|
+
executed = []
|
|
26
|
+
providers_for(event.name).each do |name, provider_config|
|
|
27
|
+
klass = Registry.fetch(name)
|
|
28
|
+
if klass.nil?
|
|
29
|
+
log("provider ':#{name}' has no registered class; skipping")
|
|
30
|
+
next
|
|
31
|
+
end
|
|
32
|
+
unless provider_config.enabled
|
|
33
|
+
log("provider ':#{name}' skipped (disabled)")
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
unless provider_config.subscribed_to?(event.name)
|
|
37
|
+
log("provider ':#{name}' skipped (not subscribed to '#{event.name}')")
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
begin
|
|
41
|
+
klass.new(config: provider_config, global_config: @configuration).deliver(event)
|
|
42
|
+
log("provider ':#{name}' executed for '#{event.name}'")
|
|
43
|
+
executed << name
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
log("provider ':#{name}' failed for '#{event.name}': #{e.class}: #{e.message}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Array(handlers).each do |handler|
|
|
50
|
+
begin
|
|
51
|
+
handler.call(event)
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
log("handler for '#{event.name}' failed: #{e.class}: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
executed
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def providers_for(_event_name)
|
|
63
|
+
@configuration.providers
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def log(message)
|
|
67
|
+
@configuration.logger&.info("[EventTrigger] #{message}")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventTrigger
|
|
4
|
+
# Value object representing a single triggered event.
|
|
5
|
+
# Carries the event +name+ ("loan.activated"), an arbitrary +payload+ hash
|
|
6
|
+
# (anything the caller passes via data:), and a timestamp.
|
|
7
|
+
class Event
|
|
8
|
+
attr_reader :name, :payload, :timestamp
|
|
9
|
+
|
|
10
|
+
def initialize(name:, payload: {})
|
|
11
|
+
@name = name.to_s
|
|
12
|
+
@payload = symbolize(payload || {})
|
|
13
|
+
@timestamp = Time.now
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def [](key)
|
|
17
|
+
payload[key.to_sym] || payload[key.to_s]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_h
|
|
21
|
+
{ name: name, payload: payload, timestamp: timestamp }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def symbolize(hash)
|
|
27
|
+
return {} unless hash.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
hash.each_with_object({}) do |(k, v), memo|
|
|
30
|
+
memo[k.to_sym] = v
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventTrigger
|
|
4
|
+
# Common interface every notification provider must implement.
|
|
5
|
+
#
|
|
6
|
+
# To add a new provider, subclass this and implement #deliver(event):
|
|
7
|
+
#
|
|
8
|
+
# class SmsProvider < EventTrigger::Provider
|
|
9
|
+
# def deliver(event)
|
|
10
|
+
# # send SMS using config ...
|
|
11
|
+
# end
|
|
12
|
+
# end
|
|
13
|
+
# EventTrigger.register_provider(:sms, SmsProvider)
|
|
14
|
+
#
|
|
15
|
+
# Helpers available to subclasses:
|
|
16
|
+
# - #config -> ProviderConfig for this provider
|
|
17
|
+
# - #global_config -> full EventTrigger::Configuration
|
|
18
|
+
# - #log(message) -> write to configured logger, if any
|
|
19
|
+
class Provider
|
|
20
|
+
attr_reader :config, :global_config
|
|
21
|
+
|
|
22
|
+
def initialize(config:, global_config:)
|
|
23
|
+
@config = config
|
|
24
|
+
@global_config = global_config
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Deliver the notification for +event+. Must be implemented by subclasses.
|
|
28
|
+
def deliver(_event)
|
|
29
|
+
raise NotImplementedError, "#{self.class} must implement #deliver(event)"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Symbolic name derived from class name, e.g. SlackProvider -> :slack.
|
|
33
|
+
# Custom providers can override .provider_name.
|
|
34
|
+
def self.provider_name
|
|
35
|
+
name.to_s.split("::").last.to_s.sub(/Provider\z/, "").downcase.to_sym
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def provider_name
|
|
39
|
+
self.class.provider_name
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
protected
|
|
43
|
+
|
|
44
|
+
def log(message)
|
|
45
|
+
logger = global_config.logger
|
|
46
|
+
logger&.info("[EventTrigger][#{provider_name}] #{message}")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../provider"
|
|
4
|
+
|
|
5
|
+
module EventTrigger
|
|
6
|
+
module Providers
|
|
7
|
+
# Sends event notifications via email.
|
|
8
|
+
#
|
|
9
|
+
# Works in two modes:
|
|
10
|
+
# 1. Rails mode -- if ActionMailer is loaded and
|
|
11
|
+
# config.email.mailer is set (a mailer class) or
|
|
12
|
+
# config.email.delivery_method == :action_mailer, the event is
|
|
13
|
+
# delivered through ActionMailer.
|
|
14
|
+
# 2. Plain-Ruby mode -- otherwise delivered via stdlib Net::SMTP using
|
|
15
|
+
# config.email.smtp_settings (+ from/to addresses).
|
|
16
|
+
#
|
|
17
|
+
# Config:
|
|
18
|
+
# config.email.enabled = true
|
|
19
|
+
# config.email.from = "no-reply@example.com"
|
|
20
|
+
# config.email.to = "ops@example.com" # default recipient
|
|
21
|
+
# config.email.subject_prefix = "[EventTrigger]" # optional
|
|
22
|
+
# config.email.events = ["loan.activated"]
|
|
23
|
+
#
|
|
24
|
+
# Per-trigger overrides: EventTrigger.trigger("x", data: { email_to:, email_subject:, ... })
|
|
25
|
+
class EmailProvider < Provider
|
|
26
|
+
def self.provider_name
|
|
27
|
+
:email
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def deliver(event)
|
|
31
|
+
from = config.from || global_config.email_from
|
|
32
|
+
to = event.payload[:email_to] || event.payload["email_to"] || config.to
|
|
33
|
+
raise Error, "Email `from` is not configured" if from.nil? || from.to_s.strip.empty?
|
|
34
|
+
raise Error, "Email `to` is not configured" if to.nil? || to.to_s.strip.empty?
|
|
35
|
+
|
|
36
|
+
subject = event.payload[:email_subject] || event.payload["email_subject"] ||
|
|
37
|
+
build_subject(event)
|
|
38
|
+
body = build_body(event)
|
|
39
|
+
|
|
40
|
+
if action_mailer_delivery?
|
|
41
|
+
deliver_via_action_mailer(from, to, subject, body, event)
|
|
42
|
+
else
|
|
43
|
+
deliver_via_smtp(from, to, subject, body)
|
|
44
|
+
end
|
|
45
|
+
log("delivered event '#{event.name}' to #{Array(to).join(', ')}")
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def action_mailer_delivery?
|
|
52
|
+
defined?(ActionMailer::Base) && (config.mailer || config.delivery_method == :action_mailer)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def deliver_via_action_mailer(from, to, subject, body, _event)
|
|
56
|
+
mailer = config.mailer
|
|
57
|
+
if mailer
|
|
58
|
+
mailer.event_notification(from, to, subject, body).deliver_now
|
|
59
|
+
else
|
|
60
|
+
Class.new(ActionMailer::Base) do
|
|
61
|
+
def event_notification(from, to, subject, body)
|
|
62
|
+
mail(from: from, to: to, subject: subject, body: body)
|
|
63
|
+
end
|
|
64
|
+
end.event_notification(from, to, subject, body).deliver_now
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def deliver_via_smtp(from, to, subject, body)
|
|
69
|
+
require "net/smtp"
|
|
70
|
+
settings = smtp_settings
|
|
71
|
+
message = <<~MSG
|
|
72
|
+
From: #{from}
|
|
73
|
+
To: #{Array(to).join(", ")}
|
|
74
|
+
Subject: #{subject}
|
|
75
|
+
MIME-Version: 1.0
|
|
76
|
+
Content-Type: text/plain; charset=UTF-8
|
|
77
|
+
|
|
78
|
+
#{body}
|
|
79
|
+
MSG
|
|
80
|
+
Net::SMTP.start(
|
|
81
|
+
settings[:address] || "localhost",
|
|
82
|
+
settings[:port] || 25,
|
|
83
|
+
settings[:domain] || "localhost",
|
|
84
|
+
settings[:user_name],
|
|
85
|
+
settings[:password],
|
|
86
|
+
settings[:authentication] || :plain
|
|
87
|
+
) do |smtp|
|
|
88
|
+
smtp.send_message(message, from, Array(to))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def smtp_settings
|
|
93
|
+
config.smtp_settings || {}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def build_subject(event)
|
|
97
|
+
prefix = config.subject_prefix || "[EventTrigger]"
|
|
98
|
+
custom = event.payload[:subject] || event.payload["subject"]
|
|
99
|
+
return custom.to_s if custom
|
|
100
|
+
|
|
101
|
+
"#{prefix} #{event.name}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_body(event)
|
|
105
|
+
lines = ["Event: #{event.name}", "Time: #{event.timestamp}", ""]
|
|
106
|
+
event.payload.each do |k, v|
|
|
107
|
+
next if [:email_to, :email_subject, "email_to", "email_subject"].include?(k)
|
|
108
|
+
|
|
109
|
+
lines << "#{k}: #{v}"
|
|
110
|
+
end
|
|
111
|
+
lines.join("\n")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
require_relative "../provider"
|
|
8
|
+
|
|
9
|
+
module EventTrigger
|
|
10
|
+
module Providers
|
|
11
|
+
# Sends event notifications to Slack (or any Slack-compatible /
|
|
12
|
+
# Discord-compatible incoming-webhook URL).
|
|
13
|
+
#
|
|
14
|
+
# Config:
|
|
15
|
+
# config.slack.enabled = true
|
|
16
|
+
# config.slack.webhook_url = ENV["SLACK_WEBHOOK_URL"]
|
|
17
|
+
# config.slack.events = ["loan.activated"]
|
|
18
|
+
#
|
|
19
|
+
# Payload behaviour:
|
|
20
|
+
# - Slack incoming webhooks (hooks.slack.com) receive { text: ... }.
|
|
21
|
+
# - All other URLs (e.g. Discord-style webhooks) receive a rich embed
|
|
22
|
+
# payload with error/backtrace/context fields, truncated to stay
|
|
23
|
+
# within message limits.
|
|
24
|
+
class SlackProvider < Provider
|
|
25
|
+
MAX_TEXT = 1500
|
|
26
|
+
|
|
27
|
+
def self.provider_name
|
|
28
|
+
:slack
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def deliver(event)
|
|
32
|
+
url = config.webhook_url || config.url
|
|
33
|
+
raise Error, "Slack webhook_url is not configured" if url.nil? || url.to_s.strip.empty?
|
|
34
|
+
|
|
35
|
+
body = build_payload(url.to_s, event)
|
|
36
|
+
post_json(url.to_s, body)
|
|
37
|
+
log("delivered event '#{event.name}'")
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def build_payload(url, event)
|
|
44
|
+
payload = event.payload
|
|
45
|
+
if url.include?("hooks.slack.com")
|
|
46
|
+
{ text: slack_text(event, payload) }
|
|
47
|
+
else
|
|
48
|
+
{
|
|
49
|
+
username: "RailsErrorNotifier",
|
|
50
|
+
embeds: [
|
|
51
|
+
{
|
|
52
|
+
title: "🚨 Error Occurred",
|
|
53
|
+
description: embed_description(event, payload),
|
|
54
|
+
color: 0xFF0000,
|
|
55
|
+
fields: [
|
|
56
|
+
{ name: "Event", value: "```\n#{truncate(event.name)}\n```", inline: false },
|
|
57
|
+
{ name: "Backtrace", value: "```\n#{backtrace_text(payload)}\n```", inline: false },
|
|
58
|
+
{ name: "Context", value: "```\n#{context_text(payload)}\n```", inline: false }
|
|
59
|
+
],
|
|
60
|
+
timestamp: Time.now.utc.iso8601
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Slack text format: human summary; falls back to error/backtrace keys
|
|
68
|
+
# when present, otherwise dumps the generic payload.
|
|
69
|
+
def slack_text(event, payload)
|
|
70
|
+
if payload[:error] || payload["error"]
|
|
71
|
+
err = payload[:error] || payload["error"]
|
|
72
|
+
bt = payload[:backtrace] || payload["backtrace"]
|
|
73
|
+
lines = [err.to_s]
|
|
74
|
+
lines += Array(bt) if bt
|
|
75
|
+
lines.join("\n")
|
|
76
|
+
else
|
|
77
|
+
summary = payload.map { |k, v| "#{k}: #{v}" }.join(", ")
|
|
78
|
+
summary = event.payload.inspect if summary.empty?
|
|
79
|
+
"[#{event.name}] #{summary}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def embed_description(event, payload)
|
|
84
|
+
err = payload[:error] || payload["error"] || event.name
|
|
85
|
+
truncate(err.to_s)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def backtrace_text(payload)
|
|
89
|
+
bt = payload[:backtrace] || payload["backtrace"] || ["No backtrace"]
|
|
90
|
+
truncate_for_discord(Array(bt).first(10).join("\n"))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def context_text(payload)
|
|
94
|
+
ctx = payload[:context] || payload["context"] || payload
|
|
95
|
+
truncate_for_discord(ctx.inspect)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def truncate(str, limit = MAX_TEXT)
|
|
99
|
+
s = str.to_s
|
|
100
|
+
s.length > limit ? "#{s[0, limit]}…" : s
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def truncate_for_discord(str, limit = 1000)
|
|
104
|
+
truncate(str, limit)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def post_json(url, body)
|
|
108
|
+
uri = URI.parse(url)
|
|
109
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
110
|
+
http.use_ssl = uri.scheme == "https"
|
|
111
|
+
http.open_timeout = 5
|
|
112
|
+
http.read_timeout = 10
|
|
113
|
+
request = Net::HTTP::Post.new(uri.request_uri, "Content-Type" => "application/json")
|
|
114
|
+
request.body = JSON.generate(body)
|
|
115
|
+
response = http.request(request)
|
|
116
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
117
|
+
raise Error, "Slack delivery failed (HTTP #{response.code}): #{response.body}"
|
|
118
|
+
end
|
|
119
|
+
response
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "../provider"
|
|
7
|
+
|
|
8
|
+
module EventTrigger
|
|
9
|
+
module Providers
|
|
10
|
+
# POSTs the event as JSON to a configured HTTP endpoint.
|
|
11
|
+
#
|
|
12
|
+
# Config:
|
|
13
|
+
# config.webhook.enabled = true
|
|
14
|
+
# config.webhook.url = "https://example.com/hooks/events"
|
|
15
|
+
# config.webhook.headers = { "Authorization" => "Bearer ..." }
|
|
16
|
+
# config.webhook.events = ["loan.activated"]
|
|
17
|
+
class WebhookProvider < Provider
|
|
18
|
+
def self.provider_name
|
|
19
|
+
:webhook
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def deliver(event)
|
|
23
|
+
url = config.url
|
|
24
|
+
raise Error, "Webhook url is not configured" if url.nil? || url.to_s.strip.empty?
|
|
25
|
+
|
|
26
|
+
post_json(url.to_s, event_to_json(event), headers)
|
|
27
|
+
log("delivered event '#{event.name}' to #{url}")
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def headers
|
|
34
|
+
{ "Content-Type" => "application/json" }.merge(config.headers || {})
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def event_to_json(event)
|
|
38
|
+
{
|
|
39
|
+
event: event.name,
|
|
40
|
+
payload: stringify(event.payload),
|
|
41
|
+
timestamp: event.timestamp.to_s
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def stringify(hash)
|
|
46
|
+
hash.each_with_object({}) { |(k, v), m| m[k.to_s] = v }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def post_json(url, body, headers)
|
|
50
|
+
uri = URI.parse(url)
|
|
51
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
52
|
+
http.use_ssl = uri.scheme == "https"
|
|
53
|
+
http.open_timeout = 5
|
|
54
|
+
http.read_timeout = 10
|
|
55
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
56
|
+
request.body = JSON.generate(body)
|
|
57
|
+
response = http.request(request)
|
|
58
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
59
|
+
raise Error, "Webhook delivery failed (HTTP #{response.code}): #{response.body}"
|
|
60
|
+
end
|
|
61
|
+
response
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventTrigger
|
|
4
|
+
# Rails integration:
|
|
5
|
+
# 1. Applies safe defaults as soon as the gem is bundled (so the app
|
|
6
|
+
# works with zero configuration).
|
|
7
|
+
# 2. Auto-creates config/initializers/event_trigger.rb in the host app
|
|
8
|
+
# when missing, so the user always has an editable copy of the
|
|
9
|
+
# default block (no manual `rails generate` step needed). Never
|
|
10
|
+
# overwrites an existing file.
|
|
11
|
+
# 3. Hooks Rails.logger in automatically (unless the app already set one).
|
|
12
|
+
#
|
|
13
|
+
# Order: Railtie initializer runs BEFORE the host app's initializers, so
|
|
14
|
+
# anything in config/initializers/event_trigger.rb wins over these defaults.
|
|
15
|
+
if defined?(Rails::Railtie)
|
|
16
|
+
class Railtie < Rails::Railtie
|
|
17
|
+
generators do
|
|
18
|
+
require "generators/event_trigger/install_generator" if defined?(Rails::Generators)
|
|
19
|
+
rescue LoadError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
initializer "event_trigger.configure_rails_defaults", before: :load_environment_config do
|
|
24
|
+
require "event_trigger" unless defined?(EventTrigger::Dispatcher)
|
|
25
|
+
EventTrigger.apply_defaults!
|
|
26
|
+
if defined?(Rails.logger) && Rails.logger && EventTrigger.configuration.logger.nil?
|
|
27
|
+
EventTrigger.configuration.logger = Rails.logger
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Runs after app initializers are loaded: if the app has no
|
|
32
|
+
# initializer of its own, write the default one so it exists on
|
|
33
|
+
# disk for the user to edit. Runs in every env except test (to
|
|
34
|
+
# keep the test suite hermetic).
|
|
35
|
+
initializer "event_trigger.ensure_initializer_present", after: :load_environment do
|
|
36
|
+
next unless defined?(Rails.root) && Rails.root
|
|
37
|
+
next if Rails.env.test? rescue nil
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
dest = Rails.root.join("config/initializers/event_trigger.rb")
|
|
41
|
+
next if File.exist?(dest)
|
|
42
|
+
|
|
43
|
+
src_candidates = [
|
|
44
|
+
File.expand_path("../../generators/event_trigger/templates/event_trigger.rb", __dir__),
|
|
45
|
+
File.expand_path("../../../generators/event_trigger/templates/event_trigger.rb", __FILE__)
|
|
46
|
+
]
|
|
47
|
+
src = src_candidates.find { |p| File.exist?(p) }
|
|
48
|
+
next unless src
|
|
49
|
+
|
|
50
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
51
|
+
FileUtils.cp(src, dest)
|
|
52
|
+
Rails.logger&.info("[EventTrigger] created #{dest} with defaults (edit to customize)")
|
|
53
|
+
rescue StandardError
|
|
54
|
+
nil # never break boot because of the initializer copy
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventTrigger
|
|
4
|
+
# Maps symbolic provider names (:slack, :email, :webhook, custom...)
|
|
5
|
+
# to provider classes. Custom providers register via
|
|
6
|
+
# EventTrigger.register_provider(:name, Klass).
|
|
7
|
+
module Registry
|
|
8
|
+
@providers = {}
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def register(name, klass)
|
|
12
|
+
@providers[name.to_sym] = klass
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fetch(name)
|
|
16
|
+
@providers[name.to_sym]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def all
|
|
20
|
+
@providers.dup
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def registered?(name)
|
|
24
|
+
@providers.key?(name.to_sym)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Restore the three built-in providers (used by reset!).
|
|
28
|
+
def reset!
|
|
29
|
+
@providers = {}
|
|
30
|
+
require_relative "providers/slack_provider"
|
|
31
|
+
require_relative "providers/email_provider"
|
|
32
|
+
require_relative "providers/webhook_provider"
|
|
33
|
+
register(:slack, Providers::SlackProvider)
|
|
34
|
+
register(:email, Providers::EmailProvider)
|
|
35
|
+
register(:webhook, Providers::WebhookProvider)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Registry.reset!
|
|
41
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "event_trigger/version"
|
|
4
|
+
require_relative "event_trigger/configuration"
|
|
5
|
+
require_relative "event_trigger/event"
|
|
6
|
+
require_relative "event_trigger/registry"
|
|
7
|
+
require_relative "event_trigger/provider"
|
|
8
|
+
require_relative "event_trigger/providers/slack_provider"
|
|
9
|
+
require_relative "event_trigger/providers/email_provider"
|
|
10
|
+
require_relative "event_trigger/providers/webhook_provider"
|
|
11
|
+
require_relative "event_trigger/dispatcher"
|
|
12
|
+
require_relative "event_trigger/defaults"
|
|
13
|
+
require_relative "event_trigger/railtie" if defined?(Rails::Railtie)
|
|
14
|
+
|
|
15
|
+
module EventTrigger
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Central configuration block:
|
|
20
|
+
#
|
|
21
|
+
# EventTrigger.configure do |config|
|
|
22
|
+
# config.enabled = true
|
|
23
|
+
# config.slack.enabled = true
|
|
24
|
+
# config.slack.webhook_url = ENV["SLACK_WEBHOOK_URL"]
|
|
25
|
+
# end
|
|
26
|
+
def configure
|
|
27
|
+
yield configuration
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Access (and memoize) the global configuration object.
|
|
31
|
+
def configuration
|
|
32
|
+
@configuration ||= Configuration.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Reset configuration + handlers + provider registry to defaults.
|
|
36
|
+
# Primarily useful for tests.
|
|
37
|
+
def reset!
|
|
38
|
+
@configuration = Configuration.new
|
|
39
|
+
@handlers = Hash.new { |h, k| h[k] = [] }
|
|
40
|
+
Registry.reset!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Register a handler block for a specific event name:
|
|
44
|
+
#
|
|
45
|
+
# EventTrigger.on("loan.activated") do |event|
|
|
46
|
+
# # custom notification logic
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# Returns the registered block.
|
|
50
|
+
def on(event_name, &block)
|
|
51
|
+
raise ArgumentError, "EventTrigger.on requires a block" unless block_given?
|
|
52
|
+
|
|
53
|
+
key = normalize_event_name(event_name)
|
|
54
|
+
handlers[key] << block
|
|
55
|
+
block
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Remove all handlers for an event (or all events when no name given).
|
|
59
|
+
def off(event_name = nil)
|
|
60
|
+
if event_name.nil?
|
|
61
|
+
@handlers = Hash.new { |h, k| h[k] = [] }
|
|
62
|
+
else
|
|
63
|
+
handlers.delete(normalize_event_name(event_name))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Registered handlers hash: { "event.name" => [proc, ...] }
|
|
68
|
+
def handlers
|
|
69
|
+
@handlers ||= Hash.new { |h, k| h[k] = [] }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Register a custom provider class under a symbolic name:
|
|
73
|
+
#
|
|
74
|
+
# class SmsProvider < EventTrigger::Provider; end
|
|
75
|
+
# EventTrigger.register_provider(:sms, SmsProvider)
|
|
76
|
+
def register_provider(name, klass)
|
|
77
|
+
Registry.register(name, klass)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The main public API:
|
|
81
|
+
#
|
|
82
|
+
# EventTrigger.trigger("loan.activated", data: { loan_id: 123 })
|
|
83
|
+
#
|
|
84
|
+
# Builds an EventTrigger::Event and hands it to the Dispatcher,
|
|
85
|
+
# which fans out to every enabled provider configured for that event.
|
|
86
|
+
# Returns the Event object.
|
|
87
|
+
def trigger(event_name, data: {})
|
|
88
|
+
event = Event.new(name: normalize_event_name(event_name), payload: data || {})
|
|
89
|
+
Dispatcher.new(configuration).dispatch(event, handlers_for(event.name))
|
|
90
|
+
event
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Handlers registered for a given event name (string).
|
|
94
|
+
def handlers_for(event_name)
|
|
95
|
+
handlers[normalize_event_name(event_name)].dup
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Normalize event names to strings ("loan.activated").
|
|
99
|
+
def normalize_event_name(name)
|
|
100
|
+
name.to_s
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module EventTrigger
|
|
6
|
+
module Generators
|
|
7
|
+
# Copies the default initializer into the host Rails app:
|
|
8
|
+
#
|
|
9
|
+
# rails generate event_trigger:install
|
|
10
|
+
#
|
|
11
|
+
# Pass --force to overwrite an existing initializer. The Railtie also
|
|
12
|
+
# auto-creates this file when missing, so in practice you rarely need
|
|
13
|
+
# to run the generator by hand.
|
|
14
|
+
class InstallGenerator < Rails::Generators::Base
|
|
15
|
+
source_root File.expand_path("templates", __dir__)
|
|
16
|
+
|
|
17
|
+
desc "Copy EventTrigger initializer to config/initializers/event_trigger.rb"
|
|
18
|
+
|
|
19
|
+
def copy_initializer
|
|
20
|
+
copy_file "event_trigger.rb",
|
|
21
|
+
"config/initializers/event_trigger.rb"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Default EventTrigger configuration for your Rails app.
|
|
4
|
+
# Auto-created by the gem on first boot (and by `rails generate event_trigger:install`).
|
|
5
|
+
# Adjust values for your environment.
|
|
6
|
+
|
|
7
|
+
EventTrigger.configure do |config|
|
|
8
|
+
config.enabled = true
|
|
9
|
+
# Optional logging (defaults to Rails.logger via the gem Railtie).
|
|
10
|
+
# Set to nil to silence, or Logger.new($stdout) for plain Ruby.
|
|
11
|
+
# config.logger = Rails.logger
|
|
12
|
+
|
|
13
|
+
config.slack.enabled = true
|
|
14
|
+
config.slack.webhook_url = ENV["SLACK_WEBHOOK_URL"]
|
|
15
|
+
# Allow-list of events for this provider. nil (or []) = ALL events.
|
|
16
|
+
# Replace the examples below with your own event names.
|
|
17
|
+
config.slack.events = ["loan.activated", "loan.overdue", "payment.received"]
|
|
18
|
+
|
|
19
|
+
config.email.enabled = false
|
|
20
|
+
config.email.from = ENV["EVENT_TRIGGER_EMAIL_FROM"]
|
|
21
|
+
config.email.to = ENV["EVENT_TRIGGER_EMAIL_TO"]
|
|
22
|
+
# nil (or []) = ALL events.
|
|
23
|
+
config.email.events = ["loan.activated", "loan.foreclosed"]
|
|
24
|
+
|
|
25
|
+
config.webhook.enabled = false
|
|
26
|
+
config.webhook.url = ENV["EVENT_TRIGGER_WEBHOOK_URL"]
|
|
27
|
+
# nil (or []) = ALL events.
|
|
28
|
+
config.webhook.events = ["loan.activated"]
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: event_trigger
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kamlesh Parmar
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-18 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rake
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '13.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '13.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: net-smtp
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.3'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.3'
|
|
55
|
+
description: 'EventTrigger provides a simple event system with pluggable notification
|
|
56
|
+
providers (Slack, Email, Webhook, custom). Configure providers once, then call EventTrigger.trigger("event.name",
|
|
57
|
+
data: {...}) and the gem dispatches to the enabled providers configured for that
|
|
58
|
+
event. Works in plain Ruby and Ruby on Rails.'
|
|
59
|
+
email:
|
|
60
|
+
- malviyak00@gmail.com
|
|
61
|
+
executables: []
|
|
62
|
+
extensions: []
|
|
63
|
+
extra_rdoc_files: []
|
|
64
|
+
files:
|
|
65
|
+
- LICENSE.txt
|
|
66
|
+
- README.md
|
|
67
|
+
- Rakefile
|
|
68
|
+
- lib/event_trigger.rb
|
|
69
|
+
- lib/event_trigger/configuration.rb
|
|
70
|
+
- lib/event_trigger/defaults.rb
|
|
71
|
+
- lib/event_trigger/dispatcher.rb
|
|
72
|
+
- lib/event_trigger/event.rb
|
|
73
|
+
- lib/event_trigger/provider.rb
|
|
74
|
+
- lib/event_trigger/providers/email_provider.rb
|
|
75
|
+
- lib/event_trigger/providers/slack_provider.rb
|
|
76
|
+
- lib/event_trigger/providers/webhook_provider.rb
|
|
77
|
+
- lib/event_trigger/railtie.rb
|
|
78
|
+
- lib/event_trigger/registry.rb
|
|
79
|
+
- lib/event_trigger/version.rb
|
|
80
|
+
- lib/generators/event_trigger/install_generator.rb
|
|
81
|
+
- lib/generators/event_trigger/templates/event_trigger.rb
|
|
82
|
+
- sig/event_trigger.rbs
|
|
83
|
+
homepage: https://github.com/mrmalvi/event_trigger
|
|
84
|
+
licenses:
|
|
85
|
+
- MIT
|
|
86
|
+
metadata:
|
|
87
|
+
homepage_uri: https://github.com/mrmalvi/event_trigger
|
|
88
|
+
source_code_uri: https://github.com/mrmalvi/event_trigger
|
|
89
|
+
changelog_uri: https://github.com/mrmalvi/event_trigger/blob/main/CHANGELOG.md
|
|
90
|
+
post_install_message:
|
|
91
|
+
rdoc_options: []
|
|
92
|
+
require_paths:
|
|
93
|
+
- lib
|
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: 2.6.0
|
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
requirements: []
|
|
105
|
+
rubygems_version: 3.4.19
|
|
106
|
+
signing_key:
|
|
107
|
+
specification_version: 4
|
|
108
|
+
summary: Configurable event-based notification system for Ruby and Rails.
|
|
109
|
+
test_files: []
|