active_delivery 0.4.3 → 1.0.0.rc2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -1
- data/LICENSE.txt +19 -17
- data/README.md +503 -32
- data/lib/.rbnext/3.0/active_delivery/base.rb +124 -0
- data/lib/.rbnext/3.0/active_delivery/callbacks.rb +97 -0
- data/lib/.rbnext/3.0/active_delivery/lines/base.rb +63 -0
- data/lib/.rbnext/3.0/active_delivery/lines/mailer.rb +25 -0
- data/lib/.rbnext/3.1/active_delivery/base.rb +124 -0
- data/lib/.rbnext/3.1/active_delivery/lines/base.rb +63 -0
- data/lib/abstract_notifier/async_adapters/active_job.rb +27 -0
- data/lib/abstract_notifier/async_adapters.rb +16 -0
- data/lib/abstract_notifier/base.rb +178 -0
- data/lib/abstract_notifier/testing/minitest.rb +51 -0
- data/lib/abstract_notifier/testing/rspec.rb +164 -0
- data/lib/abstract_notifier/testing.rb +49 -0
- data/lib/abstract_notifier/version.rb +5 -0
- data/lib/abstract_notifier.rb +74 -0
- data/lib/active_delivery/base.rb +156 -27
- data/lib/active_delivery/callbacks.rb +25 -25
- data/lib/active_delivery/ext/string_constantize.rb +24 -0
- data/lib/active_delivery/lines/base.rb +24 -17
- data/lib/active_delivery/lines/mailer.rb +7 -18
- data/lib/active_delivery/lines/notifier.rb +53 -0
- data/lib/active_delivery/raitie.rb +9 -0
- data/lib/active_delivery/testing/rspec.rb +59 -12
- data/lib/active_delivery/testing.rb +19 -5
- data/lib/active_delivery/version.rb +1 -1
- data/lib/active_delivery.rb +8 -0
- metadata +61 -56
- data/.gem_release.yml +0 -3
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -24
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -23
- data/.github/workflows/docs-lint.yml +0 -72
- data/.github/workflows/rspec-jruby.yml +0 -35
- data/.github/workflows/rspec.yml +0 -51
- data/.github/workflows/rubocop.yml +0 -21
- data/.gitignore +0 -43
- data/.mdlrc +0 -1
- data/.rspec +0 -2
- data/.rubocop-md.yml +0 -16
- data/.rubocop.yml +0 -28
- data/Gemfile +0 -17
- data/RELEASING.md +0 -43
- data/Rakefile +0 -20
- data/active_delivery.gemspec +0 -35
- data/forspell.dict +0 -8
- data/gemfiles/jruby.gemfile +0 -5
- data/gemfiles/rails42.gemfile +0 -8
- data/gemfiles/rails5.gemfile +0 -5
- data/gemfiles/rails50.gemfile +0 -8
- data/gemfiles/rails6.gemfile +0 -5
- data/gemfiles/railsmaster.gemfile +0 -6
- data/gemfiles/rubocop.gemfile +0 -4
- data/lefthook.yml +0 -18
- 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,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"
|