active_delivery 0.4.4 → 1.0.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 +4 -4
- data/CHANGELOG.md +62 -0
- data/LICENSE.txt +19 -17
- data/README.md +595 -33
- data/lib/.rbnext/3.0/abstract_notifier/async_adapters/active_job.rb +27 -0
- data/lib/.rbnext/3.0/active_delivery/base.rb +248 -0
- data/lib/.rbnext/3.0/active_delivery/callbacks.rb +101 -0
- data/lib/.rbnext/3.0/active_delivery/lines/base.rb +89 -0
- data/lib/.rbnext/3.0/active_delivery/lines/mailer.rb +26 -0
- data/lib/.rbnext/3.0/active_delivery/testing.rb +62 -0
- data/lib/.rbnext/3.1/abstract_notifier/base.rb +217 -0
- data/lib/.rbnext/3.1/active_delivery/base.rb +248 -0
- data/lib/.rbnext/3.1/active_delivery/lines/base.rb +89 -0
- data/lib/abstract_notifier/async_adapters/active_job.rb +27 -0
- data/lib/abstract_notifier/async_adapters.rb +16 -0
- data/lib/abstract_notifier/base.rb +217 -0
- data/lib/abstract_notifier/callbacks.rb +94 -0
- data/lib/abstract_notifier/testing/minitest.rb +51 -0
- data/lib/abstract_notifier/testing/rspec.rb +164 -0
- data/lib/abstract_notifier/testing.rb +53 -0
- data/lib/abstract_notifier/version.rb +5 -0
- data/lib/abstract_notifier.rb +75 -0
- data/lib/active_delivery/base.rb +147 -27
- data/lib/active_delivery/callbacks.rb +25 -25
- data/lib/active_delivery/ext/string_constantize.rb +24 -0
- data/lib/active_delivery/lines/base.rb +42 -16
- data/lib/active_delivery/lines/mailer.rb +7 -18
- data/lib/active_delivery/lines/notifier.rb +53 -0
- data/lib/active_delivery/raitie.rb +9 -0
- data/lib/active_delivery/testing/rspec.rb +59 -12
- data/lib/active_delivery/testing.rb +19 -5
- data/lib/active_delivery/version.rb +1 -1
- data/lib/active_delivery.rb +8 -0
- metadata +63 -54
- data/.gem_release.yml +0 -3
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -24
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -23
- data/.github/workflows/docs-lint.yml +0 -72
- data/.github/workflows/rspec-jruby.yml +0 -35
- data/.github/workflows/rspec.yml +0 -51
- data/.github/workflows/rubocop.yml +0 -21
- data/.gitignore +0 -43
- data/.mdlrc +0 -1
- data/.rspec +0 -2
- data/.rubocop-md.yml +0 -16
- data/.rubocop.yml +0 -28
- data/Gemfile +0 -17
- data/RELEASING.md +0 -43
- data/Rakefile +0 -20
- data/active_delivery.gemspec +0 -35
- data/forspell.dict +0 -8
- data/gemfiles/jruby.gemfile +0 -5
- data/gemfiles/rails42.gemfile +0 -8
- data/gemfiles/rails5.gemfile +0 -5
- data/gemfiles/rails50.gemfile +0 -8
- data/gemfiles/rails6.gemfile +0 -5
- data/gemfiles/railsmaster.gemfile +0 -6
- data/gemfiles/rubocop.gemfile +0 -4
- data/lefthook.yml +0 -18
- data/lib/active_delivery/action_mailer/parameterized.rb +0 -92
data/README.md
CHANGED
@@ -4,7 +4,9 @@
|
|
4
4
|
|
5
5
|
# Active Delivery
|
6
6
|
|
7
|
-
|
7
|
+
Active Delivery is a framework providing an entry point (single _interface_ or _abstraction_) for all types of notifications: mailers, push notifications, whatever you want.
|
8
|
+
|
9
|
+
Since v1.0, Active Delivery is bundled with [Abstract Notifier](https://github.com/palkan/abstract_notifier). See the docs on how to create custom notifiers [below](#abstract-notifier).
|
8
10
|
|
9
11
|
📖 Read the introduction post: ["Crafting user notifications in Rails with Active Delivery"](https://evilmartians.com/chronicles/crafting-user-notifications-in-rails-with-active-delivery)
|
10
12
|
|
@@ -13,7 +15,8 @@ Framework providing an entry point (single _interface_) for all types of notific
|
|
13
15
|
|
14
16
|
Requirements:
|
15
17
|
|
16
|
-
- Ruby ~> 2.
|
18
|
+
- Ruby ~> 2.7
|
19
|
+
- Rails 6+ (optional).
|
17
20
|
|
18
21
|
**NOTE**: although most of the examples in this readme are Rails-specific, this gem could be used without Rails/ActiveSupport.
|
19
22
|
|
@@ -21,7 +24,7 @@ Requirements:
|
|
21
24
|
|
22
25
|
We need a way to handle different notifications _channel_ (mail, push) in one place.
|
23
26
|
|
24
|
-
From the business-logic point of view we want to _notify_ a user, hence we need a _separate abstraction layer_ as an entry point to different types of notifications.
|
27
|
+
From the business-logic point of view, we want to _notify_ a user, hence we need a _separate abstraction layer_ as an entry point to different types of notifications.
|
25
28
|
|
26
29
|
## The solution
|
27
30
|
|
@@ -31,18 +34,18 @@ In the simplest case when we have only mailers Active Delivery is just a wrapper
|
|
31
34
|
|
32
35
|
Motivations behind Active Delivery:
|
33
36
|
|
34
|
-
- Organize notifications
|
37
|
+
- Organize notifications-related logic:
|
35
38
|
|
36
39
|
```ruby
|
37
40
|
# Before
|
38
41
|
def after_some_action
|
39
|
-
MyMailer.with(user: user).some_action.deliver_later if user.receive_emails?
|
42
|
+
MyMailer.with(user: user).some_action(resource).deliver_later if user.receive_emails?
|
40
43
|
NotifyService.send_notification(user, "action") if whatever_else?
|
41
44
|
end
|
42
45
|
|
43
46
|
# After
|
44
47
|
def after_some_action
|
45
|
-
MyDelivery.with(user: user).
|
48
|
+
MyDelivery.with(user: user).some_action(resource).deliver_later
|
46
49
|
end
|
47
50
|
```
|
48
51
|
|
@@ -53,48 +56,173 @@ end
|
|
53
56
|
Add this line to your application's Gemfile:
|
54
57
|
|
55
58
|
```ruby
|
56
|
-
gem "active_delivery"
|
59
|
+
gem "active_delivery", "1.0.0.rc2"
|
57
60
|
```
|
58
61
|
|
59
62
|
And then execute:
|
60
63
|
|
61
64
|
```sh
|
62
|
-
bundle
|
65
|
+
bundle install
|
63
66
|
```
|
64
67
|
|
65
68
|
## Usage
|
66
69
|
|
67
|
-
The _Delivery_ class is used to trigger notifications. It describes how to notify a user (e.g., via email or push notification or both)
|
70
|
+
The _Delivery_ class is used to trigger notifications. It describes how to notify a user (e.g., via email or push notification or both).
|
71
|
+
|
72
|
+
First, it's recommended to create a base class for all deliveries with the configuration of the lines:
|
68
73
|
|
69
74
|
```ruby
|
70
|
-
class
|
71
|
-
|
72
|
-
|
75
|
+
# In the base class, you configure delivery lines
|
76
|
+
class ApplicationDelivery < ActiveDelivery::Base
|
77
|
+
self.abstract_class = true
|
78
|
+
|
79
|
+
# Mailers are enabled by default, everything else must be declared explicitly
|
80
|
+
|
81
|
+
# For example, you can use a notifier line (see below) with a custom resolver
|
82
|
+
# (the argument is the delivery class)
|
83
|
+
register_line :sms, ActiveDelivery::Lines::Notifier,
|
84
|
+
resolver: -> { _1.name.gsub(/Delivery$/, "SMSNotifier").safe_constantize } #=> PostDelivery -> PostSMSNotifier
|
85
|
+
|
86
|
+
# Or you can use a name pattern to resolve notifier classes for delivery classes
|
87
|
+
# Available placeholders are:
|
88
|
+
# - delivery_class — full delivery class name
|
89
|
+
# - delivery_name — full delivery class name without the "Delivery" suffix
|
90
|
+
register_line :webhook, ActiveDelivery::Lines::Notifier,
|
91
|
+
resolver_pattern: "%{delivery_name}WebhookNotifier" #=> PostDelivery -> PostWebhookNotifier
|
92
|
+
|
93
|
+
register_line :cable, ActionCableDeliveryLine
|
94
|
+
# and more
|
73
95
|
end
|
74
96
|
```
|
75
97
|
|
76
|
-
|
98
|
+
Then, you can create a delivery class for a specific notification type. We follow Action Mailer conventions, and create a delivery class per resource:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class PostsDelivery < ApplicationDelivery
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
In most cases, you just leave this class blank. The corresponding mailers, notifiers, etc., will be inferred automatically using the naming convention.
|
106
|
+
|
107
|
+
You don't need to define notification methods explicitly. Whenever you invoke a method on a delivery class, it will be proxied to the underlying _line handlers_ (mailers, notifiers, etc.):
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
PostsDelivery.published(user, post).deliver_later
|
111
|
+
|
112
|
+
# Under the hood it calls
|
113
|
+
PostsMailer.published(user, post).deliver_later
|
114
|
+
PostsSMSNotifier.published(user, post).notify_later
|
115
|
+
|
116
|
+
# and whaterver your ActionCableDeliveryLine does
|
117
|
+
# under the hood.
|
118
|
+
```
|
119
|
+
|
120
|
+
Alternatively, you call the `#notify` method with the notification name and the arguments:
|
77
121
|
|
78
122
|
```ruby
|
79
123
|
PostsDelivery.notify(:published, user, post)
|
80
124
|
|
81
|
-
#
|
125
|
+
# Under the hood it calls
|
82
126
|
PostsMailer.published(user, post).deliver_later
|
127
|
+
PostsSMSNotifier.published(user, post).notify_later
|
128
|
+
# ...
|
129
|
+
```
|
83
130
|
|
84
|
-
|
85
|
-
|
131
|
+
You can also define a notification method explicitly if you want to add some logic:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
class PostsDelivery < ApplicationDelivery
|
135
|
+
def published(user, post)
|
136
|
+
# do something
|
137
|
+
|
138
|
+
# return a delivery object (to chain #deliver_later, etc.)
|
139
|
+
delivery(
|
140
|
+
notification: :published,
|
141
|
+
params: [user, post],
|
142
|
+
# For kwargs, you options
|
143
|
+
options: {},
|
144
|
+
# Metadata that can be used by line handlers
|
145
|
+
metadata: {}
|
146
|
+
)
|
147
|
+
end
|
148
|
+
end
|
86
149
|
```
|
87
150
|
|
88
|
-
|
151
|
+
Finally, you can disable the default automatic proxying behaviour via the `ActiveDelivery.deliver_actions_required = true` configuration option. Then, in each delivery class, you can specify the available actions via the `.delivers` method:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
class PostDelivery < ApplicationDelivery
|
155
|
+
delivers :published
|
156
|
+
end
|
89
157
|
|
90
|
-
|
158
|
+
ActiveDelivery.deliver_actions_required = true
|
159
|
+
|
160
|
+
PostDelivery.published(post) #=> ok
|
161
|
+
PostDelivery.whatever(post) #=> raises NoMethodError
|
162
|
+
```
|
163
|
+
|
164
|
+
### Organizing delivery and notifier classes
|
165
|
+
|
166
|
+
There are two common ways to organize delivery and notifier classes in your codebase:
|
167
|
+
|
168
|
+
```txt
|
169
|
+
app/
|
170
|
+
deliveries/ deliveries/
|
171
|
+
application_delivery.rb application_delivery.rb
|
172
|
+
post_delivery.rb post_delivery/
|
173
|
+
user_delivery.rb post_mailer.rb
|
174
|
+
mailers/ post_sms_notifier.rb
|
175
|
+
application_mailer.rb post_webhook_notifier.rb
|
176
|
+
post_mailer.rb post_delivery.rb
|
177
|
+
user_mailer.rb user_delivery/
|
178
|
+
notifiers/ user_mailer.rb
|
179
|
+
application_notifier.rb user_sms_notifier.rb
|
180
|
+
post_sms_notifier.rb user_webhook_notifier.rb
|
181
|
+
post_webhook_notifier.rb user_delivery.rb
|
182
|
+
user_sms_notifier.rb
|
183
|
+
user_webhook_notifier.rb
|
184
|
+
```
|
185
|
+
|
186
|
+
The left side is a _flat_ structure, more typical for classic Rails applications. The right side follows the _sidecar pattern_ and aims to localize all the code related to a specific delivery class in a single directory. To use the sidecar version, you need to configure your delivery lines as follows:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class ApplicationDelivery < ActiveDelivery::Base
|
190
|
+
self.abstract_class = true
|
191
|
+
|
192
|
+
register_line :mailer, ActiveDelivery::Lines::Mailer,
|
193
|
+
resolver_pattern: "%{delivery_class}::%{delivery_name}_mailer"
|
194
|
+
register_line :sms,
|
195
|
+
notifier: true,
|
196
|
+
resolver_pattern: "%{delivery_class}::%{delivery_name}_sms_notifier"
|
197
|
+
register_line :webhook,
|
198
|
+
notifier: true,
|
199
|
+
resolver_pattern: "%{delivery_class}::%{delivery_name}_webhook_notifier"
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
### Customizing delivery handlers
|
204
|
+
|
205
|
+
You can specify a mailer class explicitly:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
class PostsDelivery < ActiveDelivery::Base
|
209
|
+
# You can pass a class name or a class itself
|
210
|
+
mailer "CustomPostsMailer"
|
211
|
+
# For other lines, you the line name as well
|
212
|
+
# sms "MyPostsSMSNotifier"
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
Or you can provide a custom resolver by re-registering the line:
|
91
217
|
|
92
218
|
```ruby
|
93
219
|
class PostsDelivery < ActiveDelivery::Base
|
94
|
-
register_line :
|
220
|
+
register_line :mailer, ActiveDelivery::Lines::Mailer, resolver: ->(_delivery_class) { CustomMailer }
|
95
221
|
end
|
96
222
|
```
|
97
223
|
|
224
|
+
### Parameterized deliveries
|
225
|
+
|
98
226
|
Delivery also supports _parameterized_ calling:
|
99
227
|
|
100
228
|
```ruby
|
@@ -103,17 +231,19 @@ PostsDelivery.with(user: user).notify(:published, post)
|
|
103
231
|
|
104
232
|
The parameters could be accessed through the `params` instance method (e.g., to implement guard-like logic).
|
105
233
|
|
106
|
-
**NOTE**: When params are
|
234
|
+
**NOTE**: When params are present, the parameterized mailer is used, i.e.:
|
107
235
|
|
108
236
|
```ruby
|
109
237
|
PostsMailer.with(user: user).published(post)
|
110
238
|
```
|
111
239
|
|
240
|
+
Other line implementations **MUST** also have the `#with` method in their public interface.
|
241
|
+
|
112
242
|
See [Rails docs](https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html) for more information on parameterized mailers.
|
113
243
|
|
114
|
-
|
244
|
+
### Callbacks support
|
115
245
|
|
116
|
-
**NOTE:** callbacks are only available if ActiveSupport is present in the
|
246
|
+
**NOTE:** callbacks are only available if ActiveSupport is present in the application's runtime.
|
117
247
|
|
118
248
|
```ruby
|
119
249
|
# Run method before delivering notification
|
@@ -133,6 +263,12 @@ after_notify :mark_user_as_notified, if: -> { params[:user].present? }
|
|
133
263
|
after_notify :cleanup
|
134
264
|
|
135
265
|
around_notify :set_context
|
266
|
+
|
267
|
+
# You can also skip callbacks in sub-classes
|
268
|
+
skip_before_notify :do_something, only: %i[some_reminder]
|
269
|
+
|
270
|
+
# NOTE: Specify `on` option for line-specific callbacks is required to skip them
|
271
|
+
skip_after_notify :do_mail_something, on: :mailer
|
136
272
|
```
|
137
273
|
|
138
274
|
Example:
|
@@ -141,7 +277,7 @@ Example:
|
|
141
277
|
# Let's log notifications
|
142
278
|
class MyDelivery < ActiveDelivery::Base
|
143
279
|
after_notify do
|
144
|
-
# You can access the
|
280
|
+
# You can access the notification name within the instance
|
145
281
|
MyLogger.info "Delivery triggered: #{notification_name}"
|
146
282
|
end
|
147
283
|
end
|
@@ -152,7 +288,9 @@ MyDeliver.notify(:something_wicked_this_way_comes)
|
|
152
288
|
|
153
289
|
## Testing
|
154
290
|
|
155
|
-
**NOTE:**
|
291
|
+
**NOTE:** Currently, only RSpec matchers are provided.
|
292
|
+
|
293
|
+
### Deliveries
|
156
294
|
|
157
295
|
Active Delivery provides an elegant way to test deliveries in your code (i.e., when you want to check whether a notification has been sent) through a `have_delivered_to` matcher:
|
158
296
|
|
@@ -163,7 +301,7 @@ it "delivers notification" do
|
|
163
301
|
end
|
164
302
|
```
|
165
303
|
|
166
|
-
You can also use such RSpec features as
|
304
|
+
You can also use such RSpec features as compound expectations and composed matchers:
|
167
305
|
|
168
306
|
```ruby
|
169
307
|
it "delivers to RSVPed members via .notify" do
|
@@ -186,7 +324,7 @@ specify "when event is not found" do
|
|
186
324
|
end
|
187
325
|
```
|
188
326
|
|
189
|
-
or use matcher
|
327
|
+
or use the `#have_not_delivered_to` matcher:
|
190
328
|
|
191
329
|
```ruby
|
192
330
|
specify "when event is not found" do
|
@@ -196,7 +334,57 @@ specify "when event is not found" do
|
|
196
334
|
end
|
197
335
|
```
|
198
336
|
|
199
|
-
|
337
|
+
### Delivery classes
|
338
|
+
|
339
|
+
You can test Delivery classes as regular Ruby classes:
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
describe PostsDelivery do
|
343
|
+
let(:user) { build_stubbed(:user) }
|
344
|
+
let(:post) { build_stubbed(:post) }
|
345
|
+
|
346
|
+
describe "#published" do
|
347
|
+
it "sends a mail" do
|
348
|
+
expect {
|
349
|
+
described_class.published(user, post).deliver_now
|
350
|
+
}.to change { ActionMailer::Base.deliveries.count }.by(1)
|
351
|
+
|
352
|
+
mail = ActionMailer::Base.deliveries.last
|
353
|
+
expect(mail.to).to eq([user.email])
|
354
|
+
expect(mail.subject).to eq("New post published")
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
```
|
359
|
+
|
360
|
+
You can also use the `#deliver_via` matchers as follows:
|
361
|
+
|
362
|
+
```ruby
|
363
|
+
describe PostsDelivery, type: :delivery do
|
364
|
+
let(:user) { build_stubbed(:user) }
|
365
|
+
let(:post) { build_stubbed(:post) }
|
366
|
+
|
367
|
+
describe "#published" do
|
368
|
+
it "delivers to mailer and sms" do
|
369
|
+
expect {
|
370
|
+
described_class.published(user, post).deliver_later
|
371
|
+
}.to deliver_via(:mailer, :sms)
|
372
|
+
end
|
373
|
+
|
374
|
+
context "when user is not subscribed to SMS notifications" do
|
375
|
+
let(:user) { build_stubbed(:user, sms_notifications: false) }
|
376
|
+
|
377
|
+
it "delivers to mailer only" do
|
378
|
+
expect {
|
379
|
+
described_class.published(user, post).deliver_now
|
380
|
+
}.to deliver_via(:mailer)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
```
|
386
|
+
|
387
|
+
**NOTE:** test mode activated automatically if `RAILS_ENV` or `RACK_ENV` env variable is equal to "test". Otherwise, add `require "active_delivery/testing/rspec"` to your `spec_helper.rb` / `rails_helper.rb` manually. This is also required if you're using Spring in the test environment (e.g. with help of [spring-commands-rspec](https://github.com/jonleighton/spring-commands-rspec)).
|
200
388
|
|
201
389
|
## Custom "lines"
|
202
390
|
|
@@ -208,13 +396,15 @@ A line connects _delivery_ to the _sender_ class responsible for sending notific
|
|
208
396
|
|
209
397
|
If you want to use parameterized deliveries, your _sender_ class must respond to `.with(params)` method.
|
210
398
|
|
399
|
+
### A full-featured line example: pigeons 🐦
|
400
|
+
|
211
401
|
Assume that we want to send messages via _pigeons_ and we have the following sender class:
|
212
402
|
|
213
403
|
```ruby
|
214
404
|
class EventPigeon
|
215
405
|
class << self
|
216
406
|
# Add `.with` method as an alias
|
217
|
-
|
407
|
+
alias_method :with, :new
|
218
408
|
|
219
409
|
# delegate delivery action to the instance
|
220
410
|
def message_arrived(*args)
|
@@ -271,7 +461,50 @@ class PigeonLine < ActiveDelivery::Lines::Base
|
|
271
461
|
end
|
272
462
|
```
|
273
463
|
|
274
|
-
|
464
|
+
In the case of parameterized calling, some update needs to be done on the new Line. Here is an example:
|
465
|
+
|
466
|
+
```ruby
|
467
|
+
class EventPigeon
|
468
|
+
attr_reader :params
|
469
|
+
|
470
|
+
class << self
|
471
|
+
# Add `.with` method as an alias
|
472
|
+
alias_method :with, :new
|
473
|
+
|
474
|
+
# delegate delivery action to the instance
|
475
|
+
def message_arrived(*args)
|
476
|
+
new.message_arrived(*args)
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
def initialize(params = {})
|
481
|
+
@params = params
|
482
|
+
# do smth with params
|
483
|
+
end
|
484
|
+
|
485
|
+
def message_arrived(msg)
|
486
|
+
# send a pigeon with the message
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
class PigeonLine < ActiveDelivery::Lines::Base
|
491
|
+
def notify_later(sender, delivery_action, *args, **kwargs)
|
492
|
+
# `to_s` is important for serialization. Unless you might have error
|
493
|
+
PigeonLaunchJob.perform_later sender.class.to_s, delivery_action, *args, **kwargs.merge(params: line.params)
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
class PigeonLaunchJob < ActiveJob::Base
|
498
|
+
def perform(sender, delivery_action, *args, params: nil, **kwargs)
|
499
|
+
klass = sender.safe_constantize
|
500
|
+
handler = params ? klass.with(**params) : klass.new
|
501
|
+
|
502
|
+
handler.public_send(delivery_action, *args, **kwargs)
|
503
|
+
end
|
504
|
+
end
|
505
|
+
```
|
506
|
+
|
507
|
+
**NOTE**: we fall back to the superclass's sender class if `resolve_class` returns nil.
|
275
508
|
You can disable automatic inference of sender classes by marking delivery as _abstract_:
|
276
509
|
|
277
510
|
```ruby
|
@@ -295,7 +528,7 @@ class EventDelivery < ActiveDelivery::Base
|
|
295
528
|
# register_line :pigeon, PigeonLine, namespace: "AngryPigeons"
|
296
529
|
#
|
297
530
|
# now you can explicitly specify pigeon class
|
298
|
-
# pigeon MyCustomPigeon
|
531
|
+
# pigeon "MyCustomPigeon"
|
299
532
|
#
|
300
533
|
# or define pigeon specific callbacks
|
301
534
|
#
|
@@ -303,7 +536,7 @@ class EventDelivery < ActiveDelivery::Base
|
|
303
536
|
end
|
304
537
|
```
|
305
538
|
|
306
|
-
You can also _unregister_ a line
|
539
|
+
You can also _unregister_ a line:
|
307
540
|
|
308
541
|
```ruby
|
309
542
|
class NonMailerDelivery < ActiveDelivery::Base
|
@@ -312,9 +545,338 @@ class NonMailerDelivery < ActiveDelivery::Base
|
|
312
545
|
end
|
313
546
|
```
|
314
547
|
|
315
|
-
|
548
|
+
### An example of a universal sender: Action Cable
|
549
|
+
|
550
|
+
Although Active Delivery is designed to work with Action Mailer-like abstraction, it's flexible enough to support other use cases.
|
551
|
+
|
552
|
+
For example, for some notification channels, we don't need to create a separate class for each resource or context; we can send the payload right to the communication channel. Let's consider an Action Cable line as an example.
|
553
|
+
|
554
|
+
For every delivery, we want to broadcast a message via Action Cable to the stream corresponding to the delivery class name. For example:
|
555
|
+
|
556
|
+
```ruby
|
557
|
+
# Our PostsDelivery example from the beginning
|
558
|
+
PostsDelivery.with(user:).notify(:published, post)
|
559
|
+
|
560
|
+
# Will results in the following Action Cable broadcast:
|
561
|
+
DeliveryChannel.broadcast_to user, {event: "posts.published", post_id: post.id}
|
562
|
+
```
|
563
|
+
|
564
|
+
The `ActionCableDeliveryLine` class can be implemented as follows:
|
565
|
+
|
566
|
+
```ruby
|
567
|
+
class ActionCableDeliveryLine < ActiveDelivery::Line::Base
|
568
|
+
# Context is our universal sender.
|
569
|
+
class Context
|
570
|
+
attr_reader :user
|
571
|
+
|
572
|
+
def initialize(scope)
|
573
|
+
@scope = scope
|
574
|
+
end
|
575
|
+
|
576
|
+
# User is required for this line
|
577
|
+
def with(user:, **)
|
578
|
+
@user = user
|
579
|
+
self
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
# The result of this callback is passed further to the `notify_now` method
|
584
|
+
def resolve_class(name)
|
585
|
+
Context.new(name.sub(/Delivery$/, "").underscore)
|
586
|
+
end
|
587
|
+
|
588
|
+
# We want to broadcast all notifications
|
589
|
+
def notify?(...) = true
|
590
|
+
|
591
|
+
def notify_now(context, delivery_action, *args, **kwargs)
|
592
|
+
# Skip if no user provided
|
593
|
+
return unless context.user
|
594
|
+
|
595
|
+
payload = {event: [context.scope, delivery_action].join(".")}
|
596
|
+
payload.merge!(serialized_args(*args, **kwargs))
|
597
|
+
|
598
|
+
DeliveryChannel.broadcast_to context.user, payload
|
599
|
+
end
|
316
600
|
|
317
|
-
|
601
|
+
# Broadcasts are asynchronous by nature, so we can just use `notify_now`
|
602
|
+
alias_method :notify_later, :notify_now
|
603
|
+
|
604
|
+
private
|
605
|
+
|
606
|
+
def serialized_args(*args, **kwargs)
|
607
|
+
# Code that convers AR objects into IDs, etc.
|
608
|
+
end
|
609
|
+
end
|
610
|
+
```
|
611
|
+
|
612
|
+
## Abstract Notifier
|
613
|
+
|
614
|
+
Abstract Notifier is a tool that allows you to describe/model any text-based notifications (such as Push Notifications) the same way Action Mailer does for email notifications.
|
615
|
+
|
616
|
+
Abstract Notifier (as the name states) doesn't provide any specific implementation for sending notifications. Instead, it offers tools to organize your notification-specific code and make it easily testable.
|
617
|
+
|
618
|
+
### Notifier classes
|
619
|
+
|
620
|
+
A **notifier object** is very similar to an Action Mailer's mailer with the `#notification` method used instead of the `#mail` method:
|
621
|
+
|
622
|
+
```ruby
|
623
|
+
class EventsNotifier < ApplicationNotifier
|
624
|
+
def canceled(profile, event)
|
625
|
+
notification(
|
626
|
+
# the only required option is `body`
|
627
|
+
body: "Event #{event.title} has been canceled",
|
628
|
+
# all other options are passed to delivery driver
|
629
|
+
identity: profile.notification_service_id
|
630
|
+
)
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
# send notification later
|
635
|
+
EventsNotifier.canceled(profile, event).notify_later
|
636
|
+
|
637
|
+
# or immediately
|
638
|
+
EventsNotifier.canceled(profile, event).notify_now
|
639
|
+
```
|
640
|
+
|
641
|
+
### Delivery drivers
|
642
|
+
|
643
|
+
To perform actual deliveries you **must** configure a _delivery driver_:
|
644
|
+
|
645
|
+
```ruby
|
646
|
+
class ApplicationNotifier < AbstractNotifier::Base
|
647
|
+
self.driver = MyFancySender.new
|
648
|
+
end
|
649
|
+
```
|
650
|
+
|
651
|
+
A driver could be any callable Ruby object (i.e., anything that responds to `#call`).
|
652
|
+
|
653
|
+
That's the developer's responsibility to implement the driver (we do not provide any drivers out-of-the-box; at least yet).
|
654
|
+
|
655
|
+
You can set different drivers for different notifiers.
|
656
|
+
|
657
|
+
### Parameterized notifiers
|
658
|
+
|
659
|
+
Abstract Notifier supports parameterization the same way as [Action Mailer](https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html):
|
660
|
+
|
661
|
+
```ruby
|
662
|
+
class EventsNotifier < ApplicationNotifier
|
663
|
+
def canceled(event)
|
664
|
+
notification(
|
665
|
+
body: "Event #{event.title} has been canceled",
|
666
|
+
identity: params[:profile].notification_service_id
|
667
|
+
)
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
EventsNotifier.with(profile: profile).canceled(event).notify_later
|
672
|
+
```
|
673
|
+
|
674
|
+
### Defaults
|
675
|
+
|
676
|
+
You can specify default notification fields at a class level:
|
677
|
+
|
678
|
+
```ruby
|
679
|
+
class EventsNotifier < ApplicationNotifier
|
680
|
+
# `category` field will be added to the notification
|
681
|
+
# if missing
|
682
|
+
default category: "EVENTS"
|
683
|
+
|
684
|
+
# ...
|
685
|
+
end
|
686
|
+
```
|
687
|
+
|
688
|
+
**NOTE**: when subclassing notifiers, default parameters are merged.
|
689
|
+
|
690
|
+
You can also specify a block or a method name as the default params _generator_.
|
691
|
+
This could be useful in combination with the `#notification_name` method to generate dynamic payloads:
|
692
|
+
|
693
|
+
```ruby
|
694
|
+
class ApplicationNotifier < AbstractNotifier::Base
|
695
|
+
default :build_defaults_from_locale
|
696
|
+
|
697
|
+
private
|
698
|
+
|
699
|
+
def build_defaults_from_locale
|
700
|
+
{
|
701
|
+
subject: I18n.t(notification_name, scope: [:notifiers, self.class.name.underscore])
|
702
|
+
}
|
703
|
+
end
|
704
|
+
end
|
705
|
+
```
|
706
|
+
|
707
|
+
### Background jobs / async notifications
|
708
|
+
|
709
|
+
To use `#notify_later` you **must** configure an async adapter for Abstract Notifier.
|
710
|
+
|
711
|
+
We provide an Active Job adapter out of the box and enable it if Active Job is found.
|
712
|
+
|
713
|
+
A custom async adapter must implement the `#enqueue` method:
|
714
|
+
|
715
|
+
```ruby
|
716
|
+
class MyAsyncAdapter
|
717
|
+
# adapters may accept options
|
718
|
+
def initialize(options = {})
|
719
|
+
end
|
720
|
+
|
721
|
+
# `enqueue` method accepts notifier class, action name and notification parameters
|
722
|
+
def enqueue(notifier_class, action_name, params:, args:, kwargs:)
|
723
|
+
# <Your implementation here>
|
724
|
+
# To trigger the notification delivery, you can use the following snippet:
|
725
|
+
#
|
726
|
+
# AbstractNotifier::NotificationDelivery.new(
|
727
|
+
# notifier_class.constantize, action_name, params:, args:, kwargs:
|
728
|
+
# ).notify_now
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
# Configure globally
|
733
|
+
AbstractNotifier.async_adapter = MyAsyncAdapter.new
|
734
|
+
|
735
|
+
# or per-notifier
|
736
|
+
class EventsNotifier < AbstractNotifier::Base
|
737
|
+
self.async_adapter = MyAsyncAdapter.new
|
738
|
+
end
|
739
|
+
```
|
740
|
+
|
741
|
+
### Action and Delivery Callbacks
|
742
|
+
|
743
|
+
**NOTE:** callbacks are only available if ActiveSupport is present in the application's runtime.
|
744
|
+
|
745
|
+
```ruby
|
746
|
+
# Run method before building a notification payload
|
747
|
+
# NOTE: when `false` is returned the execution is halted
|
748
|
+
before_action :do_something
|
749
|
+
|
750
|
+
# Run method before delivering notification
|
751
|
+
# NOTE: when `false` is returned the execution is halted
|
752
|
+
before_deliver :do_something
|
753
|
+
|
754
|
+
# Run method after the notification payload was build but before delivering
|
755
|
+
after_action :verify_notification_payload
|
756
|
+
|
757
|
+
# Run method after the actual delivery was performed
|
758
|
+
after_deliver :mark_user_as_notified, if: -> { params[:user].present? }
|
759
|
+
|
760
|
+
# after_ and around_ callbacks are also supported
|
761
|
+
after_action_ :cleanup
|
762
|
+
|
763
|
+
around_deliver :set_context
|
764
|
+
|
765
|
+
# You can also skip callbacks in sub-classes
|
766
|
+
skip_before_action :do_something, only: %i[some_reminder]
|
767
|
+
```
|
768
|
+
|
769
|
+
Example:
|
770
|
+
|
771
|
+
```ruby
|
772
|
+
class MyNotifier < AbstractNotifier::Base
|
773
|
+
# Log sent notifications
|
774
|
+
after_deliver do
|
775
|
+
# You can access the notification name within the instance or
|
776
|
+
MyLogger.info "Notification sent: #{notification_name}"
|
777
|
+
end
|
778
|
+
|
779
|
+
def some_event(body)
|
780
|
+
notification(body:)
|
781
|
+
end
|
782
|
+
end
|
783
|
+
|
784
|
+
MyNotifier.some_event("hello")
|
785
|
+
#=> Notification sent: some_event
|
786
|
+
```
|
787
|
+
|
788
|
+
### Delivery modes
|
789
|
+
|
790
|
+
For test/development purposes there are two special _global_ delivery modes:
|
791
|
+
|
792
|
+
```ruby
|
793
|
+
# Track all sent notifications without peforming real actions.
|
794
|
+
# Required for using RSpec matchers.
|
795
|
+
#
|
796
|
+
# config/environments/test.rb
|
797
|
+
AbstractNotifier.delivery_mode = :test
|
798
|
+
|
799
|
+
# If you don't want to trigger notifications in development,
|
800
|
+
# you can make Abstract Notifier no-op.
|
801
|
+
#
|
802
|
+
# config/environments/development.rb
|
803
|
+
AbstractNotifier.delivery_mode = :noop
|
804
|
+
|
805
|
+
# Default delivery mode is "normal"
|
806
|
+
AbstractNotifier.delivery_mode = :normal
|
807
|
+
```
|
808
|
+
|
809
|
+
**NOTE:** we set `delivery_mode = :test` if `RAILS_ENV` or `RACK_ENV` env variable is equal to "test".
|
810
|
+
Otherwise add `require "abstract_notifier/testing"` to your `spec_helper.rb` / `rails_helper.rb` manually.
|
811
|
+
|
812
|
+
**NOTE:** delivery mode affects all drivers.
|
813
|
+
|
814
|
+
### Testing notifier deliveries
|
815
|
+
|
816
|
+
Abstract Notifier provides two convenient RSpec matchers:
|
817
|
+
|
818
|
+
```ruby
|
819
|
+
# for testing sync notifications (sent with `notify_now`)
|
820
|
+
expect { EventsNotifier.with(profile: profile).canceled(event).notify_now }
|
821
|
+
.to have_sent_notification(identify: "123", body: "Alarma!")
|
822
|
+
|
823
|
+
# for testing async notifications (sent with `notify_later`)
|
824
|
+
expect { EventsNotifier.with(profile: profile).canceled(event).notify_later }
|
825
|
+
.to have_enqueued_notification(via: EventNotifier, identify: "123", body: "Alarma!")
|
826
|
+
|
827
|
+
# you can also specify the expected notifier class (useful when ypu have multiple notifier lines)
|
828
|
+
expect { EventsNotifier.with(profile: profile).canceled(event).notify_now }
|
829
|
+
.to have_sent_notification(via: EventsNotifier, identify: "123", body: "Alarma!")
|
830
|
+
```
|
831
|
+
|
832
|
+
Abstract Notifier also provides Minitest assertions:
|
833
|
+
|
834
|
+
```ruby
|
835
|
+
require "abstract_notifier/testing/minitest"
|
836
|
+
|
837
|
+
class EventsNotifierTestCase < Minitest::Test
|
838
|
+
include AbstractNotifier::TestHelper
|
839
|
+
|
840
|
+
test "canceled" do
|
841
|
+
assert_notifications_sent 1, identify: "321", body: "Alarma!" do
|
842
|
+
EventsNotifier.with(profile: profile).canceled(event).notify_now
|
843
|
+
end
|
844
|
+
|
845
|
+
assert_notifications_sent 1, via: EventNofitier, identify: "123", body: "Alarma!" do
|
846
|
+
EventsNotifier.with(profile: profile).canceled(event).notify_now
|
847
|
+
end
|
848
|
+
|
849
|
+
assert_notifications_enqueued 1, via: EventNofitier, identify: "123", body: "Alarma!" do
|
850
|
+
EventsNotifier.with(profile: profile).canceled(event).notify_later
|
851
|
+
end
|
852
|
+
end
|
853
|
+
end
|
854
|
+
```
|
855
|
+
|
856
|
+
**NOTE:** test mode activated automatically if `RAILS_ENV` or `RACK_ENV` env variable is equal to "test". Otherwise, add `require "abstract_notifier/testing/rspec"` to your `spec_helper.rb` / `rails_helper.rb` manually. This is also required if you're using Spring in a test environment (e.g. with help of [spring-commands-rspec](https://github.com/jonleighton/spring-commands-rspec)).
|
857
|
+
|
858
|
+
### Notifier lines for Active Delivery
|
859
|
+
|
860
|
+
Abstract Notifier provides a _notifier_ line for Active Delivery:
|
861
|
+
|
862
|
+
```ruby
|
863
|
+
class ApplicationDelivery < ActiveDelivery::Base
|
864
|
+
# Add notifier line to you delivery
|
865
|
+
# By default, we use `*Delivery` -> `*Notifier` resolution mechanism
|
866
|
+
register_line :notifier, notifier: true
|
867
|
+
|
868
|
+
# You can define a custom suffix to use for notifier classes:
|
869
|
+
# `*Delivery` -> `*CustomNotifier`
|
870
|
+
register_line :custom_notifier, notifier: true, suffix: "CustomNotifier"
|
871
|
+
|
872
|
+
# Or using a custom pattern
|
873
|
+
register_line :custom_notifier, notifier: true, resolver_pattern: "%{delivery_name}CustomNotifier"
|
874
|
+
|
875
|
+
# Or you can specify a Proc object to do custom resolution:
|
876
|
+
register_line :some_notifier, notifier: true,
|
877
|
+
resolver: ->(delivery_class) { resolve_somehow(delivery_class) }
|
878
|
+
end
|
879
|
+
```
|
318
880
|
|
319
881
|
## Contributing
|
320
882
|
|