mattsnyder-stately 0.2.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.
@@ -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