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
| @@ -1,20 +1,28 @@ | |
| 1 1 | 
             
            module StateMachine
         | 
| 2 2 | 
             
              # Represents a collection of state machines for a class
         | 
| 3 3 | 
             
              class MachineCollection < Hash
         | 
| 4 | 
            -
                # Initializes the state of each machine in the given object.   | 
| 5 | 
            -
                #  | 
| 6 | 
            -
                #  | 
| 4 | 
            +
                # Initializes the state of each machine in the given object.  This can allow
         | 
| 5 | 
            +
                # states to be initialized in two groups: static and dynamic.  For example:
         | 
| 6 | 
            +
                # 
         | 
| 7 | 
            +
                #   machines.initialize_states(object) do
         | 
| 8 | 
            +
                #     # After static state initialization, before dynamic state initialization
         | 
| 9 | 
            +
                #   end
         | 
| 10 | 
            +
                # 
         | 
| 11 | 
            +
                # If no block is provided, then all states will still be initialized.
         | 
| 7 12 | 
             
                def initialize_states(object, options = {})
         | 
| 8 | 
            -
                   | 
| 9 | 
            -
             | 
| 10 | 
            -
                   | 
| 13 | 
            +
                  options = {:static => true, :dynamic => true}.merge(options)
         | 
| 14 | 
            +
                  
         | 
| 15 | 
            +
                  each_value do |machine| 
         | 
| 16 | 
            +
                    machine.initialize_state(object, options) unless machine.dynamic_initial_state?
         | 
| 17 | 
            +
                  end if options[:static]
         | 
| 18 | 
            +
                  
         | 
| 19 | 
            +
                  result = yield if block_given?
         | 
| 11 20 |  | 
| 12 21 | 
             
                  each_value do |machine|
         | 
| 13 | 
            -
                     | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
                  end
         | 
| 22 | 
            +
                    machine.initialize_state(object, options) if machine.dynamic_initial_state?
         | 
| 23 | 
            +
                  end if options[:dynamic]
         | 
| 24 | 
            +
                  
         | 
| 25 | 
            +
                  result
         | 
| 18 26 | 
             
                end
         | 
| 19 27 |  | 
| 20 28 | 
             
                # Runs one or more events in parallel on the given object.  See
         | 
| @@ -28,12 +36,12 @@ module StateMachine | |
| 28 36 | 
             
                    event = nil
         | 
| 29 37 | 
             
                    detect {|name, machine| event = machine.events[event_name, :qualified_name]}
         | 
| 30 38 |  | 
| 31 | 
            -
                    raise | 
| 39 | 
            +
                    raise(InvalidEvent.new(object, event_name)) unless event
         | 
| 32 40 |  | 
| 33 41 | 
             
                    # Get the transition that will be performed for the event
         | 
| 34 42 | 
             
                    unless transition = event.transition_for(object)
         | 
| 35 43 | 
             
                      machine = event.machine
         | 
| 36 | 
            -
                       | 
| 44 | 
            +
                      event.on_failure(object)
         | 
| 37 45 | 
             
                    end
         | 
| 38 46 |  | 
| 39 47 | 
             
                    transition
         | 
| @@ -34,7 +34,7 @@ module StateMachine | |
| 34 34 | 
             
                  nodes = @nodes
         | 
| 35 35 | 
             
                  @nodes = []
         | 
| 36 36 | 
             
                  @indices = @indices.inject({}) {|indices, (name, index)| indices[name] = {}; indices}
         | 
| 37 | 
            -
                  nodes. | 
| 37 | 
            +
                  concat(nodes.map {|n| n.dup})
         | 
| 38 38 | 
             
                end
         | 
| 39 39 |  | 
| 40 40 | 
             
                # Changes the current machine associated with the collection.  In turn, this
         | 
| @@ -62,6 +62,11 @@ module StateMachine | |
| 62 62 | 
             
                  self
         | 
| 63 63 | 
             
                end
         | 
| 64 64 |  | 
| 65 | 
            +
                # Appends a group of nodes to the collection
         | 
