active_delivery 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +60 -25
  4. data/lib/.rbnext/3.0/abstract_notifier/async_adapters/active_job.rb +12 -3
  5. data/lib/.rbnext/3.0/abstract_notifier/base.rb +223 -0
  6. data/lib/.rbnext/3.0/active_delivery/base.rb +14 -13
  7. data/lib/.rbnext/3.0/active_delivery/lines/base.rb +14 -2
  8. data/lib/.rbnext/3.0/active_delivery/lines/mailer.rb +4 -0
  9. data/lib/.rbnext/3.0/active_delivery/testing/minitest.rb +58 -0
  10. data/lib/.rbnext/3.0/active_delivery/testing.rb +1 -0
  11. data/lib/.rbnext/3.1/abstract_notifier/async_adapters/active_job.rb +36 -0
  12. data/lib/.rbnext/3.1/abstract_notifier/base.rb +16 -10
  13. data/lib/.rbnext/3.1/active_delivery/base.rb +14 -13
  14. data/lib/.rbnext/3.1/active_delivery/lines/base.rb +14 -2
  15. data/lib/.rbnext/3.1/active_delivery/testing/minitest.rb +58 -0
  16. data/lib/.rbnext/3.2/abstract_notifier/async_adapters/active_job.rb +36 -0
  17. data/lib/.rbnext/3.2/abstract_notifier/base.rb +223 -0
  18. data/lib/.rbnext/3.2/abstract_notifier/testing/rspec.rb +164 -0
  19. data/lib/.rbnext/3.2/abstract_notifier/testing.rb +53 -0
  20. data/lib/.rbnext/3.2/active_delivery/base.rb +249 -0
  21. data/lib/.rbnext/3.2/active_delivery/lines/base.rb +101 -0
  22. data/lib/.rbnext/3.2/active_delivery/lines/notifier.rb +57 -0
  23. data/lib/.rbnext/3.2/active_delivery/testing/rspec.rb +222 -0
  24. data/lib/abstract_notifier/async_adapters/active_job.rb +12 -3
  25. data/lib/abstract_notifier/base.rb +15 -9
  26. data/lib/abstract_notifier/testing/minitest.rb +1 -1
  27. data/lib/abstract_notifier/testing/rspec.rb +4 -4
  28. data/lib/abstract_notifier/testing.rb +2 -2
  29. data/lib/active_delivery/base.rb +14 -13
  30. data/lib/active_delivery/lines/base.rb +14 -2
  31. data/lib/active_delivery/lines/mailer.rb +4 -0
  32. data/lib/active_delivery/lines/notifier.rb +8 -4
  33. data/lib/active_delivery/testing/minitest.rb +58 -0
  34. data/lib/active_delivery/testing/rspec.rb +4 -4
  35. data/lib/active_delivery/testing.rb +1 -0
  36. data/lib/active_delivery/version.rb +1 -1
  37. metadata +20 -7
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDelivery
4
+ module TestHelper
5
+ def assert_deliveries(count)
6
+ TestDelivery.enable { yield }
7
+
8
+ assert_equal TestDelivery.store.count, count, "Expected #{count} deliveries, got #{TestDelivery.store.count}"
9
+ end
10
+
11
+ def assert_no_deliveries(&__block__) ; assert_deliveries(0, &__block__); end
12
+
13
+ def assert_delivery_enqueued(delivery_class, event, count: 1, params: nil, with: nil)
14
+ TestDelivery.enable { yield }
15
+
16
+ deliveries = TestDelivery.store
17
+
18
+ if with
19
+ args = with
20
+ kwargs = args.pop if args.last.is_a?(Hash)
21
+ end
22
+
23
+ matching_deliveries, _unmatching_deliveries =
24
+ deliveries.partition do |(delivery, options)|
25
+ next false if delivery_class != delivery.owner.class
26
+
27
+ next false if event != delivery.notification
28
+
29
+ next false if params && !hash_include?(delivery.owner.params, params)
30
+
31
+ next true unless with
32
+
33
+ actual_args = delivery.params
34
+ actual_kwargs = delivery.options
35
+
36
+ next false unless args.each.with_index.all? do |arg, i|
37
+ arg === actual_args[i]
38
+ end
39
+
40
+ next false unless kwargs.all? do |k, v|
41
+ v === actual_kwargs[k]
42
+ end
43
+
44
+ true
45
+ end
46
+
47
+ assert_equal count, matching_deliveries.count, "Expected #{count} deliveries, got #{deliveries.count}"
48
+ end
49
+
50
+ private
51
+
52
+ def hash_include?(haystack, needle)
53
+ needle.all? do |k, v|
54
+ haystack.key?(k) && haystack[k] == v
55
+ end
56
+ end
57
+ end
58
+ end
@@ -60,3 +60,4 @@ end
60
60
  ActiveDelivery::Base.prepend ActiveDelivery::TestDelivery
