state_machine 0.7.5 → 0.7.6
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +10 -0
- data/README.rdoc +1 -1
- data/Rakefile +9 -2
- data/lib/state_machine.rb +17 -12
- data/lib/state_machine/assertions.rb +4 -5
- data/lib/state_machine/callback.rb +27 -21
- data/lib/state_machine/condition_proxy.rb +1 -1
- data/lib/state_machine/eval_helpers.rb +1 -2
- data/lib/state_machine/event.rb +3 -3
- data/lib/state_machine/event_collection.rb +5 -6
- data/lib/state_machine/integrations/active_record.rb +25 -16
- data/lib/state_machine/integrations/active_record/locale.rb +1 -0
- data/lib/state_machine/integrations/data_mapper.rb +6 -9
- data/lib/state_machine/integrations/data_mapper/observer.rb +5 -5
- data/lib/state_machine/integrations/sequel.rb +5 -7
- data/lib/state_machine/machine.rb +65 -55
- data/lib/state_machine/machine_collection.rb +15 -17
- data/lib/state_machine/matcher_helpers.rb +1 -0
- data/lib/state_machine/state.rb +6 -4
- data/lib/state_machine/state_collection.rb +3 -3
- data/lib/state_machine/transition.rb +3 -3
- data/test/unit/eval_helpers_test.rb +18 -0
- data/test/unit/event_collection_test.rb +30 -0
- data/test/unit/integrations/active_record_test.rb +78 -8
- data/test/unit/integrations/data_mapper_test.rb +77 -0
- data/test/unit/integrations/sequel_test.rb +54 -2
- data/test/unit/machine_collection_test.rb +20 -0
- data/test/unit/machine_test.rb +154 -5
- data/test/unit/state_test.rb +17 -3
- data/test/unit/transition_test.rb +27 -0
- metadata +2 -2
data/lib/state_machine/state.rb
CHANGED
@@ -119,7 +119,9 @@ module StateMachine
|
|
119
119
|
def value(eval = true)
|
120
120
|
if @value.is_a?(Proc) && eval
|
121
121
|
if cache_value?
|
122
|
-
|
122
|
+
@value = @value.call
|
123
|
+
machine.states.update(self)
|
124
|
+
@value
|
123
125
|
else
|
124
126
|
@value.call
|
125
127
|
end
|
@@ -154,11 +156,11 @@ module StateMachine
|
|
154
156
|
# a new module will be included in the owner class.
|
155
157
|
def context(&block)
|
156
158
|
owner_class = machine.owner_class
|
157
|
-
|
159
|
+
machine_name = machine.name
|
158
160
|
name = self.name
|
159
161
|
|
160
162
|
# Evaluate the method definitions
|
161
|
-
context = ConditionProxy.new(owner_class, lambda {|object| object.
|
163
|
+
context = ConditionProxy.new(owner_class, lambda {|object| object.class.state_machine(machine_name).states.matches?(object, name)})
|
162
164
|
context.class_eval(&block)
|
163
165
|
context.instance_methods.each do |method|
|
164
166
|
methods[method.to_sym] = context.instance_method(method)
|
@@ -166,7 +168,7 @@ module StateMachine
|
|
166
168
|
# Calls the method defined by the current state of the machine
|
167
169
|
context.class_eval <<-end_eval, __FILE__, __LINE__
|
168
170
|
def #{method}(*args, &block)
|
169
|
-
self.class.state_machine(#{
|
171
|
+
self.class.state_machine(#{machine_name.inspect}).states.match!(self).call(self, #{method.inspect}, *args, &block)
|
170
172
|
end
|
171
173
|
end_eval
|
172
174
|
end
|
@@ -27,7 +27,7 @@ module StateMachine
|
|
27
27
|
# states.matches?(vehicle, :idling) # => false
|
28
28
|
# states.matches?(vehicle, :invalid) # => IndexError: :invalid is an invalid key for :name index
|
29
29
|
def matches?(object, name)
|
30
|
-
fetch(name).matches?(machine.read(object))
|
30
|
+
fetch(name).matches?(machine.read(object, :state))
|
31
31
|
end
|
32
32
|
|
33
33
|
# Determines the current state of the given object as configured by this
|
@@ -53,7 +53,7 @@ module StateMachine
|
|
53
53
|
# vehicle.state = 'invalid'
|
54
54
|
# states.match(vehicle) # => nil
|
55
55
|
def match(object)
|
56
|
-
value = machine.read(object)
|
56
|
+
value = machine.read(object, :state)
|
57
57
|
self[value, :value] || detect {|state| state.matches?(value)}
|
58
58
|
end
|
59
59
|
|
@@ -77,7 +77,7 @@ module StateMachine
|
|
77
77
|
# vehicle.state = 'invalid'
|
78
78
|
# states.match!(vehicle) # => ArgumentError: "invalid" is not a known state value
|
79
79
|
def match!(object)
|
80
|
-
match(object) || raise(ArgumentError, "#{machine.read(object).inspect} is not a known #{machine.
|
80
|
+
match(object) || raise(ArgumentError, "#{machine.read(object, :state).inspect} is not a known #{machine.name} value")
|
81
81
|
end
|
82
82
|
|
83
83
|
# Gets the order in which states should be displayed based on where they
|
@@ -136,7 +136,7 @@ module StateMachine
|
|
136
136
|
|
137
137
|
# From state information
|
138
138
|
from_state = machine.states.fetch(from_name)
|
139
|
-
@from = machine.read(object)
|
139
|
+
@from = machine.read(object, :state)
|
140
140
|
@from_name = from_state.name
|
141
141
|
@qualified_from_name = from_state.qualified_name
|
142
142
|
|
@@ -259,7 +259,7 @@ module StateMachine
|
|
259
259
|
#
|
260
260
|
# vehicle.state # => 'idling'
|
261
261
|
def persist
|
262
|
-
machine.write(object, to)
|
262
|
+
machine.write(object, :state, to)
|
263
263
|
end
|
264
264
|
|
265
265
|
# Runs the machine's +after+ callbacks for this transition. Only
|
@@ -326,7 +326,7 @@ module StateMachine
|
|
326
326
|
# transition.rollback
|
327
327
|
# vehicle.state # => "parked"
|
328
328
|
def rollback
|
329
|
-
machine.write(object, from)
|
329
|
+
machine.write(object, :state, from)
|
330
330
|
end
|
331
331
|
|
332
332
|
# Generates a nicely formatted description of this transitions's contents.
|
@@ -45,6 +45,24 @@ class EvalHelpersSymbolWithArgumentsTest < Test::Unit::TestCase
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
+
class EvalHelpersSymbolTaintedMethodTest < Test::Unit::TestCase
|
49
|
+
include StateMachine::EvalHelpers
|
50
|
+
|
51
|
+
def setup
|
52
|
+
class << (@object = Object.new)
|
53
|
+
def callback
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
taint
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_should_not_raise_security_error
|
62
|
+
assert_nothing_raised { evaluate_method(@object, :callback, 1, 2, 3) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
48
66
|
class EvalHelpersStringTest < Test::Unit::TestCase
|
49
67
|
include StateMachine::EvalHelpers
|
50
68
|
|
@@ -261,3 +261,33 @@ class EventCollectionWithValidationsTest < Test::Unit::TestCase
|
|
261
261
|
StateMachine::Integrations.send(:remove_const, 'Custom')
|
262
262
|
end
|
263
263
|
end
|
264
|
+
|
265
|
+
class EventCollectionWithCustomMachineAttributeTest < Test::Unit::TestCase
|
266
|
+
def setup
|
267
|
+
@klass = Class.new do
|
268
|
+
def save
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
@machine = StateMachine::Machine.new(@klass, :state, :attribute => :state_id, :initial => :parked, :action => :save)
|
273
|
+
@events = StateMachine::EventCollection.new(@machine)
|
274
|
+
|
275
|
+
@machine.event :ignite
|
276
|
+
@machine.state :parked, :idling
|
277
|
+
@events << @ignite = StateMachine::Event.new(@machine, :ignite)
|
278
|
+
|
279
|
+
@object = @klass.new
|
280
|
+
end
|
281
|
+
|
282
|
+
def test_should_not_have_transition_if_nil
|
283
|
+
@object.state_event = nil
|
284
|
+
assert_nil @events.attribute_transition_for(@object)
|
285
|
+
end
|
286
|
+
|
287
|
+
def test_should_have_valid_transition_if_event_can_be_fired
|
288
|
+
@ignite.transition :parked => :idling
|
289
|
+
@object.state_event = 'ignite'
|
290
|
+
|
291
|
+
assert_instance_of StateMachine::Transition, @events.attribute_transition_for(@object)
|
292
|
+
end
|
293
|
+
end
|
@@ -172,6 +172,13 @@ begin
|
|
172
172
|
assert_equal 'cannot transition via "park"', record.errors.on(:state)
|
173
173
|
end
|
174
174
|
|
175
|
+
def test_should_auto_prefix_custom_attributes_on_invalidation
|
176
|
+
record = @model.new
|
177
|
+
@machine.invalidate(record, :event, :invalid)
|
178
|
+
|
179
|
+
assert_equal 'is invalid', record.errors.on(:state_event)
|
180
|
+
end
|
181
|
+
|
175
182
|
def test_should_clear_errors_on_reset
|
176
183
|
record = @model.new
|
177
184
|
record.state = 'parked'
|
@@ -360,6 +367,60 @@ begin
|
|
360
367
|
end
|
361
368
|
end
|
362
369
|
|
370
|
+
class MachineWithOwnerSubclassTest < ActiveRecord::TestCase
|
371
|
+
def setup
|
372
|
+
@model = new_model
|
373
|
+
@machine = StateMachine::Machine.new(@model, :state)
|
374
|
+
|
375
|
+
@subclass = Class.new(@model)
|
376
|
+
@subclass_machine = @subclass.state_machine(:state) {}
|
377
|
+
@subclass_machine.state :parked, :idling, :first_gear
|
378
|
+
end
|
379
|
+
|
380
|
+
def test_should_only_include_records_with_subclass_states_in_with_scope
|
381
|
+
parked = @subclass.create :state => 'parked'
|
382
|
+
idling = @subclass.create :state => 'idling'
|
383
|
+
|
384
|
+
assert_equal [parked, idling], @subclass.with_states(:parked, :idling)
|
385
|
+
end
|
386
|
+
|
387
|
+
def test_should_only_include_records_without_subclass_states_in_without_scope
|
388
|
+
parked = @subclass.create :state => 'parked'
|
389
|
+
idling = @subclass.create :state => 'idling'
|
390
|
+
first_gear = @subclass.create :state => 'first_gear'
|
391
|
+
|
392
|
+
assert_equal [parked, idling], @subclass.without_states(:first_gear)
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
class MachineWithCustomAttributeTest < ActiveRecord::TestCase
|
397
|
+
def setup
|
398
|
+
@model = new_model
|
399
|
+
@machine = StateMachine::Machine.new(@model, :status, :attribute => :state)
|
400
|
+
@machine.state :parked
|
401
|
+
|
402
|
+
@record = @model.new
|
403
|
+
end
|
404
|
+
|
405
|
+
def test_should_add_validation_errors_to_custom_attribute
|
406
|
+
@record.state = 'invalid'
|
407
|
+
|
408
|
+
assert !@record.valid?
|
409
|
+
assert_equal ['State is invalid'], @record.errors.full_messages
|
410
|
+
|
411
|
+
@record.state = 'parked'
|
412
|
+
assert @record.valid?
|
413
|
+
end
|
414
|
+
|
415
|
+
def test_should_check_custom_attribute_for_predicate
|
416
|
+
@record.state = nil
|
417
|
+
assert !@record.status?(:parked)
|
418
|
+
|
419
|
+
@record.state = 'parked'
|
420
|
+
assert @record.status?(:parked)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
363
424
|
class MachineWithCallbacksTest < ActiveRecord::TestCase
|
364
425
|
def setup
|
365
426
|
@model = new_model
|
@@ -965,16 +1026,25 @@ begin
|
|
965
1026
|
machine.invalidate(record, :state, :invalid_transition, [[:event, :ignite]])
|
966
1027
|
assert_equal 'cannot ignite', record.errors.on(:state)
|
967
1028
|
end
|
968
|
-
end
|
969
|
-
|
970
|
-
def test_should_invalidate_using_customized_i18n_string_if_specified
|
971
|
-
machine = StateMachine::Machine.new(@model, :messages => {:invalid_transition => 'cannot {{event}}'})
|
972
|
-
machine.state :parked, :idling
|
973
1029
|
|
974
|
-
|
1030
|
+
def test_should_invalidate_using_customized_i18n_string_if_specified
|
1031
|
+
machine = StateMachine::Machine.new(@model, :messages => {:invalid_transition => 'cannot {{event}}'})
|
1032
|
+
machine.state :parked, :idling
|
1033
|
+
|
1034
|
+
record = @model.new(:state => 'idling')
|
1035
|
+
|
1036
|
+
machine.invalidate(record, :state, :invalid_transition, [[:event, :ignite]])
|
1037
|
+
assert_equal 'cannot ignite', record.errors.on(:state)
|
1038
|
+
end
|
975
1039
|
|
976
|
-
|
977
|
-
|
1040
|
+
def test_should_only_add_locale_once_in_load_path
|
1041
|
+
assert_equal 1, I18n.load_path.select {|path| path =~ %r{state_machine/integrations/active_record/locale\.rb$}}.length
|
1042
|
+
|
1043
|
+
# Create another ActiveRecord model that will triger the i18n feature
|
1044
|
+
new_model
|
1045
|
+
|
1046
|
+
assert_equal 1, I18n.load_path.select {|path| path =~ %r{state_machine/integrations/active_record/locale\.rb$}}.length
|
1047
|
+
end
|
978
1048
|
end
|
979
1049
|
else
|
980
1050
|
$stderr.puts 'Skipping ActiveRecord I18n tests. `gem install active_record` >= v2.2.0 and try again.'
|
@@ -232,6 +232,57 @@ begin
|
|
232
232
|
end
|
233
233
|
end
|
234
234
|
|
235
|
+
class MachineWithOwnerSubclassTest < BaseTestCase
|
236
|
+
def setup
|
237
|
+
@resource = new_resource
|
238
|
+
@machine = StateMachine::Machine.new(@resource, :state)
|
239
|
+
|
240
|
+
@subclass = Class.new(@resource)
|
241
|
+
@subclass_machine = @subclass.state_machine(:state) {}
|
242
|
+
@subclass_machine.state :parked, :idling, :first_gear
|
243
|
+
end
|
244
|
+
|
245
|
+
def test_should_only_include_records_with_subclass_states_in_with_scope
|
246
|
+
parked = @subclass.create :state => 'parked'
|
247
|
+
idling = @subclass.create :state => 'idling'
|
248
|
+
|
249
|
+
assert_equal [parked, idling], @subclass.with_states(:parked, :idling)
|
250
|
+
end
|
251
|
+
|
252
|
+
def test_should_only_include_records_without_subclass_states_in_without_scope
|
253
|
+
parked = @subclass.create :state => 'parked'
|
254
|
+
idling = @subclass.create :state => 'idling'
|
255
|
+
first_gear = @subclass.create :state => 'first_gear'
|
256
|
+
|
257
|
+
assert_equal [parked, idling], @subclass.without_states(:first_gear)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
class MachineWithTransactionsTest < BaseTestCase
|
262
|
+
def setup
|
263
|
+
@resource = new_resource
|
264
|
+
@machine = StateMachine::Machine.new(@resource, :use_transactions => true)
|
265
|
+
end
|
266
|
+
|
267
|
+
def test_should_rollback_transaction_if_false
|
268
|
+
@machine.within_transaction(@resource.new) do
|
269
|
+
@resource.create
|
270
|
+
false
|
271
|
+
end
|
272
|
+
|
273
|
+
assert_equal 0, @resource.all.size
|
274
|
+
end
|
275
|
+
|
276
|
+
def test_should_not_rollback_transaction_if_true
|
277
|
+
@machine.within_transaction(@resource.new) do
|
278
|
+
@resource.create
|
279
|
+
true
|
280
|
+
end
|
281
|
+
|
282
|
+
assert_equal 1, @resource.all.size
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
235
286
|
class MachineWithCallbacksTest < BaseTestCase
|
236
287
|
def setup
|
237
288
|
@resource = new_resource
|
@@ -544,6 +595,12 @@ begin
|
|
544
595
|
assert_equal ['cannot transition via "park"'], @record.errors.on(:state)
|
545
596
|
end
|
546
597
|
|
598
|
+
def test_should_auto_prefix_custom_attributes_on_invalidation
|
599
|
+
@machine.invalidate(@record, :event, :invalid)
|
600
|
+
|
601
|
+
assert_equal ['is invalid'], @record.errors.on(:state_event)
|
602
|
+
end
|
603
|
+
|
547
604
|
def test_should_clear_errors_on_reset
|
548
605
|
@record.state = 'parked'
|
549
606
|
@record.errors.add(:state, 'is invalid')
|
@@ -566,6 +623,26 @@ begin
|
|
566
623
|
end
|
567
624
|
end
|
568
625
|
|
626
|
+
class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
|
627
|
+
def setup
|
628
|
+
@resource = new_resource
|
629
|
+
@machine = StateMachine::Machine.new(@resource, :status, :attribute => :state)
|
630
|
+
@machine.state :parked
|
631
|
+
|
632
|
+
@record = @resource.new
|
633
|
+
end
|
634
|
+
|
635
|
+
def test_should_add_validation_errors_to_custom_attribute
|
636
|
+
@record.state = 'invalid'
|
637
|
+
|
638
|
+
assert !@record.valid?
|
639
|
+
assert_equal ['is invalid'], @record.errors.on(:state)
|
640
|
+
|
641
|
+
@record.state = 'parked'
|
642
|
+
assert @record.valid?
|
643
|
+
end
|
644
|
+
end
|
645
|
+
|
569
646
|
class MachineWithStateDrivenValidationsTest < BaseTestCase
|
570
647
|
def setup
|
571
648
|
@resource = new_resource do
|
@@ -25,10 +25,9 @@ begin
|
|
25
25
|
end if auto_migrate
|
26
26
|
model = Class.new(Sequel::Model(:foo)) do
|
27
27
|
self.raise_on_save_failure = false
|
28
|
-
plugin :validation_class_methods if respond_to?(:plugin)
|
29
|
-
|
30
28
|
def self.name; 'SequelTest::Foo'; end
|
31
29
|
end
|
30
|
+
model.plugin(:validation_class_methods) if model.respond_to?(:plugin)
|
32
31
|
model.class_eval(&block) if block_given?
|
33
32
|
model
|
34
33
|
end
|
@@ -146,6 +145,13 @@ begin
|
|
146
145
|
assert_equal ['cannot transition via "park"'], record.errors.on(:state)
|
147
146
|
end
|
148
147
|
|
148
|
+
def test_should_auto_prefix_custom_attributes_on_invalidation
|
149
|
+
record = @model.new
|
150
|
+
@machine.invalidate(record, :event, :invalid)
|
151
|
+
|
152
|
+
assert_equal ['is invalid'], record.errors.on(:state_event)
|
153
|
+
end
|
154
|
+
|
149
155
|
def test_should_clear_errors_on_reset
|
150
156
|
record = @model.new
|
151
157
|
record.state = 'parked'
|
@@ -245,6 +251,52 @@ begin
|
|
245
251
|
end
|
246
252
|
end
|
247
253
|
|
254
|
+
class MachineWithOwnerSubclassTest < BaseTestCase
|
255
|
+
def setup
|
256
|
+
@model = new_model
|
257
|
+
@machine = StateMachine::Machine.new(@model, :state)
|
258
|
+
|
259
|
+
@subclass = Class.new(@model)
|
260
|
+
@subclass_machine = @subclass.state_machine(:state) {}
|
261
|
+
@subclass_machine.state :parked, :idling, :first_gear
|
262
|
+
end
|
263
|
+
|
264
|
+
def test_should_only_include_records_with_subclass_states_in_with_scope
|
265
|
+
parked = @subclass.create :state => 'parked'
|
266
|
+
idling = @subclass.create :state => 'idling'
|
267
|
+
|
268
|
+
assert_equal [parked, idling], @subclass.with_states(:parked, :idling).all
|
269
|
+
end
|
270
|
+
|
271
|
+
def test_should_only_include_records_without_subclass_states_in_without_scope
|
272
|
+
parked = @subclass.create :state => 'parked'
|
273
|
+
idling = @subclass.create :state => 'idling'
|
274
|
+
first_gear = @subclass.create :state => 'first_gear'
|
275
|
+
|
276
|
+
assert_equal [parked, idling], @subclass.without_states(:first_gear).all
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
class MachineWithCustomAttributeTest < BaseTestCase
|
281
|
+
def setup
|
282
|
+
@model = new_model
|
283
|
+
@machine = StateMachine::Machine.new(@model, :status, :attribute => :state)
|
284
|
+
@machine.state :parked
|
285
|
+
|
286
|
+
@record = @model.new
|
287
|
+
end
|
288
|
+
|
289
|
+
def test_should_add_validation_errors_to_custom_attribute
|
290
|
+
@record.state = 'invalid'
|
291
|
+
|
292
|
+
assert !@record.valid?
|
293
|
+
assert_equal ['is invalid'], @record.errors.on(:state)
|
294
|
+
|
295
|
+
@record.state = 'parked'
|
296
|
+
assert @record.valid?
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
248
300
|
class MachineWithCallbacksTest < BaseTestCase
|
249
301
|
def setup
|
250
302
|
@model = new_model
|
@@ -688,3 +688,23 @@ class MachineCollectionFireImplicitWithValidationsTest < Test::Unit::TestCase
|
|
688
688
|
StateMachine::Integrations.send(:remove_const, 'Custom')
|
689
689
|
end
|
690
690
|
end
|
691
|
+
|
692
|
+
class MachineCollectionFireImplicitWithCustomMachineNameTest < MachineCollectionFireImplicitTest
|
693
|
+
def setup
|
694
|
+
super
|
695
|
+
|
696
|
+
@object.state_event = 'ignite'
|
697
|
+
end
|
698
|
+
|
699
|
+
def test_should_be_successful_on_complete_file
|
700
|
+
assert @machines.fire_event_attributes(@object, :save) { true }
|
701
|
+
assert_equal 'idling', @object.state
|
702
|
+
assert_nil @object.state_event
|
703
|
+
end
|
704
|
+
|
705
|
+
def test_should_be_successful_on_partial_fire
|
706
|
+
@machines.fire_event_attributes(@object, :save, false) { true }
|
707
|
+
assert_equal 'idling', @object.state
|
708
|
+
assert_equal :ignite, @object.state_event
|
709
|
+
end
|
710
|
+
end
|