| 66 | 
            +
                def concat(nodes)
         | 
| 67 | 
            +
                  nodes.each {|node| self << node}
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
                
         | 
| 65 70 | 
             
                # Updates the indexed keys for the given node.  If the node's attribute
         | 
| 66 71 | 
             
                # has changed since it was added to the collection, the old indexed keys
         | 
| 67 72 | 
             
                # will be replaced with the updated ones.
         | 
| @@ -0,0 +1,120 @@ | |
| 1 | 
            +
            module StateMachine
         | 
| 2 | 
            +
              # A path represents a sequence of transitions that can be run for a particular
         | 
| 3 | 
            +
              # object.  Paths can walk to new transitions, revealing all of the possible
         | 
| 4 | 
            +
              # branches that can be encountered in the object's state machine.
         | 
| 5 | 
            +
              class Path < Array
         | 
| 6 | 
            +
                include Assertions
         | 
| 7 | 
            +
                
         | 
| 8 | 
            +
                # The object whose state machine is being walked
         | 
| 9 | 
            +
                attr_reader :object
         | 
| 10 | 
            +
                
         | 
| 11 | 
            +
                # The state machine this path is walking
         | 
| 12 | 
            +
                attr_reader :machine
         | 
| 13 | 
            +
                
         | 
| 14 | 
            +
                # Creates a new transition path for the given object.  Initially this is an
         | 
| 15 | 
            +
                # empty path.  In order to start walking the path, it must be populated with
         | 
| 16 | 
            +
                # an initial transition.
         | 
| 17 | 
            +
                # 
         | 
| 18 | 
            +
                # Configuration options:
         | 
| 19 | 
            +
                # * <tt>:target</tt> - The target state to end the path on
         | 
| 20 | 
            +
                # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
         | 
| 21 | 
            +
                #   conditionals defined for each one
         | 
| 22 | 
            +
                def initialize(object, machine, options = {})
         | 
| 23 | 
            +
                  assert_valid_keys(options, :target, :guard)
         | 
| 24 | 
            +
                  
         | 
| 25 | 
            +
                  @object = object
         | 
| 26 | 
            +
                  @machine = machine
         | 
| 27 | 
            +
                  @target = options[:target]
         | 
| 28 | 
            +
                  @guard = options[:guard]
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
                
         | 
| 31 | 
            +
                def initialize_copy(orig) #:nodoc:
         | 
| 32 | 
            +
                  super
         | 
| 33 | 
            +
                  @transitions = nil
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
                
         | 
| 36 | 
            +
                # The initial state name for this path
         | 
| 37 | 
            +
                def from_name
         | 
| 38 | 
            +
                  first && first.from_name
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
                
         | 
| 41 | 
            +
                # Lists all of the from states that can be reached through this path.
         | 
| 42 | 
            +
                # 
         | 
| 43 | 
            +
                # For example,
         | 
| 44 | 
            +
                # 
         | 
| 45 | 
            +
                #   path.to_states  # => [:parked, :idling, :first_gear, ...]
         | 
| 46 | 
            +
                def from_states
         | 
| 47 | 
            +
                  map {|transition| transition.from_name}.uniq
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
                
         | 
| 50 | 
            +
                # The end state name for this path.  If a target state was specified for
         | 
| 51 | 
            +
                # the path, then that will be returned if the path is complete.
         | 
| 52 | 
            +
                def to_name
         | 
| 53 | 
            +
                  last && last.to_name
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
                
         | 
| 56 | 
            +
                # Lists all of the to states that can be reached through this path.
         | 
| 57 | 
            +
                # 
         | 
| 58 | 
            +
                # For example,
         | 
| 59 | 
            +
                # 
         | 
| 60 | 
            +
                #   path.to_states  # => [:parked, :idling, :first_gear, ...]
         | 
| 61 | 
            +
                def to_states
         | 
| 62 | 
            +
                  map {|transition| transition.to_name}.uniq
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
                
         | 
| 65 | 
            +
                # Lists all of the events that can be fired through this path.
         | 
