active_delivery 1.0.0.rc2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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