state_machine 0.7.5 → 0.7.6
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/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
|