| 66 | 
            +
                # 
         | 
| 67 | 
            +
                # For example,
         | 
| 68 | 
            +
                # 
         | 
| 69 | 
            +
                #   path.events # => [:park, :ignite, :shift_up, ...]
         | 
| 70 | 
            +
                def events
         | 
| 71 | 
            +
                  map {|transition| transition.event}.uniq
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
                
         | 
| 74 | 
            +
                # Walks down the next transitions at the end of this path.  This will only
         | 
| 75 | 
            +
                # walk down paths that are considered valid.
         | 
| 76 | 
            +
                def walk
         | 
| 77 | 
            +
                  transitions.each {|transition| yield dup.push(transition)}
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
                
         | 
| 80 | 
            +
                # Determines whether or not this path has completed.  A path is considered
         | 
| 81 | 
            +
                # complete when one of the following conditions is met:
         | 
| 82 | 
            +
                # * The last transition in the path ends on the target state
         | 
| 83 | 
            +
                # * There are no more transitions remaining to walk and there is no target
         | 
| 84 | 
            +
                #   state
         | 
| 85 | 
            +
                def complete?
         | 
| 86 | 
            +
                  !empty? && (@target ? to_name == @target : transitions.empty?)
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
                
         | 
| 89 | 
            +
                private
         | 
| 90 | 
            +
                  # Calculates the number of times the given state has been walked to
         | 
| 91 | 
            +
                  def times_walked_to(state)
         | 
| 92 | 
            +
                    select {|transition| transition.to_name == state}.length
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
                  
         | 
| 95 | 
            +
                  # Determines whether the given transition has been recently walked down in
         | 
| 96 | 
            +
                  # this path.  If a target is configured for this path, then this will only
         | 
| 97 | 
            +
                  # look at transitions walked down since the target was last reached.
         | 
| 98 | 
            +
                  def recently_walked?(transition)
         | 
| 99 | 
            +
                    transitions = self
         | 
| 100 | 
            +
                    if @target && @target != to_name && target_transition = detect {|t| t.to_name == @target}
         | 
| 101 | 
            +
                      transitions = transitions[index(target_transition) + 1..-1]
         | 
| 102 | 
            +
                    end
         | 
| 103 | 
            +
                    transitions.include?(transition)
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
                  
         | 
| 106 | 
            +
                  # Determines whether it's possible to walk to the given transition from
         | 
| 107 | 
            +
                  # the current path.  A transition can be walked to if:
         | 
| 108 | 
            +
                  # * It has not been recently walked and
         | 
| 109 | 
            +
                  # * If a target is specified, it has not been walked to twice yet
         | 
| 110 | 
            +
                  def can_walk_to?(transition)
         | 
| 111 | 
            +
                    !recently_walked?(transition) && (!@target || times_walked_to(@target) < 2)
         | 
| 112 | 
            +
                  end
         | 
| 113 | 
            +
                  
         | 
| 114 | 
            +
                  # Get the next set of transitions that can be walked to starting from the
         | 
| 115 | 
            +
                  # end of this path
         | 
| 116 | 
            +
                  def transitions
         | 
| 117 | 
            +
                    @transitions ||= empty? ? [] : machine.events.transitions_for(object, :from => to_name, :guard => @guard).select {|transition| can_walk_to?(transition)}
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
              end
         | 
| 120 | 
            +
            end
         | 
| @@ -0,0 +1,90 @@ | |
| 1 | 
            +
            require 'state_machine/path'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module StateMachine
         | 
| 4 | 
            +
              # Represents a collection of paths that are generated based on a set of
         | 
| 5 | 
            +
              # requirements regarding what states to start and end on
         | 
| 6 | 
            +
              class PathCollection < Array
         | 
| 7 | 
            +
                include Assertions
         | 
| 8 | 
            +
                
         | 
| 9 | 
            +
                # The object whose state machine is being walked
         | 
| 10 | 
            +
                attr_reader :object
         | 
| 11 | 
            +
                
         | 
