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 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
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
10
+ Gemfile.local
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
+ [![Gem Version](https://badge.fury.io/rb/abstract_notifier.svg)](https://badge.fury.io/rb/abstract_notifier)
2
+ [![Build Status](https://travis-ci.org/palkan/abstract_notifier.svg?branch=master)](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,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "rubocop/rake_task"
4
+
5
+ RuboCop::RakeTask.new
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: [:rubocop, :spec]
@@ -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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 4.2"
4
+
5
+ gemspec path: ".."
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", github: "rails/rails"
4
+ gem "rubocop", github: "rubocop-hq/rubocop"
5
+
6
+ gemspec path: ".."
@@ -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
@@ -0,0 +1,3 @@
1
+ module AbstractNotifier
2
+ VERSION = "0.1.0"
3
+ 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: []