active_delivery 0.4.4 → 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 +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