siftery-wisper 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +4 -0
- data/.travis.yml +22 -0
- data/CHANGELOG.md +129 -0
- data/CONTRIBUTING.md +61 -0
- data/Gemfile +13 -0
- data/README.md +373 -0
- data/Rakefile +10 -0
- data/bin/console +8 -0
- data/bin/setup +6 -0
- data/gem-public_cert.pem +21 -0
- data/lib/wisper.rb +65 -0
- data/lib/wisper/broadcasters/logger_broadcaster.rb +37 -0
- data/lib/wisper/broadcasters/send_broadcaster.rb +9 -0
- data/lib/wisper/configuration.rb +44 -0
- data/lib/wisper/global_listeners.rb +74 -0
- data/lib/wisper/publisher.rb +89 -0
- data/lib/wisper/registration/block.rb +11 -0
- data/lib/wisper/registration/object.rb +77 -0
- data/lib/wisper/registration/registration.rb +19 -0
- data/lib/wisper/temporary_listeners.rb +41 -0
- data/lib/wisper/value_objects/events.rb +61 -0
- data/lib/wisper/value_objects/prefix.rb +29 -0
- data/lib/wisper/version.rb +3 -0
- data/siftery-wisper.gemspec +31 -0
- data/spec/lib/global_listeners_spec.rb +82 -0
- data/spec/lib/integration_spec.rb +56 -0
- data/spec/lib/simple_example_spec.rb +21 -0
- data/spec/lib/temporary_global_listeners_spec.rb +67 -0
- data/spec/lib/wisper/broadcasters/logger_broadcaster_spec.rb +93 -0
- data/spec/lib/wisper/broadcasters/send_broadcaster_spec.rb +28 -0
- data/spec/lib/wisper/configuration/broadcasters_spec.rb +11 -0
- data/spec/lib/wisper/configuration_spec.rb +36 -0
- data/spec/lib/wisper/publisher_spec.rb +331 -0
- data/spec/lib/wisper/registrations/object_spec.rb +14 -0
- data/spec/lib/wisper/value_objects/events_spec.rb +107 -0
- data/spec/lib/wisper/value_objects/prefix_spec.rb +46 -0
- data/spec/lib/wisper_spec.rb +99 -0
- data/spec/spec_helper.rb +24 -0
- metadata +101 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
# @api private
|
2
|
+
|
3
|
+
module Wisper
|
4
|
+
class Registration
|
5
|
+
attr_reader :on, :listener, :options
|
6
|
+
|
7
|
+
def initialize(listener, options)
|
8
|
+
@listener = listener
|
9
|
+
@options = options
|
10
|
+
@on = ValueObjects::Events.new options[:on]
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def should_broadcast?(event)
|
16
|
+
on.include? event
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Handles temporary global subscriptions
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
|
5
|
+
module Wisper
|
6
|
+
class TemporaryListeners
|
7
|
+
|
8
|
+
def self.subscribe(*listeners, &block)
|
9
|
+
new.subscribe(*listeners, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.registrations
|
13
|
+
new.registrations
|
14
|
+
end
|
15
|
+
|
16
|
+
def subscribe(*listeners, &block)
|
17
|
+
options = listeners.last.is_a?(Hash) ? listeners.pop : {}
|
18
|
+
begin
|
19
|
+
listeners.each { |listener| registrations << ObjectRegistration.new(listener, options) }
|
20
|
+
yield
|
21
|
+
ensure
|
22
|
+
clear
|
23
|
+
end
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def registrations
|
28
|
+
Thread.current[key] ||= Set.new
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def clear
|
34
|
+
registrations.clear
|
35
|
+
end
|
36
|
+
|
37
|
+
def key
|
38
|
+
'__wisper_temporary_listeners'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Wisper
|
2
|
+
module ValueObjects #:nodoc:
|
3
|
+
# Describes allowed events
|
4
|
+
#
|
5
|
+
# Duck-types the argument to quack like array of strings
|
6
|
+
# when responding to the {#include?} method call.
|
7
|
+
class Events
|
8
|
+
|
9
|
+
# @!scope class
|
10
|
+
# @!method new(on)
|
11
|
+
# Initializes a 'list' of events
|
12
|
+
#
|
13
|
+
# @param [NilClass, String, Symbol, Array, Regexp] list
|
14
|
+
#
|
15
|
+
# @raise [ArgumentError]
|
16
|
+
# if an argument if of unsupported type
|
17
|
+
#
|
18
|
+
# @return [undefined]
|
19
|
+
def initialize(list)
|
20
|
+
@list = list
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check if given event is 'included' to the 'list' of events
|
24
|
+
#
|
25
|
+
# @param [#to_s] event
|
26
|
+
#
|
27
|
+
# @return [Boolean]
|
28
|
+
def include?(event)
|
29
|
+
appropriate_method.call(event.to_s)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def methods
|
35
|
+
{
|
36
|
+
NilClass => ->(_event) { true },
|
37
|
+
String => ->(event) { list == event },
|
38
|
+
Symbol => ->(event) { list.to_s == event },
|
39
|
+
Enumerable => ->(event) { list.map(&:to_s).include? event },
|
40
|
+
Regexp => ->(event) { list.match(event) || false }
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def list
|
45
|
+
@list
|
46
|
+
end
|
47
|
+
|
48
|
+
def appropriate_method
|
49
|
+
@appropriate_method ||= methods[recognized_type]
|
50
|
+
end
|
51
|
+
|
52
|
+
def recognized_type
|
53
|
+
methods.keys.detect(&list.method(:is_a?)) || type_not_recognized
|
54
|
+
end
|
55
|
+
|
56
|
+
def type_not_recognized
|
57
|
+
fail(ArgumentError, "#{list.class} not supported for `on` argument")
|
58
|
+
end
|
59
|
+
end # class Events
|
60
|
+
end # module ValueObjects
|
61
|
+
end # module Wisper
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Wisper
|
2
|
+
module ValueObjects #:nodoc:
|
3
|
+
# Prefix for notifications
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# Wisper::ValueObjects::Prefix.new nil # => ""
|
7
|
+
# Wisper::ValueObjects::Prefix.new "when" # => "when_"
|
8
|
+
# Wisper::ValueObjects::Prefix.new true # => "on_"
|
9
|
+
class Prefix < String
|
10
|
+
class << self
|
11
|
+
attr_accessor :default
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param [true, nil, #to_s] value
|
15
|
+
#
|
16
|
+
# @return [undefined]
|
17
|
+
def initialize(value = nil)
|
18
|
+
super "#{ (value == true) ? default : value }_"
|
19
|
+
replace "" if self == "_"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def default
|
25
|
+
self.class.default || 'on'
|
26
|
+
end
|
27
|
+
end # class Prefix
|
28
|
+
end # module ValueObjects
|
29
|
+
end # module Wisper
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'wisper/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "siftery-wisper"
|
8
|
+
gem.version = Wisper::VERSION
|
9
|
+
gem.authors = ["Shubham Kumar"]
|
10
|
+
gem.email = ["shubh2336@gmail.com"]
|
11
|
+
gem.description = <<-DESC
|
12
|
+
A micro library providing objects with Publish-Subscribe capabilities.
|
13
|
+
Both synchronous (in-process) and asynchronous (out-of-process) subscriptions are supported.
|
14
|
+
Check out the Wiki for articles, guides and examples: https://github.com/shubh2336/wisper/wiki
|
15
|
+
DESC
|
16
|
+
gem.summary = "A micro library providing objects with Publish-Subscribe capabilities"
|
17
|
+
gem.homepage = "https://github.com/shubh2336/wisper"
|
18
|
+
gem.license = "MIT"
|
19
|
+
|
20
|
+
signing_key = File.expand_path(ENV['HOME'].to_s + '/.ssh/gem-private_key.pem')
|
21
|
+
|
22
|
+
if File.exist?(signing_key)
|
23
|
+
gem.signing_key = signing_key
|
24
|
+
gem.cert_chain = ['gem-public_cert.pem']
|
25
|
+
end
|
26
|
+
|
27
|
+
gem.files = `git ls-files`.split($/)
|
28
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
29
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
30
|
+
gem.require_paths = ["lib"]
|
31
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
describe Wisper::GlobalListeners do
|
2
|
+
let(:global_listener) { double('listener') }
|
3
|
+
let(:local_listener) { double('listener') }
|
4
|
+
let(:publisher) { publisher_class.new }
|
5
|
+
|
6
|
+
describe '.subscribe' do
|
7
|
+
it 'adds given listener to every publisher' do
|
8
|
+
Wisper::GlobalListeners.subscribe(global_listener)
|
9
|
+
expect(global_listener).to receive(:it_happened)
|
10
|
+
publisher.send(:broadcast, :it_happened)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'works with options' do
|
14
|
+
Wisper::GlobalListeners.subscribe(global_listener, :on => :it_happened,
|
15
|
+
:with => :woot)
|
16
|
+
expect(global_listener).to receive(:woot).once
|
17
|
+
expect(global_listener).not_to receive(:it_happened_again)
|
18
|
+
publisher.send(:broadcast, :it_happened)
|
19
|
+
publisher.send(:broadcast, :it_happened_again)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'works along side local listeners' do
|
23
|
+
# global listener
|
24
|
+
Wisper::GlobalListeners.subscribe(global_listener)
|
25
|
+
|
26
|
+
# local listener
|
27
|
+
publisher.subscribe(local_listener)
|
28
|
+
|
29
|
+
expect(global_listener).to receive(:it_happened)
|
30
|
+
expect(local_listener).to receive(:it_happened)
|
31
|
+
|
32
|
+
publisher.send(:broadcast, :it_happened)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'can be scoped to classes' do
|
36
|
+
publisher_1 = publisher_class.new
|
37
|
+
publisher_2 = publisher_class.new
|
38
|
+
publisher_3 = publisher_class.new
|
39
|
+
|
40
|
+
Wisper::GlobalListeners.subscribe(global_listener, :scope => [publisher_1.class,
|
41
|
+
publisher_2.class])
|
42
|
+
|
43
|
+
expect(global_listener).to receive(:it_happened_1).once
|
44
|
+
expect(global_listener).to receive(:it_happened_2).once
|
45
|
+
expect(global_listener).not_to receive(:it_happened_3)
|
46
|
+
|
47
|
+
publisher_1.send(:broadcast, :it_happened_1)
|
48
|
+
publisher_2.send(:broadcast, :it_happened_2)
|
49
|
+
publisher_3.send(:broadcast, :it_happened_3)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'is threadsafe' do
|
53
|
+
num_threads = 100
|
54
|
+
(1..num_threads).to_a.map do
|
55
|
+
Thread.new do
|
56
|
+
Wisper::GlobalListeners.subscribe(Object.new)
|
57
|
+
sleep(rand) # a little chaos
|
58
|
+
end
|
59
|
+
end.each(&:join)
|
60
|
+
|
61
|
+
expect(Wisper::GlobalListeners.listeners.size).to eq num_threads
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '.listeners' do
|
66
|
+
it 'returns collection of global listeners' do
|
67
|
+
Wisper::GlobalListeners.subscribe(global_listener)
|
68
|
+
expect(Wisper::GlobalListeners.listeners).to eq [global_listener]
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'returns an immutable collection' do
|
72
|
+
expect(Wisper::GlobalListeners.listeners).to be_frozen
|
73
|
+
expect { Wisper::GlobalListeners.listeners << global_listener }.to raise_error(RuntimeError)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it '.clear clears all global listeners' do
|
78
|
+
Wisper::GlobalListeners.subscribe(global_listener)
|
79
|
+
Wisper::GlobalListeners.clear
|
80
|
+
expect(Wisper::GlobalListeners.listeners).to be_empty
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Example
|
2
|
+
class MyCommand
|
3
|
+
include Wisper::Publisher
|
4
|
+
|
5
|
+
def execute(be_successful)
|
6
|
+
if be_successful
|
7
|
+
broadcast('success', 'hello')
|
8
|
+
else
|
9
|
+
broadcast('failure', 'world')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe Wisper do
|
15
|
+
|
16
|
+
it 'subscribes object to all published events' do
|
17
|
+
listener = double('listener')
|
18
|
+
expect(listener).to receive(:success).with('hello')
|
19
|
+
|
20
|
+
command = MyCommand.new
|
21
|
+
|
22
|
+
command.subscribe(listener)
|
23
|
+
|
24
|
+
command.execute(true)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'maps events to different methods' do
|
28
|
+
listener_1 = double('listener')
|
29
|
+
listener_2 = double('listener')
|
30
|
+
expect(listener_1).to receive(:happy_days).with('hello')
|
31
|
+
expect(listener_2).to receive(:sad_days).with('world')
|
32
|
+
|
33
|
+
command = MyCommand.new
|
34
|
+
|
35
|
+
command.subscribe(listener_1, :on => :success, :with => :happy_days)
|
36
|
+
command.subscribe(listener_2, :on => :failure, :with => :sad_days)
|
37
|
+
|
38
|
+
command.execute(true)
|
39
|
+
command.execute(false)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'subscribes block can be chained' do
|
43
|
+
insider = double('Insider')
|
44
|
+
|
45
|
+
expect(insider).to receive(:render).with('success')
|
46
|
+
expect(insider).to receive(:render).with('failure')
|
47
|
+
|
48
|
+
command = MyCommand.new
|
49
|
+
|
50
|
+
command.on(:success) { |message| insider.render('success') }
|
51
|
+
.on(:failure) { |message| insider.render('failure') }
|
52
|
+
|
53
|
+
command.execute(true)
|
54
|
+
command.execute(false)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class MyPublisher
|
2
|
+
include Wisper::Publisher
|
3
|
+
|
4
|
+
def do_something
|
5
|
+
# ...
|
6
|
+
broadcast(:bar, self)
|
7
|
+
broadcast(:foo, self)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'simple publishing' do
|
12
|
+
it 'subscribes listener to events' do
|
13
|
+
listener = double('listener')
|
14
|
+
expect(listener).to receive(:foo).with instance_of MyPublisher
|
15
|
+
expect(listener).to receive(:bar).with instance_of MyPublisher
|
16
|
+
|
17
|
+
my_publisher = MyPublisher.new
|
18
|
+
my_publisher.subscribe(listener)
|
19
|
+
my_publisher.do_something
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
describe Wisper::TemporaryListeners do
|
2
|
+
let(:listener_1) { double('listener', :to_a => nil) } # [1]
|
3
|
+
let(:listener_2) { double('listener', :to_a => nil) }
|
4
|
+
|
5
|
+
let(:publisher) { publisher_class.new }
|
6
|
+
|
7
|
+
describe '.subscribe' do
|
8
|
+
it 'globally subscribes listener for duration of given block' do
|
9
|
+
|
10
|
+
expect(listener_1).to receive(:success)
|
11
|
+
expect(listener_1).to_not receive(:failure)
|
12
|
+
|
13
|
+
Wisper::TemporaryListeners.subscribe(listener_1) do
|
14
|
+
publisher.instance_eval { broadcast(:success) }
|
15
|
+
end
|
16
|
+
|
17
|
+
publisher.instance_eval { broadcast(:failure) }
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'globally subscribes listeners for duration of given block' do
|
21
|
+
|
22
|
+
expect(listener_1).to receive(:success)
|
23
|
+
expect(listener_1).to_not receive(:failure)
|
24
|
+
|
25
|
+
expect(listener_2).to receive(:success)
|
26
|
+
expect(listener_2).to_not receive(:failure)
|
27
|
+
|
28
|
+
Wisper::TemporaryListeners.subscribe(listener_1, listener_2) do
|
29
|
+
publisher.instance_eval { broadcast(:success) }
|
30
|
+
end
|
31
|
+
|
32
|
+
publisher.instance_eval { broadcast(:failure) }
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'is thread safe' do
|
36
|
+
num_threads = 20
|
37
|
+
(1..num_threads).to_a.map do
|
38
|
+
Thread.new do
|
39
|
+
Wisper::TemporaryListeners.registrations << Object.new
|
40
|
+
expect(Wisper::TemporaryListeners.registrations.size).to eq 1
|
41
|
+
end
|
42
|
+
end.each(&:join)
|
43
|
+
|
44
|
+
expect(Wisper::TemporaryListeners.registrations).to be_empty
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'clears registrations when an exception occurs' do
|
48
|
+
MyError = Class.new(StandardError)
|
49
|
+
|
50
|
+
begin
|
51
|
+
Wisper::TemporaryListeners.subscribe(listener_1) do
|
52
|
+
raise MyError
|
53
|
+
end
|
54
|
+
rescue MyError
|
55
|
+
end
|
56
|
+
|
57
|
+
expect(Wisper::TemporaryListeners.registrations).to be_empty
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'returns self' do
|
61
|
+
expect(Wisper::TemporaryListeners.subscribe {}).to be_an_instance_of(Wisper::TemporaryListeners)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# [1] stubbing `to_a` prevents `Double "listener" received unexpected message
|
67
|
+
# :to_a with (no args)` on MRI 1.9.2 when a double is passed to `Array()`.
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Wisper
|
2
|
+
module Broadcasters
|
3
|
+
|
4
|
+
describe LoggerBroadcaster do
|
5
|
+
|
6
|
+
describe 'integration tests:' do
|
7
|
+
let(:publisher) { publisher_class.new }
|
8
|
+
let(:listener) { double }
|
9
|
+
let(:logger) { double.as_null_object }
|
10
|
+
|
11
|
+
it 'broadcasts the event to the listener' do
|
12
|
+
publisher.subscribe(listener, :broadcaster => LoggerBroadcaster.new(logger, Wisper::Broadcasters::SendBroadcaster.new))
|
13
|
+
expect(listener).to receive(:it_happened).with(1, 2)
|
14
|
+
publisher.send(:broadcast, :it_happened, 1, 2)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'unit tests:' do
|
19
|
+
let(:publisher) { classy_double('Publisher', id: 1) }
|
20
|
+
let(:listener) { classy_double('Listener', id: 2) }
|
21
|
+
let(:logger) { double('Logger').as_null_object }
|
22
|
+
let(:broadcaster) { double('Broadcaster').as_null_object }
|
23
|
+
let(:event) { 'thing_created' }
|
24
|
+
|
25
|
+
subject { LoggerBroadcaster.new(logger, broadcaster) }
|
26
|
+
|
27
|
+
describe '#broadcast' do
|
28
|
+
context 'without arguments' do
|
29
|
+
let(:args) { [] }
|
30
|
+
|
31
|
+
it 'logs published event' do
|
32
|
+
expect(logger).to receive(:info).with('[WISPER] Publisher#1 published thing_created to Listener#2 with no arguments')
|
33
|
+
subject.broadcast(listener, publisher, event, args)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'delegates broadcast to a given broadcaster' do
|
37
|
+
expect(broadcaster).to receive(:broadcast).with(listener, publisher, event, args)
|
38
|
+
subject.broadcast(listener, publisher, event, args)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'with arguments' do
|
43
|
+
let(:args) { [arg_double(id: 3), arg_double(id: 4)] }
|
44
|
+
|
45
|
+
it 'logs published event and arguments' do
|
46
|
+
expect(logger).to receive(:info).with('[WISPER] Publisher#1 published thing_created to Listener#2 with Argument#3, Argument#4')
|
47
|
+
subject.broadcast(listener, publisher, event, args)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'delegates broadcast to a given broadcaster' do
|
51
|
+
expect(broadcaster).to receive(:broadcast).with(listener, publisher, event, args)
|
52
|
+
subject.broadcast(listener, publisher, event, args)
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when argument is a hash' do
|
56
|
+
let(:args) { [hash] }
|
57
|
+
let(:hash) { {key: 'value'} }
|
58
|
+
|
59
|
+
it 'logs published event and arguments' do
|
60
|
+
expect(logger).to receive(:info).with("[WISPER] Publisher#1 published thing_created to Listener#2 with Hash##{hash.object_id}: #{hash.inspect}")
|
61
|
+
subject.broadcast(listener, publisher, event, args)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'when argument is an integer' do
|
66
|
+
let(:args) { [number] }
|
67
|
+
let(:number) { 10 }
|
68
|
+
|
69
|
+
it 'logs published event and arguments' do
|
70
|
+
expect(logger).to receive(:info).with("[WISPER] Publisher#1 published thing_created to Listener#2 with #{number.class.name}##{number.object_id}: 10")
|
71
|
+
subject.broadcast(listener, publisher, event, args)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
# provides a way to specify `double.class.name` easily
|
79
|
+
def classy_double(klass, options)
|
80
|
+
double(klass, options.merge(class: double_class(klass)))
|
81
|
+
end
|
82
|
+
|
83
|
+
def arg_double(options)
|
84
|
+
classy_double('Argument', options)
|
85
|
+
end
|
86
|
+
|
87
|
+
def double_class(name)
|
88
|
+
double(name: name)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|