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.
Files changed (68) hide show
  1. data/CHANGELOG.rdoc +20 -0
  2. data/LICENSE +1 -1
  3. data/README.rdoc +74 -4
  4. data/Rakefile +3 -3
  5. data/lib/state_machine.rb +51 -24
  6. data/lib/state_machine/{guard.rb → branch.rb} +34 -40
  7. data/lib/state_machine/callback.rb +13 -18
  8. data/lib/state_machine/error.rb +13 -0
  9. data/lib/state_machine/eval_helpers.rb +3 -0
  10. data/lib/state_machine/event.rb +67 -30
  11. data/lib/state_machine/event_collection.rb +20 -3
  12. data/lib/state_machine/extensions.rb +3 -3
  13. data/lib/state_machine/integrations.rb +7 -0
  14. data/lib/state_machine/integrations/active_model.rb +149 -59
  15. data/lib/state_machine/integrations/active_model/versions.rb +30 -0
  16. data/lib/state_machine/integrations/active_record.rb +74 -148
  17. data/lib/state_machine/integrations/active_record/locale.rb +0 -7
  18. data/lib/state_machine/integrations/active_record/versions.rb +149 -0
  19. data/lib/state_machine/integrations/base.rb +64 -0
  20. data/lib/state_machine/integrations/data_mapper.rb +50 -39
  21. data/lib/state_machine/integrations/data_mapper/observer.rb +47 -12
  22. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  23. data/lib/state_machine/integrations/mongo_mapper.rb +37 -64
  24. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  25. data/lib/state_machine/integrations/mongo_mapper/versions.rb +102 -0
  26. data/lib/state_machine/integrations/mongoid.rb +297 -0
  27. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  28. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  29. data/lib/state_machine/integrations/sequel.rb +99 -55
  30. data/lib/state_machine/integrations/sequel/versions.rb +40 -0
  31. data/lib/state_machine/machine.rb +273 -136
  32. data/lib/state_machine/machine_collection.rb +21 -13
  33. data/lib/state_machine/node_collection.rb +6 -1
  34. data/lib/state_machine/path.rb +120 -0
  35. data/lib/state_machine/path_collection.rb +90 -0
  36. data/lib/state_machine/state.rb +28 -9
  37. data/lib/state_machine/state_collection.rb +1 -1
  38. data/lib/state_machine/transition.rb +65 -6
  39. data/lib/state_machine/transition_collection.rb +1 -1
  40. data/test/files/en.yml +8 -0
  41. data/test/functional/state_machine_test.rb +15 -2
  42. data/test/unit/branch_test.rb +890 -0
  43. data/test/unit/callback_test.rb +9 -36
  44. data/test/unit/error_test.rb +43 -0
  45. data/test/unit/event_collection_test.rb +67 -33
  46. data/test/unit/event_test.rb +165 -38
  47. data/test/unit/integrations/active_model_test.rb +103 -3
  48. data/test/unit/integrations/active_record_test.rb +90 -43
  49. data/test/unit/integrations/base_test.rb +87 -0
  50. data/test/unit/integrations/data_mapper_test.rb +105 -44
  51. data/test/unit/integrations/mongo_mapper_test.rb +261 -64
  52. data/test/unit/integrations/mongoid_test.rb +1529 -0
  53. data/test/unit/integrations/sequel_test.rb +33 -49
  54. data/test/unit/integrations_test.rb +4 -0
  55. data/test/unit/invalid_event_test.rb +15 -2
  56. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  57. data/test/unit/invalid_transition_test.rb +72 -2
  58. data/test/unit/machine_collection_test.rb +55 -61
  59. data/test/unit/machine_test.rb +388 -26
  60. data/test/unit/node_collection_test.rb +14 -4
  61. data/test/unit/path_collection_test.rb +266 -0
  62. data/test/unit/path_test.rb +485 -0
  63. data/test/unit/state_collection_test.rb +30 -0
  64. data/test/unit/state_test.rb +82 -35
  65. data/test/unit/transition_collection_test.rb +48 -44
  66. data/test/unit/transition_test.rb +198 -41
  67. metadata +111 -74
  68. 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, the
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) based on the
294
- # class definition. To see how each integration affects the machine's
295
- # behavior, refer to all constants defined under the StateMachine::Integrations
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) if integration.respond_to?(:defaults)
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
- class_helper_module = @class_helper_module = Module.new
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 class_helper_module
482
- include instance_helper_module
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. This will always write to the
512
- # attribute regardless of whether a value is already present.
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 instance method with the given name on the machine's owner
524
- # class. If the method is already defined in the class, then this will not
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
- # machine.define_instance_method(:state_name) do |machine, object|
532
+ # # Instance helper
533
+ # machine.define_helper(:instance, :state_name) do |machine, object, _super|
530
534
  # machine.states.match(object)
531
535
  # end
532
- def define_instance_method(method, &block)
533
- name = self.name
534
-
535
- @instance_helper_module.class_eval do
536
- define_method(method) do |*args|
537
- block.call(self.class.state_machine(name), self, *args)
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
- end
548
+ end_eval
540
549
  end
541
- attr_reader :instance_helper_module
542
550
 
543
- # Defines a new class method with the given name on the machine's owner
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
- # machine.define_class_method(:states) do |machine, klass|
550
- # machine.states.keys
551
- # end
552
- def define_class_method(method, &block)
553
- name = self.name
554
-
555
- @class_helper_module.class_eval do
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
- object.send("#{self.attribute(attribute)}=", value)
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 a helper method was defined for firing attribute-based
1424
- # event transitions when the configuration action gets called.
1425
- def action_helper_defined?
1426
- @action_helper_defined
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
- define_action_helpers if action
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
- @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
1449
- def initialize(*args)
1450
- initialize_state_machines(:dynamic => false)
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
- @instance_helper_module.class_eval do
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
- define_instance_method("#{name}?") do |machine, object, state|
1470
- machine.states.matches?(object, state)
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
- define_instance_method(attribute(:events)) do |machine, object|
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
- define_instance_method(attribute(:transitions)) do |machine, object, *args|
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 interacting with the action
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
- event_transition_attribute = attribute(:event_transition)
1493
- @instance_helper_module.class_eval do
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(action_hook = self.action)
1511
- private_action = owner_class.private_method_defined?(action_hook)
1512
- action_defined = @action_helper_defined = owner_class.ancestors.any? do |ancestor|
1513
- ancestor != owner_class && (ancestor.method_defined?(action_hook) || ancestor.private_method_defined?(action_hook))
1632
+ def define_action_helpers
1633
+ if action_hook
1634
+ @action_hook_defined = true
1635
+ define_action_hook
1514
1636
  end
1515
- action_overridden = owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
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
- # 1. Action was originally defined somewhere other than the owner class
1519
- # 2. It hasn't already been overridden by another machine
1520
- if action_defined && !action_overridden
1521
- action = self.action
1522
- @instance_helper_module.class_eval do
1523
- define_method(action_hook) do |*args|
1524
- self.class.state_machines.transitions(self, action).perform { super(*args) }
1525
- end
1526
-
1527
- private action_hook if private_action
1528
- end
1529
-
1530
- true
1531
- else
1532
- false
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
- define_class_method("human_#{attribute(:name)}") do |machine, klass, state|
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
- define_class_method("human_#{attribute(:event_name)}") do |machine, klass, event|
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
- define_instance_method(attribute(:name)) do |machine, object|
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
- define_instance_method("human_#{attribute(:name)}") do |machine, object|
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
- define_class_method(method) do |machine, klass, *states|
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