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
@@ -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
@@ -0,0 +1,248 @@
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 ; 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
+
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; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify)
77
+
78
+ # The same as .notify but delivers synchronously
79
+ # (i.e. #deliver_now for mailers)
80
+ def notify!(mid, *args, **hargs)
81
+ notify(mid, *args, **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, **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
+
104
+ delivery_lines[line_id] = line_class.new(id: line_id, owner: self, **options)
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; 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
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, *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)
220
+ delivery_lines.each do |type, line|
221
+ next unless line.notify?(delivery.notification)
222
+
223
+ notify_line(type, line, delivery, sync: sync)
224
+ end
225
+ end
226
+
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
243
+
244
+ def delivery_lines
245
+ self.class.delivery_lines
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/version"
4
+ require "active_support/callbacks"
5
+ require "active_support/concern"
6
+
7
+ module ActiveDelivery
8
+ # Add callbacks support to Active Delivery (requires ActiveSupport::Callbacks)
9
+ #
10
+ # # Run method before delivering notification
11
+ # # NOTE: when `false` is returned the execution is halted
12
+ # before_notify :do_something
13
+ #
14
+ # # You can specify a notification method (to run callback only for that method)
15
+ # before_notify :do_mail_something, on: :mail
16
+ #
17
+ # # or for push notifications
18
+ # before_notify :do_mail_something, on: :push
19
+ #
20
+ # # after_ and around_ callbacks are also supported
21
+ # after_notify :cleanup
22
+ #
23
+ # around_notify :set_context
24
+ module Callbacks
25
+ extend ActiveSupport::Concern
26
+
27
+ include ActiveSupport::Callbacks
28
+
29
+ CALLBACK_TERMINATOR = ->(_target, result) { result.call == false }
30
+
31
+ included do
32
+ # Define "global" callbacks
33
+ define_line_callbacks :notify
34
+
35
+ prepend InstanceExt
36
+ singleton_class.prepend SingltonExt
37
+ end
38
+
39
+ module InstanceExt
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)
45
+
46
+ def notify_line(kind, *__rest__, &__block__)
47
+ run_callbacks(kind) { super(kind, *__rest__, &__block__) }
48
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_line)
49
+ end
50
+
51
+ module SingltonExt
52
+ def register_line(line_id, *__rest__, &__block__)
53
+ super
54
+ define_line_callbacks line_id
55
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :register_line)
56
+ end
57
+
58
+ class_methods do
59
+ def _normalize_callback_options(options)
60
+ _normalize_callback_option(options, :only, :if)
61
+ _normalize_callback_option(options, :except, :unless)
62
+ end
63
+
64
+ def _normalize_callback_option(options, from, to)
65
+ if (from = options[from])
66
+ from_set = Array(from).map(&:to_s).to_set
67
+ from = proc { |c| from_set.include? c.notification_name.to_s }
68
+ options[to] = Array(options[to]).unshift(from)
69
+ end
70
+ end
71
+
72
+ def define_line_callbacks(name)
73
+ define_callbacks name,
74
+ terminator: CALLBACK_TERMINATOR,
75
+ skip_after_callbacks_if_terminated: true
76
+ end
77
+
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
+
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)
91
+
92
+ names.each do |name|
93
+ skip_callback(on, kind, name, options)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ ActiveDelivery::Base.include ActiveDelivery::Callbacks
@@ -0,0 +1,89 @@
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: 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, *__rest__, &__block__)
35
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_now)
36
+
37
+ def notify_later(handler, mid, *__rest__, &__block__)
38
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_later)
39
+
40
+ def notify(mid, *args, params:, sync:, **kwargs)
41
+ clazz = params.empty? ? handler_class : handler_class.with(**params)
42
+ sync ? notify_now(clazz, mid, *args, **kwargs) : notify_later(clazz, mid, *args, **kwargs)
43
+ end
44
+
45
+ def handler_class
46
+ if ::ActiveDelivery.cache_classes
47
+ return @handler_class if instance_variable_defined?(:@handler_class)
48
+ end
49
+
50
+ return @handler_class = nil if owner.abstract_class?
51
+
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
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :resolver
68
+
69
+ def build_pattern_resolver(pattern)
70
+ return unless pattern
71
+
72
+ proc do |delivery|
73
+ delivery_class = delivery.name
74
+
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
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDelivery
4
+ module Lines
5
+ class Mailer < Base
6
+ alias_method :mailer_class, :handler_class
7
+
8
+ DEFAULT_RESOLVER = ->(klass) { klass.name&.gsub(/Delivery$/, "Mailer")&.safe_constantize }
9
+
10
+ def notify?(method_name)
11
+ return unless mailer_class
12
+ mailer_class.action_methods.include?(method_name.to_s)
13
+ end
14
+
15
+ def notify_now(mailer, mid, *__rest__, &__block__)
16
+ mailer.public_send(mid, *__rest__, &__block__).deliver_now
17
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_now)
18
+
19
+ def notify_later(mailer, mid, *__rest__, &__block__)
20
+ mailer.public_send(mid, *__rest__, &__block__).deliver_later
21
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_later)
22
+ end
23
+
24
+ ActiveDelivery::Base.register_line :mailer, Mailer, resolver: Mailer::DEFAULT_RESOLVER
25
+ end
26
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDelivery
4
+ module TestDelivery
5
+ class << self
6
+ def enable
7
+ raise ArgumentError, "block is required" unless block_given?
8
+ begin
9
+ clear
10
+ Thread.current.thread_variable_set(:active_delivery_testing, true)
11
+ yield
12
+ ensure
13
+ Thread.current.thread_variable_set(:active_delivery_testing, false)
14
+ end
15
+ end
16
+
17
+ def enabled?
18
+ Thread.current.thread_variable_get(:active_delivery_testing) == true
19
+ end
20
+
21
+ def track(delivery, options)
22
+ store << [delivery, options]
23
+ end
24
+
25
+ def track_line(line)
26
+ lines << line
27
+ end
28
+
29
+ def store
30
+ Thread.current.thread_variable_get(:active_delivery_testing_store) || Thread.current.thread_variable_set(:active_delivery_testing_store, [])
31
+ end
32
+
33
+ def lines
34
+ Thread.current.thread_variable_get(:active_delivery_testing_lines) || Thread.current.thread_variable_set(:active_delivery_testing_lines, [])
35
+ end
36
+
37
+ def clear
38
+ store.clear
39
+ lines.clear
40
+ end
41
+ end
42
+
43
+ def perform_notify(delivery, **options)
44
+ return super unless test?
45
+ TestDelivery.track(delivery, options)
46
+ nil
47
+ end
48
+
49
+ def notify_line(line, *__rest__, &__block__)
50
+ res = super
51
+ TestDelivery.track_line(line) if res
52
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_line)
53
+
54
+ def test?
55
+ TestDelivery.enabled?
56
+ end
57
+ end
58
+ end
59
+
60
+ ActiveDelivery::Base.prepend ActiveDelivery::TestDelivery
61
+
62
+ require "active_delivery/testing/rspec" if defined?(RSpec::Core)