active_delivery 1.0.0.rc2 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9c65eedb249c1a52253ad6e2155271e8931b55e2e7e429baf9f72682a4bd60a
4
- data.tar.gz: '048565d99b6da3903905740b90c726f1805376237e7af4cb7f526b83bea0857e'
3
+ metadata.gz: 34afeceddc78be6864aed136f68bc0a0f0eb43c203ef1520260fe94a8c934b39
4
+ data.tar.gz: dab4270f8b7a4c5e14b18c3fd36ceb081587425f26a50060191778bab768d688
5
5
  SHA512:
6
- metadata.gz: 0c753da1c2b0abf43e3361eff188c87e2f3eefa9f1c1a35c3a238810b8fd399e0f91196c5af94a3d50a66ccd1439adb166be95e416b531b00ffa4021550c1608
7
- data.tar.gz: 225e522994457b30caad32e7ae5d0c93df5f2bca295fe63c460d3aa87f52478d387fc30950bc0b95a1f5ce93e42352588bc5310075bd55765cd67bb31b6e839f
6
+ metadata.gz: 67992b515155ae977dd2f839448b3fe89d8c1391906e3c20bd425782241378f70d0b933834433a5e30904a7f3a1176c1313cd690267f001258f9584b078f0367
7
+ data.tar.gz: c811abf777c5c0c7552749fcedd228e7914bc80ded1a14129f4194097d74049f72e80c229c59549ac09141fed3145ab4c4199706ca29055ce6a405c3c16e13a2
data/CHANGELOG.md CHANGED
@@ -2,47 +2,57 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 📬 1.0.0 (2023-08-29)
6
+
7
+ - Add `resolver_pattern` option to specify naming pattern for notifiers without using Procs. ([@palkan][])
8
+
9
+ - [!IMPORTANT] Notifier's `#notify_later` now do not process the action right away, only enqueue the job. ([@palkan][]).
10
+
11
+ This matches the Action Mailer behaviour. Now, the action is only invoked before the delivery attempt.
12
+
13
+ - Add callbacks support to Abstract Notifier (`before_action`, `after_deliver`, etc.). ([@palkan][])
14
+
5
15
  - **Merge in abstract_notifier** ([@palkan][])
6
16
 