| 12 | 
            +
                # The state machine these path are walking
         | 
| 13 | 
            +
                attr_reader :machine
         | 
| 14 | 
            +
                
         | 
| 15 | 
            +
                # The initial state to start each path from
         | 
| 16 | 
            +
                attr_reader :from_name
         | 
| 17 | 
            +
                
         | 
| 18 | 
            +
                # The target state for each path
         | 
| 19 | 
            +
                attr_reader :to_name
         | 
| 20 | 
            +
                
         | 
| 21 | 
            +
                # Creates a new collection of paths with the given requirements.
         | 
| 22 | 
            +
                # 
         | 
| 23 | 
            +
                # Configuration options:
         | 
| 24 | 
            +
                # * <tt>:from</tt> - The initial state to start from
         | 
| 25 | 
            +
                # * <tt>:to</tt> - The target end state
         | 
| 26 | 
            +
                # * <tt>:deep</tt> - Whether to enable deep searches for the target state.
         | 
| 27 | 
            +
                # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
         | 
| 28 | 
            +
                #   conditionals defined for each one
         | 
| 29 | 
            +
                def initialize(object, machine, options = {})
         | 
| 30 | 
            +
                  options = {:deep => false, :from => machine.states.match!(object).name}.merge(options)
         | 
| 31 | 
            +
                  assert_valid_keys(options, :from, :to, :deep, :guard)
         | 
| 32 | 
            +
                  
         | 
| 33 | 
            +
                  @object = object
         | 
| 34 | 
            +
                  @machine = machine
         | 
| 35 | 
            +
                  @from_name = machine.states.fetch(options[:from]).name
         | 
| 36 | 
            +
                  @to_name = options[:to] && machine.states.fetch(options[:to]).name
         | 
| 37 | 
            +
                  @guard = options[:guard]
         | 
| 38 | 
            +
                  @deep = options[:deep]
         | 
| 39 | 
            +
                  
         | 
| 40 | 
            +
                  initial_paths.each {|path| walk(path)}
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
                
         | 
| 43 | 
            +
                # Lists all of the states that can be transitioned from through the paths in
         | 
| 44 | 
            +
                # this collection.
         | 
| 45 | 
            +
                # 
         | 
| 46 | 
            +
                # For example,
         | 
| 47 | 
            +
                # 
         | 
| 48 | 
            +
                #   paths.from_states # => [:parked, :idling, :first_gear, ...]
         | 
| 49 | 
            +
                def from_states
         | 
| 50 | 
            +
                  map {|path| path.from_states}.flatten.uniq
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
                
         | 
| 53 | 
            +
                # Lists all of the states that can be transitioned to through the paths in
         | 
| 54 | 
            +
                # this collection.
         | 
| 55 | 
            +
                # 
         | 
| 56 | 
            +
                # For example,
         | 
| 57 | 
            +
                # 
         | 
| 58 | 
            +
                #   paths.to_states # => [:idling, :first_gear, :second_gear, ...]
         | 
| 59 | 
            +
                def to_states
         | 
| 60 | 
            +
                  map {|path| path.to_states}.flatten.uniq
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
                
         | 
| 63 | 
            +
                # Lists all of the events that can be fired through the paths in this
         | 
| 64 | 
            +
                # collection.
         | 
| 65 | 
            +
                # 
         | 
| 66 | 
            +
                # For example,
         | 
| 67 | 
            +
                # 
         | 
| 68 | 
            +
                #   paths.events  # => [:park, :ignite, :shift_up, ...]
         | 
| 69 | 
            +
                def events
         | 
| 70 | 
            +
                  map {|path| path.events}.flatten.uniq
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
                
         | 
| 73 | 
            +
                private
         | 
| 74 | 
            +
                  # Gets the initial set of paths to walk
         | 
| 75 | 
            +
                  def initial_paths
         | 
| 76 | 
            +
                    machine.events.transitions_for(object, :from => from_name, :guard => @guard).map do |transition|
         | 
