notify-engine 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/CHANGELOG.md +21 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +514 -0
- data/Rakefile +8 -0
- data/lib/notify/adapters/base.rb +15 -0
- data/lib/notify/adapters/email.rb +88 -0
- data/lib/notify/config.rb +28 -0
- data/lib/notify/dispatcher.rb +239 -0
- data/lib/notify/engine.rb +35 -0
- data/lib/notify/errors.rb +25 -0
- data/lib/notify/mailer.rb +48 -0
- data/lib/notify/payload_class.rb +52 -0
- data/lib/notify/registry.rb +105 -0
- data/lib/notify/test_helper.rb +44 -0
- data/lib/notify/version.rb +5 -0
- data/lib/notify.rb +93 -0
- metadata +205 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notify
|
|
4
|
+
class Config
|
|
5
|
+
attr_accessor :templates_path
|
|
6
|
+
attr_reader :adapters, :messages
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@templates_path = "app/notify_templates"
|
|
10
|
+
@adapters = {
|
|
11
|
+
email: {
|
|
12
|
+
enabled: true,
|
|
13
|
+
from: nil,
|
|
14
|
+
delivery_method: :deliver_later,
|
|
15
|
+
default_recipients: [],
|
|
16
|
+
layout: false,
|
|
17
|
+
helpers: [],
|
|
18
|
+
subject_prefix: nil
|
|
19
|
+
},
|
|
20
|
+
telegram: {
|
|
21
|
+
enabled: false,
|
|
22
|
+
bots: []
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
@messages = {}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/inflections"
|
|
4
|
+
require "active_support/notifications"
|
|
5
|
+
|
|
6
|
+
module Notify
|
|
7
|
+
class Dispatcher
|
|
8
|
+
ADAPTER_RESOLUTION = {
|
|
9
|
+
email: {
|
|
10
|
+
recipient_method: :email_recipients,
|
|
11
|
+
recipient_kwarg: :email_to,
|
|
12
|
+
config_recipients_key: :email_recipients,
|
|
13
|
+
default_recipients_key: :default_recipients,
|
|
14
|
+
requires_subject: true
|
|
15
|
+
},
|
|
16
|
+
telegram: {
|
|
17
|
+
recipient_method: :tg_recipients,
|
|
18
|
+
recipient_kwarg: :tg_bots,
|
|
19
|
+
config_recipients_key: :tg_recipients,
|
|
20
|
+
default_recipients_key: nil,
|
|
21
|
+
requires_subject: false
|
|
22
|
+
}
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
def dispatch(name, **payload)
|
|
26
|
+
name = name.to_sym
|
|
27
|
+
validate_message!(name)
|
|
28
|
+
|
|
29
|
+
payload_instance = resolve_payload_class(name, payload)
|
|
30
|
+
adapter_keys = resolve_enabled_adapters(name)
|
|
31
|
+
errors = {}
|
|
32
|
+
|
|
33
|
+
ActiveSupport::Notifications.instrument("notify.message.dispatch",
|
|
34
|
+
message_name: name, adapters: adapter_keys) do
|
|
35
|
+
adapter_keys.each do |adapter_key|
|
|
36
|
+
dispatch_to_adapter(adapter_key, name, payload, payload_instance, errors)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
raise DeliveryError.new(errors) if errors.any? && errors.size == adapter_keys.size
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def capture_for_test(name, **payload)
|
|
44
|
+
name = name.to_sym
|
|
45
|
+
validate_message!(name)
|
|
46
|
+
|
|
47
|
+
payload_instance = resolve_payload_class(name, payload)
|
|
48
|
+
adapter_keys = resolve_enabled_adapters(name)
|
|
49
|
+
|
|
50
|
+
resolved_recipients = {}
|
|
51
|
+
resolved_subject = nil
|
|
52
|
+
resolved_locals = nil
|
|
53
|
+
errors = {}
|
|
54
|
+
|
|
55
|
+
adapter_keys.each do |adapter_key|
|
|
56
|
+
resolution = adapter_resolution_config(adapter_key)
|
|
57
|
+
begin
|
|
58
|
+
recipients = resolve_recipients(adapter_key, resolution, payload_instance, payload, name)
|
|
59
|
+
subj = resolve_subject(adapter_key, resolution, payload_instance, payload, name)
|
|
60
|
+
locals = resolve_locals(payload_instance, payload)
|
|
61
|
+
|
|
62
|
+
resolved_recipients[adapter_key] = recipients
|
|
63
|
+
resolved_subject = subj if subj && resolved_subject.nil?
|
|
64
|
+
resolved_locals ||= locals
|
|
65
|
+
rescue => e
|
|
66
|
+
errors[adapter_key] = e
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
delivery = {
|
|
71
|
+
name: name,
|
|
72
|
+
adapters: adapter_keys,
|
|
73
|
+
payload: payload,
|
|
74
|
+
resolved: {
|
|
75
|
+
subject: resolved_subject,
|
|
76
|
+
recipients: resolved_recipients,
|
|
77
|
+
locals: resolved_locals || resolve_locals(payload_instance, payload)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
delivery[:errors] = errors if errors.any?
|
|
81
|
+
delivery
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def validate_message!(name)
|
|
87
|
+
unless Notify.registry&.message?(name)
|
|
88
|
+
raise UnknownMessage, "Unknown message: #{name.inspect}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def resolve_payload_class(name, payload)
|
|
93
|
+
class_name = "NotifyTemplates::#{name.to_s.camelize}"
|
|
94
|
+
klass = class_name.safe_constantize
|
|
95
|
+
return nil unless klass.is_a?(Class) && klass < PayloadClass
|
|
96
|
+
|
|
97
|
+
klass.new(**payload)
|
|
98
|
+
rescue => e
|
|
99
|
+
log_warn("[Notify] PayloadClass #{class_name} failed to initialize: #{e.message}")
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def resolve_enabled_adapters(name)
|
|
104
|
+
registry_adapters = Notify.registry.adapters_for(name)
|
|
105
|
+
registered_classes = Notify.registered_adapters
|
|
106
|
+
|
|
107
|
+
registry_adapters.select do |adapter_key|
|
|
108
|
+
adapter_class = registered_classes[adapter_key]
|
|
109
|
+
unless adapter_class
|
|
110
|
+
log_warn("[Notify] Adapter :#{adapter_key} has templates but no registered class — skipping")
|
|
111
|
+
next false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
adapter_cfg = adapter_config(adapter_key)
|
|
115
|
+
adapter_cfg.fetch(:enabled, true)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def dispatch_to_adapter(adapter_key, name, raw_payload, payload_instance, errors)
|
|
120
|
+
adapter_class = Notify.registered_adapters[adapter_key]
|
|
121
|
+
resolution = adapter_resolution_config(adapter_key)
|
|
122
|
+
|
|
123
|
+
recipients = resolve_recipients(adapter_key, resolution, payload_instance, raw_payload, name)
|
|
124
|
+
subject = resolve_subject(adapter_key, resolution, payload_instance, raw_payload, name)
|
|
125
|
+
locals = resolve_locals(payload_instance, raw_payload)
|
|
126
|
+
options = resolve_options(adapter_key, payload_instance, raw_payload, name)
|
|
127
|
+
template_path = Notify.registry.templates_path.join(adapter_key.to_s)
|
|
128
|
+
|
|
129
|
+
ActiveSupport::Notifications.instrument("notify.adapter.deliver",
|
|
130
|
+
message_name: name, adapter: adapter_key, to: recipients) do
|
|
131
|
+
adapter_class.new.deliver(
|
|
132
|
+
message_name: name,
|
|
133
|
+
to: recipients,
|
|
134
|
+
subject: subject,
|
|
135
|
+
locals: locals,
|
|
136
|
+
template_path: template_path,
|
|
137
|
+
options: options
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
rescue => e
|
|
141
|
+
errors[adapter_key] = e
|
|
142
|
+
ActiveSupport::Notifications.instrument("notify.adapter.error",
|
|
143
|
+
message_name: name, adapter: adapter_key, error: e)
|
|
144
|
+
log_error("[Notify] Adapter :#{adapter_key} failed for :#{name}: #{e.message}")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def adapter_resolution_config(adapter_key)
|
|
148
|
+
ADAPTER_RESOLUTION[adapter_key] || {
|
|
149
|
+
recipient_method: :"#{adapter_key}_recipients",
|
|
150
|
+
recipient_kwarg: :to,
|
|
151
|
+
config_recipients_key: :recipients,
|
|
152
|
+
default_recipients_key: :default_recipients,
|
|
153
|
+
requires_subject: false
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def resolve_recipients(adapter_key, resolution, payload_instance, raw_payload, name)
|
|
158
|
+
recipients =
|
|
159
|
+
payload_instance&.resolve(resolution[:recipient_method]) ||
|
|
160
|
+
raw_payload[resolution[:recipient_kwarg]] ||
|
|
161
|
+
message_config(name)&.dig(resolution[:config_recipients_key]) ||
|
|
162
|
+
(resolution[:default_recipients_key] && adapter_config(adapter_key)&.dig(resolution[:default_recipients_key]))
|
|
163
|
+
|
|
164
|
+
recipients = Array(recipients)
|
|
165
|
+
|
|
166
|
+
if recipients.empty?
|
|
167
|
+
raise MissingRecipients, "No recipients resolvable for adapter :#{adapter_key}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
recipients
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def resolve_subject(adapter_key, resolution, payload_instance, raw_payload, name)
|
|
174
|
+
subject =
|
|
175
|
+
payload_instance&.resolve(:subject) ||
|
|
176
|
+
raw_payload[:subject] ||
|
|
177
|
+
message_config(name)&.dig(:subject)
|
|
178
|
+
|
|
179
|
+
if resolution[:requires_subject] && (subject.nil? || subject.to_s.strip.empty?)
|
|
180
|
+
raise MissingSubject, "No subject resolvable for adapter :#{adapter_key}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
subject
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def resolve_locals(payload_instance, raw_payload)
|
|
187
|
+
if payload_instance
|
|
188
|
+
payload_instance.resolve_locals
|
|
189
|
+
else
|
|
190
|
+
raw_payload.reject { |k, _| PayloadClass::RESERVED_KEYS.include?(k) }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def resolve_options(adapter_key, payload_instance, raw_payload, name)
|
|
195
|
+
options = {}
|
|
196
|
+
|
|
197
|
+
if adapter_key == :email
|
|
198
|
+
options[:from] =
|
|
199
|
+
payload_instance&.resolve(:email_from) ||
|
|
200
|
+
raw_payload[:email_from] ||
|
|
201
|
+
message_config(name)&.dig(:email_from) ||
|
|
202
|
+
adapter_config(adapter_key)&.dig(:from)
|
|
203
|
+
|
|
204
|
+
cc = payload_instance&.resolve(:email_cc) ||
|
|
205
|
+
raw_payload[:email_cc] ||
|
|
206
|
+
message_config(name)&.dig(:email_cc)
|
|
207
|
+
options[:cc] = cc if cc
|
|
208
|
+
|
|
209
|
+
bcc = payload_instance&.resolve(:email_bcc) ||
|
|
210
|
+
raw_payload[:email_bcc] ||
|
|
211
|
+
message_config(name)&.dig(:email_bcc)
|
|
212
|
+
options[:bcc] = bcc if bcc
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
options[:delivery_method] =
|
|
216
|
+
raw_payload[:delivery_method] ||
|
|
217
|
+
message_config(name)&.dig(:delivery_method) ||
|
|
218
|
+
adapter_config(adapter_key)&.dig(:delivery_method)
|
|
219
|
+
|
|
220
|
+
options
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def message_config(name)
|
|
224
|
+
Notify.config.messages[name]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def adapter_config(adapter_key)
|
|
228
|
+
Notify.config.adapters[adapter_key] || {}
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def log_warn(message)
|
|
232
|
+
Rails.logger.warn(message) if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def log_error(message)
|
|
236
|
+
Rails.logger.error(message) if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/module/delegation"
|
|
4
|
+
require "rails/engine"
|
|
5
|
+
|
|
6
|
+
module Notify
|
|
7
|
+
class Engine < ::Rails::Engine
|
|
8
|
+
initializer "notify_engine.autoload_paths" do |app|
|
|
9
|
+
templates_path = app.root.join(Notify.config.templates_path).to_s
|
|
10
|
+
|
|
11
|
+
app.config.autoload_paths << templates_path unless app.config.autoload_paths.include?(templates_path)
|
|
12
|
+
app.config.eager_load_paths << templates_path unless app.config.eager_load_paths.include?(templates_path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "notify_engine.register_adapters" do
|
|
16
|
+
Notify.register_adapter(:email, Notify::Adapters::Email)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
initializer "notify_engine.registry" do |app|
|
|
20
|
+
app.config.to_prepare do
|
|
21
|
+
Notify.build_registry!(app.root.join(Notify.config.templates_path))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
initializer "notify_engine.freeze_registry" do |app|
|
|
26
|
+
app.config.after_initialize do
|
|
27
|
+
Notify.registry&.freeze! if Rails.env.production?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
initializer "notify_engine.test_mode", after: "notify_engine.registry" do
|
|
32
|
+
Notify.test_mode! if Rails.env.test?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notify
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
class UnknownMessage < Error; end
|
|
6
|
+
class MissingRecipients < Error; end
|
|
7
|
+
class MissingSubject < Error; end
|
|
8
|
+
|
|
9
|
+
class DeliveryError < Error
|
|
10
|
+
attr_reader :adapter_errors
|
|
11
|
+
|
|
12
|
+
def initialize(adapter_errors = {})
|
|
13
|
+
@adapter_errors = adapter_errors
|
|
14
|
+
if adapter_errors.any?
|
|
15
|
+
adapter_msgs = adapter_errors.map { |adapter, err| "#{adapter}: #{err.message}" }
|
|
16
|
+
super("All adapters failed — #{adapter_msgs.join('; ')}")
|
|
17
|
+
else
|
|
18
|
+
super("All adapters failed")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class AdapterNotRegistered < Error; end
|
|
24
|
+
class NotImplementedError < Error; end
|
|
25
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_mailer"
|
|
4
|
+
|
|
5
|
+
module Notify
|
|
6
|
+
class Mailer < ActionMailer::Base
|
|
7
|
+
def dispatch(message_name, locals:, to:, subject:, from: nil, cc: nil, bcc: nil, template_path: nil)
|
|
8
|
+
prepend_view_path(template_path.to_s) if template_path
|
|
9
|
+
|
|
10
|
+
bridge_locals(locals)
|
|
11
|
+
|
|
12
|
+
mail_options = {
|
|
13
|
+
to: to,
|
|
14
|
+
subject: subject,
|
|
15
|
+
template_name: message_name.to_s,
|
|
16
|
+
template_path: ""
|
|
17
|
+
}
|
|
18
|
+
mail_options[:from] = from if from
|
|
19
|
+
mail_options[:cc] = cc if cc
|
|
20
|
+
mail_options[:bcc] = bcc if bcc
|
|
21
|
+
|
|
22
|
+
mail(mail_options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def bridge_locals(locals)
|
|
28
|
+
return unless locals.is_a?(Hash)
|
|
29
|
+
|
|
30
|
+
locals.each do |key, value|
|
|
31
|
+
key_s = key.to_s
|
|
32
|
+
if key_s.start_with?("_")
|
|
33
|
+
log_ivar_skip(key_s)
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
instance_variable_set(:"@#{key_s}", value)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def log_ivar_skip(key)
|
|
41
|
+
return unless defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
42
|
+
|
|
43
|
+
Rails.logger.warn(
|
|
44
|
+
"[Notify::Mailer] Skipping local '#{key}' — underscore-prefixed keys are reserved for framework internals"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notify
|
|
4
|
+
class PayloadClass
|
|
5
|
+
RESERVED_KEYS = %i[subject email_to email_from email_cc email_bcc tg_bots delivery_method].freeze
|
|
6
|
+
|
|
7
|
+
DSL_METHODS = %i[subject email_recipients email_from email_cc email_bcc tg_recipients locals].freeze
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def dsl_definitions
|
|
11
|
+
@_dsl_definitions ||= {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
DSL_METHODS.each do |method_name|
|
|
15
|
+
define_method(method_name) do |value = nil, &block|
|
|
16
|
+
if value.nil? && block.nil?
|
|
17
|
+
dsl_definitions[method_name]
|
|
18
|
+
else
|
|
19
|
+
dsl_definitions[method_name] = block || value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.new(**payload)
|
|
26
|
+
instance = allocate
|
|
27
|
+
instance.instance_variable_set(:@_raw_payload, payload.freeze)
|
|
28
|
+
instance.send(:initialize, **payload)
|
|
29
|
+
instance
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(**payload); end
|
|
33
|
+
|
|
34
|
+
def resolve(name)
|
|
35
|
+
if respond_to?(name)
|
|
36
|
+
public_send(name)
|
|
37
|
+
elsif self.class.dsl_definitions.key?(name)
|
|
38
|
+
dsl_val = self.class.dsl_definitions[name]
|
|
39
|
+
dsl_val.respond_to?(:call) ? instance_exec(&dsl_val) : dsl_val
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resolve_locals
|
|
44
|
+
result = resolve(:locals)
|
|
45
|
+
result.nil? ? default_locals : result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def default_locals
|
|
49
|
+
@_raw_payload.reject { |k, _| RESERVED_KEYS.include?(k) }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_view"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module Notify
|
|
7
|
+
class Registry
|
|
8
|
+
attr_reader :templates_path
|
|
9
|
+
|
|
10
|
+
def initialize(templates_path)
|
|
11
|
+
@templates_path = Pathname(templates_path)
|
|
12
|
+
@message_adapters = {}
|
|
13
|
+
scan! if @templates_path.directory?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def messages
|
|
17
|
+
@messages ||= build_messages_view
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def adapters
|
|
21
|
+
@adapters_view ||= build_adapters_view
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def message?(name)
|
|
25
|
+
@message_adapters.key?(name.to_sym)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def adapters_for(message_name)
|
|
29
|
+
set = @message_adapters[message_name.to_sym]
|
|
30
|
+
return [].freeze unless set
|
|
31
|
+
|
|
32
|
+
set.to_a.sort.freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def freeze!
|
|
36
|
+
@message_adapters.each_value(&:freeze)
|
|
37
|
+
@message_adapters.freeze
|
|
38
|
+
messages
|
|
39
|
+
adapters
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def frozen?
|
|
44
|
+
@message_adapters.frozen?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def scan!
|
|
50
|
+
handler_extensions = ActionView::Template::Handlers.extensions
|
|
51
|
+
|
|
52
|
+
@templates_path.children.select(&:directory?).each do |adapter_dir|
|
|
53
|
+
adapter_key = adapter_dir.basename.to_s.to_sym
|
|
54
|
+
|
|
55
|
+
adapter_dir.children.select { |entry| entry.file? }.each do |template_file|
|
|
56
|
+
basename = template_file.basename.to_s
|
|
57
|
+
next if basename.start_with?("_")
|
|
58
|
+
|
|
59
|
+
message_name = extract_message_name(basename, handler_extensions)
|
|
60
|
+
next unless message_name
|
|
61
|
+
|
|
62
|
+
@message_adapters[message_name] ||= Set.new
|
|
63
|
+
@message_adapters[message_name] << adapter_key
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def extract_message_name(filename, handler_extensions)
|
|
69
|
+
parts = filename.split(".")
|
|
70
|
+
return nil if parts.size < 2
|
|
71
|
+
|
|
72
|
+
handler = parts.last.to_sym
|
|
73
|
+
return nil unless handler_extensions.include?(handler)
|
|
74
|
+
|
|
75
|
+
parts.pop
|
|
76
|
+
parts.pop if parts.size > 1
|
|
77
|
+
|
|
78
|
+
parts.join(".").to_sym
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def build_messages_view
|
|
82
|
+
result = {}
|
|
83
|
+
@message_adapters.keys.sort.each do |msg|
|
|
84
|
+
result[msg] = @message_adapters[msg].to_a.sort.freeze
|
|
85
|
+
end
|
|
86
|
+
result.freeze
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_adapters_view
|
|
90
|
+
inverted = {}
|
|
91
|
+
@message_adapters.each do |msg, adapter_set|
|
|
92
|
+
adapter_set.each do |adapter|
|
|
93
|
+
inverted[adapter] ||= []
|
|
94
|
+
inverted[adapter] << msg
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
result = {}
|
|
99
|
+
inverted.keys.sort.each do |adapter|
|
|
100
|
+
result[adapter] = inverted[adapter].sort.freeze
|
|
101
|
+
end
|
|
102
|
+
result.freeze
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notify
|
|
4
|
+
module TestHelper
|
|
5
|
+
def assert_notify_dispatched(name, count: nil, to: nil)
|
|
6
|
+
name = name.to_sym
|
|
7
|
+
matching = Notify.deliveries.select { |d| d[:name] == name }
|
|
8
|
+
|
|
9
|
+
if matching.empty?
|
|
10
|
+
delivered = Notify.deliveries.map { |d| d[:name] }.inspect
|
|
11
|
+
raise "Expected Notify.message(:#{name}) to have been dispatched, but it was not. " \
|
|
12
|
+
"Deliveries: #{delivered}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if count && matching.size != count
|
|
16
|
+
raise "Expected #{count} dispatch(es) of :#{name}, got #{matching.size}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if to
|
|
20
|
+
expected = Array(to)
|
|
21
|
+
all_recipients = matching.flat_map do |d|
|
|
22
|
+
(d[:resolved][:recipients] || {}).values.flatten
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
missing = expected - all_recipients
|
|
26
|
+
unless missing.empty?
|
|
27
|
+
raise "Expected recipients #{expected.inspect} for :#{name}, " \
|
|
28
|
+
"but #{missing.inspect} not found in resolved recipients: #{all_recipients.inspect}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def last_notify_delivery
|
|
36
|
+
Notify.deliveries.last
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def setup_notify_test_mode
|
|
40
|
+
Notify.test_mode!
|
|
41
|
+
Notify.deliveries.clear
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/notify.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "notify/version"
|
|
4
|
+
require_relative "notify/errors"
|
|
5
|
+
require_relative "notify/config"
|
|
6
|
+
require_relative "notify/payload_class"
|
|
7
|
+
require_relative "notify/registry"
|
|
8
|
+
require_relative "notify/adapters/base"
|
|
9
|
+
require_relative "notify/mailer"
|
|
10
|
+
require_relative "notify/adapters/email"
|
|
11
|
+
require_relative "notify/dispatcher"
|
|
12
|
+
require_relative "notify/engine"
|
|
13
|
+
require_relative "notify/test_helper"
|
|
14
|
+
|
|
15
|
+
module Notify
|
|
16
|
+
class << self
|
|
17
|
+
def config
|
|
18
|
+
@config ||= Config.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configure
|
|
22
|
+
yield config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reset_config!
|
|
26
|
+
@config = Config.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def register_adapter(key, klass)
|
|
30
|
+
adapter_classes[key.to_sym] = klass
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def registered_adapters
|
|
34
|
+
adapter_classes.dup
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reset_adapters!
|
|
38
|
+
@adapter_classes = {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def registry
|
|
42
|
+
@registry
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def messages
|
|
46
|
+
registry&.messages || {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def adapters
|
|
50
|
+
registry&.adapters || {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_registry!(templates_path)
|
|
54
|
+
@registry = Registry.new(templates_path)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def reset_registry!
|
|
58
|
+
@registry = nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_mode!
|
|
62
|
+
@test_mode = true
|
|
63
|
+
@deliveries ||= []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_mode?
|
|
67
|
+
@test_mode == true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def deliveries
|
|
71
|
+
@deliveries ||= []
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def reset_test_mode!
|
|
75
|
+
@test_mode = false
|
|
76
|
+
@deliveries&.clear
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def message(name, **payload)
|
|
80
|
+
if test_mode?
|
|
81
|
+
deliveries << Dispatcher.new.capture_for_test(name, **payload)
|
|
82
|
+
else
|
|
83
|
+
Dispatcher.new.dispatch(name, **payload)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def adapter_classes
|
|
90
|
+
@adapter_classes ||= {}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|