active_delivery 0.4.3 → 1.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -1
  3. data/LICENSE.txt +19 -17
  4. data/README.md +503 -32
  5. data/lib/.rbnext/3.0/active_delivery/base.rb +124 -0
  6. data/lib/.rbnext/3.0/active_delivery/callbacks.rb +97 -0
  7. data/lib/.rbnext/3.0/active_delivery/lines/base.rb +63 -0
  8. data/lib/.rbnext/3.0/active_delivery/lines/mailer.rb +25 -0
  9. data/lib/.rbnext/3.1/active_delivery/base.rb +124 -0
  10. data/lib/.rbnext/3.1/active_delivery/lines/base.rb +63 -0
  11. data/lib/abstract_notifier/async_adapters/active_job.rb +27 -0
  12. data/lib/abstract_notifier/async_adapters.rb +16 -0
  13. data/lib/abstract_notifier/base.rb +178 -0
  14. data/lib/abstract_notifier/testing/minitest.rb +51 -0
  15. data/lib/abstract_notifier/testing/rspec.rb +164 -0
  16. data/lib/abstract_notifier/testing.rb +49 -0
  17. data/lib/abstract_notifier/version.rb +5 -0
  18. data/lib/abstract_notifier.rb +74 -0
  19. data/lib/active_delivery/base.rb +156 -27
  20. data/lib/active_delivery/callbacks.rb +25 -25
  21. data/lib/active_delivery/ext/string_constantize.rb +24 -0
  22. data/lib/active_delivery/lines/base.rb +24 -17
  23. data/lib/active_delivery/lines/mailer.rb +7 -18
  24. data/lib/active_delivery/lines/notifier.rb +53 -0
  25. data/lib/active_delivery/raitie.rb +9 -0
  26. data/lib/active_delivery/testing/rspec.rb +59 -12
  27. data/lib/active_delivery/testing.rb +19 -5
  28. data/lib/active_delivery/version.rb +1 -1
  29. data/lib/active_delivery.rb +8 -0
  30. metadata +61 -56
  31. data/.gem_release.yml +0 -3
  32. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
  33. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -24
  34. data/.github/PULL_REQUEST_TEMPLATE.md +0 -23
  35. data/.github/workflows/docs-lint.yml +0 -72
  36. data/.github/workflows/rspec-jruby.yml +0 -35
  37. data/.github/workflows/rspec.yml +0 -51
  38. data/.github/workflows/rubocop.yml +0 -21
  39. data/.gitignore +0 -43
  40. data/.mdlrc +0 -1
  41. data/.rspec +0 -2
  42. data/.rubocop-md.yml +0 -16
  43. data/.rubocop.yml +0 -28
  44. data/Gemfile +0 -17
  45. data/RELEASING.md +0 -43
  46. data/Rakefile +0 -20
  47. data/active_delivery.gemspec +0 -35
  48. data/forspell.dict +0 -8
  49. data/gemfiles/jruby.gemfile +0 -5
  50. data/gemfiles/rails42.gemfile +0 -8
  51. data/gemfiles/rails5.gemfile +0 -5
  52. data/gemfiles/rails50.gemfile +0 -8
  53. data/gemfiles/rails6.gemfile +0 -5
  54. data/gemfiles/railsmaster.gemfile +0 -6
  55. data/gemfiles/rubocop.gemfile +0 -4
  56. data/lefthook.yml +0 -18
  57. data/lib/active_delivery/action_mailer/parameterized.rb +0 -92
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractNotifier
4
+ # Notificaiton payload wrapper which contains
5
+ # information about the current notifier class
6
+ # and knows how to trigger the delivery
7
+ class Notification
8
+ attr_reader :payload, :owner
9
+
10
+ def initialize(owner, payload)
11
+ @owner = owner
12
+ @payload = payload
13
+ end
14
+
15
+ def notify_later
16
+ return if AbstractNotifier.noop?
17
+ owner.async_adapter.enqueue owner, payload
18
+ end
19
+
20
+ def notify_now
21
+ return if AbstractNotifier.noop?
22
+ owner.driver.call(payload)
23
+ end
24
+ end
25
+
26
+ # Base class for notifiers
27
+ class Base
28
+ class ParamsProxy
29
+ attr_reader :notifier_class, :params
30
+
31
+ def initialize(notifier_class, params)
32
+ @notifier_class = notifier_class
33
+ @params = params
34
+ end
35
+
36
+ # rubocop:disable Style/MethodMissingSuper
37
+ def method_missing(method_name, *args, **kwargs)
38
+ if kwargs.empty?
39
+ notifier_class.new(method_name, **params).public_send(method_name, *args)
40
+ else
41
+ notifier_class.new(method_name, **params).public_send(method_name, *args, **kwargs)
42
+ end
43
+ end
44
+ # rubocop:enable Style/MethodMissingSuper
45
+
46
+ def respond_to_missing?(*args)
47
+ notifier_class.respond_to_missing?(*args)
48
+ end
49
+ end
50
+
51
+ class << self
52
+ attr_writer :driver
53
+
54
+ def driver
55
+ return @driver if instance_variable_defined?(:@driver)
56
+
57
+ @driver =
58
+ if superclass.respond_to?(:driver)
59
+ superclass.driver
60
+ else
61
+ raise "Driver not found for #{name}. " \
62
+ "Please, specify driver via `self.driver = MyDriver`"
63
+ end
64
+ end
65
+
66
+ def async_adapter=(args)
67
+ adapter, options = Array(args)
68
+ @async_adapter = AsyncAdapters.lookup(adapter, options)
69
+ end
70
+
71
+ def async_adapter
72
+ return @async_adapter if instance_variable_defined?(:@async_adapter)
73
+
74
+ @async_adapter =
75
+ if superclass.respond_to?(:async_adapter)
76
+ superclass.async_adapter
77
+ else
78
+ AbstractNotifier.async_adapter
79
+ end
80
+ end
81
+
82
+ def default(method_name = nil, **hargs, &block)
83
+ return @defaults_generator = block if block
84
+
85
+ return @defaults_generator = proc { send(method_name) } unless method_name.nil?
86
+
87
+ @default_params =
88
+ if superclass.respond_to?(:default_params)
89
+ superclass.default_params.merge(hargs).freeze
90
+ else
91
+ hargs.freeze
92
+ end
93
+ end
94
+
95
+ def defaults_generator
96
+ return @defaults_generator if instance_variable_defined?(:@defaults_generator)
97
+
98
+ @defaults_generator =
99
+ if superclass.respond_to?(:defaults_generator)
100
+ superclass.defaults_generator
101
+ end
102
+ end
103
+
104
+ def default_params
105
+ return @default_params if instance_variable_defined?(:@default_params)
106
+
107
+ @default_params =
108
+ if superclass.respond_to?(:default_params)
109
+ superclass.default_params.dup
110
+ else
111
+ {}
112
+ end
113
+ end
114
+
115
+ def method_missing(method_name, *args)
116
+ if action_methods.include?(method_name.to_s)
117
+ new(method_name).public_send(method_name, *args)
118
+ else
119
+ super
120
+ end
121
+ end
122
+
123
+ def with(params)
124
+ ParamsProxy.new(self, params)
125
+ end
126
+
127
+ def respond_to_missing?(method_name, _include_private = false)
128
+ action_methods.include?(method_name.to_s) || super
129
+ end
130
+
131
+ # See https://github.com/rails/rails/blob/b13a5cb83ea00d6a3d71320fd276ca21049c2544/actionpack/lib/abstract_controller/base.rb#L74
132
+ def action_methods
133
+ @action_methods ||= begin
134
+ # All public instance methods of this class, including ancestors
135
+ methods = (public_instance_methods(true) -
136
+ # Except for public instance methods of Base and its ancestors
137
+ Base.public_instance_methods(true) +
138
+ # Be sure to include shadowed public instance methods of this class
139
+ public_instance_methods(false))
140
+
141
+ methods.map!(&:to_s)
142
+
143
+ methods.to_set
144
+ end
145
+ end
146
+ end
147
+
148
+ attr_reader :params, :notification_name
149
+
150
+ def initialize(notification_name, **params)
151
+ @notification_name = notification_name
152
+ @params = params.freeze
153
+ end
154
+
155
+ def notification(**payload)
156
+ merge_defaults!(payload)
157
+
158
+ raise ArgumentError, "Notification body must be present" if
159
+ payload[:body].nil? || payload[:body].empty?
160
+ Notification.new(self.class, payload)
161
+ end
162
+
163
+ private
164
+
165
+ def merge_defaults!(payload)
166
+ defaults =
167
+ if self.class.defaults_generator
168
+ instance_exec(&self.class.defaults_generator)
169
+ else
170
+ self.class.default_params
171
+ end
172
+
173
+ defaults.each do |k, v|
174
+ payload[k] = v unless payload.key?(k)
175
+ end
176
+ end
177
+ end
178
+ end
@@ -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,49 @@
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 Notification
31
+ def notify_now
32
+ return super unless AbstractNotifier.test?
33
+
34
+ Driver.send_notification payload.merge(via: owner)
35
+ end
36
+
37
+ def notify_later
38
+ return super unless AbstractNotifier.test?
39
+
40
+ Driver.enqueue_notification payload.merge(via: owner)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ AbstractNotifier::Notification.prepend AbstractNotifier::Testing::Notification
47
+
48
+ require "abstract_notifier/testing/rspec" if defined?(RSpec::Core)
49
+ require "abstract_notifier/testing/minitest" if defined?(Minitest::Assertions)
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractNotifier
4
+ VERSION = "0.3.2"
5
+ end
@@ -0,0 +1,74 @@
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/async_adapters/active_job" if defined?(ActiveJob)
73
+
74
+ require "abstract_notifier/testing" if ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test"