composable_state_machine 1.0.2
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 +15 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.simplecov +4 -0
- data/.travis.yml +8 -0
- data/.yardopts +4 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +352 -0
- data/Rakefile +19 -0
- data/assets/class-diagram.yuml +24 -0
- data/assets/uml-class-diagram.png +0 -0
- data/composable_state_machine.gemspec +35 -0
- data/lib/composable_state_machine.rb +45 -0
- data/lib/composable_state_machine/behaviors.rb +48 -0
- data/lib/composable_state_machine/callback_runner.rb +19 -0
- data/lib/composable_state_machine/callbacks.rb +56 -0
- data/lib/composable_state_machine/default_callback_runner.rb +16 -0
- data/lib/composable_state_machine/invalid_event.rb +7 -0
- data/lib/composable_state_machine/invalid_transition.rb +7 -0
- data/lib/composable_state_machine/invalid_trigger.rb +7 -0
- data/lib/composable_state_machine/machine.rb +21 -0
- data/lib/composable_state_machine/machine_with_external_state.rb +41 -0
- data/lib/composable_state_machine/model.rb +55 -0
- data/lib/composable_state_machine/transitions.rb +73 -0
- data/lib/composable_state_machine/version.rb +3 -0
- data/spec/integration/auto_update_state_spec.rb +38 -0
- data/spec/integration/instance_callbacks_spec.rb +47 -0
- data/spec/integration/leave_callbacks_spec.rb +60 -0
- data/spec/integration/leave_callbacks_with_composition_spec.rb +68 -0
- data/spec/lib/composable_state_machine/behaviors_spec.rb +83 -0
- data/spec/lib/composable_state_machine/callback_runner_spec.rb +54 -0
- data/spec/lib/composable_state_machine/callbacks_spec.rb +106 -0
- data/spec/lib/composable_state_machine/machine_spec.rb +25 -0
- data/spec/lib/composable_state_machine/machine_with_external_state_spec.rb +97 -0
- data/spec/lib/composable_state_machine/model_spec.rb +76 -0
- data/spec/lib/composable_state_machine/transitions_spec.rb +77 -0
- data/spec/lib/composable_state_machine_spec.rb +53 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/delegation.rb +196 -0
- metadata +218 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ComposableStateMachine::Machine do
|
4
|
+
|
5
|
+
describe '#initialization' do
|
6
|
+
it 'can set the initial state' do
|
7
|
+
model = double(callback_runner: proc {})
|
8
|
+
machine = described_class.new(model, state: :test_state)
|
9
|
+
|
10
|
+
machine.should be_kind_of ComposableStateMachine::MachineWithExternalState
|
11
|
+
machine.state.should eq :test_state
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'delegates to MachineWithExternalState hooking up #state updates' do
|
15
|
+
model = ComposableStateMachine.model(
|
16
|
+
initial_state: :first, transitions: {next: {first: :second}})
|
17
|
+
machine = described_class.new(model)
|
18
|
+
|
19
|
+
machine.state.should eq :first
|
20
|
+
machine.trigger(:next).should eq :second
|
21
|
+
machine.state.should eq :second
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ComposableStateMachine::MachineWithExternalState do
|
4
|
+
|
5
|
+
let(:transitions) do
|
6
|
+
ComposableStateMachine::Transitions.new(
|
7
|
+
{
|
8
|
+
hire: {candidate: :hired},
|
9
|
+
leave: {hired: :departed},
|
10
|
+
fire: {hired: :fired},
|
11
|
+
}
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:model) do
|
16
|
+
ComposableStateMachine::Model.new(
|
17
|
+
initial_state: :candidate,
|
18
|
+
transitions: transitions
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:model_with_behavior) do
|
23
|
+
ComposableStateMachine::Model.new(
|
24
|
+
initial_state: :candidate,
|
25
|
+
transitions: transitions,
|
26
|
+
behaviors: ComposableStateMachine::Behaviors.new(
|
27
|
+
{
|
28
|
+
enter: {hired: proc {}}
|
29
|
+
}
|
30
|
+
)
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
before do
|
35
|
+
@state = nil
|
36
|
+
@state_reader = lambda { @state }
|
37
|
+
@state_writer = lambda { |new_state| @state = new_state }
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#initialization' do
|
41
|
+
it 'sets the state to the initial state of the model by default' do
|
42
|
+
described_class.new(model, @state_reader, @state_writer)
|
43
|
+
|
44
|
+
@state.should eq :candidate
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'can set the initial state' do
|
48
|
+
described_class.new(model, @state_reader, @state_writer, state: :hired)
|
49
|
+
|
50
|
+
@state.should eq :hired
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'can specify the callback runner' do
|
54
|
+
runner = double(run_state_machine_callback: nil)
|
55
|
+
machine = described_class.new(model_with_behavior, @state_reader, @state_writer,
|
56
|
+
callback_runner: runner, state: :candidate)
|
57
|
+
|
58
|
+
runner.should_receive(:run_state_machine_callback).with(an_instance_of(Proc), :candidate, :hire, :hired)
|
59
|
+
|
60
|
+
machine.trigger(:hire)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'main API' do
|
65
|
+
subject { described_class.new(model, @state_reader, @state_writer) }
|
66
|
+
|
67
|
+
describe '#trigger' do
|
68
|
+
it 'transitions via the model' do
|
69
|
+
model.should_receive(:transition).with(
|
70
|
+
:candidate, :hire, [], ComposableStateMachine::DefaultCallbackRunner).
|
71
|
+
and_call_original
|
72
|
+
|
73
|
+
subject.trigger(:hire).should eq :hired
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'updates the state when a transition is made' do
|
77
|
+
subject.trigger(:hire)
|
78
|
+
|
79
|
+
@state.should eq :hired
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'does not update the state when a transition is not made' do
|
83
|
+
subject.trigger(:hire)
|
84
|
+
|
85
|
+
subject.trigger(:hire).should be_nil
|
86
|
+
@state.should eq :hired
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#==' do
|
91
|
+
it 'compares equality based on the state' do
|
92
|
+
subject.should == :candidate
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ComposableStateMachine::Model do
|
4
|
+
|
5
|
+
let(:transition_map) do
|
6
|
+
{
|
7
|
+
heat: {
|
8
|
+
cold: :warm,
|
9
|
+
warm: :hot,
|
10
|
+
hot: :hot,
|
11
|
+
},
|
12
|
+
cool: {
|
13
|
+
cold: :cold,
|
14
|
+
warm: :cold,
|
15
|
+
hot: :warm
|
16
|
+
}
|
17
|
+
}
|
18
|
+
end
|
19
|
+
let(:transitions) { ComposableStateMachine::Transitions.new(transition_map) }
|
20
|
+
let(:behaviors) { double(call: nil) }
|
21
|
+
|
22
|
+
subject do
|
23
|
+
described_class.new(initial_state: :cold,
|
24
|
+
transitions: transitions, behaviors: behaviors)
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#initial_state' do
|
28
|
+
it 'stores the initial state of the model' do
|
29
|
+
subject.initial_state.should eq :cold
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#transition' do
|
34
|
+
|
35
|
+
it 'forwards to transitions' do
|
36
|
+
transitions.should_receive(:transition).with(:cold, :heat)
|
37
|
+
|
38
|
+
subject.transition(:cold, :heat)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'executes :enter behaviors with the new state and the event to behaviors' do
|
42
|
+
behaviors.should_receive(:call).with(
|
43
|
+
ComposableStateMachine::DefaultCallbackRunner,
|
44
|
+
:enter, :warm, :cold, :heat, :warm)
|
45
|
+
|
46
|
+
subject.transition(:cold, :heat).should eq :warm
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'yields the new state on state change' do
|
50
|
+
expect do |b|
|
51
|
+
subject.transition(:cold, :heat, &b)
|
52
|
+
end.to yield_with_args(:warm)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'does not execute :enter behaviors when the state does not transition' do
|
56
|
+
behaviors.should_not_receive(:call)
|
57
|
+
|
58
|
+
subject.transition(:cold, :cool).should eq :cold
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'does not yield if there is no state change' do
|
62
|
+
expect do |b|
|
63
|
+
subject.transition(:cold, :cool, &b)
|
64
|
+
end.not_to yield_control
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'can send optional event arguments and callback runner' do
|
68
|
+
runner = double
|
69
|
+
|
70
|
+
behaviors.should_receive(:call).with(runner, :enter, :warm, :cold, :heat, :warm, 1, 2, 3)
|
71
|
+
|
72
|
+
subject.transition(:cold, :heat, [1, 2, 3], runner).should eq :warm
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ComposableStateMachine::Transitions do
|
4
|
+
|
5
|
+
let(:transition_map) do
|
6
|
+
{
|
7
|
+
add: {
|
8
|
+
nil => :created
|
9
|
+
},
|
10
|
+
update: {
|
11
|
+
created: :updated,
|
12
|
+
updated: :updated,
|
13
|
+
},
|
14
|
+
remove: {
|
15
|
+
created: :removed,
|
16
|
+
updated: :removed,
|
17
|
+
}
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
subject { described_class.new(transition_map) }
|
22
|
+
|
23
|
+
describe '#events' do
|
24
|
+
it 'returns all events' do
|
25
|
+
subject.events == [:add, :update, :remove]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#states' do
|
30
|
+
it 'returns all states' do
|
31
|
+
subject.states =~ [nil, :created, :updated, :removed]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#on' do
|
36
|
+
it 'adds to the transition map' do
|
37
|
+
subject.
|
38
|
+
on(:remove, nil => :error).
|
39
|
+
on(:restore, :removed => :updated)
|
40
|
+
|
41
|
+
subject.events.should eq [:add, :update, :remove, :restore]
|
42
|
+
subject.states =~ [nil, :created, :updated, :removed, :error]
|
43
|
+
|
44
|
+
subject.transition(nil, :add).should eq :created
|
45
|
+
subject.transition(nil, :remove).should eq :error
|
46
|
+
subject.transition(:removed, :restore).should eq :updated
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'raises InvalidTransition on transitions to nil' do
|
50
|
+
expect do
|
51
|
+
subject.on(:expunge, created: nil)
|
52
|
+
end.to raise_error ComposableStateMachine::InvalidTransition
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#transition' do
|
57
|
+
it 'raises InvalidEvent for unknown events' do
|
58
|
+
expect do
|
59
|
+
subject.transition(:updated, :unknown)
|
60
|
+
end.to raise_error ComposableStateMachine::InvalidEvent
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'returns the state to transition to when possible' do
|
64
|
+
subject.transition(nil, :add).should eq :created
|
65
|
+
subject.transition(:created, :update).should eq :updated
|
66
|
+
subject.transition(:updated, :update).should eq :updated
|
67
|
+
subject.transition(:created, :remove).should eq :removed
|
68
|
+
subject.transition(:updated, :remove).should eq :removed
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'returns nil if no transition is possible' do
|
72
|
+
subject.transition(nil, :remove).should be_nil
|
73
|
+
subject.transition(:created, :add).should be_nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ComposableStateMachine do
|
4
|
+
|
5
|
+
let(:options) do
|
6
|
+
{
|
7
|
+
initial_state: :foo,
|
8
|
+
transitions: ComposableStateMachine::Transitions.new({}),
|
9
|
+
behaviors: ComposableStateMachine::Behaviors.new({}),
|
10
|
+
callback_runner: proc {}
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '.model' do
|
15
|
+
it 'creates a model' do
|
16
|
+
ComposableStateMachine::Model.should_receive(:new).with(options).and_call_original
|
17
|
+
|
18
|
+
result = described_class.model(options)
|
19
|
+
result.should be_kind_of(ComposableStateMachine::Model)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'creates a Transitions object if necessary' do
|
23
|
+
options[:transitions] = {}
|
24
|
+
|
25
|
+
ComposableStateMachine::Model.should_receive(:new) do |options|
|
26
|
+
options[:transitions].should be_kind_of(ComposableStateMachine::Transitions)
|
27
|
+
end
|
28
|
+
|
29
|
+
described_class.model(options)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'creates a Behaviors object if necessary' do
|
33
|
+
options[:behaviors] = {}
|
34
|
+
|
35
|
+
ComposableStateMachine::Model.should_receive(:new) do |options|
|
36
|
+
options[:behaviors].should be_kind_of(ComposableStateMachine::Behaviors)
|
37
|
+
end
|
38
|
+
|
39
|
+
described_class.model(options)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '.machine' do
|
44
|
+
it 'creates a machine from a model' do
|
45
|
+
model = described_class.model(options)
|
46
|
+
|
47
|
+
ComposableStateMachine::Machine.should_receive(:new).with(model, initial_state: :bar).and_call_original
|
48
|
+
|
49
|
+
result = described_class.machine(model, initial_state: :bar)
|
50
|
+
result.should be_kind_of(ComposableStateMachine::Machine)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
SimpleCov.start
|
3
|
+
SimpleCov.minimum_coverage 100
|
4
|
+
|
5
|
+
if ENV['TRAVIS']
|
6
|
+
require 'coveralls'
|
7
|
+
Coveralls.wear!
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'pp'
|
11
|
+
|
12
|
+
require 'composable_state_machine'
|
13
|
+
|
14
|
+
Dir['spec/support/**/*.rb'].each { |f| require File.expand_path(f) }
|
@@ -0,0 +1,196 @@
|
|
1
|
+
class Module
|
2
|
+
# Provides a +delegate+ class method to easily expose contained objects'
|
3
|
+
# public methods as your own.
|
4
|
+
#
|
5
|
+
# The macro receives one or more method names (specified as symbols or
|
6
|
+
# strings) and the name of the target object via the <tt>:to</tt> option
|
7
|
+
# (also a symbol or string).
|
8
|
+
#
|
9
|
+
# Delegation is particularly useful with Active Record associations:
|
10
|
+
#
|
11
|
+
# class Greeter < ActiveRecord::Base
|
12
|
+
# def hello
|
13
|
+
# 'hello'
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# def goodbye
|
17
|
+
# 'goodbye'
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# class Foo < ActiveRecord::Base
|
22
|
+
# belongs_to :greeter
|
23
|
+
# delegate :hello, to: :greeter
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# Foo.new.hello # => "hello"
|
27
|
+
# Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
|
28
|
+
#
|
29
|
+
# Multiple delegates to the same target are allowed:
|
30
|
+
#
|
31
|
+
# class Foo < ActiveRecord::Base
|
32
|
+
# belongs_to :greeter
|
33
|
+
# delegate :hello, :goodbye, to: :greeter
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# Foo.new.goodbye # => "goodbye"
|
37
|
+
#
|
38
|
+
# Methods can be delegated to instance variables, class variables, or constants
|
39
|
+
# by providing them as a symbols:
|
40
|
+
#
|
41
|
+
# class Foo
|
42
|
+
# CONSTANT_ARRAY = [0,1,2,3]
|
43
|
+
# @@class_array = [4,5,6,7]
|
44
|
+
#
|
45
|
+
# def initialize
|
46
|
+
# @instance_array = [8,9,10,11]
|
47
|
+
# end
|
48
|
+
# delegate :sum, to: :CONSTANT_ARRAY
|
49
|
+
# delegate :min, to: :@@class_array
|
50
|
+
# delegate :max, to: :@instance_array
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# Foo.new.sum # => 6
|
54
|
+
# Foo.new.min # => 4
|
55
|
+
# Foo.new.max # => 11
|
56
|
+
#
|
57
|
+
# It's also possible to delegate a method to the class by using +:class+:
|
58
|
+
#
|
59
|
+
# class Foo
|
60
|
+
# def self.hello
|
61
|
+
# "world"
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# delegate :hello, to: :class
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# Foo.new.hello # => "world"
|
68
|
+
#
|
69
|
+
# Delegates can optionally be prefixed using the <tt>:prefix</tt> option. If the value
|
70
|
+
# is <tt>true</tt>, the delegate methods are prefixed with the name of the object being
|
71
|
+
# delegated to.
|
72
|
+
#
|
73
|
+
# Person = Struct.new(:name, :address)
|
74
|
+
#
|
75
|
+
# class Invoice < Struct.new(:client)
|
76
|
+
# delegate :name, :address, to: :client, prefix: true
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# john_doe = Person.new('John Doe', 'Vimmersvej 13')
|
80
|
+
# invoice = Invoice.new(john_doe)
|
81
|
+
# invoice.client_name # => "John Doe"
|
82
|
+
# invoice.client_address # => "Vimmersvej 13"
|
83
|
+
#
|
84
|
+
# It is also possible to supply a custom prefix.
|
85
|
+
#
|
86
|
+
# class Invoice < Struct.new(:client)
|
87
|
+
# delegate :name, :address, to: :client, prefix: :customer
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# invoice = Invoice.new(john_doe)
|
91
|
+
# invoice.customer_name # => 'John Doe'
|
92
|
+
# invoice.customer_address # => 'Vimmersvej 13'
|
93
|
+
#
|
94
|
+
# If the target is +nil+ and does not respond to the delegated method a
|
95
|
+
# +NoMethodError+ is raised, as with any other value. Sometimes, however, it
|
96
|
+
# makes sense to be robust to that situation and that is the purpose of the
|
97
|
+
# <tt>:allow_nil</tt> option: If the target is not +nil+, or it is and
|
98
|
+
# responds to the method, everything works as usual. But if it is +nil+ and
|
99
|
+
# does not respond to the delegated method, +nil+ is returned.
|
100
|
+
#
|
101
|
+
# class User < ActiveRecord::Base
|
102
|
+
# has_one :profile
|
103
|
+
# delegate :age, to: :profile
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# User.new.age # raises NoMethodError: undefined method `age'
|
107
|
+
#
|
108
|
+
# But if not having a profile yet is fine and should not be an error
|
109
|
+
# condition:
|
110
|
+
#
|
111
|
+
# class User < ActiveRecord::Base
|
112
|
+
# has_one :profile
|
113
|
+
# delegate :age, to: :profile, allow_nil: true
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# User.new.age # nil
|
117
|
+
#
|
118
|
+
# Note that if the target is not +nil+ then the call is attempted regardless of the
|
119
|
+
# <tt>:allow_nil</tt> option, and thus an exception is still raised if said object
|
120
|
+
# does not respond to the method:
|
121
|
+
#
|
122
|
+
# class Foo
|
123
|
+
# def initialize(bar)
|
124
|
+
# @bar = bar
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# delegate :name, to: :@bar, allow_nil: true
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# Foo.new("Bar").name # raises NoMethodError: undefined method `name'
|
131
|
+
#
|
132
|
+
def delegate(*methods)
|
133
|
+
options = methods.pop
|
134
|
+
unless options.is_a?(Hash) && to = options[:to]
|
135
|
+
raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
|
136
|
+
end
|
137
|
+
|
138
|
+
prefix, allow_nil = options.values_at(:prefix, :allow_nil)
|
139
|
+
|
140
|
+
if prefix == true && to =~ /^[^a-z_]/
|
141
|
+
raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.'
|
142
|
+
end
|
143
|
+
|
144
|
+
method_prefix = \
|
145
|
+
if prefix
|
146
|
+
"#{prefix == true ? to : prefix}_"
|
147
|
+
else
|
148
|
+
''
|
149
|
+
end
|
150
|
+
|
151
|
+
file, line = caller.first.split(':', 2)
|
152
|
+
line = line.to_i
|
153
|
+
|
154
|
+
to = to.to_s
|
155
|
+
to = 'self.class' if to == 'class'
|
156
|
+
|
157
|
+
methods.each do |method|
|
158
|
+
# Attribute writer methods only accept one argument. Makes sure []=
|
159
|
+
# methods still accept two arguments.
|
160
|
+
definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
|
161
|
+
|
162
|
+
# The following generated methods call the target exactly once, storing
|
163
|
+
# the returned value in a dummy variable.
|
164
|
+
#
|
165
|
+
# Reason is twofold: On one hand doing less calls is in general better.
|
166
|
+
# On the other hand it could be that the target has side-effects,
|
167
|
+
# whereas conceptualy, from the user point of view, the delegator should
|
168
|
+
# be doing one call.
|
169
|
+
if allow_nil
|
170
|
+
module_eval(<<-EOS, file, line - 3)
|
171
|
+
def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
|
172
|
+
_ = #{to} # _ = client
|
173
|
+
if !_.nil? || nil.respond_to?(:#{method}) # if !_.nil? || nil.respond_to?(:name)
|
174
|
+
_.#{method}(#{definition}) # _.name(*args, &block)
|
175
|
+
end # end
|
176
|
+
end # end
|
177
|
+
EOS
|
178
|
+
else
|
179
|
+
exception = %(raise "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
|
180
|
+
|
181
|
+
module_eval(<<-EOS, file, line - 2)
|
182
|
+
def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
|
183
|
+
_ = #{to} # _ = client
|
184
|
+
_.#{method}(#{definition}) # _.name(*args, &block)
|
185
|
+
rescue NoMethodError # rescue NoMethodError
|
186
|
+
if _.nil? # if _.nil?
|
187
|
+
#{exception} # # add helpful message to the exception
|
188
|
+
else # else
|
189
|
+
raise # raise
|
190
|
+
end # end
|
191
|
+
end # end
|
192
|
+
EOS
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|