7
- [Abstract Notifier](https://github.com/palkan/abstract_notifier) is now a part of Active Delivery.
17
+ [Abstract Notifier](https://github.com/palkan/abstract_notifier) is now a part of Active Delivery.
8
18
 
9
19
  - Add ability to specify delivery actions explicitly and disable implicit proxying. ([@palkan][])
10
20
 
11
- You can disable default Active Delivery behaviour of proxying action methods to underlying lines via the `ActiveDelivery.deliver_actions_required = true` configuration option. Then, in each delivery class, you can specify the available actions via the `.delivers` method:
21
+ You can disable default Active Delivery behaviour of proxying action methods to underlying lines via the `ActiveDelivery.deliver_actions_required = true` configuration option. Then, in each delivery class, you can specify the available actions via the `.delivers` method:
12
22
 
13
- ```ruby
14
- class PostMailer < ApplicationMailer
15
- def published(post)
16
- # ...
17
- end
23
+ ```ruby
24
+ class PostMailer < ApplicationMailer
25
+ def published(post)
26
+ # ...
27
+ end
18
28
 
19
- def whatever(post)
20
- # ...
29
+ def whatever(post)
30
+ # ...
31
+ end
21
32
  end
22
- end
23
33
 
24
- ActiveDelivery.deliver_actions_required = true
34
+ ActiveDelivery.deliver_actions_required = true
25
35
 
26
- class PostDelivery < ApplicationDelivery
27
- delivers :published
28
- end
36
+ class PostDelivery < ApplicationDelivery
37
+ delivers :published
38
+ end
29
39
 
30
- PostDelivery.published(post) #=> ok
31
- PostDelivery.whatever(post) #=> raises NoMethodError
32
- ```
40
+ PostDelivery.published(post) #=> ok
41
+ PostDelivery.whatever(post) #=> raises NoMethodError
42
+ ```
33
43
 
34
44
  - Add `#deliver_via(*lines)` RSpec matcher. ([@palkan][])
35
45
 
36
46
  - Provide ActionMailer-like interface to trigger notifications. ([@palkan][])
37
47
 
38
- Now you can send notifications as follows:
48
+ Now you can send notifications as follows:
39
49
 
40
- ```ruby
41
- MyDelivery.with(user:).new_notification(payload).deliver_later
50
+ ```ruby
51
+ MyDelivery.with(user:).new_notification(payload).deliver_later
42
52
 
43
- # Equals to the old (and still supported)
44
- MyDelivery.with(user:).notify(:new_notification, payload)
45
- ```
53
+ # Equals to the old (and still supported)
54
+ MyDelivery.with(user:).notify(:new_notification, payload)
55
+ ```
46
56
 
47
57
  - Support passing a string class name as a handler class. ([@palkan][])
48
58
 
data/README.md CHANGED
@@ -79,8 +79,16 @@ class ApplicationDelivery < ActiveDelivery::Base
79
79
  # Mailers are enabled by default, everything else must be declared explicitly
80
80
 
81
81
  # For example, you can use a notifier line (see below) with a custom resolver
82
+ # (the argument is the delivery class)
82
83
  register_line :sms, ActiveDelivery::Lines::Notifier,
83
- resolver: -> { _1.name.gsub(/Delivery$/, "SMSNotifier").safe_constantize }
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
84
92
 
85
93
  register_line :cable, ActionCableDeliveryLine
86
94
  # and more
@@ -153,6 +161,45 @@ PostDelivery.published(post) #=> ok
153
161
  PostDelivery.whatever(post) #=> raises NoMethodError
154
162
  ```
155
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
+
156
203
  ### Customizing delivery handlers
157
204
 
158
205
  You can specify a mailer class explicitly:
@@ -671,11 +718,14 @@ class MyAsyncAdapter
671
718
  def initialize(options = {})
672
719
  end
673
720
 
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
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
679
729
  end
680
730
  end
681
731
 
@@ -688,6 +738,53 @@ class EventsNotifier < AbstractNotifier::Base
688
738
  end
689
739
  ```
690
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
+
691
788
  ### Delivery modes
692
789
 
693
790
  For test/development purposes there are two special _global_ delivery modes:
@@ -772,6 +869,9 @@ class ApplicationDelivery < ActiveDelivery::Base
772
869
  # `*Delivery` -> `*CustomNotifier`
773
870
  register_line :custom_notifier, notifier: true, suffix: "CustomNotifier"
774
871
 
872
+ # Or using a custom pattern
873
+ register_line :custom_notifier, notifier: true, resolver_pattern: "%{delivery_name}CustomNotifier"
874
+
775
875
  # Or you can specify a Proc object to do custom resolution:
776
876
  register_line :some_notifier, notifier: true,
777
877
  resolver: ->(delivery_class) { resolve_somehow(delivery_class) }
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractNotifier
4
+ module AsyncAdapters
5
+ class ActiveJob
6
+ class DeliveryJob < ::ActiveJob::Base
7
+ def perform(notifier_class, *__rest__, &__block__)
8
+ AbstractNotifier::NotificationDelivery.new(notifier_class.constantize, *__rest__, &__block__).notify_now
9
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :perform)
10
+ end
11
+
12
+ DEFAULT_QUEUE = "notifiers"
13
+
14
+ attr_reader :job
15
+
16
+ def initialize(queue: DEFAULT_QUEUE, job: DeliveryJob)
17
+ @job = job.set(queue: queue)
18
+ end
19
+
20
+ def enqueue(...)
21
+ job.perform_later(...)
22
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :enqueue)
23
+ end
24
+ end
25
+ end
26
+
27
+ AbstractNotifier.async_adapter ||= :active_job
@@ -1,6 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveDelivery
4
+ class Delivery # :nodoc:
5
+ attr_reader :params, :options, :metadata, :notification, :owner
6
+
7
+ def initialize(owner, notification:, params:, options:, metadata:)
8
+ @owner = owner
9
+ @notification = notification
10
+ @params = params.freeze
11
+ @options = options.freeze
12
+ @metadata = metadata.freeze
13
+ end
14
+
15
+ def deliver_later ; owner.perform_notify(self); end
16
+
17
+ def deliver_now ; owner.perform_notify(self, sync: true); end
18
+
19
+ def delivery_class ; owner.class; end
20
+ end
21
+
22
+ class << self
23
+ # Whether to memoize resolved handler classes or not.
24
+ # Set to false if you're using a code reloader (e.g., Zeitwerk).
25
+ #
26
+ # Defaults to true (i.e. memoization is enabled
27
+ attr_accessor :cache_classes
28
+ # Whether to enforce specifying available delivery actions via .delivers in the
29
+ # delivery classes
30
+ attr_accessor :deliver_actions_required
31
+ end
32
+
33
+ self.cache_classes = true
34
+ self.deliver_actions_required = false
35
+
4
36
  # Base class for deliveries.
