active_delivery 0.4.3 → 1.0.0.rc2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -1
- data/LICENSE.txt +19 -17
- data/README.md +503 -32
- data/lib/.rbnext/3.0/active_delivery/base.rb +124 -0
- data/lib/.rbnext/3.0/active_delivery/callbacks.rb +97 -0
- data/lib/.rbnext/3.0/active_delivery/lines/base.rb +63 -0
- data/lib/.rbnext/3.0/active_delivery/lines/mailer.rb +25 -0
- data/lib/.rbnext/3.1/active_delivery/base.rb +124 -0
- data/lib/.rbnext/3.1/active_delivery/lines/base.rb +63 -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 +178 -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 +49 -0
- data/lib/abstract_notifier/version.rb +5 -0
- data/lib/abstract_notifier.rb +74 -0
- data/lib/active_delivery/base.rb +156 -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 +24 -17
- 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 +61 -56
- 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,126 @@ 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
|
+
register_line :sms, ActiveDelivery::Lines::Notifier,
|
83
|
+
resolver: -> { _1.name.gsub(/Delivery$/, "SMSNotifier").safe_constantize }
|
84
|
+
|
85
|
+
register_line :cable, ActionCableDeliveryLine
|
86
|
+
# and more
|
73
87
|
end
|
74
88
|
```
|
75
89
|
|
76
|
-
|
90
|
+
Then, you can create a delivery class for a specific notification type. We follow Action Mailer conventions, and create a delivery class per resource:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
class PostsDelivery < ApplicationDelivery
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
In most cases, you just leave this class blank. The corresponding mailers, notifiers, etc., will be inferred automatically using the naming convention.
|
98
|
+
|
99
|
+
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.):
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
PostsDelivery.published(user, post).deliver_later
|
103
|
+
|
104
|
+
# Under the hood it calls
|
105
|
+
PostsMailer.published(user, post).deliver_later
|
106
|
+
PostsSMSNotifier.published(user, post).notify_later
|
107
|
+
|
108
|
+
# and whaterver your ActionCableDeliveryLine does
|
109
|
+
# under the hood.
|
110
|
+
```
|
111
|
+
|
112
|
+
Alternatively, you call the `#notify` method with the notification name and the arguments:
|
77
113
|
|
78
114
|
```ruby
|
79
115
|
PostsDelivery.notify(:published, user, post)
|
80
116
|
|
81
|
-
#
|
117
|
+
# Under the hood it calls
|
82
118
|
PostsMailer.published(user, post).deliver_later
|
119
|
+
PostsSMSNotifier.published(user, post).notify_later
|
120
|
+
# ...
|
121
|
+
```
|
83
122
|
|
84
|
-
|
85
|
-
|
123
|
+
You can also define a notification method explicitly if you want to add some logic:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
class PostsDelivery < ApplicationDelivery
|
127
|
+
def published(user, post)
|
128
|
+
# do something
|
129
|
+
|
130
|
+
# return a delivery object (to chain #deliver_later, etc.)
|
131
|
+
delivery(
|
132
|
+
notification: :published,
|
133
|
+
params: [user, post],
|
134
|
+
# For kwargs, you options
|
135
|
+
options: {},
|
136
|
+
# Metadata that can be used by line handlers
|
137
|
+
metadata: {}
|
138
|
+
)
|
139
|
+
end
|
140
|
+
end
|
86
141
|
```
|
87
142
|
|
88
|
-
|
143
|
+
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:
|
89
144
|
|
90
|
-
|
145
|
+
```ruby
|
146
|
+
class PostDelivery < ApplicationDelivery
|
147
|
+
delivers :published
|
148
|
+
end
|
149
|
+
|
150
|
+
ActiveDelivery.deliver_actions_required = true
|
151
|
+
|
152
|
+
PostDelivery.published(post) #=> ok
|
153
|
+
PostDelivery.whatever(post) #=> raises NoMethodError
|
154
|
+
```
|
155
|
+
|
156
|
+
### Customizing delivery handlers
|
157
|
+
|
158
|
+
You can specify a mailer class explicitly:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
class PostsDelivery < ActiveDelivery::Base
|
162
|
+
# You can pass a class name or a class itself
|
163
|
+
mailer "CustomPostsMailer"
|
164
|
+
# For other lines, you the line name as well
|
165
|
+
# sms "MyPostsSMSNotifier"
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
Or you can provide a custom resolver by re-registering the line:
|
91
170
|
|
92
171
|
```ruby
|
93
172
|
class PostsDelivery < ActiveDelivery::Base
|
94
|
-
register_line :
|
173
|
+
register_line :mailer, ActiveDelivery::Lines::Mailer, resolver: ->(_delivery_class) { CustomMailer }
|
95
174
|
end
|
96
175
|
```
|
97
176
|
|
177
|
+
### Parameterized deliveries
|
178
|
+
|
98
179
|
Delivery also supports _parameterized_ calling:
|
99
180
|
|
100
181
|
```ruby
|
@@ -103,17 +184,19 @@ PostsDelivery.with(user: user).notify(:published, post)
|
|
103
184
|
|
104
185
|
The parameters could be accessed through the `params` instance method (e.g., to implement guard-like logic).
|
105
186
|
|
106
|
-
**NOTE**: When params are
|
187
|
+
**NOTE**: When params are present, the parameterized mailer is used, i.e.:
|
107
188
|
|
108
189
|
```ruby
|
109
190
|
PostsMailer.with(user: user).published(post)
|
110
191
|
```
|
111
192
|
|
193
|
+
Other line implementations **MUST** also have the `#with` method in their public interface.
|
194
|
+
|
112
195
|
See [Rails docs](https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html) for more information on parameterized mailers.
|
113
196
|
|
114
|
-
|
197
|
+
### Callbacks support
|
115
198
|
|
116
|
-
**NOTE:** callbacks are only available if ActiveSupport is present in the
|
199
|
+
**NOTE:** callbacks are only available if ActiveSupport is present in the application's runtime.
|
117
200
|
|
118
201
|
```ruby
|
119
202
|
# Run method before delivering notification
|
@@ -133,6 +216,12 @@ after_notify :mark_user_as_notified, if: -> { params[:user].present? }
|
|
133
216
|
after_notify :cleanup
|
134
217
|
|
135
218
|
around_notify :set_context
|
219
|
+
|
220
|
+
# You can also skip callbacks in sub-classes
|
221
|
+
skip_before_notify :do_something, only: %i[some_reminder]
|
222
|
+
|
223
|
+
# NOTE: Specify `on` option for line-specific callbacks is required to skip them
|
224
|
+
skip_after_notify :do_mail_something, on: :mailer
|
136
225
|
```
|
137
226
|
|
138
227
|
Example:
|
@@ -141,7 +230,7 @@ Example:
|
|
141
230
|
# Let's log notifications
|
142
231
|
class MyDelivery < ActiveDelivery::Base
|
143
232
|
after_notify do
|
144
|
-
# You can access the
|
233
|
+
# You can access the notification name within the instance
|
145
234
|
MyLogger.info "Delivery triggered: #{notification_name}"
|
146
235
|
end
|
147
236
|
end
|
@@ -152,7 +241,9 @@ MyDeliver.notify(:something_wicked_this_way_comes)
|
|
152
241
|
|
153
242
|
## Testing
|
154
243
|
|
155
|
-
**NOTE:**
|
244
|
+
**NOTE:** Currently, only RSpec matchers are provided.
|
245
|
+
|
246
|
+
### Deliveries
|
156
247
|
|
157
248
|
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
249
|
|
@@ -163,7 +254,7 @@ it "delivers notification" do
|
|
163
254
|
end
|
164
255
|
```
|
165
256
|
|
166
|
-
You can also use such RSpec features as
|
257
|
+
You can also use such RSpec features as compound expectations and composed matchers:
|
167
258
|
|
168
259
|
```ruby
|
169
260
|
it "delivers to RSVPed members via .notify" do
|
@@ -186,7 +277,7 @@ specify "when event is not found" do
|
|
186
277
|
end
|
187
278
|
```
|
188
279
|
|
189
|
-
or use matcher
|
280
|
+
or use the `#have_not_delivered_to` matcher:
|
190
281
|
|
191
282
|
```ruby
|
192
283
|
specify "when event is not found" do
|
@@ -196,7 +287,57 @@ specify "when event is not found" do
|
|
196
287
|
end
|
197
288
|
```
|
198
289
|
|
199
|
-
|
290
|
+
### Delivery classes
|
291
|
+
|
292
|
+
You can test Delivery classes as regular Ruby classes:
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
describe PostsDelivery do
|
296
|
+
let(:user) { build_stubbed(:user) }
|
297
|
+
let(:post) { build_stubbed(:post) }
|
298
|
+
|
299
|
+
describe "#published" do
|
300
|
+
it "sends a mail" do
|
301
|
+
expect {
|
302
|
+
described_class.published(user, post).deliver_now
|
303
|
+
}.to change { ActionMailer::Base.deliveries.count }.by(1)
|
304
|
+
|
305
|
+
mail = ActionMailer::Base.deliveries.last
|
306
|
+
expect(mail.to).to eq([user.email])
|
307
|
+
expect(mail.subject).to eq("New post published")
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
```
|
312
|
+
|
313
|
+
You can also use the `#deliver_via` matchers as follows:
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
describe PostsDelivery, type: :delivery do
|
317
|
+
let(:user) { build_stubbed(:user) }
|
318
|
+
let(:post) { build_stubbed(:post) }
|
319
|
+
|
320
|
+
describe "#published" do
|
321
|
+
it "delivers to mailer and sms" do
|
322
|
+
expect {
|
323
|
+
described_class.published(user, post).deliver_later
|
324
|
+
}.to deliver_via(:mailer, :sms)
|
325
|
+
end
|
326
|
+
|
327
|
+
context "when user is not subscribed to SMS notifications" do
|
328
|
+
let(:user) { build_stubbed(:user, sms_notifications: false) }
|
329
|
+
|
330
|
+
it "delivers to mailer only" do
|
331
|
+
expect {
|
332
|
+
described_class.published(user, post).deliver_now
|
333
|
+
}.to deliver_via(:mailer)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
```
|
339
|
+
|
340
|
+
**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
341
|
|
201
342
|
## Custom "lines"
|
202
343
|
|
@@ -208,13 +349,15 @@ A line connects _delivery_ to the _sender_ class responsible for sending notific
|
|
208
349
|
|
209
350
|
If you want to use parameterized deliveries, your _sender_ class must respond to `.with(params)` method.
|
210
351
|
|
352
|
+
### A full-featured line example: pigeons 🐦
|
353
|
+
|
211
354
|
Assume that we want to send messages via _pigeons_ and we have the following sender class:
|
212
355
|
|
213
356
|
```ruby
|
214
357
|
class EventPigeon
|
215
358
|
class << self
|
216
359
|
# Add `.with` method as an alias
|
217
|
-
|
360
|
+
alias_method :with, :new
|
218
361
|
|
219
362
|
# delegate delivery action to the instance
|
220
363
|
def message_arrived(*args)
|
@@ -271,7 +414,50 @@ class PigeonLine < ActiveDelivery::Lines::Base
|
|
271
414
|
end
|
272
415
|
```
|
273
416
|
|
274
|
-
|
417
|
+
In the case of parameterized calling, some update needs to be done on the new Line. Here is an example:
|
418
|
+
|
419
|
+
```ruby
|
420
|
+
class EventPigeon
|
421
|
+
attr_reader :params
|
422
|
+
|
423
|
+
class << self
|
424
|
+
# Add `.with` method as an alias
|
425
|
+
alias_method :with, :new
|
426
|
+
|
427
|
+
# delegate delivery action to the instance
|
428
|
+
def message_arrived(*args)
|
429
|
+
new.message_arrived(*args)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def initialize(params = {})
|
434
|
+
@params = params
|
435
|
+
# do smth with params
|
436
|
+
end
|
437
|
+
|
438
|
+
def message_arrived(msg)
|
439
|
+
# send a pigeon with the message
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
class PigeonLine < ActiveDelivery::Lines::Base
|
444
|
+
def notify_later(sender, delivery_action, *args, **kwargs)
|
445
|
+
# `to_s` is important for serialization. Unless you might have error
|
446
|
+
PigeonLaunchJob.perform_later sender.class.to_s, delivery_action, *args, **kwargs.merge(params: line.params)
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
class PigeonLaunchJob < ActiveJob::Base
|
451
|
+
def perform(sender, delivery_action, *args, params: nil, **kwargs)
|
452
|
+
klass = sender.safe_constantize
|
453
|
+
handler = params ? klass.with(**params) : klass.new
|
454
|
+
|
455
|
+
handler.public_send(delivery_action, *args, **kwargs)
|
456
|
+
end
|
457
|
+
end
|
458
|
+
```
|
459
|
+
|
460
|
+
**NOTE**: we fall back to the superclass's sender class if `resolve_class` returns nil.
|
275
461
|
You can disable automatic inference of sender classes by marking delivery as _abstract_:
|
276
462
|
|
277
463
|
```ruby
|
@@ -295,7 +481,7 @@ class EventDelivery < ActiveDelivery::Base
|
|
295
481
|
# register_line :pigeon, PigeonLine, namespace: "AngryPigeons"
|
296
482
|
#
|
297
483
|
# now you can explicitly specify pigeon class
|
298
|
-
# pigeon MyCustomPigeon
|
484
|
+
# pigeon "MyCustomPigeon"
|
299
485
|
#
|
300
486
|
# or define pigeon specific callbacks
|
301
487
|
#
|
@@ -303,9 +489,294 @@ class EventDelivery < ActiveDelivery::Base
|
|
303
489
|
end
|
304
490
|
```
|
305
491
|
|
306
|
-
|
492
|
+
You can also _unregister_ a line:
|
493
|
+
|
494
|
+
```ruby
|
495
|
+
class NonMailerDelivery < ActiveDelivery::Base
|
496
|
+
# Use unregister_line to remove any default or inherited lines
|
497
|
+
unregister_line :mailer
|
498
|
+
end
|
499
|
+
```
|
500
|
+
|
501
|
+
### An example of a universal sender: Action Cable
|
502
|
+
|
503
|
+
Although Active Delivery is designed to work with Action Mailer-like abstraction, it's flexible enough to support other use cases.
|
504
|
+
|
505
|
+
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.
|
506
|
+
|
507
|
+
For every delivery, we want to broadcast a message via Action Cable to the stream corresponding to the delivery class name. For example:
|
508
|
+
|
509
|
+
```ruby
|
510
|
+
# Our PostsDelivery example from the beginning
|
511
|
+
PostsDelivery.with(user:).notify(:published, post)
|
512
|
+
|
513
|
+
# Will results in the following Action Cable broadcast:
|
514
|
+
DeliveryChannel.broadcast_to user, {event: "posts.published", post_id: post.id}
|
515
|
+
```
|
516
|
+
|
517
|
+
The `ActionCableDeliveryLine` class can be implemented as follows:
|
518
|
+
|
519
|
+
```ruby
|
520
|
+
class ActionCableDeliveryLine < ActiveDelivery::Line::Base
|
521
|
+
# Context is our universal sender.
|
522
|
+
class Context
|
523
|
+
attr_reader :user
|
307
524
|
|
308
|
-
|
525
|
+
def initialize(scope)
|
526
|
+
@scope = scope
|
527
|
+
end
|
528
|
+
|
529
|
+
# User is required for this line
|
530
|
+
def with(user:, **)
|
531
|
+
@user = user
|
532
|
+
self
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
# The result of this callback is passed further to the `notify_now` method
|
537
|
+
def resolve_class(name)
|
538
|
+
Context.new(name.sub(/Delivery$/, "").underscore)
|
539
|
+
end
|
540
|
+
|
541
|
+
# We want to broadcast all notifications
|
542
|
+
def notify?(...) = true
|
543
|
+
|
544
|
+
def notify_now(context, delivery_action, *args, **kwargs)
|
545
|
+
# Skip if no user provided
|
546
|
+
return unless context.user
|
547
|
+
|
548
|
+
payload = {event: [context.scope, delivery_action].join(".")}
|
549
|
+
payload.merge!(serialized_args(*args, **kwargs))
|
550
|
+
|
551
|
+
DeliveryChannel.broadcast_to context.user, payload
|
552
|
+
end
|
553
|
+
|
554
|
+
# Broadcasts are asynchronous by nature, so we can just use `notify_now`
|
555
|
+
alias_method :notify_later, :notify_now
|
556
|
+
|
557
|
+
private
|
558
|
+
|
559
|
+
def serialized_args(*args, **kwargs)
|
560
|
+
# Code that convers AR objects into IDs, etc.
|
561
|
+
end
|
562
|
+
end
|
563
|
+
```
|
564
|
+
|
565
|
+
## Abstract Notifier
|
566
|
+
|
567
|
+
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.
|
568
|
+
|
569
|
+
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.
|
570
|
+
|
571
|
+
### Notifier classes
|
572
|
+
|
573
|
+
A **notifier object** is very similar to an Action Mailer's mailer with the `#notification` method used instead of the `#mail` method:
|
574
|
+
|
575
|
+
```ruby
|
576
|
+
class EventsNotifier < ApplicationNotifier
|
577
|
+
def canceled(profile, event)
|
578
|
+
notification(
|
579
|
+
# the only required option is `body`
|
580
|
+
body: "Event #{event.title} has been canceled",
|
581
|
+
# all other options are passed to delivery driver
|
582
|
+
identity: profile.notification_service_id
|
583
|
+
)
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
# send notification later
|
588
|
+
EventsNotifier.canceled(profile, event).notify_later
|
589
|
+
|
590
|
+
# or immediately
|
591
|
+
EventsNotifier.canceled(profile, event).notify_now
|
592
|
+
```
|
593
|
+
|
594
|
+
### Delivery drivers
|
595
|
+
|
596
|
+
To perform actual deliveries you **must** configure a _delivery driver_:
|
597
|
+
|
598
|
+
```ruby
|
599
|
+
class ApplicationNotifier < AbstractNotifier::Base
|
600
|
+
self.driver = MyFancySender.new
|
601
|
+
end
|
602
|
+
```
|
603
|
+
|
604
|
+
A driver could be any callable Ruby object (i.e., anything that responds to `#call`).
|
605
|
+
|
606
|
+
That's the developer's responsibility to implement the driver (we do not provide any drivers out-of-the-box; at least yet).
|
607
|
+
|
608
|
+
You can set different drivers for different notifiers.
|
609
|
+
|
610
|
+
### Parameterized notifiers
|
611
|
+
|
612
|
+
Abstract Notifier supports parameterization the same way as [Action Mailer](https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html):
|
613
|
+
|
614
|
+
```ruby
|
615
|
+
class EventsNotifier < ApplicationNotifier
|
616
|
+
def canceled(event)
|
617
|
+
notification(
|
618
|
+
body: "Event #{event.title} has been canceled",
|
619
|
+
identity: params[:profile].notification_service_id
|
620
|
+
)
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
EventsNotifier.with(profile: profile).canceled(event).notify_later
|
625
|
+
```
|
626
|
+
|
627
|
+
### Defaults
|
628
|
+
|
629
|
+
You can specify default notification fields at a class level:
|
630
|
+
|
631
|
+
```ruby
|
632
|
+
class EventsNotifier < ApplicationNotifier
|
633
|
+
# `category` field will be added to the notification
|
634
|
+
# if missing
|
635
|
+
default category: "EVENTS"
|
636
|
+
|
637
|
+
# ...
|
638
|
+
end
|
639
|
+
```
|
640
|
+
|
641
|
+
**NOTE**: when subclassing notifiers, default parameters are merged.
|
642
|
+
|
643
|
+
You can also specify a block or a method name as the default params _generator_.
|
644
|
+
This could be useful in combination with the `#notification_name` method to generate dynamic payloads:
|
645
|
+
|
646
|
+
```ruby
|
647
|
+
class ApplicationNotifier < AbstractNotifier::Base
|
648
|
+
default :build_defaults_from_locale
|
649
|
+
|
650
|
+
private
|
651
|
+
|
652
|
+
def build_defaults_from_locale
|
653
|
+
{
|
654
|
+
subject: I18n.t(notification_name, scope: [:notifiers, self.class.name.underscore])
|
655
|
+
}
|
656
|
+
end
|
657
|
+
end
|
658
|
+
```
|
659
|
+
|
660
|
+
### Background jobs / async notifications
|
661
|
+
|
662
|
+
To use `#notify_later` you **must** configure an async adapter for Abstract Notifier.
|
663
|
+
|
664
|
+
We provide an Active Job adapter out of the box and enable it if Active Job is found.
|
665
|
+
|
666
|
+
A custom async adapter must implement the `#enqueue` method:
|
667
|
+
|
668
|
+
```ruby
|
669
|
+
class MyAsyncAdapter
|
670
|
+
# adapters may accept options
|
671
|
+
def initialize(options = {})
|
672
|
+
end
|
673
|
+
|
674
|
+
# `enqueue` method accepts notifier class and notification
|
675
|
+
# payload.
|
676
|
+
# We need to know notifier class to use its driver.
|
677
|
+
def enqueue(notifier_class, payload)
|
678
|
+
# your implementation here
|
679
|
+
end
|
680
|
+
end
|
681
|
+
|
682
|
+
# Configure globally
|
683
|
+
AbstractNotifier.async_adapter = MyAsyncAdapter.new
|
684
|
+
|
685
|
+
# or per-notifier
|
686
|
+
class EventsNotifier < AbstractNotifier::Base
|
687
|
+
self.async_adapter = MyAsyncAdapter.new
|
688
|
+
end
|
689
|
+
```
|
690
|
+
|
691
|
+
### Delivery modes
|
692
|
+
|
693
|
+
For test/development purposes there are two special _global_ delivery modes:
|
694
|
+
|
695
|
+
```ruby
|
696
|
+
# Track all sent notifications without peforming real actions.
|
697
|
+
# Required for using RSpec matchers.
|
698
|
+
#
|
699
|
+
# config/environments/test.rb
|
700
|
+
AbstractNotifier.delivery_mode = :test
|
701
|
+
|
702
|
+
# If you don't want to trigger notifications in development,
|
703
|
+
# you can make Abstract Notifier no-op.
|
704
|
+
#
|
705
|
+
# config/environments/development.rb
|
706
|
+
AbstractNotifier.delivery_mode = :noop
|
707
|
+
|
708
|
+
# Default delivery mode is "normal"
|
709
|
+
AbstractNotifier.delivery_mode = :normal
|
710
|
+
```
|
711
|
+
|
712
|
+
**NOTE:** we set `delivery_mode = :test` if `RAILS_ENV` or `RACK_ENV` env variable is equal to "test".
|
713
|
+
Otherwise add `require "abstract_notifier/testing"` to your `spec_helper.rb` / `rails_helper.rb` manually.
|
714
|
+
|
715
|
+
**NOTE:** delivery mode affects all drivers.
|
716
|
+
|
717
|
+
### Testing notifier deliveries
|
718
|
+
|
719
|
+
Abstract Notifier provides two convenient RSpec matchers:
|
720
|
+
|
721
|
+
```ruby
|
722
|
+
# for testing sync notifications (sent with `notify_now`)
|
723
|
+
expect { EventsNotifier.with(profile: profile).canceled(event).notify_now }
|
724
|
+
.to have_sent_notification(identify: "123", body: "Alarma!")
|
725
|
+
|
726
|
+
# for testing async notifications (sent with `notify_later`)
|
727
|
+
expect { EventsNotifier.with(profile: profile).canceled(event).notify_later }
|
728
|
+
.to have_enqueued_notification(via: EventNotifier, identify: "123", body: "Alarma!")
|
729
|
+
|
730
|
+
# you can also specify the expected notifier class (useful when ypu have multiple notifier lines)
|
731
|
+
expect { EventsNotifier.with(profile: profile).canceled(event).notify_now }
|
732
|
+
.to have_sent_notification(via: EventsNotifier, identify: "123", body: "Alarma!")
|
733
|
+
```
|
734
|
+
|
735
|
+
Abstract Notifier also provides Minitest assertions:
|
736
|
+
|
737
|
+
```ruby
|
738
|
+
require "abstract_notifier/testing/minitest"
|
739
|
+
|
740
|
+
class EventsNotifierTestCase < Minitest::Test
|
741
|
+
include AbstractNotifier::TestHelper
|
742
|
+
|
743
|
+
test "canceled" do
|
744
|
+
assert_notifications_sent 1, identify: "321", body: "Alarma!" do
|
745
|
+
EventsNotifier.with(profile: profile).canceled(event).notify_now
|
746
|
+
end
|
747
|
+
|
748
|
+
assert_notifications_sent 1, via: EventNofitier, identify: "123", body: "Alarma!" do
|
749
|
+
EventsNotifier.with(profile: profile).canceled(event).notify_now
|
750
|
+
end
|
751
|
+
|
752
|
+
assert_notifications_enqueued 1, via: EventNofitier, identify: "123", body: "Alarma!" do
|
753
|
+
EventsNotifier.with(profile: profile).canceled(event).notify_later
|
754
|
+
end
|
755
|
+
end
|
756
|
+
end
|
757
|
+
```
|
758
|
+
|
759
|
+
**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)).
|
760
|
+
|
761
|
+
### Notifier lines for Active Delivery
|
762
|
+
|
763
|
+
Abstract Notifier provides a _notifier_ line for Active Delivery:
|
764
|
+
|
765
|
+
```ruby
|
766
|
+
class ApplicationDelivery < ActiveDelivery::Base
|
767
|
+
# Add notifier line to you delivery
|
768
|
+
# By default, we use `*Delivery` -> `*Notifier` resolution mechanism
|
769
|
+
register_line :notifier, notifier: true
|
770
|
+
|
771
|
+
# You can define a custom suffix to use for notifier classes:
|
772
|
+
# `*Delivery` -> `*CustomNotifier`
|
773
|
+
register_line :custom_notifier, notifier: true, suffix: "CustomNotifier"
|
774
|
+
|
775
|
+
# Or you can specify a Proc object to do custom resolution:
|
776
|
+
register_line :some_notifier, notifier: true,
|
777
|
+
resolver: ->(delivery_class) { resolve_somehow(delivery_class) }
|
778
|
+
end
|
779
|
+
```
|
309
780
|
|
310
781
|
## Contributing
|
311
782
|
|