active_delivery 0.4.4 → 1.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE.txt +19 -17
  4. data/README.md +495 -33
  5. data/lib/.rbnext/3.0/active_delivery/base.rb +124 -0
  6. data/lib/.rbnext/3.0/active_delivery/callbacks.rb +97 -0
  7. data/lib/.rbnext/3.0/active_delivery/lines/base.rb +63 -0
  8. data/lib/.rbnext/3.0/active_delivery/lines/mailer.rb +25 -0
  9. data/lib/.rbnext/3.1/active_delivery/base.rb +124 -0
  10. data/lib/.rbnext/3.1/active_delivery/lines/base.rb +63 -0
  11. data/lib/abstract_notifier/async_adapters/active_job.rb +27 -0
  12. data/lib/abstract_notifier/async_adapters.rb +16 -0
  13. data/lib/abstract_notifier/base.rb +178 -0
  14. data/lib/abstract_notifier/testing/minitest.rb +51 -0
  15. data/lib/abstract_notifier/testing/rspec.rb +164 -0
  16. data/lib/abstract_notifier/testing.rb +49 -0
  17. data/lib/abstract_notifier/version.rb +5 -0
  18. data/lib/abstract_notifier.rb +74 -0
  19. data/lib/active_delivery/base.rb +147 -27
  20. data/lib/active_delivery/callbacks.rb +25 -25
  21. data/lib/active_delivery/ext/string_constantize.rb +24 -0
  22. data/lib/active_delivery/lines/base.rb +24 -17
  23. data/lib/active_delivery/lines/mailer.rb +7 -18
  24. data/lib/active_delivery/lines/notifier.rb +53 -0
  25. data/lib/active_delivery/raitie.rb +9 -0
  26. data/lib/active_delivery/testing/rspec.rb +59 -12
  27. data/lib/active_delivery/testing.rb +19 -5
  28. data/lib/active_delivery/version.rb +1 -1
  29. data/lib/active_delivery.rb +8 -0
  30. metadata +61 -56
  31. data/.gem_release.yml +0 -3
  32. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
  33. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -24
  34. data/.github/PULL_REQUEST_TEMPLATE.md +0 -23
  35. data/.github/workflows/docs-lint.yml +0 -72
  36. data/.github/workflows/rspec-jruby.yml +0 -35
  37. data/.github/workflows/rspec.yml +0 -51
  38. data/.github/workflows/rubocop.yml +0 -21
  39. data/.gitignore +0 -43
  40. data/.mdlrc +0 -1
  41. data/.rspec +0 -2
  42. data/.rubocop-md.yml +0 -16
  43. data/.rubocop.yml +0 -28
  44. data/Gemfile +0 -17
  45. data/RELEASING.md +0 -43
  46. data/Rakefile +0 -20
  47. data/active_delivery.gemspec +0 -35
  48. data/forspell.dict +0 -8
  49. data/gemfiles/jruby.gemfile +0 -5
  50. data/gemfiles/rails42.gemfile +0 -8
  51. data/gemfiles/rails5.gemfile +0 -5
  52. data/gemfiles/rails50.gemfile +0 -8
  53. data/gemfiles/rails6.gemfile +0 -5
  54. data/gemfiles/railsmaster.gemfile +0 -6
  55. data/gemfiles/rubocop.gemfile +0 -4
  56. data/lefthook.yml +0 -18
  57. 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
- Framework providing an entry point (single _interface_) for all types of notifications: mailers, push notifications, whatever you want.
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.5
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 related logic:
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).notify(:some_action)
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 PostsDelivery < ActiveDelivery::Base
71
- # in most cases you don't have to specify anything in this class,
72
- # 'cause default transport-level classes (such as mailers)
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
- It acts as a proxy in front of the different delivery channels (i.e., mailers, notifiers). That means that calling a method on delivery class invokes the same method on the corresponding _sender_ class, e.g.:
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
- # under the hood it calls
117
+ # Under the hood it calls
82
118
  PostsMailer.published(user, post).deliver_later
119
+ PostsSMSNotifier.published(user, post).notify_later
120
+ # ...
121
+ ```
122
+
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
141
+ ```
142
+
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:
83
144
 
84
- # and if you have a notifier (or any other line, see below)
85
- PostsNotifier.published(user, post).notify_later
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
86
154
  ```
87
155
 
88
- P.S. Naming ("delivery") is inspired by Basecamp: https://www.youtube.com/watch?v=m1jOWu7woKM.
156
+ ### Customizing delivery handlers
157
+
158
+ You can specify a mailer class explicitly:
89
159
 
90
- **NOTE**: You could specify Mailer class explicitly or by custom pattern, using resolver:
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 :custom_mailer, ActiveDelivery::Lines::Mailer, resolver: ->(name) { CustomMailer }
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 presents the parameterized mailer is used, i.e.:
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
- ## Callbacks support
197
+ ### Callbacks support
115
198
 
116
- **NOTE:** callbacks are only available if ActiveSupport is present in the app's runtime.
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 notificaion name within the instance
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:** RSpec only for the time being.
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 [compound expectations](https://relishapp.com/rspec/rspec-expectations/docs/compound-expectations) and [composed matchers](https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/composing-matchers):
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
- **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 test environment (e.g. with help of [spring-commands-rspec](https://github.com/jonleighton/spring-commands-rspec)).
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
- alias with new
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
- **NOTE**: we fallback to superclass's sender class if `resolve_class` returns nil.
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,7 +489,7 @@ class EventDelivery < ActiveDelivery::Base
303
489
  end
304
490
  ```
305
491
 
306
- You can also _unregister_ a line. For example, when subclassing another `Delivery` class or to remove any of the automatically added lines (e.g., `mailer`):
492
+ You can also _unregister_ a line:
307
493
 
308
494
  ```ruby
309
495
  class NonMailerDelivery < ActiveDelivery::Base
@@ -312,9 +498,285 @@ class NonMailerDelivery < ActiveDelivery::Base
312
498
  end
313
499
  ```
314
500
 
315
- ## Related projects
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
524
+
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
316
649
 
317
- - [`abstract_notifier`](https://github.com/palkan/abstract_notifier) – Action Mailer-like interface for text-based notifications.
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
+ ```
318
780
 
319
781
  ## Contributing
320
782