siftery-wisper 2.0.1
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 +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
|