61
61
 
62
62
  require "active_delivery/testing/rspec" if defined?(RSpec::Core)
63
+ require "active_delivery/testing/minitest" if defined?(Minitest::Assertions)
@@ -0,0 +1,36 @@
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, ...)
8
+ AbstractNotifier::NotificationDelivery.new(notifier_class.constantize, ...).notify_now
9
+ end
10
+ end
11
+
12
+ DEFAULT_QUEUE = "notifiers"
13
+
14
+ attr_reader :job, :queue
15
+
16
+ def initialize(queue: DEFAULT_QUEUE, job: DeliveryJob)
17
+ @job = job
18
+ @queue = queue
19
+ end
20
+
21
+ def enqueue(...)
22
+ job.set(queue: queue).perform_later(...)
23
+ end
24
+
25
+ def enqueue_delivery(delivery, **__kwrest__)
26
+ job.set(queue: queue, **__kwrest__).perform_later(
27
+ delivery.notifier_class.name,
28
+ delivery.action_name,
29
+ **delivery.delivery_params
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ AbstractNotifier.async_adapter ||= :active_job
@@ -5,10 +5,10 @@ module AbstractNotifier
5
5
  # information about the current notifier class
6
6
  # and knows how to trigger the delivery
7
7
  class NotificationDelivery
8
- attr_reader :action_name
8
+ attr_reader :action_name, :notifier_class
9
9
 
10
- def initialize(owner_class, action_name, params: {}, args: [], kwargs: {})
11
- @owner_class = owner_class
10
+ def initialize(notifier_class, action_name, params: {}, args: [], kwargs: {})
11
+ @notifier_class = notifier_class
12
12
  @action_name = action_name
13
13
  @params = params
14
14
  @args = args
@@ -23,8 +23,12 @@ module AbstractNotifier
23
23
 
24
24
  alias_method :notification, :processed
25
25
 
26
- def notify_later
27
- owner_class.async_adapter.enqueue(owner_class.name, action_name, params: params, args: args, kwargs: kwargs)
26
+ def notify_later(**__kwrest__)
27
+ if notifier_class.async_adapter.respond_to?(:enqueue_delivery)
28
+ notifier_class.async_adapter.enqueue_delivery(self, **__kwrest__)
29
+ else
30
+ notifier_class.async_adapter.enqueue(notifier_class.name, action_name, params: params, args: args, kwargs: kwargs)
31
+ end
28
32
  end
29
33
 
30
34
  def notify_now
@@ -33,12 +37,14 @@ module AbstractNotifier
33
37
  notifier.deliver!(notification)
34
38
  end
35
39
 
40
+ def delivery_params ; {params: params, args: args, kwargs: kwargs}; end
41
+
36
42
  private
37
43
 
38
- attr_reader :owner_class, :params, :args, :kwargs
44
+ attr_reader :params, :args, :kwargs
39
45
 
40
46
  def notifier
41
- @notifier ||= owner_class.new(action_name, **params)
47
+ @notifier ||= notifier_class.new(action_name, **params)
42
48
  end
43
49
  end
44
50
 
@@ -67,8 +73,8 @@ module AbstractNotifier
67
73
  end
68
74
  # rubocop:enable Style/MethodMissingSuper
69
75
 
70
- def respond_to_missing?(*args)
71
- notifier_class.respond_to_missing?(*args)
76
+ def respond_to_missing?(*__rest__)
77
+ notifier_class.respond_to_missing?(*__rest__)
72
78
  end
73
79
  end
74
80
 
@@ -178,7 +184,7 @@ module AbstractNotifier
178
184
 
179
185
  def process_action(...)
180
186
  public_send(...)
181
- end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :process_action)
187
+ end
182
188
 
