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,217 @@
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
9
+
10
+ def initialize(owner_class, action_name, params: {}, args: [], kwargs: {})
11
+ @owner_class = owner_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
27
+ owner_class.async_adapter.enqueue(owner_class.name, action_name, params:, args:, kwargs:)
28
+ end
29
+
30
+ def notify_now
31
+ return unless notification.payload
32
+
33
+ notifier.deliver!(notification)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :owner_class, :params, :args, :kwargs
39
+
40
+ def notifier
41
+ @notifier ||= owner_class.new(action_name, **params)
42
+ end
43
+ end
44
+
45
+ # Notification object contains the compiled payload to be delivered
46
+ class Notification
47
+ attr_reader :payload
48
+
49
+ def initialize(payload)
50
+ @payload = payload
51
+ end
52
+ end
53
+
54
+ # Base class for notifiers
55
+ class Base
56
+ class ParamsProxy
57
+ attr_reader :notifier_class, :params
58
+
59
+ def initialize(notifier_class, params)
60
+ @notifier_class = notifier_class
61
+ @params = params
62
+ end
63
+
64
+ # rubocop:disable Style/MethodMissingSuper
65
+ def method_missing(method_name, *args, **kwargs)
66
+ NotificationDelivery.new(notifier_class, method_name, params:, args:, kwargs:)
67
+ end
68
+ # rubocop:enable Style/MethodMissingSuper
69
+
70
+ def respond_to_missing?(*args)
71
+ notifier_class.respond_to_missing?(*args)
72
+ end
73
+ end
74
+
75
+ class << self
76
+ attr_writer :driver
77
+
78
+ def driver
79
+ return @driver if instance_variable_defined?(:@driver)
80
+
81
+ @driver =
82
+ if superclass.respond_to?(:driver)
83
+ superclass.driver
84
+ else
85
+ raise "Driver not found for #{name}. " \
86
+ "Please, specify driver via `self.driver = MyDriver`"
87
+ end
88
+ end
89
+
90
+ def async_adapter=(args)
91
+ adapter, options = Array(args)
92
+ @async_adapter = AsyncAdapters.lookup(adapter, options)
93
+ end
94
+
95
+ def async_adapter
96
+ return @async_adapter if instance_variable_defined?(:@async_adapter)
97
+
98
+ @async_adapter =
99
+ if superclass.respond_to?(:async_adapter)
100
+ superclass.async_adapter
101
+ else
102
+ AbstractNotifier.async_adapter
103
+ end
104
+ end
105
+
106
+ def default(method_name = nil, **hargs, &block)
107
+ return @defaults_generator = block if block
108
+
109
+ return @defaults_generator = proc { send(method_name) } unless method_name.nil?
110
+
111
+ @default_params =
112
+ if superclass.respond_to?(:default_params)
113
+ superclass.default_params.merge(hargs).freeze
114
+ else
115
+ hargs.freeze
116
+ end
117
+ end
118
+
119
+ def defaults_generator
120
+ return @defaults_generator if instance_variable_defined?(:@defaults_generator)
121
+
122
+ @defaults_generator =
123
+ if superclass.respond_to?(:defaults_generator)
124
+ superclass.defaults_generator
125
+ end
126
+ end
127
+
128
+ def default_params
129
+ return @default_params if instance_variable_defined?(:@default_params)
130
+
131
+ @default_params =
132
+ if superclass.respond_to?(:default_params)
133
+ superclass.default_params.dup
134
+ else
135
+ {}
136
+ end
137
+ end
138
+
139
+ def method_missing(method_name, *args, **kwargs)
140
+ if action_methods.include?(method_name.to_s)
141
+ NotificationDelivery.new(self, method_name, args:, kwargs:)
142
+ else
143
+ super
144
+ end
145
+ end
146
+
147
+ def with(params)
148
+ ParamsProxy.new(self, params)
149
+ end
150
+
151
+ def respond_to_missing?(method_name, _include_private = false)
152
+ action_methods.include?(method_name.to_s) || super
153
+ end
154
+
155
+ # See https://github.com/rails/rails/blob/b13a5cb83ea00d6a3d71320fd276ca21049c2544/actionpack/lib/abstract_controller/base.rb#L74
156
+ def action_methods
157
+ @action_methods ||= begin
158
+ # All public instance methods of this class, including ancestors
159
+ methods = (public_instance_methods(true) -
160
+ # Except for public instance methods of Base and its ancestors
161
+ Base.public_instance_methods(true) +
162
+ # Be sure to include shadowed public instance methods of this class
163
+ public_instance_methods(false))
164
+
165
+ methods.map!(&:to_s)
166
+
167
+ methods.to_set
168
+ end
169
+ end
170
+ end
171
+
172
+ attr_reader :params, :notification_name
173
+
174
+ def initialize(notification_name, **params)
175
+ @notification_name = notification_name
176
+ @params = params.freeze
177
+ end
178
+
179
+ def process_action(...)
180
+ public_send(...)
181
+ end
182
+
183
+ def deliver!(notification)
184
+ self.class.driver.call(notification.payload)
185
+ end
186
+
187
+ def notification(**payload)
188
+ merge_defaults!(payload)
189
+
190
+ payload[:body] = implicit_payload_body unless payload.key?(:body)
191
+
192
+ raise ArgumentError, "Notification body must be present" if
193
+ payload[:body].nil? || payload[:body].empty?
194
+
195
+ @notification = Notification.new(payload)
196
+ end
197
+
198
+ private
199
+
200
+ def implicit_payload_body
201
+ # no-op — override to provide custom logic
202
+ end
203
+
204
+ def merge_defaults!(payload)
205
+ defaults =
206
+ if self.class.defaults_generator
207
+ instance_exec(&self.class.defaults_generator)
208
+ else
209
+ self.class.default_params
210
+ end
211
+
212
+ defaults.each do |k, v|
213
+ payload[k] = v unless payload.key?(k)
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/version"
4
+ require "active_support/callbacks"
5
+ require "active_support/concern"
6
+
7
+ module AbstractNotifier
8
+ # Add callbacks support to Abstract Notifier (requires ActiveSupport::Callbacks)
9
+ #
10
+ # # Run method before seding notification
11
+ # # NOTE: when `false` is returned the execution is halted
12
+ # before_action :do_something
13
+ #
14
+ # # after_ and around_ callbacks are also supported
15
+ # after_action :cleanup
16
+ #
17
+ # around_action :set_context
18
+ #
19
+ # # Deliver callbacks are also available
20
+ # before_deliver :do_something
21
+ #
22
+ # # after_ and around_ callbacks are also supported
23
+ # after_deliver :cleanup
24
+ #
25
+ # around_deliver :set_context
26
+ module Callbacks
27
+ extend ActiveSupport::Concern
28
+
29
+ include ActiveSupport::Callbacks
30
+
31
+ CALLBACK_TERMINATOR = ->(_target, result) { result.call == false }
32
+
33
+ included do
34
+ define_callbacks :action,
35
+ terminator: CALLBACK_TERMINATOR,
36
+ skip_after_callbacks_if_terminated: true
37
+
38
+ define_callbacks :deliver,
39
+ terminator: CALLBACK_TERMINATOR,
40
+ skip_after_callbacks_if_terminated: true
41
+
42
+ prepend InstanceExt
43
+ end
44
+
45
+ module InstanceExt
46
+ def process_action(...)
47
+ run_callbacks(:action) { super(...) }
48
+ end
49
+
50
+ def deliver!(...)
51
+ run_callbacks(:deliver) { super(...) }
52
+ end
53
+ end
54
+
55
+ class_methods do
56
+ def _normalize_callback_options(options)
57
+ _normalize_callback_option(options, :only, :if)
58
+ _normalize_callback_option(options, :except, :unless)
59
+ end
60
+
61
+ def _normalize_callback_option(options, from, to)
62
+ if (from = options[from])
63
+ from_set = Array(from).map(&:to_s).to_set
64
+ from = proc { |c| from_set.include? c.notification_name.to_s }
65
+ options[to] = Array(options[to]).unshift(from)
66
+ end
67
+ end
68
+
69
+ %i[before after around].each do |kind|
70
+ %i[action deliver].each do |event|
71
+ define_method "#{kind}_#{event}" do |*names, on: event, **options, &block|
72
+ _normalize_callback_options(options)
73
+
74
+ names.each do |name|
75
+ set_callback on, kind, name, options
76
+ end
77
+
78
+ set_callback on, kind, block, options if block
79
+ end
80
+
81
+ define_method "skip_#{kind}_#{event}" do |*names, on: event, **options|
82
+ _normalize_callback_options(options)
83
+
84
+ names.each do |name|
85
+ skip_callback(on, kind, name, options)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ AbstractNotifier::Base.include AbstractNotifier::Callbacks
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractNotifier
4
+ module TestHelper
5
+ def assert_notifications_sent(count, params)
6
+ yield
7
+ assert_equal deliveries.count, count
8
+ count.times do |i|
9
+ delivery = deliveries[0 - i]
10
+ if !params[:via]
11
+ delivery = delivery.dup
12
+ delivery.delete(:via)
13
+ end
14
+
15
+ msg = message(msg) { "Expected #{mu_pp(delivery)} to include #{mu_pp(params)}" }
16
+ assert hash_include?(delivery, params), msg
17
+ end
18
+ end
19
+
20
+ def assert_notifications_enqueued(count, params)
21
+ yield
22
+ assert_equal enqueued_deliveries.count, count
23
+ count.times do |i|
24
+ delivery = enqueued_deliveries[0 - i]
25
+ if !params[:via]
26
+ delivery = delivery.dup
27
+ delivery.delete(:via)
28
+ end
29
+
30
+ msg = message(msg) { "Expected #{mu_pp(delivery)} to include #{mu_pp(params)}" }
31
+ assert hash_include?(delivery, params), msg
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def deliveries
38
+ AbstractNotifier::Testing::Driver.deliveries
39
+ end
40
+
41
+ def enqueued_deliveries
42
+ AbstractNotifier::Testing::Driver.enqueued_deliveries
43
+ end
44
+
45
+ def hash_include?(haystack, needle)
46
+ needle.all? do |k, v|
47
+ haystack.key?(k) && haystack[k] == v
48
+ end
49
+ end
50
+ end
51
+ end
@@ -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(*args)
157
+ AbstractNotifier::HaveSentNotification.new(*args)
158
+ end
159
+
160
+ def have_enqueued_notification(*args)
161
+ AbstractNotifier::HaveEnqueuedNotification.new(*args)
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
40
+ return super unless AbstractNotifier.test?
41
+
42
+ payload = notification.payload
43
+
44
+ Driver.enqueue_notification payload.merge(via: notifier.class)
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractNotifier
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "abstract_notifier/version"
4
+
5
+ # Abstract Notifier is responsible for generating and triggering text-based notifications
6
+ # (like Action Mailer for email notifications).
7
+ #
8
+ # Example:
9
+ #
10
+ # class ApplicationNotifier < AbstractNotifier::Base
11
+ # self.driver = NotifyService.new
12
+ #
13
+ # def profile
14
+ # params[:profile] if params
15
+ # end
16
+ # end
17
+ #
18
+ # class EventsNotifier < ApplicationNotifier
19
+ # def canceled(event)
20
+ # notification(
21
+ # # the only required option is `body`
22
+ # body: "Event #{event.title} has been canceled",
23
+ # # all other options are passed to delivery driver
24
+ # identity: profile.notification_service_id
25
+ # )
26
+ # end
27
+ # end
28
+ #
29
+ # EventsNotifier.with(profile: profile).canceled(event).notify_later
30
+ #
31
+ module AbstractNotifier
32
+ DELIVERY_MODES = %i[test noop normal].freeze
33
+
34
+ class << self
35
+ attr_reader :delivery_mode
36
+ attr_reader :async_adapter
37
+
38
+ def delivery_mode=(val)
39
+ unless DELIVERY_MODES.include?(val)
40
+ raise ArgumentError, "Unsupported delivery mode: #{val}. " \
41
+ "Supported values: #{DELIVERY_MODES.join(", ")}"
42
+ end
43
+
44
+ @delivery_mode = val
45
+ end
46
+
47
+ def async_adapter=(args)
48
+ adapter, options = Array(args)
49
+ @async_adapter = AsyncAdapters.lookup(adapter, options)
50
+ end
51
+
52
+ def noop?
53
+ delivery_mode == :noop
54
+ end
55
+
56
+ def test?
57
+ delivery_mode == :test
58
+ end
59
+ end
60
+
61
+ self.delivery_mode =
62
+ if ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test"
63
+ :test
64
+ else
65
+ :normal
66
+ end
67
+ end
68
+
69
+ require "abstract_notifier/base"
70
+ require "abstract_notifier/async_adapters"
71
+
72
+ require "abstract_notifier/callbacks" if defined?(ActiveSupport)
73
+ require "abstract_notifier/async_adapters/active_job" if defined?(ActiveJob)
74
+
75
+ require "abstract_notifier/testing" if ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test"