5
37
  #
6
38
  # Delivery object describes how to notify a user about
@@ -10,6 +42,8 @@ module ActiveDelivery
10
42
  # (i.e. mailers, notifiers). That means that calling a method on delivery class invokes the
11
43
  # same method on the corresponding class, e.g.:
12
44
  #
45
+ # EventsDelivery.one_hour_before(profile, event).deliver_later
46
+ # # or
13
47
  # EventsDelivery.notify(:one_hour_before, profile, event)
14
48
  #
15
49
  # # under the hood it calls
@@ -20,14 +54,14 @@ module ActiveDelivery
20
54
  #
21
55
  # Delivery also supports _parameterized_ calling:
22
56
  #
23
- # EventsDelivery.with(profile: profile).notify(:canceled, event)
57
+ # EventsDelivery.with(profile: profile).canceled(event).deliver_later
24
58
  #
25
59
  # The parameters could be accessed through `params` instance method (e.g.
26
60
  # to implement guard-like logic).
27
61
  #
28
62
  # When params are presents the parametrized mailer is used, i.e.:
29
63
  #
30
- # EventsMailer.with(profile: profile).canceled(event)
64
+ # EventsMailer.with(profile: profile).canceled(event).deliver_later
31
65
  #
32
66
  # See https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html
33
67
  class Base
@@ -47,6 +81,8 @@ module ActiveDelivery
47
81
  notify(mid, *args, **hargs, sync: true)
48
82
  end
49
83
 
84
+ alias_method :notify_now, :notify!
85
+
50
86
  def delivery_lines
51
87
  @lines ||= if superclass.respond_to?(:delivery_lines)
52
88
  superclass.delivery_lines.each_with_object({}) do |(key, val), acc|
@@ -57,12 +93,19 @@ module ActiveDelivery
57
93
  end
58
94
  end
59
95
 
60
- def register_line(line_id, line_class, **options)
96
+ def register_line(line_id, line_class = nil, notifier: nil, **options)
97
+ raise ArgumentError, "A line class or notifier configuration must be provided" if line_class.nil? && notifier.nil?
98
+
99
+ # Configure Notifier
100
+ if line_class.nil?
101
+ line_class = ActiveDelivery::Lines::Notifier
102
+ end
103
+
61
104
  delivery_lines[line_id] = line_class.new(id: line_id, owner: self, **options)
62
105
 
63
106
  instance_eval <<~CODE, __FILE__, __LINE__ + 1
64
107
  def #{line_id}(val)