183
189
  def deliver!(notification)
184
190
  self.class.driver.call(notification.payload)
@@ -12,9 +12,9 @@ module ActiveDelivery
12
12
  @metadata = metadata.freeze
13
13
  end
14
14
 
15
- def deliver_later = owner.perform_notify(self)
15
+ def deliver_later(**opts) = owner.perform_notify(self, enqueue_options: opts)
16
16
 
17
- def deliver_now = owner.perform_notify(self, sync: true)
17
+ def deliver_now(**opts) = owner.perform_notify(self, sync: true)
18
18
 
19
19
  def delivery_class = owner.class
20
20
  end
@@ -77,8 +77,8 @@ module ActiveDelivery
77
77
 
78
78
  # The same as .notify but delivers synchronously
79
79
  # (i.e. #deliver_now for mailers)
80
- def notify!(mid, *args, **hargs)
81
- notify(mid, *args, **hargs, sync: true)
80
+ def notify!(mid, *__rest__, **hargs)
81
+ notify(mid, *__rest__, **hargs, sync: true)
82
82
  end
83
83
 
84
84
  alias_method :notify_now, :notify!
@@ -93,7 +93,7 @@ module ActiveDelivery
93
93
  end
94
94
  end
95
95
 
96
- def register_line(line_id, line_class = nil, notifier: nil, **options)
96
+ def register_line(line_id, line_class = nil, notifier: nil, **__kwrest__)
97
97
  raise ArgumentError, "A line class or notifier configuration must be provided" if line_class.nil? && notifier.nil?
98
98
 
99
99
  # Configure Notifier
@@ -101,7 +101,7 @@ module ActiveDelivery
101
101
  line_class = ActiveDelivery::Lines::Notifier
102
102
  end
103
103
 
104
- delivery_lines[line_id] = line_class.new(id: line_id, owner: self, **options)
104
+ delivery_lines[line_id] = line_class.new(id: line_id, owner: self, **__kwrest__)
105
105
 
106
106
  instance_eval <<~CODE, __FILE__, __LINE__ + 1
107
107
  def #{line_id}(val)
@@ -152,13 +152,13 @@ module ActiveDelivery
152
152
  super
153
153
  end
154
154
 
155
- def method_missing(mid, *args, **kwargs)
155
+ def method_missing(mid, *__rest__, **__kwrest__)
156
156
  return super unless respond_to_missing?(mid)
157
157
 
158
158
  # Lazily define a class method to avoid lookups
159
159
  delivers(mid)
160
160
 
161
- public_send(mid, *args, **kwargs)
161
+ public_send(mid, *__rest__, **__kwrest__)
162
162
  end
163
163
  end
164
164
 
@@ -197,7 +197,7 @@ module ActiveDelivery
197
197
  super
198
198
  end
199
199
 
200
- def method_missing(mid, *args, **kwargs)
200
+ def method_missing(mid, *__rest__, **__kwrest__)
201
201
  return super unless respond_to_missing?(mid)
202
202
 
203
203
  # Lazily define a method to avoid future lookups
@@ -211,27 +211,28 @@ module ActiveDelivery
211
211
  end
212
212
  CODE
213
213
 
214
- public_send(mid, *args, **kwargs)
214
+ public_send(mid, *__rest__, **__kwrest__)
215
215
  end
