mattsnyder-stately 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,305 @@
1
+ require 'ostruct'
2
+ require 'spec_helper'
3
+
4
+ describe Stately do
5
+ before do
6
+ @order_class = Class.new(OpenStruct) do
7
+ stately :start => :processing do
8
+ state :completed do
9
+ prevent_from :refunded
10
+
11
+ before_transition :from => :processing, :do => :before_completed
12
+ before_transition :from => :invalid, :do => :cleanup_invalid
13
+ after_transition :do => :after_completed
14
+
15
+ validate :validates_amount
16
+ validate :validates_credit_card
17
+ end
18
+
19
+ state :invalid do
20
+ prevent_from :completed, :refunded
21
+ end
22
+
23
+ state :processing do
24
+ prevent_from :completed, :invalid, :refunded
25
+ end
26
+
27
+ state :refunded do
28
+ allow_from :completed
29
+
30
+ before_transition :from => :completed, :do => :before_refunded
31
+ after_transition :from => :completed, :do => :after_refunded
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def before_completed
38
+ self.serial_number = Time.now.usec
39
+ end
40
+
41
+ def after_completed
42
+ end
43
+
44
+ def before_refunded
45
+ self.refunded_reason = 'Overcharged'
46
+ end
47
+
48
+ def after_refunded
49
+ end
50
+
51
+ def cleanup_invalid
52
+ self.serial_number = nil
53
+ end
54
+
55
+ def validates_amount
56
+ amount > 0.0 && amount < 100.0
57
+ end
58
+
59
+ def validates_credit_card
60
+ self.cc_number == 123
61
+ end
62
+ end
63
+ end
64
+
65
+ def self.should_call_callbacks_on_complete(order)
66
+ @order = order
67
+
68
+ describe 'callbacks' do
69
+ it 'calls callbacks in order' do
70
+ @order.should_receive(:before_completed).ordered
71
+ @order.should_receive(:after_completed).ordered
72
+ @order.should_not_receive :cleanup_invalid
73
+
74
+ @order.complete
75
+ end
76
+
77
+ it 'sets serial_number' do
78
+ @order.serial_number.should be_nil
79
+ @order.complete
80
+ @order.serial_number.should_not be_nil
81
+ end
82
+ end
83
+ end
84
+
85
+ def self.should_call_validations_on_complete(order)
86
+ @order = order
87
+
88
+ describe 'validations' do
89
+ it 'calls validations in order' do
90
+ @order.should_receive(:validates_amount).ordered
91
+ @order.should_receive(:validates_credit_card).ordered
92
+
93
+ @order.complete
94
+ end
95
+
96
+ describe 'return values' do
97
+ before do
98
+ @order.stub :validates_amount => false
99
+ end
100
+
101
+ it 'should halt on false' do
102
+ @order.should_receive :validates_amount
103
+ @order.should_receive :validates_credit_card
104
+ @order.should_not_receive :before_completed
105
+ @order.should_not_receive :after_completed
106
+ @order.should_not_receive :cleanup_invalid
107
+
108
+ current_state = @order.state
109
+ @order.complete
110
+ @order.state.should == current_state
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ def self.should_prevent_transition(from, to, action)
117
+ before do
118
+ @order = @order_class.new(:amount => 99, :cc_number => 123)
119
+ @order.state = from
120
+ end
121
+
122
+ it 'should be prevented' do
123
+ lambda { @order.send(action) }.should raise_error(Stately::InvalidTransition,
124
+ "Prevented transition from #{from} to #{to}.")
125
+ end
126
+ end
127
+
128
+ def self.should_set_state(new_state, order, action)
129
+ @order = order
130
+
131
+ describe 'on success' do
132
+ before do
133
+ @order.send action
134
+ end
135
+
136
+ it 'sets state' do
137
+ @order.state.should == new_state
138
+ end
139
+ end
140
+ end
141
+
142
+ describe 'initial state' do
143
+ before do
144
+ @order = @order_class.new(:amount => 99, :cc_number => 123)
145
+ end
146
+
147
+ it 'creates actions for each state' do
148
+ @order_class.method_defined?(:complete).should be_true
149
+ @order_class.method_defined?(:process).should be_true
150
+ @order_class.method_defined?(:refund).should be_true
151
+ end
152
+
153
+ it 'finds all states' do
154
+ @order.states.should == [:completed, :invalid, :processing, :refunded]
155
+ end
156
+
157
+ it 'sets initial state to processing' do
158
+ @order.state.should == 'processing'
159
+ end
160
+ end
161
+
162
+ describe '#process' do
163
+ describe 'from processing' do
164
+ should_prevent_transition('processing', 'processing', :process)
165
+ end
166
+
167
+ describe 'from completed' do
168
+ should_prevent_transition('completed', 'processing', :process)
169
+ end
170
+
171
+ describe 'from invalid' do
172
+ should_prevent_transition('invalid', 'processing', :process)
173
+ end
174
+
175
+ describe 'from refunded' do
176
+ should_prevent_transition('refunded', 'processing', :process)
177
+ end
178
+ end
179
+
180
+ describe '#complete' do
181
+ before do
182
+ @order = @order_class.new(:amount => 99, :cc_number => 123)
183
+ end
184
+
185
+ describe 'from processing' do
186
+ should_call_validations_on_complete(@order)
187
+
188
+ describe 'callbacks' do
189
+ it 'calls callbacks in order' do
190
+ @order.should_receive(:before_completed).ordered
191
+ @order.should_receive(:after_completed).ordered
192
+ @order.should_not_receive :cleanup_invalid
193
+
194
+ @order.complete
195
+ end
196
+
197
+ it 'sets serial_number' do
198
+ @order.serial_number.should be_nil
199
+ @order.complete
200
+ @order.serial_number.should_not be_nil
201
+ end
202
+ end
203
+
204
+ should_set_state('completed', @order, :complete)
205
+ end
206
+
207
+ describe 'from completed' do
208
+ should_prevent_transition('completed', 'completed', :complete)
209
+ end
210
+
211
+ describe 'from invalid' do
212
+ before do
213
+ @order.serial_number = Time.now.usec
214
+ @order.state = 'invalid'
215
+ end
216
+
217
+ should_call_validations_on_complete(@order)
218
+
219
+ describe 'callbacks' do
220
+ it 'calls callbacks in order' do
221
+ @order.should_receive(:cleanup_invalid).ordered
222
+ @order.should_receive(:after_completed).ordered
223
+ @order.should_not_receive :before_completed
224
+
225
+ @order.complete
226
+ end
227
+
228
+ it 'sets serial_number to nil' do
229
+ @order.serial_number.should_not be_nil
230
+ @order.complete
231
+ @order.serial_number.should be_nil
232
+ end
233
+ end
234
+
235
+ should_set_state('completed', @order, :complete)
236
+ end
237
+
238
+ describe 'from refunded' do
239
+ should_prevent_transition('refunded', 'completed', :complete)
240
+ end
241
+ end
242
+
243
+ describe '#invalidate' do
244
+ describe 'from processing' do
245
+ before do
246
+ @order = @order_class.new(:amount => 99, :cc_number => 123)
247
+ @order.invalidate
248
+ end
249
+
250
+ it 'sets state' do
251
+ @order.state.should == 'invalid'
252
+ end
253
+ end
254
+
255
+ describe 'from completed' do
256
+ should_prevent_transition('completed', 'invalid', :invalidate)
257
+ end
258
+
259
+ describe 'from invalid' do
260
+ should_prevent_transition('invalid', 'invalid', :invalidate)
261
+ end
262
+
263
+ describe 'from refunded' do
264
+ should_prevent_transition('refunded', 'invalid', :invalidate)
265
+ end
266
+ end
267
+
268
+ describe '#refund' do
269
+ describe 'from processing' do
270
+ should_prevent_transition('processing', 'refunded', :refund)
271
+ end
272
+
273
+ describe 'from completed' do
274
+ before do
275
+ @order = @order_class.new(:amount => 99, :cc_number => 123)
276
+ @order.state = 'completed'
277
+ end
278
+
279
+ describe 'callbacks' do
280
+ it 'calls callbacks in order' do
281
+ @order.should_receive(:before_refunded).ordered
282
+ @order.should_receive(:after_refunded).ordered
283
+
284
+ @order.refund
285
+ end
286
+
287
+ it 'sets refunded_reason' do
288
+ @order.refunded_reason.should be_nil
289
+ @order.refund
290
+ @order.refunded_reason.should_not be_nil
291
+ end
292
+ end
293
+
294
+ should_set_state('refunded', @order, :refund)
295
+ end
296
+
297
+ describe 'from invalid' do
298
+ should_prevent_transition('invalid', 'refunded', :refund)
299
+ end
300
+
301
+ describe 'from refunded' do
302
+ should_prevent_transition('refunded', 'refunded', :refund)
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,5 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
+ $LOAD_PATH << File.join(File.dirname(__FILE__))
3
+
4
+ require 'rspec'
5
+ require 'stately'
@@ -0,0 +1,119 @@
1
+ require 'spec_helper'
2
+
3
+ describe Stately::Machine do
4
+ before do
5
+ @machine = Stately::Machine.new(:state, :processing)
6
+ end
7
+
8
+ describe 'initialize' do
9
+ it 'sets initial vars' do
10
+ @machine.start.should == :processing
11
+ @machine.state_attr.should == :state
12
+ @machine.states.map(&:to_s).should == ['processing']
13
+ end
14
+
15
+ it 'guesses the initial action' do
16
+ @machine.states.first.action.should == 'process'
17
+ end
18
+ end
19
+
20
+ describe '#state' do
21
+ describe 'with name only' do
22
+ describe 'of a new state' do
23
+ before do
24
+ @machine.state(:completed)
25
+ end
26
+
27
+ it 'adds a new state' do
28
+ @machine.states.map(&:to_s).should == ['processing', 'completed']
29
+ end
30
+ end
31
+
32
+ describe 'of a previously defined state' do
33
+ before do
34
+ @machine.state(:processing)
35
+ end
36
+
37
+ it "doesn't add a new state" do
38
+ @machine.states.map(&:to_s).should == ['processing']
39
+ end
40
+ end
41
+ end
42
+
43
+ describe 'with name and action' do
44
+ describe 'of a new state' do
45
+ before do
46
+ @machine.state(:new_state, :action => :transition_to_new_state)
47
+ end
48
+
49
+ it 'adds a new state' do
50
+ @machine.states.map(&:to_s).should == ['processing', 'new_state']
51
+ end
52
+
53
+ it 'adds the correct action to the new state' do
54
+ @machine.states.last.action.should == 'transition_to_new_state'
55
+ end
56
+ end
57
+
58
+ describe 'of a previously defined state' do
59
+ before do
60
+ @machine.state(:processing, :action => :transition_to_processing)
61
+ end
62
+
63
+ it "doesn't add a new state" do
64
+ @machine.states.map(&:to_s).should == ['processing']
65
+ end
66
+
67
+ it 'adds the correct action to the existing state' do
68
+ @machine.states.first.action.should == 'transition_to_processing'
69
+ end
70
+ end
71
+ end
72
+
73
+ describe 'with name, action, and block' do
74
+ describe 'of a new state' do
75
+ before do
76
+ @machine.state(:new_state, :action => :transition_to_new_state) do
77
+ allow_from :completed
78
+ end
79
+
80
+ @new_state = @machine.states.last
81
+ end
82
+
83
+ it 'adds a new state' do
84
+ @machine.states.map(&:to_s).should == ['processing', 'new_state']
85
+ end
86
+
87
+ it 'adds the correct action to the new state' do
88
+ @new_state.action.should == 'transition_to_new_state'
89
+ end
90
+
91
+ it 'includes the allow_from param' do
92
+ @new_state.allow_from_states.should == [:completed]
93
+ end
94
+ end
95
+
96
+ describe 'of a previously defined state' do
97
+ before do
98
+ @machine.state(:processing, :action => :transition_to_processing) do
99
+ allow_from :completed
100
+ end
101
+
102
+ @new_state = @machine.states.last
103
+ end
104
+
105
+ it "doesn't add a new state" do
106
+ @machine.states.map(&:to_s).should == ['processing']
107
+ end
108
+
109
+ it 'adds the correct action to the new state' do
110
+ @new_state.action.should == 'transition_to_processing'
111
+ end
112
+
113
+ it 'includes the allow_from param' do
114
+ @new_state.allow_from_states.should == [:completed]
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+
3
+ describe Stately::State do
4
+ describe 'initialize' do
5
+ describe 'with a block given' do
6
+ describe 'new' do
7
+ before do
8
+ @state = Stately::State.new(:invalid, nil) do
9
+ allow_from :completed
10
+ prevent_from :completed, :refunded
11
+
12
+ before_transition :do => :prepare
13
+ before_transition :from => :processing, :do => :before_completed
14
+ after_transition :do => :cleanup
15
+ after_transition :from => :processing, :do => :after_processing
16
+
17
+ validate :validates_amount
18
+ validate :validates_credit_card
19
+ end
20
+ end
21
+
22
+ it 'should set initial values' do
23
+ @state.name.should == :invalid
24
+
25
+ @state.allow_from_states.should == [:completed]
26
+ @state.prevent_from_states.should == [:completed, :refunded]
27
+
28
+ @state.before_transitions.should == [{:do => :prepare}, {:from => :processing,
29
+ :do => :before_completed}]
30
+ @state.after_transitions.should == [{:do => :cleanup}, {:from => :processing,
31
+ :do => :after_processing}]
32
+ @state.validations.should == [:validates_amount, :validates_credit_card]
33
+ end
34
+ end
35
+ end
36
+
37
+ describe 'without a block given' do
38
+ describe 'new' do
39
+ before do
40
+ @state = Stately::State.new(:test_state)
41
+ end
42
+
43
+ it 'should set initial values' do
44
+ @state.name.should == :test_state
45
+
46
+ @state.allow_from_states.should == []
47
+ @state.prevent_from_states.should == []
48
+
49
+ @state.before_transitions.should == []
50
+ @state.after_transitions.should == []
51
+ @state.validations.should == []
52
+ end
53
+ end
54
+
55
+ describe 'with a given action' do
56
+ before do
57
+ @state = Stately::State.new(:test_state, :test_action)
58
+ end
59
+
60
+ it 'should set the given action name' do
61
+ @state.action.should == 'test_action'
62
+ end
63
+ end
64
+
65
+ describe 'without a given action' do
66
+ before do
67
+ @actions = { :completed => :complete, :converting => :convert, :invalid => :invalidate,
68
+ :preparing => :prepare, :processing => :process, :refunded => :refund, :reticulating => :reticulate,
69
+ :saving => :save, :searching => :search, :started => :start, :stopped => :stop }
70
+ end
71
+
72
+ it 'should set the correct action verb' do
73
+ @actions.map do |state_name, action_name|
74
+ state = Stately::State.new(state_name)
75
+ state.action.should == action_name.to_s
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ describe '#to_s' do
83
+ before do
84
+ @state = Stately::State.new(:test_state)
85
+ end
86
+
87
+ it 'should return a string' do
88
+ @state.to_s.should == 'test_state'
89
+ end
90
+ end
91
+
92
+ describe '#to_sym' do
93
+ before do
94
+ @state = Stately::State.new('test_state')
95
+ end
96
+
97
+ it 'should return a symbol' do
98
+ @state.to_sym.should == :test_state
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,58 @@
1
+ require 'ostruct'
2
+ require 'spec_helper'
3
+
4
+ describe Stately::InstanceMethods do
5
+ before do
6
+ @test_class = Class.new(Object) do
7
+ attr_accessor :state
8
+
9
+ stately :start => :processing do
10
+ state :completed
11
+ end
12
+ end
13
+
14
+ @object = @test_class.new
15
+ end
16
+
17
+ describe 'initialize' do
18
+ it 'creates a new Stately::Machine' do
19
+ @object.stately_machine.class.should == Stately::Machine
20
+ @object.stately_machine.should == @test_class.stately_machine
21
+ end
22
+
23
+ it 'sets initial state' do
24
+ @object.state.should == 'processing'
25
+ end
26
+ end
27
+
28
+ describe '#states' do
29
+ it 'returns known state names in order' do
30
+ @object.states.should == [:processing, :completed]
31
+ end
32
+ end
33
+
34
+ describe 'actions' do
35
+ it 'defines action methods' do
36
+ @test_class.method_defined?(:complete).should be_true
37
+ @test_class.method_defined?(:process).should be_true
38
+ end
39
+ end
40
+
41
+ describe 'stately_machine' do
42
+ it 'defines a class-level accessor called stately_machine' do
43
+ @test_class.respond_to?(:stately_machine).should be_true
44
+ end
45
+
46
+ it 'defines an instance-level accessor called stately_machine' do
47
+ @test_class.method_defined?(:stately_machine).should be_true
48
+ end
49
+
50
+ it 'defines a class-level setter called stately_machine=' do
51
+ @test_class.respond_to?(:stately_machine=).should be_true
52
+ end
53
+
54
+ it 'defines an instance-level setter called stately_machine=' do
55
+ @test_class.method_defined?(:stately_machine=).should be_true
56
+ end
57
+ end
58
+ end
data/stately.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ $LOAD_PATH << File.expand_path('../lib', __FILE__)
2
+ require 'stately/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'mattsnyder-stately'
6
+ s.version = Stately::VERSION
7
+ s.authors = ['Ryan Twomey']
8
+ s.email = ['rtwomey@gmail.com']
9
+ s.homepage = 'http://github.com/rtwomey/stately'
10
+ s.summary = 'A simple, elegant state machine for Ruby'
11
+ s.description = 'Add an elegant state machine to your ruby objects with a simple DSL'
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
15
+
16
+ s.add_development_dependency 'redcarpet', '~> 2.2.2'
17
+ s.add_development_dependency 'rspec', '~> 2.0'
18
+ s.add_development_dependency 'yard', '~> 0.8.3'
19
+ s.add_development_dependency 'rdoc'
20
+
21
+ s.required_ruby_version = Gem::Requirement.new('>= 1.8.7')
22
+ s.require_paths = ['lib']
23
+ end