event_trigger 0.1.1 → 0.1.2

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.
@@ -1,12 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- require "twilio-ruby"
5
- rescue LoadError
6
- # Optional at load time: #deliver raises a clear EventTrigger::Error
7
- # when the gem is missing, which the Dispatcher logs without
8
- # crashing the host app.
9
- end
3
+ # NOTE: twilio-ruby is required lazily inside #deliver — see SmsProvider.
4
+ # Keeping it out of load time means the gem pays nothing when WhatsApp
5
+ # is disabled / unused (requirement: no eager provider deps).
10
6
 
11
7
  require_relative "../provider"
12
8
 
@@ -21,7 +17,7 @@ module EventTrigger
21
17
  # config.whatsapp.twilio_token = ENV["TWILIO_AUTH_TOKEN"]
22
18
  # config.whatsapp.twilio_from = "whatsapp:+14155238886"
23
19
  # config.whatsapp.twilio_to = "whatsapp:+919876543210"
24
- # config.whatsapp.events = ["app.error"]
20
+ # config.whatsapp.events = ["payment.received", "loan.overdue"]
25
21
  #
26
22
  # Aliases: account_sid/sid, auth_token/token, from/to.
27
23
  # Per-trigger override: data: { whatsapp_to: "whatsapp:+..." }
@@ -37,8 +33,9 @@ module EventTrigger
37
33
  end
38
34
 
39
35
  def deliver(event)
40
- unless defined?(Twilio::REST::Client)
41
- raise Error, 'twilio-ruby gem is not installed (add `gem "twilio-ruby"` to your Gemfile)'
36
+ client_class = twilio_client_class
37
+ unless client_class
38
+ raise Error, TwilioClient::GEM_MISSING_MESSAGE
42
39
  end
43
40
 
44
41
  sid = config.twilio_sid || config.account_sid || config.sid
@@ -52,7 +49,7 @@ module EventTrigger
52
49
  raise Error, "WhatsApp sender (twilio_from) is not configured" if blank?(from)
53
50
  raise Error, "WhatsApp recipient (twilio_to) is not configured" if blank?(to)
54
51
 
