lib/state_machine/machine.rb in state_machine-0.7.5 vs lib/state_machine/machine.rb in state_machine-0.7.6

- old
+ new

@@ -111,18 +111,18 @@ # # == Callbacks # # Callbacks are supported for hooking before and after every possible # transition in the machine. Each callback is invoked in the order in which - # it was defined. See StateMachine::Machine#before_transition - # and StateMachine::Machine#after_transition for documentation - # on how to define new callbacks. + # it was defined. See StateMachine::Machine#before_transition and + # StateMachine::Machine#after_transition for documentation on how to define + # new callbacks. # - # *Note* that callbacks only get executed within the context of an event. - # As a result, if a class has an initial state when it's created, any - # callbacks that would normally get executed when the object enters that - # state will *not* get triggered. + # *Note* that callbacks only get executed within the context of an event. As + # a result, if a class has an initial state when it's created, any callbacks + # that would normally get executed when the object enters that state will + # *not* get triggered. # # For example, # # class Vehicle # state_machine :initial => :parked do @@ -227,11 +227,11 @@ # end # end # # [Vehicle, Switch, Project].each do |klass| # klass.state_machines.each do |attribute, machine| - # machine.before_transition klass.method(:before_transition) + # machine.before_transition StateMachineObserver.method(:before_transition) # end # end # # Additional observer-like behavior may be exposed by the various integrations # available. See below for more information on integrations. @@ -298,14 +298,14 @@ # If a machine of the given name already exists in one of the class's # superclasses, then a copy of that machine will be created and stored # in the new owner class (the original will remain unchanged). def find_or_create(owner_class, *args, &block) options = args.last.is_a?(Hash) ? args.pop : {} - attribute = args.first || :state + name = args.first || :state # Find an existing machine - if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[attribute] + if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name] # Only create a new copy if changes are being made to the machine in # a subclass if machine.owner_class != owner_class && (options.any? || block_given?) machine = machine.clone machine.initial_state = options[:initial] if options.include?(:initial) @@ -314,11 +314,11 @@ # Evaluate DSL machine.instance_eval(&block) if block_given? else # No existing machine: create a new one - machine = new(owner_class, attribute, options, &block) + machine = new(owner_class, name, options, &block) end machine end @@ -345,11 +345,11 @@ class_name.split('::').each do |name| klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name) end # Draw each of the class's state machines - klass.state_machines.each do |name, machine| + klass.state_machines.each_value do |machine| machine.draw(options) end end end end @@ -363,12 +363,13 @@ } # The class that the machine is defined in attr_accessor :owner_class - # The attribute for which the machine is being defined - attr_reader :attribute + # The name of the machine, used for scoping methods generated for the + # machine as a whole (not states or events) + attr_reader :name # The events that trigger transitions. These are sorted, by default, in # the order in which they were defined. attr_reader :events @@ -400,11 +401,11 @@ attr_reader :use_transactions # Creates a new state machine for the given attribute def initialize(owner_class, *args, &block) options = args.last.is_a?(Hash) ? args.pop : {} - assert_valid_keys(options, :initial, :action, :plural, :namespace, :integration, :messages, :use_transactions) + assert_valid_keys(options, :attribute, :initial, :action, :plural, :namespace, :integration, :messages, :use_transactions) # Find an integration that matches this machine's owner class if integration = options[:integration] ? StateMachine::Integrations.find(options[:integration]) : StateMachine::Integrations.match(owner_class) extend integration options = integration.defaults.merge(options) if integration.respond_to?(:defaults) @@ -412,11 +413,12 @@ # Add machine-wide defaults options = {:use_transactions => true}.merge(options) # Set machine configuration - @attribute = args.first || :state + @name = args.first || :state + @attribute = options[:attribute] || @name @events = EventCollection.new(self) @states = StateCollection.new(self) @callbacks = {:before => [], :after => []} @namespace = options[:namespace] @messages = options[:messages] || {} @@ -465,14 +467,13 @@ owner_class.class_eval do extend class_helper_module include instance_helper_module end - # Record this machine as matched to the attribute in the current owner - # class. This will override any machines mapped to the same attribute - # in any superclasses. - owner_class.state_machines[attribute] = self + # Record this machine as matched to the name in the current owner class. + # This will override any machines mapped to the same name in any superclasses. + owner_class.state_machines[name] = self end # Sets the initial state of the machine. This can be either the static name # of a state or a lambda block which determines the initial state at # creation time. @@ -482,26 +483,31 @@ # Update all states to reflect the new initial state states.each {|state| state.initial = (state.name == @initial_state)} end + # Gets the actual name of the attribute on the machine's owner class that + # stores data with the given name. + def attribute(name = :state) + name == :state ? @attribute : :"#{self.name}_#{name}" + end + # Defines a new instance method with the given name on the machine's owner # class. If the method is already defined in the class, then this will not # override it. # # Example: # - # attribute = machine.attribute # machine.define_instance_method(:state_name) do |machine, object| # machine.states.match(object) # end def define_instance_method(method, &block) - attribute = self.attribute + name = self.name @instance_helper_module.class_eval do define_method(method) do |*args| - block.call(self.class.state_machine(attribute), self, *args) + block.call(self.class.state_machine(name), self, *args) end end end attr_reader :instance_helper_module @@ -513,15 +519,15 @@ # # machine.define_class_method(:states) do |machine, klass| # machine.states.keys # end def define_class_method(method, &block) - attribute = self.attribute + name = self.name @class_helper_module.class_eval do define_method(method) do |*args| - block.call(self.state_machine(attribute), self, *args) + block.call(self.state_machine(name), self, *args) end end end # Gets the initial state of the machine for the given object. If a dynamic @@ -614,11 +620,11 @@ # # class VehicleState < ActiveRecord::Base # end # # class Vehicle < ActiveRecord::Base - # state_machine :state_id, :initial => :parked do + # state_machine :attribute => :state_id, :initial => :parked do # event :ignite do # transition :parked => :idling # end # # states.each do |state| @@ -822,41 +828,45 @@ states.length == 1 ? states.first : states end alias_method :other_states, :state - # Gets the current value stored in the given object's state. + # Gets the current value stored in the given object's attribute. # # For example, # # class Vehicle # state_machine :initial => :parked do # ... # end # end # - # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked"> - # Vehicle.state_machine.read(vehicle) # => "parked" - def read(object) - object.send(attribute) + # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked"> + # Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state + # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event + def read(object, attribute, ivar = false) + attribute = self.attribute(attribute) + ivar ? object.instance_variable_get("@#{attribute}") : object.send(attribute) end - # Sets a new value in the given object's state. + # Sets a new value in the given object's attribute. # # For example, # # class Vehicle # state_machine :initial => :parked do # ... # end # end # - # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked"> - # Vehicle.state_machine.write(vehicle, 'idling') - # vehicle.state # => "idling" - def write(object, value) - object.send("#{attribute}=", value) + # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked"> + # Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling' + # Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park' + # vehicle.state # => "idling" + # vehicle.event # => "park" + def write(object, attribute, value) + object.send("#{self.attribute(attribute)}=", value) end # Defines one or more events for the machine and the transitions that can # be performed when those events are run. # @@ -1110,16 +1120,16 @@ # # For example, # # class Vehicle # # Only specifies one parameter (the object being transitioned) - # before_transition :to => :parked do |vehicle| + # before_transition all => :parked do |vehicle| # vehicle.set_alarm # end # # # Specifies 2 parameters (object being transitioned and actual transition) - # before_transition :to => :parked do |vehicle, transition| + # before_transition all => :parked do |vehicle, transition| # vehicle.set_alarm(transition) # end # end # # *Note* that the object in the callback will only be passed in as an @@ -1141,11 +1151,11 @@ # # # Before specific transition: # before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt # # # With conditional callback: - # before_transition :to => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on? + # before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on? # # # Using helpers: # before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard # ... # end @@ -1203,11 +1213,11 @@ # This requires both the Ruby graphviz gem and the graphviz library be # installed on the system. # # Configuration options: # * <tt>:name</tt> - The name of the file to write to (without the file extension). - # Default is "#{owner_class.name}_#{attribute}" + # Default is "#{owner_class.name}_#{name}" # * <tt>:path</tt> - The path to write the graph file to. Default is the # current directory ("."). # * <tt>:format</tt> - The image format to generate the graph in. # Default is "png'. # * <tt>:font</tt> - The name of the font to draw state names in. @@ -1215,11 +1225,11 @@ # * <tt>:orientation</tt> - The direction of the graph ("portrait" or # "landscape"). Default is "portrait". # * <tt>:output</tt> - Whether to generate the output of the graph def draw(options = {}) options = { - :name => "#{owner_class.name}_#{attribute}", + :name => "#{owner_class.name}_#{name}", :path => '.', :format => 'png', :font => 'Arial', :orientation => 'portrait', :output => true @@ -1270,11 +1280,11 @@ define_state_predicate define_event_helpers define_action_helpers if action # Gets the state name for the current value - define_instance_method("#{attribute}_name") do |machine, object| + define_instance_method(attribute(:name)) do |machine, object| machine.states.match!(object).name end end # Adds reader/writer methods for accessing the state attribute @@ -1287,44 +1297,44 @@ end # Adds predicate method to the owner class for determining the name of the # current state def define_state_predicate - define_instance_method("#{attribute}?") do |machine, object, state| + define_instance_method("#{name}?") do |machine, object, state| machine.states.matches?(object, state) end end # Adds helper methods for getting information about this state machine's # events def define_event_helpers # Gets the events that are allowed to fire on the current object - define_instance_method("#{attribute}_events") do |machine, object| + define_instance_method(attribute(:events)) do |machine, object| machine.events.valid_for(object).map {|event| event.name} end # Gets the next possible transitions that can be run on the current # object - define_instance_method("#{attribute}_transitions") do |machine, object, *args| + define_instance_method(attribute(:transitions)) do |machine, object, *args| machine.events.transitions_for(object, *args) end # Add helpers for interacting with the action if action - attribute = self.attribute - # Tracks the event / transition to invoke when the action is called + event_attribute = attribute(:event) + event_transition_attribute = attribute(:event_transition) @instance_helper_module.class_eval do - attr_writer "#{attribute}_event" + attr_writer event_attribute protected - attr_accessor "#{attribute}_event_transition" + attr_accessor event_transition_attribute end # Interpret non-blank events as present - define_instance_method("#{attribute}_event") do |machine, object| - event = object.instance_variable_get("@#{attribute}_event") + define_instance_method(attribute(:event)) do |machine, object| + event = machine.read(object, :event, true) event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil end end end @@ -1332,11 +1342,11 @@ # is invoked def define_action_helpers(action_hook = self.action) action = self.action private_method = owner_class.private_method_defined?(action_hook) - if (owner_class.method_defined?(action_hook) || private_method) && !owner_class.state_machines.any? {|attribute, machine| machine.action == action && machine != self} + if (owner_class.method_defined?(action_hook) || private_method) && !owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self} # Action is defined and hasn't already been overridden by another machine @instance_helper_module.class_eval do # Override the default action to invoke the before / after hooks define_method(action_hook) do |*args| self.class.state_machines.fire_event_attributes(self, action) { super(*args) } @@ -1356,12 +1366,12 @@ # singular and plural versions of the attribute are defined for each # scope helper. A custom plural can be specified if it cannot be # automatically determined by either calling +pluralize+ on the attribute # name or adding an "s" to the end of the name. def define_scopes(custom_plural = nil) - plural = custom_plural || (attribute.to_s.respond_to?(:pluralize) ? attribute.to_s.pluralize : "#{attribute}s") + plural = custom_plural || (name.to_s.respond_to?(:pluralize) ? name.to_s.pluralize : "#{name}s") - [attribute, plural].uniq.each do |name| + [name, plural].uniq.each do |name| [:with, :without].each do |kind| method = "#{kind}_#{name}" if scope = send("create_#{kind}_scope", method) # Converts state names to their corresponding values so that they