65
- delivery_lines[:#{line_id}].handler_class = val
108
+ delivery_lines[:#{line_id}].handler_class_name = val
66
109
  end
67
110
 
68
111
  def #{line_id}_class
@@ -81,8 +124,46 @@ module ActiveDelivery
81
124
  end
82
125
 
83
126
  def abstract_class? ; abstract_class == true; end
127
+
128
+ # Specify explicitly which actions are supported by the delivery.
129
+ def delivers(*actions)
130
+ actions.each do |mid|
131
+ class_eval <<~CODE, __FILE__, __LINE__ + 1
132
+ def self.#{mid}(...)
133
+ new.#{mid}(...)
134
+ end
135
+
136
+ def #{mid}(*args, **kwargs)
137
+ delivery(
138
+ notification: :#{mid},
139
+ params: args,
140
+ options: kwargs
141
+ )
142
+ end
143
+ CODE
144
+ end
145
+ end
146
+
147
+ def respond_to_missing?(mid, include_private = false)
148
+ unless ActiveDelivery.deliver_actions_required
149
+ return true if delivery_lines.any? { |_, line| line.notify?(mid) }
150
+ end
151
+
152
+ super
153
+ end
154
+
155
+ def method_missing(mid, *args, **kwargs)
156
+ return super unless respond_to_missing?(mid)
157
+
158
+ # Lazily define a class method to avoid lookups
159
+ delivers(mid)
160
+
161
+ public_send(mid, *args, **kwargs)
162
+ end
84
163
  end
85
164
 
165
+ self.abstract_class = true
166
+
86
167
  attr_reader :params, :notification_name
87
168
 
88
169
  def initialize(**params)
@@ -91,31 +172,74 @@ module ActiveDelivery
91
172
  end
92
173
 
93
174
  # Enqueues delivery (i.e. uses #deliver_later for mailers)
94
- def notify(mid, *__rest__, &__block__)
95
- @notification_name = mid
96
- do_notify(*__rest__, &__block__)
97
- end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify)
175
+ def notify(mid, *args, **kwargs)
176
+ perform_notify(
177
+ delivery(notification: mid, params: args, options: kwargs)
178
+ )
179
+ end
98
180
 
99
181
  # The same as .notify but delivers synchronously
100
182
  # (i.e. #deliver_now for mailers)
101
- def notify!(mid, *args, **hargs)
102
- notify(mid, *args, **hargs, sync: true)
183
+ def notify!(mid, *args, **kwargs)
184
+ perform_notify(
185
+ delivery(notification: mid, params: args, options: kwargs),
186
+ sync: true
187
+ )
103
188
  end
104
189
 
105
- private
190
+ alias_method :notify_now, :notify!
191
+
192
+ def respond_to_missing?(mid, include_private = false)
193
+ unless ActiveDelivery.deliver_actions_required
194
+ return true if delivery_lines.any? { |_, line| line.notify?(mid) }
195
+ end
196
+
197
+ super
198
+ end
106
199
 
107
- def do_notify(*args, sync: false, **kwargs)
200
+ def method_missing(mid, *args, **kwargs)
201
+ return super unless respond_to_missing?(mid)
202
+
203
+ # Lazily define a method to avoid future lookups
204
+ self.class.class_eval <<~CODE, __FILE__, __LINE__ + 1
205
+ def #{mid}(*args, **kwargs)
206
+ delivery(
207
+ notification: :#{mid},
208
+ params: args,
209
+ options: kwargs
210
+ )
211
+ end
212
+ CODE
213
+
214
+ public_send(mid, *args, **kwargs)
215
+ end
216
+
217
+ protected
218
+
219
+ def perform_notify(delivery, sync: false)
108
220
  delivery_lines.each do |type, line|
109
- next if line.handler_class.nil?
110
- next unless line.notify?(notification_name)
221
+ next unless line.notify?(delivery.notification)
111
222
 
112
- notify_line(type, *args, params: params, sync: sync, **kwargs)
223
+ notify_line(type, line, delivery, sync: sync)
113
224
  end
114
225
  end
115
226
 
116
- def notify_line(type, *__rest__, &__block__)
117
- delivery_lines[type].notify(notification_name, *__rest__, &__block__)
118
- end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_line)
227
+ private
228
+
229
+ def notify_line(type, line, delivery, sync:)
230
+ line.notify(
231
+ delivery.notification,
232
+ *delivery.params,
233
+ params: params,
234
+ sync: sync,
235
+ **delivery.options
236
+ )
237
+ true
238
+ end
239
+
240
+ def delivery(notification:, params: nil, options: nil, metadata: nil)
241
+ Delivery.new(self, notification: notification, params: params, options: options, metadata: metadata)
242
+ end
119
243
 
120
244
  def delivery_lines
121
245
  self.class.delivery_lines
@@ -37,9 +37,11 @@ module ActiveDelivery
37
37
  end
38
38
 
39
39
  module InstanceExt
40
- def do_notify(...)
41
- run_callbacks(:notify) { super(...) }
42
- end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :do_notify)
40
+ def perform_notify(delivery, *__rest__, &__block__)
41
+ # We need to store the notification name to be able to use it in callbacks if/unless
42
+ @notification_name = delivery.notification
43
+ run_callbacks(:notify) { super(delivery, *__rest__, &__block__) }
44
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :perform_notify)
43
45
 
44
46
  def notify_line(kind, *__rest__, &__block__)
45
47
  run_callbacks(kind) { super(kind, *__rest__, &__block__) }
@@ -73,22 +75,24 @@ module ActiveDelivery
73
75
  skip_after_callbacks_if_terminated: true
74
76
  end
75
77
 
76
- def before_notify(method_or_block = nil, on: :notify, **options, &block)
77
- method_or_block ||= block
78
- _normalize_callback_options(options)
79
- set_callback on, :before, method_or_block, options
80
- end
78
+ %i[before after around].each do |kind|
79
+ define_method "#{kind}_notify" do |*names, on: :notify, **options, &block|
80
+ _normalize_callback_options(options)
81
81
 
82
- def after_notify(method_or_block = nil, on: :notify, **options, &block)
83
- method_or_block ||= block
84
- _normalize_callback_options(options)
85
- set_callback on, :after, method_or_block, options
86
- end
82
+ names.each do |name|
83
+ set_callback on, kind, name, options
84
+ end
85
+
86
+ set_callback on, kind, block, options if block
87
+ end
88
+
89
+ define_method "skip_#{kind}_notify" do |*names, on: :notify, **options|
90
+ _normalize_callback_options(options)
87
91
 