55
- client = Twilio::REST::Client.new(sid.to_s, token.to_s)
52
+ client = TwilioClient.resolve.new(sid.to_s, token.to_s)
56
53
  client.messages.create(
57
54
  from: normalize_number(from),
58
55
  to: normalize_number(to),
@@ -62,8 +59,15 @@ module EventTrigger
62
59
  true
63
60
  end
64
61
 
62
+
65
63
  private
66
64
 
65
+ # Thin wrapper around the lazy resolver — kept as an instance method
66
+ # so tests can stub it (e.g. simulate "gem missing" deterministically).
67
+ def twilio_client_class
68
+ TwilioClient.resolve
69
+ end
70
+
67
71
  def build_body(event)
68
72
  payload = event.payload
69
73
  err = payload[:error] || payload["error"]
@@ -1,5 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Explicit: FileUtils is used by the initializer bootstrap below.
4
+ # Must never raise at require time.
5
+ begin
6
+ require "fileutils"
7
+ rescue LoadError
8
+ nil
9
+ end
10
+
3
11
  module EventTrigger
4
12
  # Rails integration:
5
13
  # 1. Applies safe defaults as soon as the gem is bundled (so the app
@@ -12,31 +20,42 @@ module EventTrigger
12
20
  #
13
21
  # Order: Railtie initializer runs BEFORE the host app's initializers, so
14
22
  # anything in config/initializers/event_trigger.rb wins over these defaults.
23
+ #
24
+ # Boot safety: every hook is wrapped so a failure here can never stop
25
+ # the host application from booting.
15
26
  if defined?(Rails::Railtie)
16
27
  class Railtie < Rails::Railtie
17
28
  generators do
18
29
  require "generators/event_trigger/install_generator" if defined?(Rails::Generators)
19
- rescue LoadError
30
+ rescue StandardError
20
31
  nil
21
32
  end
22
33
 
23
34
  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
35
+ begin
36
+ require "event_trigger" unless defined?(EventTrigger::Dispatcher)
37
+ EventTrigger.apply_defaults!
38
+ if defined?(Rails.logger) && Rails.logger && EventTrigger.configuration.logger.nil?
39
+ EventTrigger.configuration.logger = Rails.logger
40
+ end
41
+ rescue StandardError => e
42
+ # Never break app boot because of notification defaults.
43
+ begin
44
+ Rails.logger&.warn("[EventTrigger] defaults could not be applied: #{e.class}: #{e.message}")
45
+ rescue StandardError
46
+ nil
47
+ end
28
48
  end
29
49
  end
30
50
 
31
51
  # Runs after app initializers are loaded: if the app has no
32
52
  # 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).
53
+ # disk for the user to edit. Skipped in test to keep suites hermetic.
35
54
  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
55
  begin
56
+ next unless defined?(Rails.root) && Rails.root
57
+ next if defined?(Rails.env) && Rails.env.test?
58
+
40
59
  dest = Rails.root.join("config/initializers/event_trigger.rb")
41
60
  next if File.exist?(dest)
42
61
 
@@ -9,22 +9,30 @@ module EventTrigger
9
9
 
10
10
  class << self
11
11
  def register(name, klass)
12
- @providers[name.to_sym] = klass
12
+ EventTrigger.lock.synchronize { @providers[name.to_sym] = klass }
13
+ rescue StandardError
14
+ nil
13
15
  end
14
16
 
15
17
  def fetch(name)
16
18
  @providers[name.to_sym]
19
+ rescue StandardError
20
+ nil
17
21
  end
18
22
 
19
23
  def all
20
24
  @providers.dup
25
+ rescue StandardError
26
+ {}
21
27
  end
22
28
 
23
29
  def registered?(name)
24
30
  @providers.key?(name.to_sym)
31
+ rescue StandardError
32
+ false
25
33
  end
26
34
 
27
- # Restore the five built-in providers (used by reset!).
35
+ # Restore the eight built-in providers (used by reset!).
28
36
  def reset!
29
37
  @providers = {}
30
38
  require_relative "providers/slack_provider"
@@ -32,11 +40,17 @@ module EventTrigger
32
40
  require_relative "providers/webhook_provider"
33
41
  require_relative "providers/discord_provider"
34
42
  require_relative "providers/whatsapp_provider"
43
+ require_relative "providers/telegram_provider"
44
+ require_relative "providers/teams_provider"
45
+ require_relative "providers/sms_provider"
35
46
  register(:slack, Providers::SlackProvider)
36
47
  register(:email, Providers::EmailProvider)
37
48
  register(:webhook, Providers::WebhookProvider)
38
49
  register(:discord, Providers::DiscordProvider)
39
50
  register(:whatsapp, Providers::WhatsappProvider)
51
+ register(:telegram, Providers::TelegramProvider)
52
+ register(:teams, Providers::TeamsProvider)
53
+ register(:sms, Providers::SmsProvider)
40
54
  end
41
55
  end
42
56
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module EventTrigger
6
+ # Reentrant lock shared by the provider registry, configuration and
7
+ # handler store (Monitor, not Mutex, so a callback may re-enter).
8
+ #
9
+ # Defined in its own file so it is available BEFORE registry.rb runs its
10
+ # load-time registration (otherwise built-in providers would silently
11
+ # fail to register at require time).
12
+ def self.lock
13
+ @lock ||= Monitor.new
14
+ rescue StandardError
15
+ # Extremely defensive: never let locking break loading.
16
+ Monitor.new
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EventTrigger
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/event_trigger.rb CHANGED
@@ -1,15 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "monitor"
3
4
  require_relative "event_trigger/version"
5
+ require_relative "event_trigger/threading"
4
6
  require_relative "event_trigger/configuration"
5
7
  require_relative "event_trigger/event"
6
8
  require_relative "event_trigger/registry"
7
9
  require_relative "event_trigger/provider"
10
+ require_relative "event_trigger/providers/twilio_client"
8
11
  require_relative "event_trigger/providers/slack_provider"
9
12
  require_relative "event_trigger/providers/email_provider"
10
13
  require_relative "event_trigger/providers/webhook_provider"
11
14
  require_relative "event_trigger/providers/discord_provider"
12
15
  require_relative "event_trigger/providers/whatsapp_provider"
16
+ require_relative "event_trigger/providers/telegram_provider"
17
+ require_relative "event_trigger/providers/teams_provider"
18
+ require_relative "event_trigger/providers/sms_provider"
13
19
  require_relative "event_trigger/dispatcher"
14
20
  require_relative "event_trigger/defaults"
15
21
  require_relative "event_trigger/railtie" if defined?(Rails::Railtie)
@@ -30,16 +36,24 @@ module EventTrigger
30
36
  end
31
37
 
32
38
  # Access (and memoize) the global configuration object.
39
+ # Never raises: returns a fresh Configuration if anything goes wrong.
33
40
  def configuration
41
+ @configuration || EventTrigger.lock.synchronize { @configuration ||= Configuration.new }
42
+ rescue StandardError
34
43
  @configuration ||= Configuration.new
35
44
  end
36
45
 
37
46
  # Reset configuration + handlers + provider registry to defaults.
38
- # Primarily useful for tests.
47
+ # Primarily useful for tests. Never raises.
39
48
  def reset!
40
- @configuration = Configuration.new
41
- @handlers = Hash.new { |h, k| h[k] = [] }
42
- Registry.reset!
49
+ EventTrigger.lock.synchronize do
50
+ @configuration = Configuration.new
51
+ @handlers = Hash.new { |h, k| h[k] = [] }
52
+ @last_result = nil
53
+ Registry.reset!
54
+ end
55
+ rescue StandardError
56
+ nil
43
57
  end
44
58
 
45
59
  # Register a handler block for a specific event name:
@@ -53,16 +67,18 @@ module EventTrigger
53
67
  raise ArgumentError, "EventTrigger.on requires a block" unless block_given?
54
68
 
55
69
  key = normalize_event_name(event_name)
56
- handlers[key] << block
70
+ EventTrigger.lock.synchronize { handlers[key] << block }
57
71
  block
58
72
  end
59
73
 
60
74
  # Remove all handlers for an event (or all events when no name given).
61
75
  def off(event_name = nil)
62
- if event_name.nil?
63
- @handlers = Hash.new { |h, k| h[k] = [] }
64
- else
65
- handlers.delete(normalize_event_name(event_name))
76
+ EventTrigger.lock.synchronize do
77
+ if event_name.nil?
78
+ @handlers = Hash.new { |h, k| h[k] = [] }
79
+ else
80
+ handlers.delete(normalize_event_name(event_name))
81
+ end
66
82
  end
67
83
  end
68
84
 
@@ -85,21 +101,67 @@ module EventTrigger
85
101
  #
86
102
  # Builds an EventTrigger::Event and hands it to the Dispatcher,
87
103
  # which fans out to every enabled provider configured for that event.
88
- # Returns the Event object.
104
+ # Returns the Event object (unchanged). The provider-wise result hash
105
+ # ({ event:, status:, providers: {...} }) is available as
106
+ # EventTrigger.last_result and as event.last_result.
107
+ #
108
+ # NEVER RAISES: any unexpected internal error is logged and captured
109
+ # in the result (status: "error") so the host app can never be harmed
110
+ # by this gem.
89
111
  def trigger(event_name, data: {})
90
- event = Event.new(name: normalize_event_name(event_name), payload: data || {})
91
- Dispatcher.new(configuration).dispatch(event, handlers_for(event.name))
112
+ event = build_event(event_name, data)
113
+ begin
114
+ result = Dispatcher.new(configuration).dispatch(event, handlers_for(event.name))
115
+ rescue StandardError => e
116
+ result = { event: event.name, status: "error", error: "#{e.class}: #{e.message}", providers: {} }
117
+ safe_log("#{e.class}: #{e.message}")
118
+ end
119
+ @last_result = result
120
+ event.last_result = result
92
121
  event
93
122
  end
94
123
 
124
+ # Provider-wise result of the most recent trigger call, e.g.:
125
+ # { event: "loan.activated", status: "success",
126
+ # providers: { slack: "sent", email: "sent", discord: "skipped (disabled)", ... } }
127
+ # Nil until the first trigger call.
128
+ def last_result
129
+ @last_result
130
+ end
131
+
95
132
  # Handlers registered for a given event name (string).
133
+ # Uses #fetch so unknown event names don't grow the handler table.
96
134
  def handlers_for(event_name)
97
- handlers[normalize_event_name(event_name)].dup
135
+ EventTrigger.lock.synchronize { handlers.fetch(normalize_event_name(event_name), []).dup }
136
+ rescue StandardError
137
+ []
98
138
  end
99
139
 
100
140
  # Normalize event names to strings ("loan.activated").
101
141
  def normalize_event_name(name)
102
142
  name.to_s
143
+ rescue StandardError
144
+ ""
145
+ end
146
+
147
+ private
148
+
149
+ # Builds the Event defensively: even a pathological payload (odd keys,
150
+ # objects whose #to_s raises, non-hash data) cannot raise here.
151
+ def build_event(event_name, data)
152
+ Event.new(name: normalize_event_name(event_name), payload: data || {})
153
+ rescue StandardError
154
+ Event.new(name: "", payload: {})
155
+ end
156
+
157
+ # Logging must never raise into the host app.
158
+ def safe_log(message)
159
+ logger = configuration.logger
160
+ return unless logger.respond_to?(:info)
161
+
162
+ logger.info("[EventTrigger] #{message}")
163
+ rescue StandardError
164
+ nil
103
165
  end
104
166
  end
105
167
  end
@@ -19,11 +19,33 @@ EventTrigger.configure do |config|
19
19
  config.email.enabled = false
20
20
  config.email.from = ENV["EVENT_TRIGGER_EMAIL_FROM"]
21
21
  config.email.to = ENV["EVENT_TRIGGER_EMAIL_TO"]
22
+ # Rails apps: uncomment so delivery goes through ActionMailer
23
+ # (which uses config.action_mailer.smtp_settings below).
24
+ # config.email.delivery_method = :action_mailer
22
25
  # nil (or []) = ALL events.
23
26
  config.email.events = ["loan.activated", "loan.foreclosed"]
24
27
 
28
+ # --- ActionMailer SMTP (Rails) ---
29
+ # Uncomment and move these lines to config/environments/development.rb
30
+ # (and production.rb). Requires `gem "net-smtp"` on Ruby 3.1+.
31
+ # config.action_mailer.delivery_method = :smtp
32
+ # config.action_mailer.smtp_settings = {
33
+ # address: ENV.fetch("SMTP_ADDRESS", "smtp.gmail.com"),
34
+ # port: ENV.fetch("SMTP_PORT", "587").to_i,
35
+ # domain: ENV.fetch("SMTP_DOMAIN", "gmail.com"),
36
+ # user_name: ENV["SMTP_USERNAME"],
37
+ # password: ENV["SMTP_PASSWORD"]&.delete(" "),
38
+ # authentication: :plain,
39
+ # enable_starttls_auto: true
40
+ # }
41
+ # .env entries needed:
42
+ # SMTP_USERNAME=youremail@gmail.com
43
+ # SMTP_PASSWORD=xxxx xxxx xxxx xxxx # Gmail App Password (16 letters), not your login password
44
+
25
45
  config.webhook.enabled = false
26
46
  config.webhook.url = ENV["EVENT_TRIGGER_WEBHOOK_URL"]
47
+ # NOTE: `method` collides with Ruby's Object#method, so set it via []:
48
+ # config.webhook[:method] = :post # :post (default) | :put | :patch | :delete | :get
27
49
  # nil (or []) = ALL events.
28
50
  config.webhook.events = ["loan.activated"]
29
51
 
@@ -39,4 +61,26 @@ EventTrigger.configure do |config|
39
61
  config.whatsapp.twilio_to = ENV["TWILIO_WHATSAPP_TO"]
40
62
  # nil (or []) = ALL events.
41
63
  config.whatsapp.events = []
64
+
65
+ # --- NEW providers (opt-in; all default to disabled) ---
66
+ config.telegram.enabled = false
67
+ config.telegram.bot_token = ENV["TELEGRAM_BOT_TOKEN"]
68
+ config.telegram.chat_id = ENV["TELEGRAM_CHAT_ID"]
69
+ # nil (or []) = ALL events.
70
+ config.telegram.events = []
71
+
72
+ config.teams.enabled = false
73
+ config.teams.access_token = ENV["TEAMS_ACCESS_TOKEN"]
74
+ config.teams.team_id = ENV["TEAMS_TEAM_ID"]
75
+ config.teams.channel_id = ENV["TEAMS_CHANNEL_ID"]
76
+ # nil (or []) = ALL events.
77
+ config.teams.events = []
78
+
79
+ config.sms.enabled = false
80
+ config.sms.provider = :twilio
81
+ config.sms.account_sid = ENV["TWILIO_ACCOUNT_SID"]
82
+ config.sms.auth_token = ENV["TWILIO_AUTH_TOKEN"]
83
+ config.sms.from = ENV["TWILIO_FROM"]
84
+ # nil (or []) = ALL events.
85
+ config.sms.events = []
42
86
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: event_trigger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kamlesh Parmar
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-19 00:00:00.000000000 Z
11
+ date: 2026-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -133,10 +133,15 @@ files:
133
133
  - lib/event_trigger/providers/email_provider.rb
134
134
  - lib/event_trigger/providers/http_poster.rb
135
135
  - lib/event_trigger/providers/slack_provider.rb
136
+ - lib/event_trigger/providers/sms_provider.rb
137
+ - lib/event_trigger/providers/teams_provider.rb
138
+ - lib/event_trigger/providers/telegram_provider.rb
139
+ - lib/event_trigger/providers/twilio_client.rb
136
140
  - lib/event_trigger/providers/webhook_provider.rb
137
141
  - lib/event_trigger/providers/whatsapp_provider.rb
138
142
  - lib/event_trigger/railtie.rb
139
143
  - lib/event_trigger/registry.rb
144
+ - lib/event_trigger/threading.rb
140
145
  - lib/event_trigger/version.rb
141
146
  - lib/generators/event_trigger/install_generator.rb
142
147
  - lib/generators/event_trigger/templates/event_trigger.rb