fcoury-aasm 2.1.3
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/.document +5 -0
- data/.gitignore +8 -0
- data/LICENSE +20 -0
- data/README.rdoc +129 -0
- data/Rakefile +108 -0
- data/VERSION +1 -0
- data/lib/aasm.rb +1 -0
- data/lib/aasm/aasm.rb +195 -0
- data/lib/aasm/event.rb +98 -0
- data/lib/aasm/persistence.rb +16 -0
- data/lib/aasm/persistence/active_record_persistence.rb +248 -0
- data/lib/aasm/state.rb +55 -0
- data/lib/aasm/state_machine.rb +36 -0
- data/lib/aasm/state_transition.rb +50 -0
- data/spec/functional/conversation.rb +49 -0
- data/spec/functional/conversation_spec.rb +8 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/unit/aasm_spec.rb +432 -0
- data/spec/unit/active_record_persistence_spec.rb +254 -0
- data/spec/unit/before_after_callbacks_spec.rb +79 -0
- data/spec/unit/event_spec.rb +126 -0
- data/spec/unit/state_spec.rb +74 -0
- data/spec/unit/state_transition_spec.rb +84 -0
- data/test/functional/auth_machine_test.rb +120 -0
- data/test/test_helper.rb +33 -0
- data/test/unit/aasm_test.rb +0 -0
- data/test/unit/event_test.rb +54 -0
- data/test/unit/state_test.rb +69 -0
- data/test/unit/state_transition_test.rb +75 -0
- metadata +130 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
module AASM
|
2
|
+
module SupportingClasses
|
3
|
+
class StateTransition
|
4
|
+
attr_reader :from, :to, :opts
|
5
|
+
alias_method :options, :opts
|
6
|
+
|
7
|
+
def initialize(opts)
|
8
|
+
@from, @to, @guard, @on_transition = opts[:from], opts[:to], opts[:guard], opts[:on_transition]
|
9
|
+
@opts = opts
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform(obj)
|
13
|
+
case @guard
|
14
|
+
when Symbol, String
|
15
|
+
obj.send(@guard)
|
16
|
+
when Proc
|
17
|
+
@guard.call(obj)
|
18
|
+
else
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def execute(obj, *args)
|
24
|
+
@on_transition.is_a?(Array) ?
|
25
|
+
@on_transition.each {|ot| _execute(obj, ot, *args)} :
|
26
|
+
_execute(obj, @on_transition, *args)
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(obj)
|
30
|
+
@from == obj.from && @to == obj.to
|
31
|
+
end
|
32
|
+
|
33
|
+
def from?(value)
|
34
|
+
@from == value
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def _execute(obj, on_transition, *args)
|
40
|
+
case on_transition
|
41
|
+
when Symbol, String
|
42
|
+
obj.send(on_transition, *args)
|
43
|
+
when Proc
|
44
|
+
on_transition.call(obj, *args)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'aasm')
|
2
|
+
|
3
|
+
class Conversation
|
4
|
+
include AASM
|
5
|
+
|
6
|
+
aasm_initial_state :needs_attention
|
7
|
+
|
8
|
+
aasm_state :needs_attention
|
9
|
+
aasm_state :read
|
10
|
+
aasm_state :closed
|
11
|
+
aasm_state :awaiting_response
|
12
|
+
aasm_state :junk
|
13
|
+
|
14
|
+
aasm_event :new_message do
|
15
|
+
end
|
16
|
+
|
17
|
+
aasm_event :view do
|
18
|
+
transitions :to => :read, :from => [:needs_attention]
|
19
|
+
end
|
20
|
+
|
21
|
+
aasm_event :reply do
|
22
|
+
end
|
23
|
+
|
24
|
+
aasm_event :close do
|
25
|
+
transitions :to => :closed, :from => [:read, :awaiting_response]
|
26
|
+
end
|
27
|
+
|
28
|
+
aasm_event :junk do
|
29
|
+
transitions :to => :junk, :from => [:read]
|
30
|
+
end
|
31
|
+
|
32
|
+
aasm_event :unjunk do
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(persister)
|
36
|
+
@persister = persister
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
private
|
41
|
+
def aasm_read_state
|
42
|
+
@persister.read_state
|
43
|
+
end
|
44
|
+
|
45
|
+
def aasm_write_state(state)
|
46
|
+
@persister.write_state(state)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "..", "spec_helper")
|
2
|
+
require File.join(File.dirname(__FILE__), 'conversation')
|
3
|
+
|
4
|
+
describe Conversation, 'description' do
|
5
|
+
it '.aasm_states should contain all of the states' do
|
6
|
+
Conversation.aasm_states.should == [:needs_attention, :read, :closed, :awaiting_response, :junk]
|
7
|
+
end
|
8
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,432 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
class Foo
|
4
|
+
include AASM
|
5
|
+
aasm_initial_state :open
|
6
|
+
aasm_state :open, :exit => :exit
|
7
|
+
aasm_state :closed, :enter => :enter
|
8
|
+
|
9
|
+
aasm_event :close, :success => :success_callback do
|
10
|
+
transitions :to => :closed, :from => [:open]
|
11
|
+
end
|
12
|
+
|
13
|
+
aasm_event :null do
|
14
|
+
transitions :to => :closed, :from => [:open], :guard => :always_false
|
15
|
+
end
|
16
|
+
|
17
|
+
def always_false
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
def success_callback
|
22
|
+
end
|
23
|
+
|
24
|
+
def enter
|
25
|
+
end
|
26
|
+
def exit
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class FooTwo < Foo
|
31
|
+
include AASM
|
32
|
+
aasm_state :foo
|
33
|
+
end
|
34
|
+
|
35
|
+
class Bar
|
36
|
+
include AASM
|
37
|
+
|
38
|
+
aasm_state :read
|
39
|
+
aasm_state :ended
|
40
|
+
|
41
|
+
aasm_event :foo do
|
42
|
+
transitions :to => :ended, :from => [:read]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Baz < Bar
|
47
|
+
end
|
48
|
+
|
49
|
+
class Banker
|
50
|
+
include AASM
|
51
|
+
aasm_initial_state Proc.new { |banker| banker.rich? ? :retired : :selling_bad_mortgages }
|
52
|
+
aasm_state :retired
|
53
|
+
aasm_state :selling_bad_mortgages
|
54
|
+
RICH = 1_000_000
|
55
|
+
attr_accessor :balance
|
56
|
+
def initialize(balance = 0); self.balance = balance; end
|
57
|
+
def rich?; self.balance >= RICH; end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
describe AASM, '- class level definitions' do
|
62
|
+
it 'should define a class level aasm_initial_state() method on its including class' do
|
63
|
+
Foo.should respond_to(:aasm_initial_state)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should define a class level aasm_state() method on its including class' do
|
67
|
+
Foo.should respond_to(:aasm_state)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should define a class level aasm_event() method on its including class' do
|
71
|
+
Foo.should respond_to(:aasm_event)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should define a class level aasm_states() method on its including class' do
|
75
|
+
Foo.should respond_to(:aasm_states)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should define a class level aasm_states_for_select() method on its including class' do
|
79
|
+
Foo.should respond_to(:aasm_states_for_select)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should define a class level aasm_events() method on its including class' do
|
83
|
+
Foo.should respond_to(:aasm_events)
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
describe AASM, '- subclassing' do
|
90
|
+
it 'should have the parent states' do
|
91
|
+
Foo.aasm_states.each do |state|
|
92
|
+
FooTwo.aasm_states.should include(state)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'should not add the child states to the parent machine' do
|
97
|
+
Foo.aasm_states.should_not include(:foo)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
describe AASM, '- aasm_states_for_select' do
|
103
|
+
it "should return a select friendly array of states in the form of [['Friendly name', 'state_name']]" do
|
104
|
+
Foo.aasm_states_for_select.should == [['Open', 'open'], ['Closed', 'closed']]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe AASM, '- instance level definitions' do
|
109
|
+
before(:each) do
|
110
|
+
@foo = Foo.new
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should define a state querying instance method on including class' do
|
114
|
+
@foo.should respond_to(:open?)
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should define an event! inance method' do
|
118
|
+
@foo.should respond_to(:close!)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe AASM, '- initial states' do
|
123
|
+
before(:each) do
|
124
|
+
@foo = Foo.new
|
125
|
+
@bar = Bar.new
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should set the initial state' do
|
129
|
+
@foo.aasm_current_state.should == :open
|
130
|
+
end
|
131
|
+
|
132
|
+
it '#open? should be initially true' do
|
133
|
+
@foo.open?.should be_true
|
134
|
+
end
|
135
|
+
|
136
|
+
it '#closed? should be initially false' do
|
137
|
+
@foo.closed?.should be_false
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'should use the first state defined if no initial state is given' do
|
141
|
+
@bar.aasm_current_state.should == :read
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'should determine initial state from the Proc results' do
|
145
|
+
Banker.new(Banker::RICH - 1).aasm_current_state.should == :selling_bad_mortgages
|
146
|
+
Banker.new(Banker::RICH + 1).aasm_current_state.should == :retired
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe AASM, '- event firing with persistence' do
|
151
|
+
it 'should fire the Event' do
|
152
|
+
foo = Foo.new
|
153
|
+
|
154
|
+
Foo.aasm_events[:close].should_receive(:fire).with(foo)
|
155
|
+
foo.close!
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'should update the current state' do
|
159
|
+
foo = Foo.new
|
160
|
+
foo.close!
|
161
|
+
|
162
|
+
foo.aasm_current_state.should == :closed
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'should call the success callback if one was provided' do
|
166
|
+
foo = Foo.new
|
167
|
+
|
168
|
+
foo.should_receive(:success_callback)
|
169
|
+
|
170
|
+
foo.close!
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'should attempt to persist if aasm_write_state is defined' do
|
174
|
+
foo = Foo.new
|
175
|
+
|
176
|
+
def foo.aasm_write_state
|
177
|
+
end
|
178
|
+
|
179
|
+
foo.should_receive(:aasm_write_state)
|
180
|
+
|
181
|
+
foo.close!
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'should return true if aasm_write_state is defined and returns true' do
|
185
|
+
foo = Foo.new
|
186
|
+
|
187
|
+
def foo.aasm_write_state(state)
|
188
|
+
true
|
189
|
+
end
|
190
|
+
|
191
|
+
foo.close!.should be_true
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'should return false if aasm_write_state is defined and returns false' do
|
195
|
+
foo = Foo.new
|
196
|
+
|
197
|
+
def foo.aasm_write_state(state)
|
198
|
+
false
|
199
|
+
end
|
200
|
+
|
201
|
+
foo.close!.should be_false
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should not update the aasm_current_state if the write fails" do
|
205
|
+
foo = Foo.new
|
206
|
+
|
207
|
+
def foo.aasm_write_state
|
208
|
+
false
|
209
|
+
end
|
210
|
+
|
211
|
+
foo.should_receive(:aasm_write_state)
|
212
|
+
|
213
|
+
foo.close!
|
214
|
+
foo.aasm_current_state.should == :open
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
describe AASM, '- event firing without persistence' do
|
219
|
+
it 'should fire the Event' do
|
220
|
+
foo = Foo.new
|
221
|
+
|
222
|
+
Foo.aasm_events[:close].should_receive(:fire).with(foo)
|
223
|
+
foo.close
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'should update the current state' do
|
227
|
+
foo = Foo.new
|
228
|
+
foo.close
|
229
|
+
|
230
|
+
foo.aasm_current_state.should == :closed
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'should attempt to persist if aasm_write_state is defined' do
|
234
|
+
foo = Foo.new
|
235
|
+
|
236
|
+
def foo.aasm_write_state
|
237
|
+
end
|
238
|
+
|
239
|
+
foo.should_receive(:aasm_write_state_without_persistence)
|
240
|
+
|
241
|
+
foo.close
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
describe AASM, '- persistence' do
|
246
|
+
it 'should read the state if it has not been set and aasm_read_state is defined' do
|
247
|
+
foo = Foo.new
|
248
|
+
def foo.aasm_read_state
|
249
|
+
end
|
250
|
+
|
251
|
+
foo.should_receive(:aasm_read_state)
|
252
|
+
|
253
|
+
foo.aasm_current_state
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
describe AASM, '- getting events for a state' do
|
258
|
+
it '#aasm_events_for_current_state should use current state' do
|
259
|
+
foo = Foo.new
|
260
|
+
foo.should_receive(:aasm_current_state)
|
261
|
+
foo.aasm_events_for_current_state
|
262
|
+
end
|
263
|
+
|
264
|
+
it '#aasm_events_for_current_state should use aasm_events_for_state' do
|
265
|
+
foo = Foo.new
|
266
|
+
foo.stub!(:aasm_current_state).and_return(:foo)
|
267
|
+
foo.should_receive(:aasm_events_for_state).with(:foo)
|
268
|
+
foo.aasm_events_for_current_state
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
describe AASM, '- event callbacks' do
|
273
|
+
describe "with aasm_event_fired defined" do
|
274
|
+
before do
|
275
|
+
@foo = Foo.new
|
276
|
+
def @foo.aasm_event_fired(event, from, to)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
it 'should call it for successful bang fire' do
|
281
|
+
@foo.should_receive(:aasm_event_fired).with(:close, :open, :closed)
|
282
|
+
@foo.close!
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'should call it for successful non-bang fire' do
|
286
|
+
@foo.should_receive(:aasm_event_fired)
|
287
|
+
@foo.close
|
288
|
+
end
|
289
|
+
|
290
|
+
it 'should not call it for failing bang fire' do
|
291
|
+
@foo.stub!(:set_aasm_current_state_with_persistence).and_return(false)
|
292
|
+
@foo.should_not_receive(:aasm_event_fired)
|
293
|
+
@foo.close!
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
describe "with aasm_event_failed defined" do
|
298
|
+
before do
|
299
|
+
@foo = Foo.new
|
300
|
+
def @foo.aasm_event_failed(event, from)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
it 'should call it when transition failed for bang fire' do
|
305
|
+
@foo.should_receive(:aasm_event_failed).with(:null, :open)
|
306
|
+
@foo.null!
|
307
|
+
end
|
308
|
+
|
309
|
+
it 'should call it when transition failed for non-bang fire' do
|
310
|
+
@foo.should_receive(:aasm_event_failed).with(:null, :open)
|
311
|
+
@foo.null
|
312
|
+
end
|
313
|
+
|
314
|
+
it 'should not call it if persist fails for bang fire' do
|
315
|
+
@foo.stub!(:set_aasm_current_state_with_persistence).and_return(false)
|
316
|
+
@foo.should_receive(:aasm_event_failed)
|
317
|
+
@foo.close!
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
describe AASM, '- state actions' do
|
323
|
+
it "should call enter when entering state" do
|
324
|
+
foo = Foo.new
|
325
|
+
foo.should_receive(:enter)
|
326
|
+
|
327
|
+
foo.close
|
328
|
+
end
|
329
|
+
|
330
|
+
it "should call exit when exiting state" do
|
331
|
+
foo = Foo.new
|
332
|
+
foo.should_receive(:exit)
|
333
|
+
|
334
|
+
foo.close
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
|
339
|
+
describe Baz do
|
340
|
+
it "should have the same states as it's parent" do
|
341
|
+
Baz.aasm_states.should == Bar.aasm_states
|
342
|
+
end
|
343
|
+
|
344
|
+
it "should have the same events as it's parent" do
|
345
|
+
Baz.aasm_events.should == Bar.aasm_events
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
|
350
|
+
class ChetanPatil
|
351
|
+
include AASM
|
352
|
+
aasm_initial_state :sleeping
|
353
|
+
aasm_state :sleeping
|
354
|
+
aasm_state :showering
|
355
|
+
aasm_state :working
|
356
|
+
aasm_state :dating
|
357
|
+
aasm_state :prettying_up
|
358
|
+
|
359
|
+
aasm_event :wakeup do
|
360
|
+
transitions :from => :sleeping, :to => [:showering, :working]
|
361
|
+
end
|
362
|
+
|
363
|
+
aasm_event :dress do
|
364
|
+
transitions :from => :sleeping, :to => :working, :on_transition => :wear_clothes
|
365
|
+
transitions :from => :showering, :to => [:working, :dating], :on_transition => Proc.new { |obj, *args| obj.wear_clothes(*args) }
|
366
|
+
transitions :from => :showering, :to => :prettying_up, :on_transition => [:condition_hair, :fix_hair]
|
367
|
+
end
|
368
|
+
|
369
|
+
def wear_clothes(shirt_color, trouser_type)
|
370
|
+
end
|
371
|
+
|
372
|
+
def condition_hair
|
373
|
+
end
|
374
|
+
|
375
|
+
def fix_hair
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
|
380
|
+
describe ChetanPatil do
|
381
|
+
it 'should transition to specified next state (sleeping to showering)' do
|
382
|
+
cp = ChetanPatil.new
|
383
|
+
cp.wakeup! :showering
|
384
|
+
|
385
|
+
cp.aasm_current_state.should == :showering
|
386
|
+
end
|
387
|
+
|
388
|
+
it 'should transition to specified next state (sleeping to working)' do
|
389
|
+
cp = ChetanPatil.new
|
390
|
+
cp.wakeup! :working
|
391
|
+
|
392
|
+
cp.aasm_current_state.should == :working
|
393
|
+
end
|
394
|
+
|
395
|
+
it 'should transition to default (first or showering) state' do
|
396
|
+
cp = ChetanPatil.new
|
397
|
+
cp.wakeup!
|
398
|
+
|
399
|
+
cp.aasm_current_state.should == :showering
|
400
|
+
end
|
401
|
+
|
402
|
+
it 'should transition to default state when on_transition invoked' do
|
403
|
+
cp = ChetanPatil.new
|
404
|
+
cp.dress!(nil, 'purple', 'dressy')
|
405
|
+
|
406
|
+
cp.aasm_current_state.should == :working
|
407
|
+
end
|
408
|
+
|
409
|
+
it 'should call on_transition method with args' do
|
410
|
+
cp = ChetanPatil.new
|
411
|
+
cp.wakeup! :showering
|
412
|
+
|
413
|
+
cp.should_receive(:wear_clothes).with('blue', 'jeans')
|
414
|
+
cp.dress! :working, 'blue', 'jeans'
|
415
|
+
end
|
416
|
+
|
417
|
+
it 'should call on_transition proc' do
|
418
|
+
cp = ChetanPatil.new
|
419
|
+
cp.wakeup! :showering
|
420
|
+
|
421
|
+
cp.should_receive(:wear_clothes).with('purple', 'slacks')
|
422
|
+
cp.dress!(:dating, 'purple', 'slacks')
|
423
|
+
end
|
424
|
+
|
425
|
+
it 'should call on_transition with an array of methods' do
|
426
|
+
cp = ChetanPatil.new
|
427
|
+
cp.wakeup! :showering
|
428
|
+
cp.should_receive(:condition_hair)
|
429
|
+
cp.should_receive(:fix_hair)
|
430
|
+
cp.dress!(:prettying_up)
|
431
|
+
end
|
432
|
+
end
|