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
@@ -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)
16
+
17
+ def deliver_now = owner.perform_notify(self, sync: true)
18
+
19
+ def delivery_class = owner.class
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,25 +54,25 @@ 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
34
68
  class << self
35
69
  attr_accessor :abstract_class
36
70
 
37
- alias with new
71
+ alias_method :with, :new
38
72
 
39
73
  # Enqueues delivery (i.e. uses #deliver_later for mailers)
40
- def notify(*args, **kwargs)
41
- new.notify(*args, **kwargs)
74
+ def notify(...)
75
+ new.notify(...)
42
76
  end
43
77
 
44
78
  # The same as .notify but delivers synchronously
@@ -47,24 +81,31 @@ 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
- @lines ||= begin
52
- if superclass.respond_to?(:delivery_lines)
53
- superclass.delivery_lines.each_with_object({}) do |(key, val), acc|
54
- acc[key] = val.dup_for(self)
55
- end
56
- else
57
- {}
87
+ @lines ||= if superclass.respond_to?(:delivery_lines)
88
+ superclass.delivery_lines.each_with_object({}) do |(key, val), acc|
89
+ acc[key] = val.dup_for(self)
58
90
  end
91
+ else
92
+ {}
59
93
  end
60
94
  end
61
95
 
62
- 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
+
63
104
  delivery_lines[line_id] = line_class.new(id: line_id, owner: self, **options)
64
105
 
65
106
  instance_eval <<~CODE, __FILE__, __LINE__ + 1
66
107
  def #{line_id}(val)
67
- delivery_lines[:#{line_id}].handler_class = val
108
+ delivery_lines[:#{line_id}].handler_class_name = val
68
109
  end
69
110
 
70
111
  def #{line_id}_class
@@ -82,11 +123,47 @@ module ActiveDelivery
82
123
  singleton_class.undef_method "#{line_id}_class"
83
124
  end
84
125
 
85
- def abstract_class?
86
- abstract_class == true
126
+ def abstract_class? = abstract_class == true
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)
87
162
  end
88
163
  end
89
164
 
165
+ self.abstract_class = true
166
+
90
167
  attr_reader :params, :notification_name
91
168
 
92
169
  def initialize(**params)
@@ -96,29 +173,72 @@ module ActiveDelivery
96
173
 
97
174
  # Enqueues delivery (i.e. uses #deliver_later for mailers)
98
175
  def notify(mid, *args, **kwargs)
99
- @notification_name = mid
100
- do_notify(*args, **kwargs)
176
+ perform_notify(
177
+ delivery(notification: mid, params: args, options: kwargs)
178
+ )
101
179
  end
102
180
 
103
181
  # The same as .notify but delivers synchronously
104
182
  # (i.e. #deliver_now for mailers)
105
- def notify!(mid, *args, **hargs)
106
- 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
+ )
107
188
  end
108
189
 
109
- private
190
+ alias_method :notify_now, :notify!
110
191
 
111
- def do_notify(*args, sync: false, **kwargs)
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
199
+
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)
112
220
  delivery_lines.each do |type, line|
113
- next if line.handler_class.nil?
114
- next unless line.notify?(notification_name)
221
+ next unless line.notify?(delivery.notification)
115
222
 
116
- notify_line(type, *args, params: params, sync: sync, **kwargs)
223
+ notify_line(type, line, delivery, sync:)
117
224
  end
118
225
  end
119
226
 
120
- def notify_line(type, *args, **kwargs)
121
- delivery_lines[type].notify(notification_name, *args, **kwargs)
227
+ private
228
+
229
+ def notify_line(type, line, delivery, sync:)
230
+ line.notify(
231
+ delivery.notification,
232
+ *delivery.params,
233
+ params:,
234
+ 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:, params:, options:, metadata:)
122
242
  end
123
243
 
124
244
  def delivery_lines
