davidlee-state-fu 0.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.
- data/LICENSE +40 -0
- data/README.textile +174 -0
- data/Rakefile +87 -0
- data/lib/no_stdout.rb +32 -0
- data/lib/state-fu.rb +93 -0
- data/lib/state_fu/binding.rb +262 -0
- data/lib/state_fu/core_ext.rb +23 -0
- data/lib/state_fu/event.rb +98 -0
- data/lib/state_fu/exceptions.rb +42 -0
- data/lib/state_fu/fu_space.rb +50 -0
- data/lib/state_fu/helper.rb +189 -0
- data/lib/state_fu/hooks.rb +28 -0
- data/lib/state_fu/interface.rb +139 -0
- data/lib/state_fu/lathe.rb +247 -0
- data/lib/state_fu/logger.rb +10 -0
- data/lib/state_fu/machine.rb +159 -0
- data/lib/state_fu/method_factory.rb +95 -0
- data/lib/state_fu/persistence/active_record.rb +27 -0
- data/lib/state_fu/persistence/attribute.rb +46 -0
- data/lib/state_fu/persistence/base.rb +98 -0
- data/lib/state_fu/persistence/session.rb +7 -0
- data/lib/state_fu/persistence.rb +50 -0
- data/lib/state_fu/sprocket.rb +27 -0
- data/lib/state_fu/state.rb +45 -0
- data/lib/state_fu/transition.rb +213 -0
- data/spec/helper.rb +86 -0
- data/spec/integration/active_record_persistence_spec.rb +189 -0
- data/spec/integration/class_accessor_spec.rb +127 -0
- data/spec/integration/event_definition_spec.rb +74 -0
- data/spec/integration/ex_machine_for_accounts_spec.rb +79 -0
- data/spec/integration/example_01_document_spec.rb +127 -0
- data/spec/integration/example_02_string_spec.rb +87 -0
- data/spec/integration/instance_accessor_spec.rb +100 -0
- data/spec/integration/machine_duplication_spec.rb +95 -0
- data/spec/integration/requirement_reflection_spec.rb +201 -0
- data/spec/integration/sanity_spec.rb +31 -0
- data/spec/integration/state_definition_spec.rb +177 -0
- data/spec/integration/transition_spec.rb +1060 -0
- data/spec/spec.opts +7 -0
- data/spec/units/binding_spec.rb +145 -0
- data/spec/units/event_spec.rb +232 -0
- data/spec/units/exceptions_spec.rb +75 -0
- data/spec/units/fu_space_spec.rb +95 -0
- data/spec/units/lathe_spec.rb +567 -0
- data/spec/units/machine_spec.rb +237 -0
- data/spec/units/method_factory_spec.rb +359 -0
- data/spec/units/sprocket_spec.rb +71 -0
- data/spec/units/state_spec.rb +50 -0
- metadata +122 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../helper")
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
describe "Transition requirement reflection" do
|
|
5
|
+
include MySpecHelper
|
|
6
|
+
|
|
7
|
+
before do
|
|
8
|
+
reset!
|
|
9
|
+
make_pristine_class("Klass")
|
|
10
|
+
@machine = Klass.machine do
|
|
11
|
+
state :soviet_russia do
|
|
12
|
+
requires( :papers_in_order?,
|
|
13
|
+
:money_for_bribe?, :on => [:entry, :exit] )
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
state :america do
|
|
17
|
+
requires( :no_turban?,
|
|
18
|
+
:us_visa?,
|
|
19
|
+
:on => :entry )
|
|
20
|
+
requires( :no_arrest_warrant, :on => [:entry,:exit] )
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
state :moon do
|
|
24
|
+
requires :spacesuit?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
event( :catch_plane,
|
|
28
|
+
:from => states.except(:moon),
|
|
29
|
+
:to => states.except(:moon) ) do
|
|
30
|
+
requires :plane_ticket?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
event( :fly_spaceship,
|
|
34
|
+
:from => :ALL,
|
|
35
|
+
:to => :ALL ) do
|
|
36
|
+
requires :fuel?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end # machine
|
|
40
|
+
@obj = Klass.new()
|
|
41
|
+
stub( @obj ).papers_in_order? { true }
|
|
42
|
+
stub( @obj ).money_for_bribe? { true }
|
|
43
|
+
stub( @obj ).no_turban? { true }
|
|
44
|
+
stub( @obj ).no_arrest_warrant? { true }
|
|
45
|
+
stub( @obj ).spacesuit? { true }
|
|
46
|
+
stub( @obj ).plane_ticket? { true }
|
|
47
|
+
stub( @obj ).fuel? { true }
|
|
48
|
+
end # before
|
|
49
|
+
|
|
50
|
+
describe "transition.valid? / transition.requirements_met?" do
|
|
51
|
+
it "should be true if all requirements are met (return truth)" do
|
|
52
|
+
@obj.state_fu.next_states[:moon].entry_requirements.should == [:spacesuit?]
|
|
53
|
+
@obj.state_fu.evaluate_requirement(:spacesuit?).should == true
|
|
54
|
+
@obj.fly_spaceship?(:moon).should == true
|
|
55
|
+
@obj.fly_spaceship(:moon).requirements_met?.should == true
|
|
56
|
+
@obj.fly_spaceship(:moon).should be_valid
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "should be false if not all requirements are met" do
|
|
60
|
+
stub( @obj ).spacesuit?() { false }
|
|
61
|
+
@obj.state_fu.next_states[:moon].entry_requirements.should == [:spacesuit?]
|
|
62
|
+
@obj.state_fu.evaluate_requirement(:spacesuit?).should == false
|
|
63
|
+
@obj.fly_spaceship?(:moon).should == false
|
|
64
|
+
@obj.fly_spaceship(:moon).requirements_met?.should == false
|
|
65
|
+
@obj.fly_spaceship(:moon).should_not be_valid
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe "transition.unmet_requirements" do
|
|
70
|
+
it "should be empty when all requirements are met" do
|
|
71
|
+
@obj.state_fu.fly_spaceship(:moon).unmet_requirements.should == []
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "should contain a list of failing requirements" do
|
|
75
|
+
mock( @obj ).spacesuit?() { false }
|
|
76
|
+
mock( @obj ).fuel?() { false }
|
|
77
|
+
@obj.state_fu.fly_spaceship(:moon).unmet_requirements.should == [:spacesuit?, :fuel?]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe "transition.unmet_requirement_messages" do
|
|
82
|
+
describe "when a string message is defined for one of two unmet_requirements" do
|
|
83
|
+
before do
|
|
84
|
+
stub( @obj ).spacesuit?() { false }
|
|
85
|
+
stub( @obj ).fuel?() { false }
|
|
86
|
+
@msg = "You got no spacesuit."
|
|
87
|
+
@machine.requirement_messages[:spacesuit?] = @msg
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "should return an array with the requirement message and nil" do
|
|
91
|
+
t = @obj.state_fu.fly_spaceship(:moon)
|
|
92
|
+
t.unmet_requirements.length.should == 2
|
|
93
|
+
messages = t.unmet_requirement_messages
|
|
94
|
+
messages.should be_kind_of( Array )
|
|
95
|
+
messages.length.should == 2
|
|
96
|
+
messages.compact.length.should == 1
|
|
97
|
+
messages.compact.first.should be_kind_of( String )
|
|
98
|
+
messages.compact.first.should == @msg
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe "when a proc message is defined for one of two unmet_requirements" do
|
|
103
|
+
before do
|
|
104
|
+
stub( @obj ).spacesuit?() { false }
|
|
105
|
+
stub( @obj ).fuel?() { false }
|
|
106
|
+
end
|
|
107
|
+
describe "when the arity of the proc is 1" do
|
|
108
|
+
before do
|
|
109
|
+
@msg = lambda { |trans| "I am a #{trans.class} and I fail it" }
|
|
110
|
+
@machine.requirement_messages[:spacesuit?] = @msg
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "should return an array with the requirement message and nil" do
|
|
114
|
+
t = @obj.state_fu.fly_spaceship(:moon)
|
|
115
|
+
t.unmet_requirements.length.should == 2
|
|
116
|
+
messages = t.unmet_requirement_messages
|
|
117
|
+
messages.should be_kind_of( Array )
|
|
118
|
+
messages.length.should == 2
|
|
119
|
+
messages.compact.length.should == 1
|
|
120
|
+
messages.compact.first.should be_kind_of( String )
|
|
121
|
+
messages.compact.first.should == "I am a StateFu::Transition and I fail it"
|
|
122
|
+
end
|
|
123
|
+
end # arity 1
|
|
124
|
+
|
|
125
|
+
describe "when the arity of the proc is 0" do
|
|
126
|
+
before do
|
|
127
|
+
@msg = lambda { "I am a #{self.class} and I fail it" }
|
|
128
|
+
@machine.requirement_messages[:spacesuit?] = @msg
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "should return an array with the requirement message and nil" do
|
|
132
|
+
t = @obj.state_fu.fly_spaceship(:moon)
|
|
133
|
+
t.unmet_requirements.length.should == 2
|
|
134
|
+
messages = t.unmet_requirement_messages
|
|
135
|
+
messages.should be_kind_of( Array )
|
|
136
|
+
messages.length.should == 2
|
|
137
|
+
messages.compact.length.should == 1
|
|
138
|
+
messages.compact.first.should be_kind_of( String )
|
|
139
|
+
messages.compact.first.should == "I am a StateFu::Transition and I fail it"
|
|
140
|
+
end
|
|
141
|
+
end # arity 1
|
|
142
|
+
|
|
143
|
+
end # 1 proc msg of 2
|
|
144
|
+
describe "when a symbol message is defined for one of two unmet_requirements" do
|
|
145
|
+
before do
|
|
146
|
+
stub( @obj ).spacesuit?() { false }
|
|
147
|
+
stub( @obj ).fuel?() { false }
|
|
148
|
+
@machine.requirement_messages[:spacesuit?] = :no_spacesuit_msg_method
|
|
149
|
+
Klass.class_eval do
|
|
150
|
+
attr_accessor :arg
|
|
151
|
+
|
|
152
|
+
def no_spacesuit_msg_method( t )
|
|
153
|
+
self.arg = t
|
|
154
|
+
raise ArgumentError unless t.is_a?( StateFu::Transition )
|
|
155
|
+
"You can't go to the #{t.target.name} without a spacesuit!"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
describe "when there is no named proc on the machine matching the symbol" do
|
|
161
|
+
|
|
162
|
+
it "should call the method on @obj given transition.evaluate_named_proc_or_method() with the method name" do
|
|
163
|
+
@obj.method( :no_spacesuit_msg_method ).arity.should == 1
|
|
164
|
+
t = @obj.state_fu.fly_spaceship(:moon)
|
|
165
|
+
x = t.evaluate_named_proc_or_method(:no_spacesuit_msg_method)
|
|
166
|
+
@obj.arg.should == t
|
|
167
|
+
x.should =~ /You can't go to the moon/
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "should call t.evaluate_named_proc_or_method(:no_spacesuit_msg_method)" do
|
|
171
|
+
t = @obj.state_fu.fly_spaceship(:moon)
|
|
172
|
+
t.unmet_requirements.length.should == 2
|
|
173
|
+
mock( t ).evaluate_named_proc_or_method(:no_spacesuit_msg_method) { :my_string }
|
|
174
|
+
messages = t.unmet_requirement_messages
|
|
175
|
+
messages.should include(:my_string )
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "should call the method on @obj with the name of the symbol, passing it a transition" do
|
|
179
|
+
t = @obj.state_fu.fly_spaceship(:moon)
|
|
180
|
+
t.unmet_requirements.length.should == 2
|
|
181
|
+
messages = t.unmet_requirement_messages
|
|
182
|
+
@obj.arg.should == t
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "should return the result of the method execution as the message" do
|
|
186
|
+
t = @obj.state_fu.fly_spaceship(:moon)
|
|
187
|
+
t.unmet_requirements.length.should == 2
|
|
188
|
+
messages = t.unmet_requirement_messages
|
|
189
|
+
messages.length.should == 2
|
|
190
|
+
messages.compact.length.should == 1
|
|
191
|
+
@obj.arg.should == t
|
|
192
|
+
messages.compact[0].should == "You can't go to the moon without a spacesuit!"
|
|
193
|
+
end
|
|
194
|
+
end # no named proc
|
|
195
|
+
end # symbol message
|
|
196
|
+
end # transition.unmet_requirement_messages
|
|
197
|
+
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../helper")
|
|
2
|
+
|
|
3
|
+
class Moo
|
|
4
|
+
def arity_1( a )
|
|
5
|
+
raise "!"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def arity_0( )
|
|
9
|
+
raise "!"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe "sanity check: rr / arity" do
|
|
14
|
+
it "should have the expected arity for standard methods" do
|
|
15
|
+
m = Moo.new()
|
|
16
|
+
m.method(:arity_1).arity.should == 1
|
|
17
|
+
m.method(:arity_0).arity.should == 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "should have the expected arity when methods are mocked" do
|
|
21
|
+
m = Moo.new()
|
|
22
|
+
a1 = Object.new
|
|
23
|
+
a0 = Object.new
|
|
24
|
+
stub( a1 ).arity() { 1 }
|
|
25
|
+
stub( a0 ).arity() { 0 }
|
|
26
|
+
stub( m ).method(:arity_1) { a1 }
|
|
27
|
+
stub( m ).method(:arity_0) { a0 }
|
|
28
|
+
m.method(:arity_1).arity.should == 1
|
|
29
|
+
m.method(:arity_0).arity.should == 0
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../helper")
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
##
|
|
5
|
+
##
|
|
6
|
+
|
|
7
|
+
describe "Adding states to a Machine" do
|
|
8
|
+
|
|
9
|
+
include MySpecHelper
|
|
10
|
+
|
|
11
|
+
before(:each) do
|
|
12
|
+
make_pristine_class 'Klass'
|
|
13
|
+
@k = Klass.new()
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "should allow me to call machine() { state(:egg) }" do
|
|
17
|
+
lambda {Klass.machine(){ state :egg } }.should_not raise_error()
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "having called machine() { state(:egg) }" do
|
|
21
|
+
|
|
22
|
+
before(:each) do
|
|
23
|
+
Klass.machine(){ state :egg }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "should return [:egg] given machine.state_names" do
|
|
27
|
+
Klass.machine.should respond_to(:state_names)
|
|
28
|
+
Klass.machine.state_names.should == [:egg]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "should return [<StateFu::State @name=:egg>] given machine.states" do
|
|
32
|
+
Klass.machine.should respond_to(:states)
|
|
33
|
+
Klass.machine.states.length.should == 1
|
|
34
|
+
Klass.machine.states.first.should be_kind_of( StateFu::State )
|
|
35
|
+
Klass.machine.states.first.name.should == :egg
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "should return :egg given machine.states.first.name" do
|
|
39
|
+
Klass.machine.should respond_to(:states)
|
|
40
|
+
Klass.machine.states.length.should == 1
|
|
41
|
+
Klass.machine.states.first.should respond_to(:name)
|
|
42
|
+
Klass.machine.states.first.name.should == :egg
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "should return a <StateFu::State @name=:egg> given machine.states[:egg]" do
|
|
46
|
+
Klass.machine.should respond_to(:states)
|
|
47
|
+
result = Klass.machine.states[:egg]
|
|
48
|
+
result.should_not be_nil
|
|
49
|
+
result.should be_kind_of( StateFu::State )
|
|
50
|
+
result.name.should == :egg
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
it "should allow me to call machine(){ state(:chick) }" do
|
|
55
|
+
lambda {Klass.machine(){ state :chick } }.should_not raise_error()
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "having called machine() { state(:chick) }" do
|
|
59
|
+
before do
|
|
60
|
+
Klass.machine() { state :chick }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "should return [:egg] given machine.state_names" do
|
|
64
|
+
Klass.machine.should respond_to(:state_names)
|
|
65
|
+
Klass.machine.state_names.should == [:egg, :chick]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "should return a <StateFu::State @name=:chick> given machine.states[:egg]" do
|
|
69
|
+
Klass.machine.should respond_to(:states)
|
|
70
|
+
result = Klass.machine.states[:chick]
|
|
71
|
+
result.should_not be_nil
|
|
72
|
+
result.should be_kind_of( StateFu::State )
|
|
73
|
+
result.name.should == :chick
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "calling machine() { state(:bird) {|s| .. } }" do
|
|
79
|
+
|
|
80
|
+
it "should yield the state to the block as |s|" do
|
|
81
|
+
state = nil
|
|
82
|
+
Klass.machine() do
|
|
83
|
+
state(:bird) do |s|
|
|
84
|
+
state = s
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
state.should be_kind_of( StateFu::State )
|
|
88
|
+
state.name.should == :bird
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe "calling machine() { state(:bird) { ... } }" do
|
|
94
|
+
|
|
95
|
+
it "should instance_eval the block as a StateFu::Lathe" do
|
|
96
|
+
lathe = nil
|
|
97
|
+
Klass.machine() do
|
|
98
|
+
state(:bird) do
|
|
99
|
+
lathe = self
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
lathe.should be_kind_of(StateFu::Lathe)
|
|
103
|
+
lathe.sprocket.should be_kind_of(StateFu::State)
|
|
104
|
+
lathe.sprocket.name.should == :bird
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe "calling state(:bird) consecutive times" do
|
|
110
|
+
|
|
111
|
+
it "should yield the same state each time" do
|
|
112
|
+
Klass.machine() { state :bird }
|
|
113
|
+
bird_1 = Klass.machine.states[:bird]
|
|
114
|
+
Klass.machine() { state :bird }
|
|
115
|
+
bird_2 = Klass.machine.states[:bird]
|
|
116
|
+
bird_1.should == bird_2
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe "calling machine() { states(:egg, :chick, :bird, :poultry => true) }" do
|
|
123
|
+
|
|
124
|
+
it "should create 3 states" do
|
|
125
|
+
Klass.machine().should be_empty
|
|
126
|
+
Klass.machine() { states(:egg, :chick, :bird, :poultry => true) }
|
|
127
|
+
Klass.machine().state_names().should == [:egg, :chick, :bird]
|
|
128
|
+
Klass.machine().states.length.should == 3
|
|
129
|
+
Klass.machine().states.map(&:name).should == [:egg, :chick, :bird]
|
|
130
|
+
Klass.machine().states().each do |s|
|
|
131
|
+
s.options[:poultry].should be_true
|
|
132
|
+
s.should be_kind_of(StateFu::State)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe "merging options" do
|
|
136
|
+
it "should merge options when states are mentioned more than once" do
|
|
137
|
+
StateFu::FuSpace.reset!
|
|
138
|
+
Klass.machine() { states(:egg, :chick, :bird, :poultry => true) }
|
|
139
|
+
machine = Klass.machine
|
|
140
|
+
machine.states.length.should == 3
|
|
141
|
+
|
|
142
|
+
# make sure they're the same states
|
|
143
|
+
states_1 = machine.states
|
|
144
|
+
Klass.machine(){ states( :egg, :chick, :bird, :covering => 'feathers')}
|
|
145
|
+
states_1.should == machine.states
|
|
146
|
+
|
|
147
|
+
# ensure options were merged
|
|
148
|
+
machine.states().each do |s|
|
|
149
|
+
s.options[:poultry].should be_true
|
|
150
|
+
s.options[:covering].should == 'feathers'
|
|
151
|
+
s.should be_kind_of(StateFu::State)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe "adding events inside a state block" do
|
|
159
|
+
before do
|
|
160
|
+
@lambda = lambda{ Klass.machine(){ state(:egg){ event(:hatch, :to => :chick) }}}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "should not throw an error" do
|
|
164
|
+
@lambda.should_not raise_error
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe "Klass.machine(){ state(:egg){ event(:hatch, :to => :chick) }}}" do
|
|
168
|
+
before() do
|
|
169
|
+
Klass.machine(){ state(:egg){ event(:hatch, :to => :chick) }}
|
|
170
|
+
end
|
|
171
|
+
it "should add an event :hatch to the machine" do
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
end
|
|
177
|
+
|