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.
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