216
216
 
217
217
  protected
218
218
 
219
- def perform_notify(delivery, sync: false)
219
+ def perform_notify(delivery, sync: false, enqueue_options: {})
220
220
  delivery_lines.each do |type, line|
221
221
  next unless line.notify?(delivery.notification)
222
222
 
223
- notify_line(type, line, delivery, sync: sync)
223
+ notify_line(type, line, delivery, sync: sync, enqueue_options: enqueue_options)
224
224
  end
225
225
  end
226
226
 
227
227
  private
228
228
 
229
- def notify_line(type, line, delivery, sync:)
229
+ def notify_line(type, line, delivery, sync:, enqueue_options:)
230
230
  line.notify(
231
231
  delivery.notification,
232
232
  *delivery.params,
233
233
  params: params,
234
234
  sync: sync,
235
+ enqueue_options: enqueue_options,
235
236
  **delivery.options
236
237
  )
237
238
  true
@@ -37,9 +37,21 @@ module ActiveDelivery
37
37
  def notify_later(handler, mid, ...)
38
38
  end
39
39
 
40
- def notify(mid, *args, params:, sync:, **kwargs)
40
+ def notify_later_with_options(handler, enqueue_options, mid, ...)
41
+ notify_later(handler, mid, ...)
42
+ end
43
+
44
+ def notify(mid, *__rest__, params:, sync:, enqueue_options:, **__kwrest__)
41
45
  clazz = params.empty? ? handler_class : handler_class.with(**params)
42
- sync ? notify_now(clazz, mid, *args, **kwargs) : notify_later(clazz, mid, *args, **kwargs)
46
+ if sync
47
+ return notify_now(clazz, mid, *__rest__, **__kwrest__)
48
+ end
49
+
50
+ if enqueue_options.empty?
51
+ notify_later(clazz, mid, *__rest__, **__kwrest__)
52
+ else
53
+ notify_later_with_options(clazz, enqueue_options, mid, *__rest__, **__kwrest__)
54
+ end
43
55
  end
44
56
 