| 77 | 
            +
                      path = Path.new(object, machine, :target => to_name, :guard => @guard)
         | 
| 78 | 
            +
                      path << transition
         | 
| 79 | 
            +
                      path
         | 
| 80 | 
            +
                    end
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
                  
         | 
| 83 | 
            +
                  # Walks down the given path.  Each new path that matches the configured
         | 
| 84 | 
            +
                  # requirements will be added to this collection.
         | 
| 85 | 
            +
                  def walk(path)
         | 
| 86 | 
            +
                    self << path if path.complete?
         | 
| 87 | 
            +
                    path.walk {|next_path| walk(next_path)} unless to_name && path.complete? && !@deep
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
              end
         | 
| 90 | 
            +
            end
         | 
    
        data/lib/state_machine/state.rb
    CHANGED
    
    | @@ -73,7 +73,20 @@ module StateMachine | |
| 73 73 | 
             
                  @methods = {}
         | 
| 74 74 | 
             
                  @initial = options[:initial] == true
         | 
| 75 75 |  | 
| 76 | 
            -
                   | 
| 76 | 
            +
                  if name
         | 
| 77 | 
            +
                    conflicting_machines = machine.owner_class.state_machines.select {|name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name]}
         | 
| 78 | 
            +
                    
         | 
| 79 | 
            +
                    # Output a warning if another machine has a conflicting qualified name
         | 
| 80 | 
            +
                    # for a different attribute
         | 
| 81 | 
            +
                    if conflict = conflicting_machines.detect {|name, other_machine| other_machine.attribute != machine.attribute}
         | 
| 82 | 
            +
                      name, other_machine = conflict
         | 
| 83 | 
            +
                      warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
         | 
| 84 | 
            +
                    elsif conflicting_machines.empty?
         | 
| 85 | 
            +
                      # Only bother adding predicates when another machine for the same
         | 
| 86 | 
            +
                      # attribute hasn't already done so
         | 
| 87 | 
            +
                      add_predicate
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
                  end
         | 
| 77 90 | 
             
                end
         | 
| 78 91 |  | 
| 79 92 | 
             
                # Creates a copy of this state in addition to the list of associated
         | 
| @@ -89,8 +102,8 @@ module StateMachine | |
| 89 102 | 
             
                # machine's definition.
         | 
| 90 103 | 
             
                def final?
         | 
| 91 104 | 
             
                  !machine.events.any? do |event|
         | 
| 92 | 
            -
                    event. | 
| 93 | 
            -
                       | 
| 105 | 
            +
                    event.branches.any? do |branch|
         | 
| 106 | 
            +
                      branch.state_requirements.any? do |requirement|
         | 
| 94 107 | 
             
                        requirement[:from].matches?(name) && !requirement[:to].matches?(name, :from => name)
         | 
| 95 108 | 
             
                      end
         | 
| 96 109 | 
             
                    end
         | 
| @@ -247,13 +260,19 @@ module StateMachine | |
| 247 260 | 
             
                  end
         | 
| 248 261 |  | 
| 249 262 | 
             
                  # Adds a predicate method to the owner class so long as a name has
         | 
| 250 | 
            -
                  # actually been configured for the state
         | 
| 263 | 
            +
                  # actually been configured for the state and the method isn't already
         | 
| 264 | 
            +
                  # defined in the owner class.
         | 
| 251 265 | 
             
                  def add_predicate
         | 
| 252 | 
            -
                     | 
| 253 | 
            -
                    
         | 
| 254 | 
            -
                     | 
| 255 | 
            -
             | 
| 256 | 
            -
                      machine. | 
| 266 | 
            +
                    owner_class = machine.owner_class
         | 
| 267 | 
            +
                    predicate = "#{qualified_name}?"
         | 
| 268 | 
            +
                    if !owner_class.method_defined?(predicate) && !owner_class.private_method_defined?(predicate)
         | 
| 269 | 
            +
                      # Checks whether the current value matches this state
         | 
| 270 | 
            +
                      machine.define_helper(:instance, predicate) do |machine, object|
         | 
