active_delivery 0.4.4 → 1.0.0

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