state_machine 0.9.4 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +20 -0
- data/LICENSE +1 -1
- data/README.rdoc +74 -4
- data/Rakefile +3 -3
- data/lib/state_machine.rb +51 -24
- data/lib/state_machine/{guard.rb → branch.rb} +34 -40
- data/lib/state_machine/callback.rb +13 -18
- data/lib/state_machine/error.rb +13 -0
- data/lib/state_machine/eval_helpers.rb +3 -0
- data/lib/state_machine/event.rb +67 -30
- data/lib/state_machine/event_collection.rb +20 -3
- data/lib/state_machine/extensions.rb +3 -3
- data/lib/state_machine/integrations.rb +7 -0
- data/lib/state_machine/integrations/active_model.rb +149 -59
- data/lib/state_machine/integrations/active_model/versions.rb +30 -0
- data/lib/state_machine/integrations/active_record.rb +74 -148
- data/lib/state_machine/integrations/active_record/locale.rb +0 -7
- data/lib/state_machine/integrations/active_record/versions.rb +149 -0
- data/lib/state_machine/integrations/base.rb +64 -0
- data/lib/state_machine/integrations/data_mapper.rb +50 -39
- data/lib/state_machine/integrations/data_mapper/observer.rb +47 -12
- data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
- data/lib/state_machine/integrations/mongo_mapper.rb +37 -64
- data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
- data/lib/state_machine/integrations/mongo_mapper/versions.rb +102 -0
- data/lib/state_machine/integrations/mongoid.rb +297 -0
- data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
- data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
- data/lib/state_machine/integrations/sequel.rb +99 -55
- data/lib/state_machine/integrations/sequel/versions.rb +40 -0
- data/lib/state_machine/machine.rb +273 -136
- data/lib/state_machine/machine_collection.rb +21 -13
- data/lib/state_machine/node_collection.rb +6 -1
- data/lib/state_machine/path.rb +120 -0
- data/lib/state_machine/path_collection.rb +90 -0
- data/lib/state_machine/state.rb +28 -9
- data/lib/state_machine/state_collection.rb +1 -1
- data/lib/state_machine/transition.rb +65 -6
- data/lib/state_machine/transition_collection.rb +1 -1
- data/test/files/en.yml +8 -0
- data/test/functional/state_machine_test.rb +15 -2
- data/test/unit/branch_test.rb +890 -0
- data/test/unit/callback_test.rb +9 -36
- data/test/unit/error_test.rb +43 -0
- data/test/unit/event_collection_test.rb +67 -33
- data/test/unit/event_test.rb +165 -38
- data/test/unit/integrations/active_model_test.rb +103 -3
- data/test/unit/integrations/active_record_test.rb +90 -43
- data/test/unit/integrations/base_test.rb +87 -0
- data/test/unit/integrations/data_mapper_test.rb +105 -44
- data/test/unit/integrations/mongo_mapper_test.rb +261 -64
- data/test/unit/integrations/mongoid_test.rb +1529 -0
- data/test/unit/integrations/sequel_test.rb +33 -49
- data/test/unit/integrations_test.rb +4 -0
- data/test/unit/invalid_event_test.rb +15 -2
- data/test/unit/invalid_parallel_transition_test.rb +18 -0
- data/test/unit/invalid_transition_test.rb +72 -2
- data/test/unit/machine_collection_test.rb +55 -61
- data/test/unit/machine_test.rb +388 -26
- data/test/unit/node_collection_test.rb +14 -4
- data/test/unit/path_collection_test.rb +266 -0
- data/test/unit/path_test.rb +485 -0
- data/test/unit/state_collection_test.rb +30 -0
- data/test/unit/state_test.rb +82 -35
- data/test/unit/transition_collection_test.rb +48 -44
- data/test/unit/transition_test.rb +198 -41
- metadata +111 -74
- data/test/unit/guard_test.rb +0 -909
@@ -0,0 +1,40 @@
|
|
1
|
+
module StateMachine
|
2
|
+
module Integrations #:nodoc:
|
3
|
+
module Sequel
|
4
|
+
version '2.8.x - 3.13.x' do
|
5
|
+
def self.active?
|
6
|
+
!defined?(::Sequel::MAJOR) || ::Sequel::MAJOR == 2 || ::Sequel::MAJOR == 3 && ::Sequel::MINOR <= 13
|
7
|
+
end
|
8
|
+
|
9
|
+
def handle_validation_failure
|
10
|
+
lambda do |object, args, yielded, result|
|
11
|
+
object.instance_eval do
|
12
|
+
raise_on_save_failure ? save_failure(:validation) : result
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def handle_save_failure
|
18
|
+
lambda do |object|
|
19
|
+
object.instance_eval do
|
20
|
+
save_failure(:save)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
version '2.8.x - 2.11.x' do
|
27
|
+
def self.active?
|
28
|
+
!defined?(::Sequel::MAJOR) || ::Sequel::MAJOR == 2 && ::Sequel::MINOR <= 11
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_inflector
|
32
|
+
end
|
33
|
+
|
34
|
+
def action_hook
|
35
|
+
action == :save ? :save : super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -8,6 +8,7 @@ require 'state_machine/callback'
|
|
8
8
|
require 'state_machine/node_collection'
|
9
9
|
require 'state_machine/state_collection'
|
10
10
|
require 'state_machine/event_collection'
|
11
|
+
require 'state_machine/path_collection'
|
11
12
|
require 'state_machine/matcher_helpers'
|
12
13
|
|
13
14
|
module StateMachine
|
@@ -90,8 +91,8 @@ module StateMachine
|
|
90
91
|
# action being invoked (and not a superclass), then it must manually run the
|
91
92
|
# StateMachine hook that checks for event attributes.
|
92
93
|
#
|
93
|
-
# For example, in ActiveRecord, DataMapper, MongoMapper, and Sequel,
|
94
|
-
# default action (+save+) is already defined in a base class. As a result,
|
94
|
+
# For example, in ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel,
|
95
|
+
# the default action (+save+) is already defined in a base class. As a result,
|
95
96
|
# when a state machine is defined in a model / resource, StateMachine can
|
96
97
|
# automatically hook into the +save+ action.
|
97
98
|
#
|
@@ -290,10 +291,10 @@ module StateMachine
|
|
290
291
|
#
|
291
292
|
# When a state machine is defined for classes using any of the above libraries,
|
292
293
|
# it will try to automatically determine the integration to use (Agnostic,
|
293
|
-
# ActiveModel, ActiveRecord, DataMapper, MongoMapper, or Sequel)
|
294
|
-
# class definition. To see how each integration affects the
|
295
|
-
# behavior, refer to all constants defined under the
|
296
|
-
# namespace.
|
294
|
+
# ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, or Sequel)
|
295
|
+
# based on the class definition. To see how each integration affects the
|
296
|
+
# machine's behavior, refer to all constants defined under the
|
297
|
+
# StateMachine::Integrations namespace.
|
297
298
|
class Machine
|
298
299
|
include Assertions
|
299
300
|
include EvalHelpers
|
@@ -416,7 +417,7 @@ module StateMachine
|
|
416
417
|
# Creates a new state machine for the given attribute
|
417
418
|
def initialize(owner_class, *args, &block)
|
418
419
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
419
|
-
assert_valid_keys(options, :attribute, :initial, :action, :plural, :namespace, :integration, :messages, :use_transactions)
|
420
|
+
assert_valid_keys(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
|
420
421
|
|
421
422
|
# Find an integration that matches this machine's owner class
|
422
423
|
if options.include?(:integration)
|
@@ -427,22 +428,24 @@ module StateMachine
|
|
427
428
|
|
428
429
|
if integration
|
429
430
|
extend integration
|
430
|
-
options = integration.defaults.merge(options)
|
431
|
+
options = (integration.defaults || {}).merge(options)
|
431
432
|
end
|
432
433
|
|
433
434
|
# Add machine-wide defaults
|
434
|
-
options = {:use_transactions => true}.merge(options)
|
435
|
+
options = {:use_transactions => true, :initialize => true}.merge(options)
|
435
436
|
|
436
437
|
# Set machine configuration
|
437
438
|
@name = args.first || :state
|
438
439
|
@attribute = options[:attribute] || @name
|
439
440
|
@events = EventCollection.new(self)
|
440
441
|
@states = StateCollection.new(self)
|
441
|
-
@callbacks = {:before => [], :after => []}
|
442
|
+
@callbacks = {:before => [], :after => [], :failure => []}
|
442
443
|
@namespace = options[:namespace]
|
443
444
|
@messages = options[:messages] || {}
|
444
445
|
@action = options[:action]
|
445
446
|
@use_transactions = options[:use_transactions]
|
447
|
+
@initialize_state = options[:initialize]
|
448
|
+
@helpers = {:instance => {}, :class => {}}
|
446
449
|
self.owner_class = owner_class
|
447
450
|
self.initial_state = options[:initial]
|
448
451
|
|
@@ -465,7 +468,8 @@ module StateMachine
|
|
465
468
|
@events.machine = self
|
466
469
|
@states = @states.dup
|
467
470
|
@states.machine = self
|
468
|
-
@callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup}
|
471
|
+
@callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup, :failure => @callbacks[:failure].dup}
|
472
|
+
@helpers = {:instance => @helpers[:instance].dup, :class => @helpers[:class].dup}
|
469
473
|
end
|
470
474
|
|
471
475
|
# Sets the class which is the owner of this state machine. Any methods
|
@@ -475,11 +479,10 @@ module StateMachine
|
|
475
479
|
@owner_class = klass
|
476
480
|
|
477
481
|
# Create modules for extending the class with state/event-specific methods
|
478
|
-
|
479
|
-
instance_helper_module = @instance_helper_module = Module.new
|
482
|
+
@helper_modules = helper_modules = {:instance => Module.new, :class => Module.new}
|
480
483
|
owner_class.class_eval do
|
481
|
-
extend
|
482
|
-
include
|
484
|
+
extend helper_modules[:class]
|
485
|
+
include helper_modules[:instance]
|
483
486
|
end
|
484
487
|
|
485
488
|
# Add class-/instance-level methods to the owner class for state initialization
|
@@ -489,7 +492,7 @@ module StateMachine
|
|
489
492
|
include StateMachine::InstanceMethods
|
490
493
|
end
|
491
494
|
|
492
|
-
define_state_initializer
|
495
|
+
define_state_initializer if @initialize_state
|
493
496
|
end
|
494
497
|
|
495
498
|
# Record this machine as matched to the name in the current owner class.
|
@@ -508,10 +511,10 @@ module StateMachine
|
|
508
511
|
states.each {|state| state.initial = (state.name == @initial_state)}
|
509
512
|
end
|
510
513
|
|
511
|
-
# Initializes the state on the given object.
|
512
|
-
#
|
513
|
-
def initialize_state(object)
|
514
|
-
write(object, :state, initial_state(object).value)
|
514
|
+
# Initializes the state on the given object. Initial values are only set if
|
515
|
+
# the machine's attribute hasn't been previously initialized.
|
516
|
+
def initialize_state(object, options = {})
|
517
|
+
write(object, :state, initial_state(object).value) if initialize_state?(object, options)
|
515
518
|
end
|
516
519
|
|
517
520
|
# Gets the actual name of the attribute on the machine's owner class that
|
@@ -520,43 +523,42 @@ module StateMachine
|
|
520
523
|
name == :state ? @attribute : :"#{self.name}_#{name}"
|
521
524
|
end
|
522
525
|
|
523
|
-
# Defines a new
|
524
|
-
#
|
526
|
+
# Defines a new helper method in an instance or class scope with the given
|
527
|
+
# name. If the method is already defined in the scope, then this will not
|
525
528
|
# override it.
|
526
529
|
#
|
527
530
|
# Example:
|
528
531
|
#
|
529
|
-
#
|
532
|
+
# # Instance helper
|
533
|
+
# machine.define_helper(:instance, :state_name) do |machine, object, _super|
|
530
534
|
# machine.states.match(object)
|
531
535
|
# end
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
536
|
+
#
|
537
|
+
# # Class helper
|
538
|
+
# machine.define_helper(:class, :state_machine_name) do |machine, klass, _super|
|
539
|
+
# "State"
|
540
|
+
# end
|
541
|
+
def define_helper(scope, method, &block)
|
542
|
+
@helpers.fetch(scope)[method] = block
|
543
|
+
@helper_modules.fetch(scope).class_eval <<-end_eval, __FILE__, __LINE__
|
544
|
+
def #{method}(*args)
|
545
|
+
_super = lambda {|*new_args| new_args.empty? ? super(*args) : super(*new_args)}
|
546
|
+
#{scope == :class ? 'self' : 'self.class'}.state_machine(#{name.inspect}).call_helper(#{scope.inspect}, #{method.inspect}, self, _super, *args)
|
538
547
|
end
|
539
|
-
|
548
|
+
end_eval
|
540
549
|
end
|
541
|
-
attr_reader :instance_helper_module
|
542
550
|
|
543
|
-
#
|
544
|
-
# class. If the method is already defined in the class, then this will not
|
545
|
-
# override it.
|
551
|
+
# Invokes the helper method defined in the given scope.
|
546
552
|
#
|
547
553
|
# Example:
|
548
554
|
#
|
549
|
-
#
|
550
|
-
#
|
551
|
-
#
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
@
|
556
|
-
define_method(method) do |*args|
|
557
|
-
block.call(self.state_machine(name), self, *args)
|
558
|
-
end
|
559
|
-
end
|
555
|
+
# # Instance helper
|
556
|
+
# machine.call_helper(:instance, :state_name, self, lambda {super})
|
557
|
+
#
|
558
|
+
# # Class helper
|
559
|
+
# machine.call_helper(:class, :state_machine_name, self, lambda {super})
|
560
|
+
def call_helper(scope, method, object, _super, *args)
|
561
|
+
@helpers.fetch(scope).fetch(method).call(self, object, _super, *args)
|
560
562
|
end
|
561
563
|
|
562
564
|
# Gets the initial state of the machine for the given object. If a dynamic
|
@@ -901,8 +903,9 @@ module StateMachine
|
|
901
903
|
# Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
|
902
904
|
# vehicle.state # => "idling"
|
903
905
|
# vehicle.event # => "park"
|
904
|
-
def write(object, attribute, value)
|
905
|
-
|
906
|
+
def write(object, attribute, value, ivar = false)
|
907
|
+
attribute = self.attribute(attribute)
|
908
|
+
ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
|
906
909
|
end
|
907
910
|
|
908
911
|
# Defines one or more events for the machine and the transitions that can
|
@@ -920,14 +923,6 @@ module StateMachine
|
|
920
923
|
#
|
921
924
|
# The following instance methods are generated when a new event is defined
|
922
925
|
# (the "park" event is used as an example):
|
923
|
-
# * <tt>can_park?</tt> - Checks whether the "park" event can be fired given
|
924
|
-
# the current state of the object. This will *not* run validations in
|
925
|
-
# ORM integrations. To check whether an event can fire *and* passes
|
926
|
-
# validations, use event attributes (e.g. state_event) as described in the
|
927
|
-
# "Events" documentation of each ORM integration.
|
928
|
-
# * <tt>park_transition</tt> - Gets the next transition that would be
|
929
|
-
# performed if the "park" event were to be fired now on the object or nil
|
930
|
-
# if no transitions can be performed.
|
931
926
|
# * <tt>park(..., run_action = true)</tt> - Fires the "park" event,
|
932
927
|
# transitioning from the current state to the next valid state. If the
|
933
928
|
# last argument is a boolean, it will control whether the machine's action
|
@@ -937,6 +932,14 @@ module StateMachine
|
|
937
932
|
# transition fails, then a StateMachine::InvalidTransition error will be
|
938
933
|
# raised. If the last argument is a boolean, it will control whether the
|
939
934
|
# machine's action gets run.
|
935
|
+
# * <tt>can_park?(requirements = {})</tt> - Checks whether the "park" event can be fired given
|
936
|
+
# the current state of the object. This will *not* run validations in
|
937
|
+
# ORM integrations. To check whether an event can fire *and* passes
|
938
|
+
# validations, use event attributes (e.g. state_event) as described in the
|
939
|
+
# "Events" documentation of each ORM integration.
|
940
|
+
# * <tt>park_transition(requirements = {})</tt> - Gets the next transition that would be
|
941
|
+
# performed if the "park" event were to be fired now on the object or nil
|
942
|
+
# if no transitions can be performed.
|
940
943
|
#
|
941
944
|
# With a namespace of "car", the above names map to the following methods:
|
942
945
|
# * <tt>can_park_car?</tt>
|
@@ -944,6 +947,16 @@ module StateMachine
|
|
944
947
|
# * <tt>park_car</tt>
|
945
948
|
# * <tt>park_car!</tt>
|
946
949
|
#
|
950
|
+
# The <tt>can_park?</tt> and <tt>park_transition</tt> helpers both take an
|
951
|
+
# optional set of requirements for determining what transitions are available
|
952
|
+
# for the current object. These requirements include:
|
953
|
+
# * <tt>:from</tt> - One or more states to transition from. If none are
|
954
|
+
# specified, then this will be the object's current state.
|
955
|
+
# * <tt>:to</tt> - One or more states to transition to. If none are
|
956
|
+
# specified, then this will match any to state.
|
957
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
958
|
+
# conditionals defined for each one. Default is true.
|
959
|
+
#
|
947
960
|
# == Defining transitions
|
948
961
|
#
|
949
962
|
# +event+ requires a block which allows you to define the possible
|
@@ -1132,22 +1145,6 @@ module StateMachine
|
|
1132
1145
|
# before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
|
1133
1146
|
# before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
|
1134
1147
|
#
|
1135
|
-
# == Result requirements
|
1136
|
-
#
|
1137
|
-
# By default, +after_transition+ callbacks and code executed after an
|
1138
|
-
# +around_transition+ callback yields will only be run if the transition
|
1139
|
-
# was performed successfully. A transition is successful if the machine's
|
1140
|
-
# action is not configured or does not return false when it is invoked.
|
1141
|
-
# In order to include failed attempts when running an +after_transition+ or
|
1142
|
-
# +around_transition+ callback, the <tt>:include_failures</tt> option can be
|
1143
|
-
# specified like so:
|
1144
|
-
#
|
1145
|
-
# after_transition :include_failures => true, :do => ... # Runs on all attempts to transition, including failures
|
1146
|
-
# after_transition :do => ... # Runs only on successful attempts to transition
|
1147
|
-
#
|
1148
|
-
# around_transition :include_failures => true, :do => ... # Runs on all attempts to transition, including failures
|
1149
|
-
# around_transition :do => ... # Runs only on successful attempts to transition
|
1150
|
-
#
|
1151
1148
|
# == Verbose Requirements
|
1152
1149
|
#
|
1153
1150
|
# Requirements can also be defined using verbose options rather than the
|
@@ -1319,6 +1316,115 @@ module StateMachine
|
|
1319
1316
|
add_callback(:around, options, &block)
|
1320
1317
|
end
|
1321
1318
|
|
1319
|
+
# Creates a callback that will be invoked *after* a transition failures to
|
1320
|
+
# be performed so long as the given requirements match the transition.
|
1321
|
+
#
|
1322
|
+
# See +before_transition+ for a description of the possible configurations
|
1323
|
+
# for defining callbacks. *Note* however that you cannot define the state
|
1324
|
+
# requirements in these callbacks. You may only define event requirements.
|
1325
|
+
#
|
1326
|
+
# = The callback
|
1327
|
+
#
|
1328
|
+
# Failure callbacks get invoked whenever an event fails to execute. This
|
1329
|
+
# can happen when no transition is available, a +before+ callback halts
|
1330
|
+
# execution, or the action associated with this machine fails to succeed.
|
1331
|
+
# In any of these cases, any failure callback that matches the attempted
|
1332
|
+
# transition will be run.
|
1333
|
+
#
|
1334
|
+
# For example,
|
1335
|
+
#
|
1336
|
+
# class Vehicle
|
1337
|
+
# state_machine do
|
1338
|
+
# after_failure do |vehicle, transition|
|
1339
|
+
# logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
|
1340
|
+
# end
|
1341
|
+
#
|
1342
|
+
# after_failure :on => :ignite, :do => :log_ignition_failure
|
1343
|
+
#
|
1344
|
+
# ...
|
1345
|
+
# end
|
1346
|
+
# end
|
1347
|
+
def after_failure(*args, &block)
|
1348
|
+
options = (args.last.is_a?(Hash) ? args.pop : {})
|
1349
|
+
options[:do] = args if args.any?
|
1350
|
+
assert_valid_keys(options, :on, :do, :if, :unless)
|
1351
|
+
|
1352
|
+
add_callback(:failure, options, &block)
|
1353
|
+
end
|
1354
|
+
|
1355
|
+
# Generates a list of the possible transition sequences that can be run on
|
1356
|
+
# the given object. These paths can reveal all of the possible states and
|
1357
|
+
# events that can be encountered in the object's state machine based on the
|
1358
|
+
# object's current state.
|
1359
|
+
#
|
1360
|
+
# Configuration options:
|
1361
|
+
# * +from+ - The initial state to start all paths from. By default, this
|
1362
|
+
# is the object's current state.
|
1363
|
+
# * +to+ - The target state to end all paths on. By default, paths will
|
1364
|
+
# end when they loop back to the first transition on the path.
|
1365
|
+
# * +deep+ - Whether to allow the target state to be crossed more than once
|
1366
|
+
# in a path. By default, paths will immediately stop when the target
|
1367
|
+
# state (if specified) is reached. If this is enabled, then paths can
|
1368
|
+
# continue even after reaching the target state; they will stop when
|
1369
|
+
# reaching the target state a second time.
|
1370
|
+
#
|
1371
|
+
# *Note* that the object is never modified when the list of paths is
|
1372
|
+
# generated.
|
1373
|
+
#
|
1374
|
+
# == Examples
|
1375
|
+
#
|
1376
|
+
# class Vehicle
|
1377
|
+
# state_machine :initial => :parked do
|
1378
|
+
# event :ignite do
|
1379
|
+
# transition :parked => :idling
|
1380
|
+
# end
|
1381
|
+
#
|
1382
|
+
# event :shift_up do
|
1383
|
+
# transition :idling => :first_gear, :first_gear => :second_gear
|
1384
|
+
# end
|
1385
|
+
#
|
1386
|
+
# event :shift_down do
|
1387
|
+
# transition :second_gear => :first_gear, :first_gear => :idling
|
1388
|
+
# end
|
1389
|
+
# end
|
1390
|
+
# end
|
1391
|
+
#
|
1392
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
|
1393
|
+
# vehicle.state # => "parked"
|
1394
|
+
#
|
1395
|
+
# vehicle.state_paths
|
1396
|
+
# # => [
|
1397
|
+
# # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
1398
|
+
# # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
|
1399
|
+
# # #<StateMachine::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
|
1400
|
+
# # #<StateMachine::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
|
1401
|
+
# # #<StateMachine::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
|
1402
|
+
# #
|
1403
|
+
# # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
1404
|
+
# # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
|
1405
|
+
# # #<StateMachine::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>]
|
1406
|
+
# # ]
|
1407
|
+
#
|
1408
|
+
# vehicle.state_paths(:from => :parked, :to => :second_gear)
|
1409
|
+
# # => [
|
1410
|
+
# # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
1411
|
+
# # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
|
1412
|
+
# # #<StateMachine::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>]
|
1413
|
+
# # ]
|
1414
|
+
#
|
1415
|
+
# In addition to getting the possible paths that can be accessed, you can
|
1416
|
+
# also get summary information about the states / events that can be
|
1417
|
+
# accessed at some point along one of the paths. For example:
|
1418
|
+
#
|
1419
|
+
# # Get the list of states that can be accessed from the current state
|
1420
|
+
# vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear]
|
1421
|
+
#
|
1422
|
+
# # Get the list of events that can be accessed from the current state
|
1423
|
+
# vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
|
1424
|
+
def paths_for(object, requirements = {})
|
1425
|
+
PathCollection.new(object, self, requirements)
|
1426
|
+
end
|
1427
|
+
|
1322
1428
|
# Marks the given object as invalid with the given message.
|
1323
1429
|
#
|
1324
1430
|
# By default, this is a no-op.
|
@@ -1400,16 +1506,12 @@ module StateMachine
|
|
1400
1506
|
|
1401
1507
|
# Generate the graph
|
1402
1508
|
graphvizVersion = Constants::RGV_VERSION.split('.')
|
1509
|
+
file = File.join(options[:path], "#{options[:name]}.#{options[:format]}")
|
1403
1510
|
|
1404
1511
|
if graphvizVersion[1] == '9' && graphvizVersion[2] == '0'
|
1405
|
-
outputOptions = {
|
1406
|
-
:output => options[:format],
|
1407
|
-
:file => File.join(options[:path], "#{options[:name]}.#{options[:format]}")
|
1408
|
-
}
|
1512
|
+
outputOptions = {:output => options[:format], :file => file}
|
1409
1513
|
else
|
1410
|
-
outputOptions = {
|
1411
|
-
options[:format] => File.join(options[:path], "#{options[:name]}.#{options[:format]}")
|
1412
|
-
}
|
1514
|
+
outputOptions = {options[:format] => file}
|
1413
1515
|
end
|
1414
1516
|
|
1415
1517
|
graph.output(outputOptions)
|
@@ -1420,10 +1522,10 @@ module StateMachine
|
|
1420
1522
|
end
|
1421
1523
|
end
|
1422
1524
|
|
1423
|
-
# Determines whether
|
1424
|
-
# event transitions when the
|
1425
|
-
def
|
1426
|
-
@
|
1525
|
+
# Determines whether an action hook was defined for firing attribute-based
|
1526
|
+
# event transitions when the configured action gets called.
|
1527
|
+
def action_hook?(self_only = false)
|
1528
|
+
@action_hook_defined || !self_only && owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self && machine.action_hook?(true)}
|
1427
1529
|
end
|
1428
1530
|
|
1429
1531
|
protected
|
@@ -1431,13 +1533,21 @@ module StateMachine
|
|
1431
1533
|
def after_initialize
|
1432
1534
|
end
|
1433
1535
|
|
1536
|
+
# Determines if the machine's attribute needs to be initialized. This
|
1537
|
+
# will only be true if the machine's attribute is blank.
|
1538
|
+
def initialize_state?(object, options = {})
|
1539
|
+
value = read(object, :state)
|
1540
|
+
value.nil? || value.respond_to?(:empty?) && value.empty?
|
1541
|
+
end
|
1542
|
+
|
1434
1543
|
# Adds helper methods for interacting with the state machine, including
|
1435
1544
|
# for states, events, and transitions
|
1436
1545
|
def define_helpers
|
1437
1546
|
define_state_accessor
|
1438
1547
|
define_state_predicate
|
1439
1548
|
define_event_helpers
|
1440
|
-
|
1549
|
+
define_path_helpers
|
1550
|
+
define_action_helpers if define_action_helpers?
|
1441
1551
|
define_name_helpers
|
1442
1552
|
end
|
1443
1553
|
|
@@ -1445,29 +1555,24 @@ module StateMachine
|
|
1445
1555
|
# are set prior to the original initialize method and dynamic values are
|
1446
1556
|
# set *after* the initialize method in case it is dependent on it.
|
1447
1557
|
def define_state_initializer
|
1448
|
-
|
1449
|
-
|
1450
|
-
|
1451
|
-
super
|
1452
|
-
initialize_state_machines(:dynamic => true)
|
1453
|
-
end
|
1454
|
-
end_eval
|
1558
|
+
define_helper(:instance, :initialize) do |machine, object, _super, *|
|
1559
|
+
object.class.state_machines.initialize_states(object) { _super.call }
|
1560
|
+
end
|
1455
1561
|
end
|
1456
1562
|
|
1457
1563
|
# Adds reader/writer methods for accessing the state attribute
|
1458
1564
|
def define_state_accessor
|
1459
1565
|
attribute = self.attribute
|
1460
1566
|
|
1461
|
-
@
|
1462
|
-
attr_accessor attribute
|
1463
|
-
end
|
1567
|
+
@helper_modules[:instance].class_eval { attr_accessor attribute }
|
1464
1568
|
end
|
1465
1569
|
|
1466
1570
|
# Adds predicate method to the owner class for determining the name of the
|
1467
1571
|
# current state
|
1468
1572
|
def define_state_predicate
|
1469
|
-
|
1470
|
-
|
1573
|
+
call_super = owner_class_ancestor_has_method?("#{name}?")
|
1574
|
+
define_helper(:instance, "#{name}?") do |machine, object, _super, *args|
|
1575
|
+
args.empty? && call_super ? _super.call : machine.states.matches?(object, args.first)
|
1471
1576
|
end
|
1472
1577
|
end
|
1473
1578
|
|
@@ -1475,61 +1580,93 @@ module StateMachine
|
|
1475
1580
|
# events
|
1476
1581
|
def define_event_helpers
|
1477
1582
|
# Gets the events that are allowed to fire on the current object
|
1478
|
-
|
1479
|
-
machine.events.valid_for(object).map {|event| event.name}
|
1583
|
+
define_helper(:instance, attribute(:events)) do |machine, object, _super, *args|
|
1584
|
+
machine.events.valid_for(object, *args).map {|event| event.name}
|
1480
1585
|
end
|
1481
1586
|
|
1482
1587
|
# Gets the next possible transitions that can be run on the current
|
1483
1588
|
# object
|
1484
|
-
|
1589
|
+
define_helper(:instance, attribute(:transitions)) do |machine, object, _super, *args|
|
1485
1590
|
machine.events.transitions_for(object, *args)
|
1486
1591
|
end
|
1487
1592
|
|
1488
|
-
# Add helpers for
|
1593
|
+
# Add helpers for tracking the event / transition to invoke when the
|
1594
|
+
# action is called
|
1489
1595
|
if action
|
1490
|
-
# Tracks the event / transition to invoke when the action is called
|
1491
1596
|
event_attribute = attribute(:event)
|
1492
|
-
|
1493
|
-
|
1494
|
-
attr_writer event_attribute
|
1495
|
-
|
1496
|
-
protected
|
1497
|
-
attr_accessor event_transition_attribute
|
1498
|
-
end
|
1499
|
-
|
1500
|
-
# Interpret non-blank events as present
|
1501
|
-
define_instance_method(attribute(:event)) do |machine, object|
|
1597
|
+
define_helper(:instance, event_attribute) do |machine, object, *|
|
1598
|
+
# Interpret non-blank events as present
|
1502
1599
|
event = machine.read(object, :event, true)
|
1503
1600
|
event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
|
1504
1601
|
end
|
1602
|
+
|
1603
|
+
# A roundabout way of writing the attribute is used here so that
|
1604
|
+
# integrations can hook into this modification
|
1605
|
+
define_helper(:instance, "#{event_attribute}=") do |machine, object, _super, value|
|
1606
|
+
machine.write(object, :event, value, true)
|
1607
|
+
end
|
1608
|
+
|
1609
|
+
event_transition_attribute = attribute(:event_transition)
|
1610
|
+
@helper_modules[:instance].class_eval { protected; attr_accessor event_transition_attribute }
|
1505
1611
|
end
|
1506
1612
|
end
|
1507
1613
|
|
1614
|
+
# Adds helper methods for getting information about this state machine's
|
1615
|
+
# available transition paths
|
1616
|
+
def define_path_helpers
|
1617
|
+
# Gets the paths of transitions available to the current object
|
1618
|
+
define_helper(:instance, attribute(:paths)) do |machine, object, _super, *args|
|
1619
|
+
machine.paths_for(object, *args)
|
1620
|
+
end
|
1621
|
+
end
|
1622
|
+
|
1623
|
+
# Determines whether action helpers should be defined for this machine.
|
1624
|
+
# This is only true if there is an action configured and no other machines
|
1625
|
+
# have process this same configuration already.
|
1626
|
+
def define_action_helpers?
|
1627
|
+
action && !owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
|
1628
|
+
end
|
1629
|
+
|
1508
1630
|
# Adds helper methods for automatically firing events when an action
|
1509
1631
|
# is invoked
|
1510
|
-
def define_action_helpers
|
1511
|
-
|
1512
|
-
|
1513
|
-
|
1632
|
+
def define_action_helpers
|
1633
|
+
if action_hook
|
1634
|
+
@action_hook_defined = true
|
1635
|
+
define_action_hook
|
1514
1636
|
end
|
1515
|
-
|
1637
|
+
end
|
1638
|
+
|
1639
|
+
# Hooks directly into actions by defining the same method in an included
|
1640
|
+
# module. As a result, when the action gets invoked, any state events
|
1641
|
+
# defined for the object will get run. Method visibility is preserved.
|
1642
|
+
def define_action_hook
|
1643
|
+
action_hook = self.action_hook
|
1644
|
+
action = self.action
|
1645
|
+
private_action_hook = owner_class.private_method_defined?(action_hook)
|
1516
1646
|
|
1517
|
-
# Only define helper if
|
1518
|
-
|
1519
|
-
|
1520
|
-
|
1521
|
-
|
1522
|
-
|
1523
|
-
|
1524
|
-
|
1525
|
-
|
1526
|
-
|
1527
|
-
|
1528
|
-
|
1529
|
-
|
1530
|
-
|
1531
|
-
|
1532
|
-
|
1647
|
+
# Only define helper if it hasn't
|
1648
|
+
define_helper(:instance, action_hook) do |machine, object, _super, *args|
|
1649
|
+
object.class.state_machines.transitions(object, action).perform { _super.call }
|
1650
|
+
end
|
1651
|
+
|
1652
|
+
@helper_modules[:instance].class_eval { private action_hook } if private_action_hook
|
1653
|
+
end
|
1654
|
+
|
1655
|
+
# The method to hook into for triggering transitions when invoked. By
|
1656
|
+
# default, this is the action configured for the machine.
|
1657
|
+
#
|
1658
|
+
# Since the default hook technique relies on module inheritance, the
|
1659
|
+
# action must be defined in an ancestor of the owner classs in order for
|
1660
|
+
# it to be the action hook.
|
1661
|
+
def action_hook
|
1662
|
+
action && owner_class_ancestor_has_method?(action) ? action : nil
|
1663
|
+
end
|
1664
|
+
|
1665
|
+
# Determines whether any of the ancestors for this machine's owner class
|
1666
|
+
# has the given method defined, even if it's private.
|
1667
|
+
def owner_class_ancestor_has_method?(method)
|
1668
|
+
owner_class.ancestors.any? do |ancestor|
|
1669
|
+
ancestor != owner_class && (ancestor.method_defined?(method) || ancestor.private_method_defined?(method))
|
1533
1670
|
end
|
1534
1671
|
end
|
1535
1672
|
|
@@ -1537,22 +1674,22 @@ module StateMachine
|
|
1537
1674
|
# events on the owner class
|
1538
1675
|
def define_name_helpers
|
1539
1676
|
# Gets the humanized version of a state
|
1540
|
-
|
1677
|
+
define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, _super, state|
|
1541
1678
|
machine.states.fetch(state).human_name(klass)
|
1542
1679
|
end
|
1543
1680
|
|
1544
1681
|
# Gets the humanized version of an event
|
1545
|
-
|
1682
|
+
define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, _super, event|
|
1546
1683
|
machine.events.fetch(event).human_name(klass)
|
1547
1684
|
end
|
1548
1685
|
|
1549
1686
|
# Gets the state name for the current value
|
1550
|
-
|
1687
|
+
define_helper(:instance, attribute(:name)) do |machine, object, *|
|
1551
1688
|
machine.states.match!(object).name
|
1552
1689
|
end
|
1553
1690
|
|
1554
1691
|
# Gets the human state name for the current value
|
1555
|
-
|
1692
|
+
define_helper(:instance, "human_#{attribute(:name)}") do |machine, object, *|
|
1556
1693
|
machine.states.match!(object).human_name(object.class)
|
1557
1694
|
end
|
1558
1695
|
end
|
@@ -1572,7 +1709,7 @@ module StateMachine
|
|
1572
1709
|
if scope = send("create_#{kind}_scope", method)
|
1573
1710
|
# Converts state names to their corresponding values so that they
|
1574
1711
|
# can be looked up properly
|
1575
|
-
|
1712
|
+
define_helper(:class, method) do |machine, klass, _super, *states|
|
1576
1713
|
values = states.flatten.map {|state| machine.states.fetch(state).value}
|
1577
1714
|
scope.call(klass, values)
|
1578
1715
|
end
|