active_delivery 0.4.3 → 1.0.0.rc2
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 +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
|
|