state_machine 0.9.4 → 0.10.0
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 +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
|