abstract_notifier 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +26 -0
- data/.travis.yml +26 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +168 -0
- data/Rakefile +8 -0
- data/abstract_notifier.gemspec +23 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/gemfiles/rails42.gemfile +5 -0
- data/gemfiles/railsmaster.gemfile +6 -0
- data/lib/abstract_notifier.rb +74 -0
- data/lib/abstract_notifier/async_adapters.rb +16 -0
- data/lib/abstract_notifier/async_adapters/active_job.rb +27 -0
- data/lib/abstract_notifier/base.rb +102 -0
- data/lib/abstract_notifier/testing.rb +48 -0
- data/lib/abstract_notifier/testing/rspec.rb +159 -0
- data/lib/abstract_notifier/version.rb +3 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e5441072e0ded98cb24401c568df58e4917cb33211b04427b624498f1d622541
|
4
|
+
data.tar.gz: 5bde27d338590df550fd8cc74c8301a0c694510e6277e94c3f014a0e2033d16d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e2c5d63ed5065c83bcb9bad41c8209cb2292d8703fedf326e7b44e4d03a7474758e3eae388283c5638c381584527652b34ccd9b584f5ecf7f6de056fd21402c9
|
7
|
+
data.tar.gz: ebc4e4e33bc64a77a9e01d31cf123f06d62337c8d88ad35264aee947153def02584766ca3ce3c8a1e7cc07f195ad573ef6632057dd6b7af8127c4d905ef30b16
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require:
|
2
|
+
- standard/cop/semantic_blocks
|
3
|
+
|
4
|
+
inherit_gem:
|
5
|
+
standard: config/base.yml
|
6
|
+
|
7
|
+
AllCops:
|
8
|
+
Exclude:
|
9
|
+
- 'bin/*'
|
10
|
+
- 'tmp/**/*'
|
11
|
+
- 'Gemfile'
|
12
|
+
- 'node_modules/**/*'
|
13
|
+
- 'vendor/**/*'
|
14
|
+
DisplayCopNames: true
|
15
|
+
|
16
|
+
Standard/SemanticBlocks:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Style/TrailingCommaInArrayLiteral:
|
20
|
+
EnforcedStyleForMultiline: no_comma
|
21
|
+
|
22
|
+
Style/TrailingCommaInHashLiteral:
|
23
|
+
EnforcedStyleForMultiline: no_comma
|
24
|
+
|
25
|
+
Layout/AlignParameters:
|
26
|
+
EnforcedStyle: with_first_parameter
|
data/.travis.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
sudo: false
|
2
|
+
language: ruby
|
3
|
+
rvm:
|
4
|
+
- 2.5.1
|
5
|
+
|
6
|
+
notifications:
|
7
|
+
email: false
|
8
|
+
|
9
|
+
matrix:
|
10
|
+
fast_finish: true
|
11
|
+
include:
|
12
|
+
- rvm: ruby-head
|
13
|
+
gemfile: gemfiles/railsmaster.gemfile
|
14
|
+
- rvm: 2.5.1
|
15
|
+
gemfile: Gemfile
|
16
|
+
- rvm: 2.5.1
|
17
|
+
gemfile: Gemfile
|
18
|
+
env:
|
19
|
+
- NO_RAILS=1
|
20
|
+
- rvm: 2.4.3
|
21
|
+
gemfile: Gemfile
|
22
|
+
- rvm: 2.3.1
|
23
|
+
gemfile: gemfiles/rails42.gemfile
|
24
|
+
allow_failures:
|
25
|
+
- rvm: ruby-head
|
26
|
+
gemfile: gemfiles/railsmaster.gemfile
|
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in abstract_notifier.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem "rspec-rails"
|
7
|
+
|
8
|
+
local_gemfile = File.join(__dir__, "Gemfile.local")
|
9
|
+
|
10
|
+
if File.exist?(local_gemfile)
|
11
|
+
eval(File.read(local_gemfile)) # rubocop:disable Security/Eval
|
12
|
+
else
|
13
|
+
gem "rails", "~> 5.2"
|
14
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Vladimir Dementyev
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
[](https://badge.fury.io/rb/abstract_notifier)
|
2
|
+
[](https://travis-ci.org/palkan/abstract_notifier)
|
3
|
+
|
4
|
+
# Abstract Notifier
|
5
|
+
|
6
|
+
Abstract Notifier is a tool which allows you to describe/model any text-based notifications (such as Push Notifications) the same way Action Mailer does for email notifications.
|
7
|
+
|
8
|
+
Abstract Notifier (as the name states) doesn't provide any specific implementaion for sending notifications. Instead if provide tools to organize your notification-specific code and make it easily testable.
|
9
|
+
|
10
|
+
<a href="https://evilmartians.com/?utm_source=action_policy">
|
11
|
+
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
12
|
+
|
13
|
+
Requirements:
|
14
|
+
- Ruby ~> 2.3
|
15
|
+
|
16
|
+
**NOTE**: although most of the examples in this readme are Rails-specific, this gem could be used without Rails/ActiveSupport.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'abstract_notifier'
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
```sh
|
29
|
+
$ bundle
|
30
|
+
```
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
Notifer class is very similar to Action Mailer mailer class with `notification` method instead of a `mail` method:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class EventsNotifier < ApplicationNotifier
|
38
|
+
def canceled(profile, event)
|
39
|
+
notification(
|
40
|
+
# the only required option is `body`
|
41
|
+
body: "Event #{event.title} has been canceled",
|
42
|
+
# all other options are passed to delivery driver
|
43
|
+
identity: profile.notification_service_id
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# send notification later
|
49
|
+
EventsNotifier.canceled(profile, event).notify_later
|
50
|
+
|
51
|
+
# or immediately
|
52
|
+
EventsNotifier.canceled(profile, event).notify_now
|
53
|
+
```
|
54
|
+
|
55
|
+
To perform actual deliveries you **must** configure a _delivery driver_:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class ApplicationNotifier < AbstractNotifier::Base
|
59
|
+
self.driver = MyFancySender.new
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
Driver could be any callbable Ruby object (i.e. anything that responds to `#call`).
|
64
|
+
|
65
|
+
That's a developer responsibility to implement the driver (we do not provide any drivers out-of-the-box; at least yet).
|
66
|
+
|
67
|
+
You can set different drivers for different notifiers.
|
68
|
+
|
69
|
+
### Parameterized notifiers
|
70
|
+
|
71
|
+
Abstract Notifier support parameterization the same way as [Action Mailer]((https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html)):
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
class EventsNotifier < ApplicationNotifier
|
75
|
+
def canceled(event)
|
76
|
+
notification(
|
77
|
+
body: "Event #{event.title} has been canceled",
|
78
|
+
identity: params[:profile].notification_service_id
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
EventsNotifier.with(profile: profile).canceled(event).notify_later
|
84
|
+
```
|
85
|
+
|
86
|
+
### Background jobs / async notifications
|
87
|
+
|
88
|
+
To use `notify_later` you **must** configure `async_adapter`.
|
89
|
+
|
90
|
+
We provide Active Job adapter out-of-the-box and use it if Active Job is present.
|
91
|
+
|
92
|
+
Custom async adapter must implement `enqueue` method:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class MyAsyncAdapter
|
96
|
+
# adapters may accept options
|
97
|
+
def initialize(options = {})
|
98
|
+
end
|
99
|
+
|
100
|
+
# `enqueue` method accepts notifier class and notification
|
101
|
+
# payload.
|
102
|
+
# We need to know notifier class to use it's driver.
|
103
|
+
def enqueue(notifier_class, payload)
|
104
|
+
# your implementation here
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Configure globally
|
109
|
+
AbstractNotifier.async_adapter = MyAsyncAdapter.new
|
110
|
+
|
111
|
+
# or per-notifier
|
112
|
+
class EventsNotifier < AbstractNotifier::Base
|
113
|
+
self.async_adapter = MyAsyncAdapter.new
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
### Delivery modes
|
118
|
+
|
119
|
+
For test/development purposes there are two special _global_ delivery modes:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
# Track all sent notifications without peforming real actions.
|
123
|
+
# Required for using RSpec matchers.
|
124
|
+
#
|
125
|
+
# config/environments/test.rb
|
126
|
+
AbstractNotifier.delivery_mode = :test
|
127
|
+
|
128
|
+
|
129
|
+
# If you don't want to trigger notifications in development,
|
130
|
+
# you can make Abstract Notifier no-op.
|
131
|
+
#
|
132
|
+
# config/environments/development.rb
|
133
|
+
ActionNotifier.delivery_mode = :noop
|
134
|
+
|
135
|
+
# Default delivery mode is "normal"
|
136
|
+
ActionNotifier.delivery_mode = :normal
|
137
|
+
```
|
138
|
+
|
139
|
+
**NOTE:** we set `delivery_mode = :test` if `RAILS_ENV` or `RACK_ENV` env variable is equal to "test".
|
140
|
+
Otherwise add `require "abstract_notifier/testing"` to your `spec_helper.rb` / `rails_helper.rb` manually.
|
141
|
+
|
142
|
+
**NOTE:** delivery mode affects all drivers.
|
143
|
+
|
144
|
+
### Testing
|
145
|
+
|
146
|
+
Abstract Notifier provides two convinient RSpec matchers:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
# for testing sync notifications (sent with `notify_now`)
|
150
|
+
expect { EventsNotifier.with(profile: profile).canceled(event).notify_now }.
|
151
|
+
to have_sent_notification(identify: '123', body: 'Alarma!')
|
152
|
+
|
153
|
+
# for testing async notifications (sent with `notify_later`)
|
154
|
+
expect { EventsNotifier.with(profile: profile).canceled(event).notify_later}.
|
155
|
+
to have_enqueued_notification(identify: '123', body: 'Alarma!')
|
156
|
+
```
|
157
|
+
|
158
|
+
## Related projects
|
159
|
+
|
160
|
+
- [`active_delivery`](https://github.com/palkan/active_delivery) – next-level abstraction which allows to combine multiple notification channels in one place.
|
161
|
+
|
162
|
+
## Contributing
|
163
|
+
|
164
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/abstract_notifier.
|
165
|
+
|
166
|
+
## License
|
167
|
+
|
168
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "abstract_notifier/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "abstract_notifier"
|
7
|
+
spec.version = AbstractNotifier::VERSION
|
8
|
+
spec.authors = ["Vladimir Dementyev"]
|
9
|
+
spec.email = ["dementiev.vm@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "ActionMailer-like interface for any type of notifications"
|
12
|
+
spec.description = "ActionMailer-like interface for any type of notifications"
|
13
|
+
spec.homepage = "https://github.com/palkan/abstract_notifier"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.require_paths = ["lib"]
|
18
|
+
|
19
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
20
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
21
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
22
|
+
spec.add_development_dependency "standard", "~> 0.0.12"
|
23
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "abstract_notifier"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -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"
|
@@ -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
|
@@ -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,102 @@
|
|
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 << self
|
29
|
+
alias with new
|
30
|
+
|
31
|
+
attr_writer :driver
|
32
|
+
|
33
|
+
def driver
|
34
|
+
return @driver if instance_variable_defined?(:@driver)
|
35
|
+
|
36
|
+
@driver =
|
37
|
+
if superclass.respond_to?(:driver)
|
38
|
+
superclass.driver
|
39
|
+
else
|
40
|
+
raise "Driver not found for #{name}. " \
|
41
|
+
"Please, specify driver via `self.driver = MyDriver`"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def async_adapter=(args)
|
46
|
+
adapter, options = Array(args)
|
47
|
+
@async_adapter = AsyncAdapters.lookup(adapter, options)
|
48
|
+
end
|
49
|
+
|
50
|
+
def async_adapter
|
51
|
+
return @async_adapter if instance_variable_defined?(:@async_adapter)
|
52
|
+
|
53
|
+
@async_adapter =
|
54
|
+
if superclass.respond_to?(:async_adapter)
|
55
|
+
superclass.async_adapter
|
56
|
+
else
|
57
|
+
AbstractNotifier.async_adapter
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def method_missing(method_name, *args)
|
62
|
+
if action_methods.include?(method_name.to_s)
|
63
|
+
new.public_send(method_name, *args)
|
64
|
+
else
|
65
|
+
super
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def respond_to_missing?(method_name, _include_private = false)
|
70
|
+
action_methods.include?(method_name.to_s) || super
|
71
|
+
end
|
72
|
+
|
73
|
+
# See https://github.com/rails/rails/blob/b13a5cb83ea00d6a3d71320fd276ca21049c2544/actionpack/lib/abstract_controller/base.rb#L74
|
74
|
+
def action_methods
|
75
|
+
@action_methods ||= begin
|
76
|
+
# All public instance methods of this class, including ancestors
|
77
|
+
methods = (public_instance_methods(true) -
|
78
|
+
# Except for public instance methods of Base and its ancestors
|
79
|
+
Base.public_instance_methods(true) +
|
80
|
+
# Be sure to include shadowed public instance methods of this class
|
81
|
+
public_instance_methods(false))
|
82
|
+
|
83
|
+
methods.map!(&:to_s)
|
84
|
+
|
85
|
+
methods.to_set
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_reader :params
|
91
|
+
|
92
|
+
def initialize(**params)
|
93
|
+
@params = params.freeze
|
94
|
+
end
|
95
|
+
|
96
|
+
def notification(**payload)
|
97
|
+
raise ArgumentError, "Notification body must be present" if
|
98
|
+
payload[:body].nil? || payload[:body].empty?
|
99
|
+
Notification.new(self.class, payload)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,48 @@
|
|
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
|
35
|
+
end
|
36
|
+
|
37
|
+
def notify_later
|
38
|
+
return super unless AbstractNotifier.test?
|
39
|
+
|
40
|
+
Driver.enqueue_notification payload
|
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)
|
@@ -0,0 +1,159 @@
|
|
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)
|
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
|
+
payload === actual_payload
|
59
|
+
end
|
60
|
+
|
61
|
+
@matching_count = @matching_deliveries.size
|
62
|
+
|
63
|
+
case @expectation_type
|
64
|
+
when :exactly then @expected_number == @matching_count
|
65
|
+
when :at_most then @expected_number >= @matching_count
|
66
|
+
when :at_least then @expected_number <= @matching_count
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def deliveries
|
73
|
+
AbstractNotifier::Testing::Driver.deliveries
|
74
|
+
end
|
75
|
+
|
76
|
+
def set_expected_number(relativity, count)
|
77
|
+
@expectation_type = relativity
|
78
|
+
@expected_number =
|
79
|
+
case count
|
80
|
+
when :once then 1
|
81
|
+
when :twice then 2
|
82
|
+
when :thrice then 3
|
83
|
+
else Integer(count)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def failure_message
|
88
|
+
(+"expected to #{verb_present} notification: #{payload_description}").tap do |msg|
|
89
|
+
msg << " #{message_expectation_modifier}, but"
|
90
|
+
|
91
|
+
if @unmatching_deliveries.any?
|
92
|
+
msg << " #{verb_past} the following notifications:"
|
93
|
+
@unmatching_deliveries.each do |unmatching_payload|
|
94
|
+
msg << "\n #{unmatching_payload}"
|
95
|
+
end
|
96
|
+
else
|
97
|
+
msg << " haven't #{verb_past} anything"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def failure_message_when_negated
|
103
|
+
"expected not to #{verb_present} #{payload}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def message_expectation_modifier
|
107
|
+
number_modifier = @expected_number == 1 ? "once" : "#{@expected_number} times"
|
108
|
+
case @expectation_type
|
109
|
+
when :exactly then "exactly #{number_modifier}"
|
110
|
+
when :at_most then "at most #{number_modifier}"
|
111
|
+
when :at_least then "at least #{number_modifier}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def payload_description
|
116
|
+
if payload.is_a?(RSpec::Matchers::Composable)
|
117
|
+
payload.description
|
118
|
+
else
|
119
|
+
payload
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def verb_past
|
124
|
+
"sent"
|
125
|
+
end
|
126
|
+
|
127
|
+
def verb_present
|
128
|
+
"send"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class HaveEqueuedNotification < HaveSentNotification
|
133
|
+
private
|
134
|
+
|
135
|
+
def deliveries
|
136
|
+
AbstractNotifier::Testing::Driver.enqueued_deliveries
|
137
|
+
end
|
138
|
+
|
139
|
+
def verb_past
|
140
|
+
"enqueued"
|
141
|
+
end
|
142
|
+
|
143
|
+
def verb_present
|
144
|
+
"enqueue"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
RSpec.configure do |config|
|
150
|
+
config.include(Module.new do
|
151
|
+
def have_sent_notification(*args)
|
152
|
+
AbstractNotifier::HaveSentNotification.new(*args)
|
153
|
+
end
|
154
|
+
|
155
|
+
def have_enqueued_notification(*args)
|
156
|
+
AbstractNotifier::HaveEqueuedNotification.new(*args)
|
157
|
+
end
|
158
|
+
end)
|
159
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: abstract_notifier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vladimir Dementyev
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-12-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: standard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.0.12
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.0.12
|
69
|
+
description: ActionMailer-like interface for any type of notifications
|
70
|
+
email:
|
71
|
+
- dementiev.vm@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".rspec"
|
78
|
+
- ".rubocop.yml"
|
79
|
+
- ".travis.yml"
|
80
|
+
- Gemfile
|
81
|
+
- LICENSE.txt
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- abstract_notifier.gemspec
|
85
|
+
- bin/console
|
86
|
+
- bin/setup
|
87
|
+
- gemfiles/rails42.gemfile
|
88
|
+
- gemfiles/railsmaster.gemfile
|
89
|
+
- lib/abstract_notifier.rb
|
90
|
+
- lib/abstract_notifier/async_adapters.rb
|
91
|
+
- lib/abstract_notifier/async_adapters/active_job.rb
|
92
|
+
- lib/abstract_notifier/base.rb
|
93
|
+
- lib/abstract_notifier/testing.rb
|
94
|
+
- lib/abstract_notifier/testing/rspec.rb
|
95
|
+
- lib/abstract_notifier/version.rb
|
96
|
+
homepage: https://github.com/palkan/abstract_notifier
|
97
|
+
licenses:
|
98
|
+
- MIT
|
99
|
+
metadata: {}
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
requirements: []
|
115
|
+
rubyforge_project:
|
116
|
+
rubygems_version: 2.7.7
|
117
|
+
signing_key:
|
118
|
+
specification_version: 4
|
119
|
+
summary: ActionMailer-like interface for any type of notifications
|
120
|
+
test_files: []
|