| 271 | 
            +
                        machine.states.matches?(object, name)
         | 
| 272 | 
            +
                      end
         | 
| 273 | 
            +
                    else
         | 
| 274 | 
            +
                      # Only output a warning since we can't defined the predicate
         | 
| 275 | 
            +
                      warn "#{owner_class.name}##{predicate} is already defined, use #{owner_class.name}##{machine.name}?(:#{name}) instead."
         | 
| 257 276 | 
             
                    end
         | 
| 258 277 | 
             
                  end
         | 
| 259 278 | 
             
              end
         | 
| @@ -4,7 +4,7 @@ module StateMachine | |
| 4 4 | 
             
              # Represents a collection of states in a state machine
         | 
| 5 5 | 
             
              class StateCollection < NodeCollection
         | 
| 6 6 | 
             
                def initialize(machine) #:nodoc:
         | 
| 7 | 
            -
                  super(machine, :index => [:name, :value])
         | 
| 7 | 
            +
                  super(machine, :index => [:name, :qualified_name, :value])
         | 
| 8 8 | 
             
                end
         | 
| 9 9 |  | 
| 10 10 | 
             
                # Determines whether the given object is in a specific state.  If the
         | 
| @@ -1,8 +1,55 @@ | |
| 1 1 | 
             
            require 'state_machine/transition_collection'
         | 
| 2 | 
            +
            require 'state_machine/error'
         | 
| 2 3 |  | 
| 3 4 | 
             
            module StateMachine
         | 
| 4 5 | 
             
              # An invalid transition was attempted
         | 
| 5 | 
            -
              class InvalidTransition <  | 
| 6 | 
            +
              class InvalidTransition < Error
         | 
| 7 | 
            +
                # The machine attempting to be transitioned
         | 
| 8 | 
            +
                attr_reader :machine
         | 
| 9 | 
            +
                
         | 
| 10 | 
            +
                # The current state value for the machine
         | 
| 11 | 
            +
                attr_reader :from
         | 
| 12 | 
            +
                
         | 
| 13 | 
            +
                def initialize(object, machine, event) #:nodoc:
         | 
| 14 | 
            +
                  @machine = machine
         | 
| 15 | 
            +
                  @from_state = machine.states.match!(object)
         | 
| 16 | 
            +
                  @from = machine.read(object, :state)
         | 
| 17 | 
            +
                  @event = machine.events.fetch(event)
         | 
| 18 | 
            +
                  
         | 
| 19 | 
            +
                  super(object, "Cannot transition #{machine.name} via :#{self.event} from #{from_name.inspect}")
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
                
         | 
| 22 | 
            +
                # The event that triggered the failed transition
         | 
| 23 | 
            +
                def event
         | 
| 24 | 
            +
                  @event.name
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
                
         | 
| 27 | 
            +
                # The fully-qualified name of the event that triggered the failed transition
         | 
| 28 | 
            +
                def qualified_event
         | 
| 29 | 
            +
                  @event.qualified_name
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
                
         | 
| 32 | 
            +
                # The name for the current state
         | 
| 33 | 
            +
                def from_name
         | 
| 34 | 
            +
                  @from_state.name
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
                
         | 
| 37 | 
            +
                # The fully-qualified name for the current state
         | 
| 38 | 
            +
                def qualified_from_name
         | 
| 39 | 
            +
                  @from_state.qualified_name
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
              
         | 
| 43 | 
            +
              # A set of transition failed to run in parallel
         | 
| 44 | 
            +
              class InvalidParallelTransition < Error
         | 
| 45 | 
            +
                # The set of events that failed the transition(s)
         | 
| 46 | 
            +
                attr_reader :events
         | 
| 47 | 
            +
                
         | 
| 48 | 
            +
                def initialize(object, events) #:nodoc:
         | 
| 49 | 
            +
                  @events = events
         | 
| 50 | 
            +
                  
         | 
| 51 | 
            +
                  super(object, "Cannot run events in parallel: #{events * ', '}")
         | 
| 52 | 
            +
                end
         | 
