lib/state_machine/machine.rb in state_machine-1.0.1 vs lib/state_machine/machine.rb in state_machine-1.0.2
- old
+ new
@@ -1,9 +1,10 @@
require 'state_machine/extensions'
require 'state_machine/assertions'
require 'state_machine/integrations'
+require 'state_machine/helper_module'
require 'state_machine/state'
require 'state_machine/event'
require 'state_machine/callback'
require 'state_machine/node_collection'
require 'state_machine/state_collection'
@@ -273,11 +274,77 @@
# Once this behavior is complete, the original method from the state machine
# is invoked by simply calling +super+.
#
# The same technique can be used for +state+, +state_name+, and all other
# instance *and* class methods on the Vehicle class.
+ #
+ # == Method conflicts
#
+ # By default state_machine does not redefine methods that exist on
+ # superclasses (*including* Object) or any modules (*including* Kernel) that
+ # were included before it was defined. This is in order to ensure that
+ # existing behavior on the class is not broken by the inclusion of
+ # state_machine.
+ #
+ # If a conflicting method is detected, state_machine will generate a warning.
+ # For example, consider the following class:
+ #
+ # class Vehicle
+ # state_machine do
+ # event :open do
+ # ...
+ # end
+ # end
+ # end
+ #
+ # In the above class, an event named "open" is defined for its state machine.
+ # However, "open" is already defined as an instance method in Ruby's Kernel
+ # module that gets included in every Object. As a result, state_machine will
+ # generate the following warning:
+ #
+ # Instance method "open" is already defined in Object, use generic helper instead.
+ #
+ # Even though you may not be using Kernel's implementation of the "open"
+ # instance method, state_machine isn't aware of this and, as a result, stays
+ # safe and just skips redefining the method.
+ #
+ # As with almost all helpers methods defined by state_machine in your class,
+ # there are generic methods available for working around this method conflict.
+ # In the example above, you can invoke the "open" event like so:
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
+ # vehicle.fire_events(:open) # => true
+ #
+ # # This will not work
+ # vehicle.open # => NoMethodError: private method `open' called for #<Vehicle:0xb72686b4 @state=nil>
+ #
+ # If you want to take on the risk of overriding existing methods and just
+ # ignore method conflicts altogether, you can do so by setting the following
+ # configuration:
+ #
+ # StateMachine::Machine.ignore_method_conflicts = true
+ #
+ # This will allow you to define events like "open" as described above and
+ # still generate the "open" instance helper method. For example:
+ #
+ # StateMachine::Machine.ignore_method_conflicts = true
+ #
+ # class Vehicle
+ # state_machine do
+ # event :open do
+ # ...
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
+ # vehicle.open # => true
+ #
+ # By default, state_machine helps prevent you from making mistakes and
+ # accidentally overriding methods that you didn't intend to. Once you
+ # understand this and what the consequences are, setting the
+ # +ignore_method_conflicts+ option is a perfectly reasonable workaround.
+ #
# == Integrations
#
# By default, state machines are library-agnostic, meaning that they work
# on any Ruby class and have no external dependencies. However, there are
# certain libraries which expose additional behavior that can be taken
@@ -449,12 +516,15 @@
@messages = options[:messages] || {}
@action = options[:action]
@use_transactions = options[:use_transactions]
@initialize_state = options[:initialize]
self.owner_class = owner_class
- self.initial_state = options[:initial] unless owner_class.state_machines.any? {|name, machine| machine.attribute == attribute && machine != self}
+ self.initial_state = options[:initial] unless sibling_machines.any?
+ # Merge with sibling machine configurations
+ add_sibling_machine_configs
+
# Define class integration
define_helpers
define_scopes(options[:plural])
after_initialize
@@ -480,11 +550,11 @@
# on the given owner class.
def owner_class=(klass)
@owner_class = klass
# Create modules for extending the class with state/event-specific methods
- @helper_modules = helper_modules = {:instance => Module.new, :class => Module.new}
+ @helper_modules = helper_modules = {:instance => HelperModule.new(self, :instance), :class => HelperModule.new(self, :class)}
owner_class.class_eval do
extend helper_modules[:class]
include helper_modules[:instance]
end
@@ -1024,48 +1094,113 @@
# transition Vehicle.safe_states => :parked
# end
# end
# end
#
- # == Defining additional arguments
+ # == Overriding the event method
#
- # Additional arguments on event actions can be defined like so:
+ # By default, this will define an instance method (with the same name as the
+ # event) that will fire the next possible transition for that. Although the
+ # +before_transition+, +after_transition+, and +around_transition+ hooks
+ # allow you to define behavior that gets executed as a result of the event's
+ # transition, you can also override the event method in order to have a
+ # little more fine-grained control.
#
+ # For example:
+ #
# class Vehicle
# state_machine do
# event :park do
# ...
# end
# end
#
- # def park(kind = :parallel, *args)
- # take_deep_breath if kind == :parallel
- # super
+ # def park(*)
+ # take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible
+ # if result = super # Runs the transition and all before/after/around hooks
+ # applaud # Executes after the transition (and after_transition hooks)
+ # end
+ # result
# end
- #
- # def take_deep_breath
- # sleep 3
- # end
# end
#
- # Note that +super+ is called instead of <tt>super(*args)</tt>. This allows
- # the entire arguments list to be accessed by transition callbacks through
- # StateMachine::Transition#args like so:
+ # There are a few important things to note here. First, the method
+ # signature is defined with an unlimited argument list in order to allow
+ # callers to continue passing arguments that are expected by state_machine.
+ # For example, it will still allow calls to +park+ with a single parameter
+ # for skipping the configured action.
#
- # after_transition :on => :park do |vehicle, transition|
- # kind = *transition.args
- # ...
+ # Second, the overridden event method must call +super+ in order to run the
+ # logic for running the next possible transition. In order to remain
+ # consistent with other events, the result of +super+ is returned.
+ #
+ # Third, any behavior defined in this method will *not* get executed if
+ # you're taking advantage of attribute-based event transitions. For example:
+ #
+ # vehicle = Vehicle.new
+ # vehicle.state_event = 'park'
+ # vehicle.save
+ #
+ # In this case, the +park+ event will run the before/after/around transition
+ # hooks and transition the state, but the behavior defined in the overriden
+ # +park+ method will *not* be executed.
+ #
+ # == Defining additional arguments
+ #
+ # Additional arguments can be passed into events and accessed by transition
+ # hooks like so:
+ #
+ # class Vehicle
+ # state_machine do
+ # after_transition :on => :park do |vehicle, transition|
+ # kind = *transition.args # :parallel
+ # ...
+ # end
+ # after_transition :on => :park, :do => :take_deep_breath
+ #
+ # event :park do
+ # ...
+ # end
+ #
+ # def take_deep_breath(transition)
+ # kind = *transition.args # :parallel
+ # ...
+ # end
+ # end
# end
+ #
+ # vehicle = Vehicle.new
+ # vehicle.park(:parallel)
#
# *Remember* that if the last argument is a boolean, it will be used as the
# +run_action+ parameter to the event action. Using the +park+ action
# example from above, you can might call it like so:
#
# vehicle.park # => Uses default args and runs machine action
# vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
# vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
#
+ # If you decide to override the +park+ event method *and* define additional
+ # arguments, you can do so as shown below:
+ #
+ # class Vehicle
+ # state_machine do
+ # event :park do
+ # ...
+ # end
+ # end
+ #
+ # def park(kind = :parallel, *args)
+ # take_deep_breath if kind == :parallel
+ # super
+ # end
+ # end
+ #
+ # Note that +super+ is called instead of <tt>super(*args)</tt>. This allow
+ # the entire arguments list to be accessed by transition callbacks through
+ # StateMachine::Transition#args.
+ #
# == Example
#
# class Vehicle
# state_machine do
# # The park, stop, and halt events will all share the given transitions
@@ -1101,10 +1236,108 @@
events.length == 1 ? events.first : events
end
alias_method :on, :event
+ # Creates a new transition that determines what to change the current state
+ # to when an event fires.
+ #
+ # == Defining transitions
+ #
+ # The options for a new transition uses the Hash syntax to map beginning
+ # states to ending states. For example,
+ #
+ # transition :parked => :idling, :idling => :first_gear, :on => :ignite
+ #
+ # In this case, when the +ignite+ event is fired, this transition will cause
+ # the state to be +idling+ if it's current state is +parked+ or +first_gear+
+ # if it's current state is +idling+.
+ #
+ # To help define these implicit transitions, a set of helpers are available
+ # for slightly more complex matching:
+ # * <tt>all</tt> - Matches every state in the machine
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
+ # * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
+ # * <tt>same</tt> - Matches the same state being transitioned from
+ #
+ # See StateMachine::MatcherHelpers for more information.
+ #
+ # Examples:
+ #
+ # transition all => nil, :on => :ignite # Transitions to nil regardless of the current state
+ # transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state
+ # transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling
+ # transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state
+ # transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked
+ # transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled
+ #
+ # transition :parked => same, :on => :park # Loops :parked back to :parked
+ # transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events
+ # transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state
+ #
+ # # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
+ # transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up
+ #
+ # == Verbose transitions
+ #
+ # Transitions can also be defined use an explicit set of configuration
+ # options:
+ # * <tt>:from</tt> - A state or array of states that can be transitioned from.
+ # If not specified, then the transition can occur for *any* state.
+ # * <tt>:to</tt> - The state that's being transitioned to. If not specified,
+ # then the transition will simply loop back (i.e. the state will not change).
+ # * <tt>:except_from</tt> - A state or array of states that *cannot* be
+ # transitioned from.
+ #
+ # These options must be used when defining transitions within the context
+ # of a state.
+ #
+ # Examples:
+ #
+ # transition :to => nil, :on => :park
+ # transition :to => :idling, :on => :ignite
+ # transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite
+ # transition :from => nil, :to => :idling, :on => :ignite
+ # transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
+ #
+ # == Conditions
+ #
+ # In addition to the state requirements for each transition, a condition
+ # can also be defined to help determine whether that transition is
+ # available. These options will work on both the normal and verbose syntax.
+ #
+ # Configuration options:
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
+ # transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
+ # The condition should return or evaluate to true or false.
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
+ # transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
+ # The condition should return or evaluate to true or false.
+ #
+ # Examples:
+ #
+ # transition :parked => :idling, :on => :ignite, :if => :moving?
+ # transition :parked => :idling, :on => :ignite, :unless => :stopped?
+ # transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on?
+ #
+ # transition :from => :parked, :to => :idling, :on => ignite, :if => :moving?
+ # transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
+ #
+ # == Order of operations
+ #
+ # Transitions are evaluated in the order in which they're defined. As a
+ # result, if more than one transition applies to a given object, then the
+ # first transition that matches will be performed.
+ def transition(options)
+ raise ArgumentError, 'Must specify :on event' unless options[:on]
+
+ branches = []
+ event(*Array(options.delete(:on))) { branches << transition(options) }
+
+ branches.length == 1 ? branches.first : branches
+ end
+
# Creates a callback that will be invoked *before* a transition is
# performed so long as the given requirements match the transition.
#
# == The callback
#
@@ -1567,10 +1800,25 @@
protected
# Runs additional initialization hooks. By default, this is a no-op.
def after_initialize
end
+ # Looks up other machines that have been defined in the owner class and
+ # are targeting the same attribute as this machine. When accessing
+ # sibling machines, they will be automatically copied for the current
+ # class if they haven't been already. This ensures that any configuration
+ # changes made to the sibling machines only affect this class and not any
+ # base class that may have originally defined the machine.
+ def sibling_machines
+ owner_class.state_machines.inject([]) do |machines, (name, machine)|
+ if machine.attribute == attribute && machine != self
+ machines << (owner_class.state_machine(name) {})
+ end
+ machines
+ end
+ end
+
# Determines if the machine's attribute needs to be initialized. This
# will only be true if the machine's attribute is blank.
def initialize_state?(object)
value = read(object, :state)
(value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value]
@@ -1610,11 +1858,11 @@
# current state
def define_state_predicate
call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
def #{name}?(*args)
- args.empty? && #{call_super} ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
+ args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
end
end_eval
end
# Adds helper methods for getting information about this state machine's
@@ -1725,11 +1973,11 @@
# Generate the list of modules that *only* occur in the owner class, but
# were included *prior* to the helper modules, in addition to the
# superclasses
ancestors = current.ancestors - superclass.ancestors + superclasses
- ancestors = ancestors[ancestors.index(@helper_modules[scope]) + 1..-1].reverse
+ ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse
# Search for for the first ancestor that defined this method
ancestors.detect do |ancestor|
ancestor = (class << ancestor; self; end) if scope == :class && ancestor.is_a?(Class)
ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
@@ -1818,10 +2066,19 @@
# Always yields
def transaction(object)
yield
end
+ # Updates this machine based on the configuration of other machines in the
+ # owner class that share the same target attribute.
+ def add_sibling_machine_configs
+ # Add existing states
+ sibling_machines.each do |machine|
+ machine.states.each {|state| states << state unless states[state.name]}
+ end
+ end
+
# Adds a new transition callback of the given type.
def add_callback(type, options, &block)
callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
add_states(callback.known_states)
callback
@@ -1831,9 +2088,12 @@
# this machine
def add_states(new_states)
new_states.map do |new_state|
unless state = states[new_state]
states << state = State.new(self, new_state)
+
+ # Copy states over to sibling machines
+ sibling_machines.each {|machine| machine.states << state}
end
state
end
end