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,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractNotifier
4
+ class HaveSentNotification < RSpec::Matchers::BuiltIn::BaseMatcher
5
+ attr_reader :payload
6
+
7
+ def initialize(payload = nil)
8
+ @payload = payload
9
+ set_expected_number(:exactly, 1)
10
+ end
11
+
12
+ def exactly(count)
13
+ set_expected_number(:exactly, count)
14
+ self
15
+ end
16
+
17
+ def at_least(count)
18
+ set_expected_number(:at_least, count)
19
+ self
20
+ end
21
+
22
+ def at_most(count)
23
+ set_expected_number(:at_most, count)
24
+ self
25
+ end
26
+
27
+ def times
28
+ self
29
+ end
30
+
31
+ def once
32
+ exactly(:once)
33
+ end
34
+
35
+ def twice
36
+ exactly(:twice)
37
+ end
38
+
39
+ def thrice
40
+ exactly(:thrice)
41
+ end
42
+
43
+ def supports_block_expectations?
44
+ true
45
+ end
46
+
47
+ def matches?(proc)
48
+ raise ArgumentError, "have_sent_notification only supports block expectations" unless Proc === proc
49
+
50
+ raise "You can only use have_sent_notification matcher in :test delivery mode" unless AbstractNotifier.test?
51
+
52
+ original_deliveries_count = deliveries.count
53
+ proc.call
54
+ in_block_deliveries = deliveries.drop(original_deliveries_count)
55
+
56
+ @matching_deliveries, @unmatching_deliveries =
57
+ in_block_deliveries.partition do |actual_payload|
58
+ next true if payload.nil?
59
+
60
+ if payload.is_a?(::Hash) && !payload[:via]
61
+ actual_payload = actual_payload.dup
62
+ actual_payload.delete(:via)
63
+ end
64
+
65
+ payload === actual_payload
66
+ end
67
+
68
+ @matching_count = @matching_deliveries.size
69
+
70
+ case @expectation_type
71
+ when :exactly then @expected_number == @matching_count
72
+ when :at_most then @expected_number >= @matching_count
73
+ when :at_least then @expected_number <= @matching_count
74
+ end
75
+ end
76
+
77
+ def deliveries
78
+ AbstractNotifier::Testing::Driver.deliveries
79
+ end
80
+
81
+ def set_expected_number(relativity, count)
82
+ @expectation_type = relativity
83
+ @expected_number =
84
+ case count
85
+ when :once then 1
86
+ when :twice then 2
87
+ when :thrice then 3
88
+ else Integer(count)
89
+ end
90
+ end
91
+
92
+ def failure_message
93
+ (+"expected to #{verb_present} notification: #{payload_description}").tap do |msg|
94
+ msg << " #{message_expectation_modifier}, but"
95
+
96
+ if @unmatching_deliveries.any?
97
+ msg << " #{verb_past} the following notifications:"
98
+ @unmatching_deliveries.each do |unmatching_payload|
99
+ msg << "\n #{unmatching_payload}"
100
+ end
101
+ else
102
+ msg << " haven't #{verb_past} anything"
103
+ end
104
+ end
105
+ end
106
+
107
+ def failure_message_when_negated
108
+ "expected not to #{verb_present} #{payload}"
109
+ end
110
+
111
+ def message_expectation_modifier
112
+ number_modifier = (@expected_number == 1) ? "once" : "#{@expected_number} times"
113
+ case @expectation_type
114
+ when :exactly then "exactly #{number_modifier}"
115
+ when :at_most then "at most #{number_modifier}"
116
+ when :at_least then "at least #{number_modifier}"
117
+ end
118
+ end
119
+
120
+ def payload_description
121
+ if payload.is_a?(RSpec::Matchers::Composable)
122
+ payload.description
123
+ else
124
+ payload
125
+ end
126
+ end
127
+
128
+ def verb_past
129
+ "sent"
130
+ end
131
+
132
+ def verb_present
133
+ "send"
134
+ end
135
+ end
136
+
137
+ class HaveEnqueuedNotification < HaveSentNotification
138
+ private
139
+
140
+ def deliveries
141
+ AbstractNotifier::Testing::Driver.enqueued_deliveries
142
+ end
143
+
144
+ def verb_past
145
+ "enqueued"
146
+ end
147
+
148
+ def verb_present
149
+ "enqueue"
150
+ end
151
+ end
152
+ end
153
+
154
+ RSpec.configure do |config|
155
+ config.include(Module.new do
156
+ def have_sent_notification(*__rest__)
157
+ AbstractNotifier::HaveSentNotification.new(*__rest__)
158
+ end
159
+
160
+ def have_enqueued_notification(*__rest__)
161
+ AbstractNotifier::HaveEnqueuedNotification.new(*__rest__)
162
+ end
163
+ end)
164
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractNotifier
4
+ module Testing
5
+ module Driver
6
+ class << self
7
+ def deliveries
8
+ Thread.current[:notifier_deliveries] ||= []
9
+ end
10
+
11
+ def enqueued_deliveries
12
+ Thread.current[:notifier_enqueued_deliveries] ||= []
13
+ end
14
+
15
+ def clear
16
+ deliveries.clear
17
+ enqueued_deliveries.clear
18
+ end
19
+
20
+ def send_notification(data)
21
+ deliveries << data
22
+ end
23
+
24
+ def enqueue_notification(data)
25
+ enqueued_deliveries << data
26
+ end
27
+ end
28
+ end
29
+
30
+ module NotificationDelivery
31
+ def notify_now
32
+ return super unless AbstractNotifier.test?
33
+
34
+ payload = notification.payload
35
+
36
+ Driver.send_notification payload.merge(via: notifier.class)
37
+ end
38
+
39
+ def notify_later(**__kwrest__)
40
+ return super unless AbstractNotifier.test?
41
+
42
+ payload = notification.payload
43
+
44
+ Driver.enqueue_notification payload.merge(via: notifier.class, **__kwrest__)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ AbstractNotifier::NotificationDelivery.prepend AbstractNotifier::Testing::NotificationDelivery
51
+
52
+ require "abstract_notifier/testing/rspec" if defined?(RSpec::Core)
53
+ require "abstract_notifier/testing/minitest" if defined?(Minitest::Assertions)
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
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(**opts) = owner.perform_notify(self, enqueue_options: opts)
16
+
17
+ def deliver_now(**opts) = 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
+
36
+ # Base class for deliveries.
37
+ #
38
+ # Delivery object describes how to notify a user about
39
+ # an event (e.g. via email or via push notification or both).
40
+ #
41
+ # Delivery class acts like a proxy in front of the different delivery channels
42
+ # (i.e. mailers, notifiers). That means that calling a method on delivery class invokes the
43
+ # same method on the corresponding class, e.g.:
44
+ #
45
+ # EventsDelivery.one_hour_before(profile, event).deliver_later
46
+ # # or
47
+ # EventsDelivery.notify(:one_hour_before, profile, event)
48
+ #
49
+ # # under the hood it calls
50
+ # EventsMailer.one_hour_before(profile, event).deliver_later
51
+ #
52
+ # # and
53
+ # EventsNotifier.one_hour_before(profile, event).notify_later
54
+ #
55
+ # Delivery also supports _parameterized_ calling:
56
+ #
57
+ # EventsDelivery.with(profile: profile).canceled(event).deliver_later
58
+ #
59
+ # The parameters could be accessed through `params` instance method (e.g.
60
+ # to implement guard-like logic).
61
+ #
62
+ # When params are presents the parametrized mailer is used, i.e.:
63
+ #
64
+ # EventsMailer.with(profile: profile).canceled(event).deliver_later
65
+ #
66
+ # See https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html
67
+ class Base
68
+ class << self
69
+ attr_accessor :abstract_class
70
+
71
+ alias_method :with, :new
72
+
73
+ # Enqueues delivery (i.e. uses #deliver_later for mailers)
74
+ def notify(...)
75
+ new.notify(...)
76
+ end
77
+
78
+ # The same as .notify but delivers synchronously
79
+ # (i.e. #deliver_now for mailers)
80
+ def notify!(mid, *__rest__, **hargs)
81
+ notify(mid, *__rest__, **hargs, sync: true)
82
+ end
83
+
84
+ alias_method :notify_now, :notify!
85
+
86
+ def delivery_lines
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)
90
+ end
91
+ else
92
+ {}
93
+ end
94
+ end
95
+
96
+ def register_line(line_id, line_class = nil, notifier: nil, **__kwrest__)
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
+
104
+ delivery_lines[line_id] = line_class.new(id: line_id, owner: self, **__kwrest__)
105
+
106
+ instance_eval <<~CODE, __FILE__, __LINE__ + 1
107
+ def #{line_id}(val)
108
+ delivery_lines[:#{line_id}].handler_class_name = val
109
+ end
110
+
111
+ def #{line_id}_class
112
+ delivery_lines[:#{line_id}].handler_class
113
+ end
114
+ CODE
115
+ end
116
+
117
+ def unregister_line(line_id)
118
+ removed_line = delivery_lines.delete(line_id)
119
+
120
+ return if removed_line.nil?
121
+
122
+ singleton_class.undef_method line_id
123
+ singleton_class.undef_method "#{line_id}_class"
124
+ end
125
+
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, *__rest__, **__kwrest__)
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, *__rest__, **__kwrest__)
162
+ end
163
+ end
164
+
165
+ self.abstract_class = true
166
+
167
+ attr_reader :params, :notification_name
168
+
169
+ def initialize(**params)
170
+ @params = params
171
+ @params.freeze
172
+ end
173
+
174
+ # Enqueues delivery (i.e. uses #deliver_later for mailers)
175
+ def notify(mid, *args, **kwargs)
176
+ perform_notify(
177
+ delivery(notification: mid, params: args, options: kwargs)
178
+ )
179
+ end
180
+
181
+ # The same as .notify but delivers synchronously
182
+ # (i.e. #deliver_now for mailers)
183
+ def notify!(mid, *args, **kwargs)
184
+ perform_notify(
185
+ delivery(notification: mid, params: args, options: kwargs),
186
+ sync: true
187
+ )
188
+ end
189
+
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
199
+
200
+ def method_missing(mid, *__rest__, **__kwrest__)
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, *__rest__, **__kwrest__)
215
+ end
216
+
217
+ protected
218
+
219
+ def perform_notify(delivery, sync: false, enqueue_options: {})
220
+ delivery_lines.each do |type, line|
221
+ next unless line.notify?(delivery.notification)
222
+
223
+ notify_line(type, line, delivery, sync:, enqueue_options:)
224
+ end
225
+ end
226
+
227
+ private
228
+
229
+ def notify_line(type, line, delivery, sync:, enqueue_options:)
230
+ line.notify(
231
+ delivery.notification,
232
+ *delivery.params,
233
+ params:,
234
+ sync:,
235
+ enqueue_options:,
236
+ **delivery.options
237
+ )
238
+ true
239
+ end
240
+
241
+ def delivery(notification:, params: nil, options: nil, metadata: nil)
242
+ Delivery.new(self, notification:, params:, options:, metadata:)
243
+ end
244
+
245
+ def delivery_lines
246
+ self.class.delivery_lines
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,101 @@
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
+ class Base
11
+ attr_reader :id, :options
12
+ attr_accessor :owner
13
+ attr_accessor :handler_class_name
14
+
15
+ def initialize(id:, owner:, **options)
16
+ @id = id
17
+ @owner = owner
18
+ @options = options.tap(&:freeze)
19
+ @resolver = options[:resolver] || build_pattern_resolver(options[:resolver_pattern])
20
+ end
21
+
22
+ def dup_for(new_owner)
23
+ self.class.new(id:, **options, owner: new_owner)
24
+ end
25
+
26
+ def resolve_class(name)
27
+ resolver&.call(name)
28
+ end
29
+
30
+ def notify?(method_name)
31
+ handler_class&.respond_to?(method_name)
32
+ end
33
+
34
+ def notify_now(handler, mid, ...)
35
+ end
36
+
37
+ def notify_later(handler, mid, ...)
38
+ end
39
+
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__)
45
+ clazz = params.empty? ? handler_class : handler_class.with(**params)
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
55
+ end
56
+
57
+ def handler_class
58
+ if ::ActiveDelivery.cache_classes
59
+ return @handler_class if instance_variable_defined?(:@handler_class)
60
+ end
61
+
62
+ return @handler_class = nil if owner.abstract_class?
63
+
64
+ superline = owner.superclass.delivery_lines[id] if owner.superclass.respond_to?(:delivery_lines) && owner.superclass.delivery_lines[id]
65
+
66
+ # If an explicit class name has been specified somewhere in the ancestor chain, use it.
67
+ class_name = @handler_class_name || superline&.handler_class_name
68
+
69
+ @handler_class =
70
+ if class_name
71
+ class_name.is_a?(Class) ? class_name : class_name.safe_constantize
72
+ else
73
+ resolve_class(owner) || superline&.handler_class
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ attr_reader :resolver
80
+
81
+ def build_pattern_resolver(pattern)
82
+ return unless pattern
83
+
84
+ proc do |delivery|
85
+ delivery_class = delivery.name
86
+
87
+ next unless delivery_class
88
+
89
+ *namespace, delivery_name = delivery_class.split("::")
90
+
91
+ delivery_namespace = ""
92
+ delivery_namespace = "#{namespace.join("::")}::" unless namespace.empty?
93
+
94
+ delivery_name = delivery_name.sub(/Delivery$/, "")
95
+
96
+ (pattern % {delivery_class:, delivery_name:, delivery_namespace:}).safe_constantize
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,57 @@
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, *__rest__)
34
+ handler.public_send(mid, *__rest__).notify_now
35
+ end
36
+
37
+ def notify_later(handler, mid, *__rest__)
38
+ handler.public_send(mid, *__rest__).notify_later
39
+ end
40
+
41
+ def notify_later_with_options(handler, enqueue_options, mid, *__rest__)
42
+ handler.public_send(mid, *__rest__).notify_later(**enqueue_options)
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :resolver
48
+
49
+ def build_resolver(suffix)
50
+ lambda do |klass|
51
+ klass_name = klass.name
52
+ klass_name&.sub(/Delivery\z/, suffix)&.safe_constantize
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end