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