45
57
  def handler_class
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDelivery
4
+ module TestHelper
5
+ def assert_deliveries(count)
6
+ TestDelivery.enable { yield }
7
+
8
+ assert_equal TestDelivery.store.count, count, "Expected #{count} deliveries, got #{TestDelivery.store.count}"
9
+ end
10
+
11
+ def assert_no_deliveries(&__block__) = assert_deliveries(0, &__block__)
12
+
13
+ def assert_delivery_enqueued(delivery_class, event, count: 1, params: nil, with: nil)
14
+ TestDelivery.enable { yield }
15
+
16
+ deliveries = TestDelivery.store
17
+
18
+ if with
19
+ args = with
20
+ kwargs = args.pop if args.last.is_a?(Hash)
21
+ end
22
+
23
+ matching_deliveries, _unmatching_deliveries =
24
+ deliveries.partition do |(delivery, options)|
25
+ next false if delivery_class != delivery.owner.class
26
+
27
+ next false if event != delivery.notification
28
+
29
+ next false if params && !hash_include?(delivery.owner.params, params)
30
+
31
+ next true unless with
32
+
33
+ actual_args = delivery.params
34
+ actual_kwargs = delivery.options
35
+
36
+ next false unless args.each.with_index.all? do |arg, i|
37
+ arg === actual_args[i]
38
+ end
39
+
40
+ next false unless kwargs.all? do |k, v|
41
+ v === actual_kwargs[k]
42
+ end
43
+
44
+ true
45
+ end
46
+
47
+ assert_equal count, matching_deliveries.count, "Expected #{count} deliveries, got #{deliveries.count}"
48
+ end
49
+
50
+ private
51
+
52
+ def hash_include?(haystack, needle)
53
+ needle.all? do |k, v|
54
+ haystack.key?(k) && haystack[k] == v
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,36 @@
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, ...)
8
+ AbstractNotifier::NotificationDelivery.new(notifier_class.constantize, ...).notify_now
9
+ end
10
+ end
11
+
12
+ DEFAULT_QUEUE = "notifiers"
13
+
14
+ attr_reader :job, :queue
15
+
16
+ def initialize(queue: DEFAULT_QUEUE, job: DeliveryJob)
17
+ @job = job
18
+ @queue = queue
19
+ end
20
+
21
+ def enqueue(...)
22
+ job.set(queue:).perform_later(...)
23
+ end
24
+
25
+ def enqueue_delivery(delivery, **__kwrest__)
26
+ job.set(queue:, **__kwrest__).perform_later(
27
+ delivery.notifier_class.name,
28
+ delivery.action_name,
29
+ **delivery.delivery_params
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ AbstractNotifier.async_adapter ||= :active_job
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractNotifier
4
+ # NotificationDelivery payload wrapper which contains
5
+ # information about the current notifier class
6
+ # and knows how to trigger the delivery
7
+ class NotificationDelivery
8
+ attr_reader :action_name, :notifier_class
9
+
10
+ def initialize(notifier_class, action_name, params: {}, args: [], kwargs: {})
11
+ @notifier_class = notifier_class
12
+ @action_name = action_name
13
+ @params = params
14
+ @args = args
15
+ @kwargs = kwargs
16
+ end
17
+
18
+ def processed
19
+ return @processed if instance_variable_defined?(:@processed)
20
+
21
+ @processed = notifier.process_action(action_name, *args, **kwargs) || Notification.new(nil)
22
+ end
23
+
24
+ alias_method :notification, :processed
25
+
26
+ def notify_later(**__kwrest__)
27
+ if notifier_class.async_adapter.respond_to?(:enqueue_delivery)
28
+ notifier_class.async_adapter.enqueue_delivery(self, **__kwrest__)
29
+ else
30
+ notifier_class.async_adapter.enqueue(notifier_class.name, action_name, params:, args:, kwargs:)
31
+ end
32
+ end
33
+
34
+ def notify_now
35
+ return unless notification.payload
36
+
37
+ notifier.deliver!(notification)
38
+ end
39
+
40
+ def delivery_params = {params:, args:, kwargs:}
41
+
42
+ private
43
+
44
+ attr_reader :params, :args, :kwargs
45
+
46
+ def notifier
47
+ @notifier ||= notifier_class.new(action_name, **params)
48
+ end
49
+ end
50
+
51
+ # Notification object contains the compiled payload to be delivered
52
+ class Notification
53
+ attr_reader :payload
54
+
55
+ def initialize(payload)
56
+ @payload = payload
57
+ end
58
+ end
59
+
60
+ # Base class for notifiers
61
+ class Base
62
+ class ParamsProxy
63
+ attr_reader :notifier_class, :params
64
+
65
+ def initialize(notifier_class, params)
66
+ @notifier_class = notifier_class
67
+ @params = params
68
+ end
69
+
70
+ # rubocop:disable Style/MethodMissingSuper
71
+ def method_missing(method_name, *args, **kwargs)
72
+ NotificationDelivery.new(notifier_class, method_name, params:, args:, kwargs:)
73
+ end
74
+ # rubocop:enable Style/MethodMissingSuper
75
+
76
+ def respond_to_missing?(*__rest__)
77
+ notifier_class.respond_to_missing?(*__rest__)
78
+ end
79
+ end
80
+
81
+ class << self
82
+ attr_writer :driver
83
+
84
+ def driver
85
+ return @driver if instance_variable_defined?(:@driver)
86
+
87
+ @driver =
88
+ if superclass.respond_to?(:driver)
89
+ superclass.driver
90
+ else
91
+ raise "Driver not found for #{name}. " \
92
+ "Please, specify driver via `self.driver = MyDriver`"
93
+ end
94
+ end
95
+
96
+ def async_adapter=(args)
97
+ adapter, options = Array(args)
98
+ @async_adapter = AsyncAdapters.lookup(adapter, options)
99
+ end
100
+
101
+ def async_adapter
102
+ return @async_adapter if instance_variable_defined?(:@async_adapter)
103
+
104
+ @async_adapter =
105
+ if superclass.respond_to?(:async_adapter)
106
+ superclass.async_adapter
107
+ else
108
+ AbstractNotifier.async_adapter
109
+ end
110
+ end
111
+
112
+ def default(method_name = nil, **hargs, &block)
113
+ return @defaults_generator = block if block
114
+
115
+ return @defaults_generator = proc { send(method_name) } unless method_name.nil?
116
+
117
+ @default_params =
118
+ if superclass.respond_to?(:default_params)
119
+ superclass.default_params.merge(hargs).freeze
120
+ else
121
+ hargs.freeze
122
+ end
123
+ end
124
+
125
+ def defaults_generator
126
+ return @defaults_generator if instance_variable_defined?(:@defaults_generator)
127
+
128
+ @defaults_generator =
129
+ if superclass.respond_to?(:defaults_generator)
130
+ superclass.defaults_generator
131
+ end
132
+ end
133
+
134
+ def default_params
135
+ return @default_params if instance_variable_defined?(:@default_params)
136
+
137
+ @default_params =
138
+ if superclass.respond_to?(:default_params)
139
+ superclass.default_params.dup
140
+ else
141
+ {}
142
+ end
143
+ end
144
+
145
+ def method_missing(method_name, *args, **kwargs)
146
+ if action_methods.include?(method_name.to_s)
147
+ NotificationDelivery.new(self, method_name, args:, kwargs:)
148
+ else
149
+ super
150
+ end
151
+ end
152
+
153
+ def with(params)
154
+ ParamsProxy.new(self, params)
155
+ end
156
+
157
+ def respond_to_missing?(method_name, _include_private = false)
158
+ action_methods.include?(method_name.to_s) || super
159
+ end
160
+
161
+ # See https://github.com/rails/rails/blob/b13a5cb83ea00d6a3d71320fd276ca21049c2544/actionpack/lib/abstract_controller/base.rb#L74
162
+ def action_methods
163
+ @action_methods ||= begin
164
+ # All public instance methods of this class, including ancestors
165
+ methods = (public_instance_methods(true) -
166
+ # Except for public instance methods of Base and its ancestors
167
+ Base.public_instance_methods(true) +
168
+ # Be sure to include shadowed public instance methods of this class
169
+ public_instance_methods(false))
170
+
171
+ methods.map!(&:to_s)
172
+
173
+ methods.to_set
174
+ end
175
+ end
176
+ end
177
+
178
+ attr_reader :params, :notification_name
179
+
180
+ def initialize(notification_name, **params)
181
+ @notification_name = notification_name
182
+ @params = params.freeze
183
+ end
184
+
185
+ def process_action(...)
186
+ public_send(...)
187
+ end
188
+
189
+ def deliver!(notification)
190
+ self.class.driver.call(notification.payload)
191
+ end
192
+
193
+ def notification(**payload)
194
+ merge_defaults!(payload)
195
+
196
+ payload[:body] = implicit_payload_body unless payload.key?(:body)
197
+
198
+ raise ArgumentError, "Notification body must be present" if
199
+ payload[:body].nil? || payload[:body].empty?
200
+
201
+ @notification = Notification.new(payload)
202
+ end
203
+
204
+ private
205
+
206
+ def implicit_payload_body
207
+ # no-op — override to provide custom logic
208
+ end
209
+
210
+ def merge_defaults!(payload)
211
+ defaults =
212
+ if self.class.defaults_generator
213
+ instance_exec(&self.class.defaults_generator)
214
+ else
215
+ self.class.default_params
216
+ end
217
+
218
+ defaults.each do |k, v|
219
+ payload[k] = v unless payload.key?(k)
220
+ end
221
+ end
222
+ end
223
+ end