active_delivery 0.4.4 → 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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE.txt +19 -17
  4. data/README.md +495 -33
  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 +147 -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,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDelivery
4
+ # Base class for deliveries.
5
+ #
6
+ # Delivery object describes how to notify a user about
7
+ # an event (e.g. via email or via push notification or both).
8
+ #
9
+ # Delivery class acts like a proxy in front of the different delivery channels
10
+ # (i.e. mailers, notifiers). That means that calling a method on delivery class invokes the
11
+ # same method on the corresponding class, e.g.:
12
+ #
13
+ # EventsDelivery.notify(:one_hour_before, profile, event)
14
+ #
15
+ # # under the hood it calls
16
+ # EventsMailer.one_hour_before(profile, event).deliver_later
17
+ #
18
+ # # and
19
+ # EventsNotifier.one_hour_before(profile, event).notify_later
20
+ #
21
+ # Delivery also supports _parameterized_ calling:
22
+ #
23
+ # EventsDelivery.with(profile: profile).notify(:canceled, event)
24
+ #
25
+ # The parameters could be accessed through `params` instance method (e.g.
26
+ # to implement guard-like logic).
27
+ #
28
+ # When params are presents the parametrized mailer is used, i.e.:
29
+ #
30
+ # EventsMailer.with(profile: profile).canceled(event)
31
+ #
32
+ # See https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html
33
+ class Base
34
+ class << self
35
+ attr_accessor :abstract_class
36
+
37
+ alias_method :with, :new
38
+
39
+ # Enqueues delivery (i.e. uses #deliver_later for mailers)
40
+ def notify(...)
41
+ new.notify(...)
42
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify)
43
+
44
+ # The same as .notify but delivers synchronously
45
+ # (i.e. #deliver_now for mailers)
46
+ def notify!(mid, *args, **hargs)
47
+ notify(mid, *args, **hargs, sync: true)
48
+ end
49
+
50
+ def delivery_lines
51
+ @lines ||= if superclass.respond_to?(:delivery_lines)
52
+ superclass.delivery_lines.each_with_object({}) do |(key, val), acc|
53
+ acc[key] = val.dup_for(self)
54
+ end
55
+ else
56
+ {}
57
+ end
58
+ end
59
+
60
+ def register_line(line_id, line_class, **options)
61
+ delivery_lines[line_id] = line_class.new(id: line_id, owner: self, **options)
62
+
63
+ instance_eval <<~CODE, __FILE__, __LINE__ + 1
64
+ def #{line_id}(val)
65
+ delivery_lines[:#{line_id}].handler_class = val
66
+ end
67
+
68
+ def #{line_id}_class
69
+ delivery_lines[:#{line_id}].handler_class
70
+ end
71
+ CODE
72
+ end
73
+
74
+ def unregister_line(line_id)
75
+ removed_line = delivery_lines.delete(line_id)
76
+
77
+ return if removed_line.nil?
78
+
79
+ singleton_class.undef_method line_id
80
+ singleton_class.undef_method "#{line_id}_class"
81
+ end
82
+
83
+ def abstract_class? ; abstract_class == true; end
84
+ end
85
+
86
+ attr_reader :params, :notification_name
87
+
88
+ def initialize(**params)
89
+ @params = params
90
+ @params.freeze
91
+ end
92
+
93
+ # Enqueues delivery (i.e. uses #deliver_later for mailers)
94
+ def notify(mid, *__rest__, &__block__)
95
+ @notification_name = mid
96
+ do_notify(*__rest__, &__block__)
97
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify)
98
+
99
+ # The same as .notify but delivers synchronously
100
+ # (i.e. #deliver_now for mailers)
101
+ def notify!(mid, *args, **hargs)
102
+ notify(mid, *args, **hargs, sync: true)
103
+ end
104
+
105
+ private
106
+
107
+ def do_notify(*args, sync: false, **kwargs)
108
+ delivery_lines.each do |type, line|
109
+ next if line.handler_class.nil?
110
+ next unless line.notify?(notification_name)
111
+
112
+ notify_line(type, *args, params: params, sync: sync, **kwargs)
113
+ end
114
+ end
115
+
116
+ def notify_line(type, *__rest__, &__block__)
117
+ delivery_lines[type].notify(notification_name, *__rest__, &__block__)
118
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_line)
119
+
120
+ def delivery_lines
121
+ self.class.delivery_lines
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,97 @@
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 do_notify(...)
41
+ run_callbacks(:notify) { super(...) }
42
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :do_notify)
43
+
44
+ def notify_line(kind, *__rest__, &__block__)
45
+ run_callbacks(kind) { super(kind, *__rest__, &__block__) }
46
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_line)
47
+ end
48
+
49
+ module SingltonExt
50
+ def register_line(line_id, *__rest__, &__block__)
51
+ super
52
+ define_line_callbacks line_id
53
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :register_line)
54
+ end
55
+
56
+ class_methods do
57
+ def _normalize_callback_options(options)
58
+ _normalize_callback_option(options, :only, :if)
59
+ _normalize_callback_option(options, :except, :unless)
60
+ end
61
+
62
+ def _normalize_callback_option(options, from, to)
63
+ if (from = options[from])
64
+ from_set = Array(from).map(&:to_s).to_set
65
+ from = proc { |c| from_set.include? c.notification_name.to_s }
66
+ options[to] = Array(options[to]).unshift(from)
67
+ end
68
+ end
69
+
70
+ def define_line_callbacks(name)
71
+ define_callbacks name,
72
+ terminator: CALLBACK_TERMINATOR,
73
+ skip_after_callbacks_if_terminated: true
74
+ end
75
+
76
+ def before_notify(method_or_block = nil, on: :notify, **options, &block)
77
+ method_or_block ||= block
78
+ _normalize_callback_options(options)
79
+ set_callback on, :before, method_or_block, options
80
+ end
81
+
82
+ def after_notify(method_or_block = nil, on: :notify, **options, &block)
83
+ method_or_block ||= block
84
+ _normalize_callback_options(options)
85
+ set_callback on, :after, method_or_block, options
86
+ end
87
+
88
+ def around_notify(method_or_block = nil, on: :notify, **options, &block)
89
+ method_or_block ||= block
90
+ _normalize_callback_options(options)
91
+ set_callback on, :around, method_or_block, options
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ ActiveDelivery::Base.include ActiveDelivery::Callbacks
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDelivery
4
+ module Lines
5
+ class Base
6
+ attr_reader :id, :options
7
+ attr_accessor :owner
8
+ attr_writer :handler_class
9
+
10
+ def initialize(id:, owner:, **options)
11
+ @id = id
12
+ @owner = owner
13
+ @options = options.tap(&:freeze)
14
+ @resolver = options[:resolver]
15
+ end
16
+
17
+ def dup_for(new_owner)
18
+ self.class.new(id: id, **options, owner: new_owner)
19
+ end
20
+
21
+ def resolve_class(name)
22
+ resolver&.call(name)
23
+ end
24
+
25
+ def notify?(method_name)
26
+ handler_class.respond_to?(method_name)
27
+ end
28
+
29
+ def notify_now(handler, mid, *__rest__, &__block__)
30
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_now)
31
+
32
+ def notify_later(handler, mid, *__rest__, &__block__)
33
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_later)
34
+
35
+ def notify(mid, *args, params:, sync:, **kwargs)
36
+ clazz = params.empty? ? handler_class : handler_class.with(**params)
37
+ sync ? notify_now(clazz, mid, *args, **kwargs) : notify_later(clazz, mid, *args, **kwargs)
38
+ end
39
+
40
+ def handler_class
41
+ return @handler_class if instance_variable_defined?(:@handler_class)
42
+
43
+ return @handler_class = nil if owner.abstract_class?
44
+
45
+ @handler_class = resolve_class(owner.name) ||
46
+ superclass_handler
47
+ end
48
+
49
+ private
50
+
51
+ def superclass_handler
52
+ handler_method = "#{id}_class"
53
+
54
+ return if owner.superclass == ActiveDelivery::Base
55
+ return unless owner.superclass.respond_to?(handler_method)
56
+
57
+ owner.superclass.public_send(handler_method)
58
+ end
59
+
60
+ attr_reader :resolver
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,25 @@
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 = ->(name) { name.gsub(/Delivery$/, "Mailer").safe_constantize }
9
+
10
+ def notify?(method_name)
11
+ mailer_class.action_methods.include?(method_name.to_s)
12
+ end
13
+
14
+ def notify_now(mailer, mid, *__rest__, &__block__)
15
+ mailer.public_send(mid, *__rest__, &__block__).deliver_now
16
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_now)
17
+
18
+ def notify_later(mailer, mid, *__rest__, &__block__)
19
+ mailer.public_send(mid, *__rest__, &__block__).deliver_later
20
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :notify_later)
21
+ end
22
+
23
+ ActiveDelivery::Base.register_line :mailer, Mailer, resolver: Mailer::DEFAULT_RESOLVER
24
+ end
25
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDelivery
4
+ # Base class for deliveries.
5
+ #
6
+ # Delivery object describes how to notify a user about
7
+ # an event (e.g. via email or via push notification or both).
8
+ #
9
+ # Delivery class acts like a proxy in front of the different delivery channels
10
+ # (i.e. mailers, notifiers). That means that calling a method on delivery class invokes the
11
+ # same method on the corresponding class, e.g.:
12
+ #
13
+ # EventsDelivery.notify(:one_hour_before, profile, event)
14
+ #
15
+ # # under the hood it calls
16
+ # EventsMailer.one_hour_before(profile, event).deliver_later
17
+ #
18
+ # # and
19
+ # EventsNotifier.one_hour_before(profile, event).notify_later
20
+ #
21
+ # Delivery also supports _parameterized_ calling:
22
+ #
23
+ # EventsDelivery.with(profile: profile).notify(:canceled, event)
24
+ #
25
+ # The parameters could be accessed through `params` instance method (e.g.
26
+ # to implement guard-like logic).
27
+ #
28
+ # When params are presents the parametrized mailer is used, i.e.:
29
+ #
30
+ # EventsMailer.with(profile: profile).canceled(event)
31
+ #
32
+ # See https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html
33
+ class Base
34
+ class << self
35
+ attr_accessor :abstract_class
36
+
37
+ alias_method :with, :new
38
+
39
+ # Enqueues delivery (i.e. uses #deliver_later for mailers)
40
+ def notify(...)
41
+ new.notify(...)
42
+ end
43
+
44
+ # The same as .notify but delivers synchronously
45
+ # (i.e. #deliver_now for mailers)
46
+ def notify!(mid, *args, **hargs)
47
+ notify(mid, *args, **hargs, sync: true)
48
+ end
49
+
50
+ def delivery_lines
51
+ @lines ||= if superclass.respond_to?(:delivery_lines)
52
+ superclass.delivery_lines.each_with_object({}) do |(key, val), acc|
53
+ acc[key] = val.dup_for(self)
54
+ end
55
+ else
56
+ {}
57
+ end
58
+ end
59
+
60
+ def register_line(line_id, line_class, **options)
61
+ delivery_lines[line_id] = line_class.new(id: line_id, owner: self, **options)
62
+
63
+ instance_eval <<~CODE, __FILE__, __LINE__ + 1
64
+ def #{line_id}(val)
65
+ delivery_lines[:#{line_id}].handler_class = val
66
+ end
67
+
68
+ def #{line_id}_class
69
+ delivery_lines[:#{line_id}].handler_class
70
+ end
71
+ CODE
72
+ end
73
+
74
+ def unregister_line(line_id)
75
+ removed_line = delivery_lines.delete(line_id)
76
+
77
+ return if removed_line.nil?
78
+
79
+ singleton_class.undef_method line_id
80
+ singleton_class.undef_method "#{line_id}_class"
81
+ end
82
+
83
+ def abstract_class? = abstract_class == true
84
+ end
85
+
86
+ attr_reader :params, :notification_name
87
+
88
+ def initialize(**params)
89
+ @params = params
90
+ @params.freeze
91
+ end
92
+
93
+ # Enqueues delivery (i.e. uses #deliver_later for mailers)
94
+ def notify(mid, ...)
95
+ @notification_name = mid
96
+ do_notify(...)
97
+ end
98
+
99
+ # The same as .notify but delivers synchronously
100
+ # (i.e. #deliver_now for mailers)
101
+ def notify!(mid, *args, **hargs)
102
+ notify(mid, *args, **hargs, sync: true)
103
+ end
104
+
105
+ private
106
+
107
+ def do_notify(*args, sync: false, **kwargs)
108
+ delivery_lines.each do |type, line|
109
+ next if line.handler_class.nil?
110
+ next unless line.notify?(notification_name)
111
+
112
+ notify_line(type, *args, params: params, sync: sync, **kwargs)
113
+ end
114
+ end
115
+
116
+ def notify_line(type, ...)
117
+ delivery_lines[type].notify(notification_name, ...)
118
+ end
119
+
120
+ def delivery_lines
121
+ self.class.delivery_lines
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDelivery
4
+ module Lines
5
+ class Base
6
+ attr_reader :id, :options
7
+ attr_accessor :owner
8
+ attr_writer :handler_class
9
+
10
+ def initialize(id:, owner:, **options)
11
+ @id = id
12
+ @owner = owner
13
+ @options = options.tap(&:freeze)
14
+ @resolver = options[:resolver]
15
+ end
16
+
17
+ def dup_for(new_owner)
18
+ self.class.new(id: id, **options, owner: new_owner)
19
+ end
20
+
21
+ def resolve_class(name)
22
+ resolver&.call(name)
23
+ end
24
+
25
+ def notify?(method_name)
26
+ handler_class.respond_to?(method_name)
27
+ end
28
+
29
+ def notify_now(handler, mid, ...)
30
+ end
31
+
32
+ def notify_later(handler, mid, ...)
33
+ end
34
+
35
+ def notify(mid, *args, params:, sync:, **kwargs)
36
+ clazz = params.empty? ? handler_class : handler_class.with(**params)
37
+ sync ? notify_now(clazz, mid, *args, **kwargs) : notify_later(clazz, mid, *args, **kwargs)
38
+ end
39
+
40
+ def handler_class
41
+ return @handler_class if instance_variable_defined?(:@handler_class)
42
+
43
+ return @handler_class = nil if owner.abstract_class?
44
+
45
+ @handler_class = resolve_class(owner.name) ||
46
+ superclass_handler
47
+ end
48
+
49
+ private
50
+
51
+ def superclass_handler
52
+ handler_method = "#{id}_class"
53
+
54
+ return if owner.superclass == ActiveDelivery::Base
55
+ return unless owner.superclass.respond_to?(handler_method)
56
+
57
+ owner.superclass.public_send(handler_method)
58
+ end
59
+
60
+ attr_reader :resolver
61
+ end
62
+ end
63
+ end
@@ -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, payload)
8
+ AbstractNotifier::Notification.new(notifier_class.constantize, payload).notify_now
9
+ end
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(notifier_class, payload)
21
+ job.perform_later(notifier_class.name, payload)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ AbstractNotifier.async_adapter ||= :active_job
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractNotifier
4
+ module AsyncAdapters
5
+ class << self
6
+ def lookup(adapter, options = nil)
7
+ return adapter unless adapter.is_a?(Symbol)
8
+
9
+ adapter_class_name = adapter.to_s.split("_").map(&:capitalize).join
10
+ AsyncAdapters.const_get(adapter_class_name).new(**(options || {}))
11
+ rescue NameError => e
12
+ raise e.class, "Notifier async adapter :#{adapter} haven't been found", e.backtrace
13
+ end
14
+ end
15
+ end
16
+ end