| 6 53 | 
             
              end
         | 
| 7 54 |  | 
| 8 55 | 
             
              # A transition represents a state change for a specific attribute.
         | 
| @@ -175,6 +222,7 @@ module StateMachine | |
| 175 222 | 
             
                # provided, then it will be executed between the before and after callbacks.
         | 
| 176 223 | 
             
                # 
         | 
| 177 224 | 
             
                # Configuration options:
         | 
| 225 | 
            +
                # * +before+ - Whether to run before callbacks.
         | 
| 178 226 | 
             
                # * +after+ - Whether to run after callbacks.  If false, then any around
         | 
| 179 227 | 
             
                #   callbacks will be paused until called again with +after+ enabled.
         | 
| 180 228 | 
             
                #   Default is true.
         | 
| @@ -182,10 +230,10 @@ module StateMachine | |
| 182 230 | 
             
                # This will return true if all before callbacks gets executed.  After
         | 
| 183 231 | 
             
                # callbacks will not have an effect on the result.
         | 
| 184 232 | 
             
                def run_callbacks(options = {}, &block)
         | 
| 185 | 
            -
                  options = {:after => true}.merge(options)
         | 
| 233 | 
            +
                  options = {:before => true, :after => true}.merge(options)
         | 
| 186 234 | 
             
                  @success = false
         | 
| 187 235 |  | 
| 188 | 
            -
                  halted = pausable { before(options[:after], &block) }
         | 
| 236 | 
            +
                  halted = pausable { before(options[:after], &block) } if options[:before]
         | 
| 189 237 |  | 
| 190 238 | 
             
                  # After callbacks are only run if:
         | 
| 191 239 | 
             
                  # * An around callback didn't halt after yielding
         | 
| @@ -257,6 +305,17 @@ module StateMachine | |
| 257 305 | 
             
                  @paused_block = nil
         | 
| 258 306 | 
             
                end
         | 
| 259 307 |  | 
| 308 | 
            +
                # Determines equality of transitions by testing whether the object, states,
         | 
| 309 | 
            +
                # and event involved in the transition are equal
         | 
| 310 | 
            +
                def ==(other)
         | 
| 311 | 
            +
                  other.instance_of?(self.class) &&
         | 
| 312 | 
            +
                  other.object == object &&
         | 
| 313 | 
            +
                  other.machine == machine &&
         | 
| 314 | 
            +
                  other.from_name == from_name &&
         | 
| 315 | 
            +
                  other.to_name == to_name &&
         | 
| 316 | 
            +
                  other.event == event
         | 
| 317 | 
            +
                end
         | 
| 318 | 
            +
                
         | 
| 260 319 | 
             
                # Generates a nicely formatted description of this transitions's contents.
         | 
| 261 320 | 
             
                # 
         | 
| 262 321 | 
             
                # For example,
         | 
| @@ -341,7 +400,7 @@ module StateMachine | |
| 341 400 | 
             
                              before(complete, index, &block)
         | 
| 342 401 |  | 
| 343 402 | 
             
                              pause if @success && !complete
         | 
| 344 | 
            -
                              throw :cancel, true unless  | 
| 403 | 
            +
                              throw :cancel, true unless @success
         | 
| 345 404 | 
             
                            end
         | 
| 346 405 | 
             
                          end
         | 
| 347 406 | 
             
                        else
         | 
| @@ -375,8 +434,8 @@ module StateMachine | |
| 375 434 | 
             
                      # First resume previously paused callbacks
         | 
| 376 435 | 
             
                      if resume
         | 
| 377 436 | 
             
                        catch(:halt) do
         | 
| 378 | 
            -
                           | 
| 379 | 
            -
                          machine.callbacks[ | 
| 437 | 
            +
                          type = @success ? :after : :failure
         | 
| 438 | 
            +
                          machine.callbacks[type].each {|callback| callback.call(object, context, self)}
         | 
| 380 439 | 
             
                        end
         | 
| 381 440 | 
             
                      end
         | 
| 382 441 |  |