@@ -8,7 +8,7 @@ module ActiveDelivery
8
8
  # Add callbacks support to Active Delivery (requires ActiveSupport::Callbacks)
9
9
  #
10
10
  # # Run method before delivering notification
11
- # # NOTE: when `false` is returned the executation is halted
11
+ # # NOTE: when `false` is returned the execution is halted
12
12
  # before_notify :do_something
13
13
  #
14
14
  # # You can specify a notification method (to run callback only for that method)
@@ -26,11 +26,7 @@ module ActiveDelivery
26
26
 
27
27
  include ActiveSupport::Callbacks
28
28
 
29
- CALLBACK_TERMINATOR = if ::ActiveSupport::VERSION::MAJOR >= 5
30
- ->(_target, result) { result.call == false }
31
- else
32
- ->(_target, result) { result == false }
33
- end
29
+ CALLBACK_TERMINATOR = ->(_target, result) { result.call == false }
34
30
 
35
31
  included do
36
32
  # Define "global" callbacks
@@ -41,17 +37,19 @@ module ActiveDelivery
41
37
  end
42
38
 
43
39
  module InstanceExt
44
- def do_notify(*args, **kwargs)
45
- run_callbacks(:notify) { super(*args, **kwargs) }
40
+ def perform_notify(delivery, ...)
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, ...) }
46
44
  end
47
45
 
48
- def notify_line(*args, **kwargs)
49
- run_callbacks(args.first) { super(*args, **kwargs) }
46
+ def notify_line(kind, ...)
47
+ run_callbacks(kind) { super(kind, ...) }
50
48
  end
51
49
  end
52
50
 
53
51
  module SingltonExt
54
- def register_line(line_id, *args, **kwargs)
52
+ def register_line(line_id, ...)
55
53
  super
56
54
  define_line_callbacks line_id
57
55
  end
@@ -77,22 +75,24 @@ module ActiveDelivery
77
75
  skip_after_callbacks_if_terminated: true
78
76
  end
79
77
 
80
- def before_notify(method_or_block = nil, on: :notify, **options, &block)
81
- method_or_block ||= block
82
- _normalize_callback_options(options)
83
- set_callback on, :before, method_or_block, options
84
- 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)
85
81
 
86
- def after_notify(method_or_block = nil, on: :notify, **options, &block)
87
- method_or_block ||= block
88
- _normalize_callback_options(options)
89
- set_callback on, :after, method_or_block, options
90
- end
82
+ names.each do |name|
83
+ set_callback on, kind, name, options
84
+ end
91
85
 
92
- def around_notify(method_or_block = nil, on: :notify, **options, &block)
93
- method_or_block ||= block
94
- _normalize_callback_options(options)
95
- set_callback on, :around, method_or_block, options
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)
91
+
92
+ names.each do |name|
93
+ skip_callback(on, kind, name, options)
94
+ end
95
+ end
96
96
  end
97
97
  end
98
98
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDelivery
4
+ module Ext
5
+ # Add simple safe_constantize method to String
6
+ module StringConstantize
7
+ refine String do
8
+ def safe_constantize
9
+ names = split("::")
10
+
11
+ return nil if names.empty?
12
+
13
+ # Remove the first blank element in case of '::ClassName' notation.
14
+ names.shift if names.size > 1 && names.first.empty?
15
+
16
+ names.inject(Object) do |constant, name|
17
+ break if constant.nil?
18
+ constant.const_get(name, false) if constant.const_defined?(name, false)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,21 +1,26 @@
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)
18
- self.class.new(id: id, **options, owner: new_owner)
23
+ self.class.new(id:, **options, owner: new_owner)
19
24
  end
20
25
 
21
26
  def resolve_class(name)
@@ -23,13 +28,13 @@ 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
- def notify_now(handler, mid, *args, **kwargs)
34
+ def notify_now(handler, mid, ...)
30
35
  end
31
36
 
32
- def notify_later(handler, mid, *args, **kwargs)
37
+ def notify_later(handler, mid, ...)
33
38
  end
