active_delivery 0.4.4 → 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.
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