88
- def around_notify(method_or_block = nil, on: :notify, **options, &block)
89
- method_or_block ||= block
90
- _normalize_callback_options(options)
91
- set_callback on, :around, method_or_block, options
92
+ names.each do |name|
93
+ skip_callback(on, kind, name, options)
94
+ end
95
+ end
92
96
  end
93
97
  end
94
98
  end
@@ -1,17 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ unless "".respond_to?(:safe_constantize)
4
+ require "active_delivery/ext/string_constantize"
5
+ using ActiveDelivery::Ext::StringConstantize
6
+ end
7
+
3
8
  module ActiveDelivery
4
9
  module Lines
5
10
  class Base
6
11
  attr_reader :id, :options
7
12
  attr_accessor :owner
8
- attr_writer :handler_class
13
+ attr_accessor :handler_class_name
9
14
 
10
15
  def initialize(id:, owner:, **options)
11
16
  @id = id
12
17
  @owner = owner
13
18
  @options = options.tap(&:freeze)
14
- @resolver = options[:resolver]
19
+ @resolver = options[:resolver] || build_pattern_resolver(options[:resolver_pattern])
15
20
  end
16
21
 
17
22
  def dup_for(new_owner)
@@ -23,7 +28,7 @@ module ActiveDelivery
23
28
  end
24
29
 
25
30
  def notify?(method_name)
26
- handler_class.respond_to?(method_name)
31
+ handler_class&.respond_to?(method_name)
27
32
  end
28
33
 
29
34
  def notify_now(handler, mid, *__rest__, &__block__)
@@ -38,26 +43,47 @@ module ActiveDelivery
38
43
  end
39
44
 
40
45
  def handler_class
41
- return @handler_class if instance_variable_defined?(:@handler_class)
46
+ if ::ActiveDelivery.cache_classes
47
+ return @handler_class if instance_variable_defined?(:@handler_class)
48
+ end
42
49
 
43
50
  return @handler_class = nil if owner.abstract_class?
44
51
 
45
- @handler_class = resolve_class(owner.name) ||
46
- superclass_handler
52
+ superline = owner.superclass.delivery_lines[id] if owner.superclass.respond_to?(:delivery_lines) && owner.superclass.delivery_lines[id]
53
+
54
+ # If an explicit class name has been specified somewhere in the ancestor chain, use it.
55
+ class_name = @handler_class_name || superline&.handler_class_name
56
+
57
+ @handler_class =
58
+ if class_name
59
+ class_name.is_a?(Class) ? class_name : class_name.safe_constantize
60
+ else
61
+ resolve_class(owner) || superline&.handler_class
62
+ end
47
63
  end
48
64
 
49
65
  private
50
66
 
51
- def superclass_handler
52
- handler_method = "#{id}_class"
67
+ attr_reader :resolver
53
68
 
54
- return if owner.superclass == ActiveDelivery::Base
55
- return unless owner.superclass.respond_to?(handler_method)
69
+ def build_pattern_resolver(pattern)
70
+ return unless pattern
56
71
 
57
- owner.superclass.public_send(handler_method)
58
- end
72
+ proc do |delivery|
73
+ delivery_class = delivery.name
59
74
 
60
- attr_reader :resolver
75
+ next unless delivery_class
76
+
77
+ *namespace, delivery_name = delivery_class.split("::")
78
+
79
+ delivery_namespace = ""
80
+ delivery_namespace = "#{namespace.join("::")}::" unless namespace.empty?
81
+
82
+ delivery_name = delivery_name.sub(/Delivery$/, "")
83
+
84
+ (pattern % {delivery_class: delivery_class, delivery_name: delivery_name, delivery_namespace: delivery_namespace}).safe_constantize
85
+ end
86
+ end
61
87
  end
62
88
  end
63
89
  end
@@ -5,9 +5,10 @@ module ActiveDelivery
5
5
  class Mailer < Base
6
6
  alias_method :mailer_class, :handler_class
7
7
 
8
- DEFAULT_RESOLVER = ->(name) { name.gsub(/Delivery$/, "Mailer").safe_constantize }
8
+ DEFAULT_RESOLVER = ->(klass) { klass.name&.gsub(/Delivery$/, "Mailer")&.safe_constantize }
9
9
 
10
10
  def notify?(method_name)
11
+ return unless mailer_class
11
12
  mailer_class.action_methods.include?(method_name.to_s)
12
13
  end
13
14