34
39
 
35
40
  def notify(mid, *args, params:, sync:, **kwargs)
@@ -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 ActiveDelivery::Base == owner.superclass
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_name:, delivery_namespace:}).safe_constantize
85
+ end
86
+ end
61
87
  end
62
88
  end
63
89
  end
@@ -1,34 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if ActionMailer::VERSION::MAJOR < 5 || (ActionMailer::VERSION::MAJOR == 5 && ActionMailer::VERSION::MINOR < 2)
4
- require "active_delivery/action_mailer/parameterized"
5
- end
6
-
7
3
  module ActiveDelivery
8
4
  module Lines
9
5
  class Mailer < Base
10
- alias mailer_class handler_class
6
+ alias_method :mailer_class, :handler_class
11
7
 
12
- DEFAULT_RESOLVER = ->(name) { name.gsub(/Delivery$/, "Mailer").safe_constantize }
8
+ DEFAULT_RESOLVER = ->(klass) { klass.name&.gsub(/Delivery$/, "Mailer")&.safe_constantize }
13
9
 
14
10
  def notify?(method_name)
11
+ return unless mailer_class
15
12
  mailer_class.action_methods.include?(method_name.to_s)
16
13
  end
17
14
 
18
- def notify_now(mailer, mid, *args, **kwargs)
19
- if kwargs.empty?
20
- mailer.public_send(mid, *args).deliver_now
21
- else
22
- mailer.public_send(mid, *args, **kwargs).deliver_now
23
- end
15
+ def notify_now(mailer, mid, ...)
16
+ mailer.public_send(mid, ...).deliver_now
24
17
  end
25
18
 
26
- def notify_later(mailer, mid, *args, **kwargs)
27
- if kwargs.empty?
28
- mailer.public_send(mid, *args).deliver_later
29
- else
30
- mailer.public_send(mid, *args, **kwargs).deliver_later
31
- end
19
+ def notify_later(mailer, mid, ...)
20
+ mailer.public_send(mid, ...).deliver_later
32
21
  end
33
22
  end
34
23
 
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless "".respond_to?(:safe_constantize)
4
+ require "active_delivery/ext/string_constantize"
5
+ using ActiveDelivery::Ext::StringConstantize
6
+ end
7
+
8
+ module ActiveDelivery
9
+ module Lines
10
+ # AbstractNotifier line for Active Delivery.
11
+ #
12
+ # You must provide custom `resolver` to infer notifier class
13
+ # (if String#safe_constantize is defined, we convert "*Delivery" -> "*Notifier").
14
+ #
15
+ # Resolver is a callable object.
16
+ class Notifier < ActiveDelivery::Lines::Base
17
+ DEFAULT_SUFFIX = "Notifier"
18
+
19
+ def initialize(**opts)
20
+ super
21
+ @resolver ||= build_resolver(options.fetch(:suffix, DEFAULT_SUFFIX))
22
+ end
23
+
24
+ def resolve_class(klass)
25
+ resolver&.call(klass)
26
+ end
27
+
28
+ def notify?(method_name)
29
+ return unless handler_class
30
+ handler_class.action_methods.include?(method_name.to_s)
31
+ end
32
+
33
+ def notify_now(handler, mid, *args)
34
+ handler.public_send(mid, *args).notify_now
35
+ end
36
+
37
+ def notify_later(handler, mid, *args)
38
+ handler.public_send(mid, *args).notify_later
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :resolver
44
+
45
+ def build_resolver(suffix)
46
+ lambda do |klass|
47
+ klass_name = klass.name
48
+ klass_name&.sub(/Delivery\z/, suffix)&.safe_constantize
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDelivery
4
+ class Railtie < Rails::Railtie
5
+ config.after_initialize do |app|
6
+ ActiveDelivery.cache_classes = app.config.cache_classes
7
+ end
8
+ end
9
+ end