module Stateful::ClassMethods

Public Instance Methods

stateful(name, options = nil) click to toggle source
# File lib/stateful.rb, line 18
def stateful(name, options = nil)
  if name.is_a?(Hash)
    options = name
    name = options[:name] || :state
  end

  options[:name] = name

  options[:events] ||= []
  options[:prefix] = name == :state ? '' : "#{name}_"

  # define the method that will contain the info objects.
  # we use instance_eval here because its easier to implement the ||= {} logic this way.
  instance_eval "def #{name}_infos; @#{name}_infos ||= {}; end"

  define_method "#{name}_events" do
    options[:events]
  end

  define_method "#{name}_info" do
    self.class.__send__("#{name}_infos")[__send__(name)]
  end

  define_method "#{name}_valid?" do
    self.class.__send__("#{name}_infos").keys.include?(__send__(name))
  end

  define_method "change_#{name}" do |new_state, options = {}, &block|
    return false if new_state == __send__(name)
    return false unless __send__("#{name}_info").can_transition_to?(new_state)
    __send__("_change_#{name}", new_state, options, [:persist_state, :save], &block)
  end

  define_method "change_#{name}!" do |new_state, options = {}, &block|
    current_info = __send__("#{name}_info")
    raise "transition from #{send(name)} to #{new_state} not allowed for #{name}" unless current_info.can_transition_to?(new_state)
    __send__("_change_#{name}", new_state, options, [:persist_state!, :save!], &block)
  end

  define_method "_change_#{name}" do |new_state, options, persist_methods, &block|
    # convert shortcut event name to options hash
    options = {event: options} if options.is_a? Symbol

    # do a little magic and infer the event name from the method name used to call change_state
    # TODO: decide if this is too magical, for now it has been commented out.
    #unless options[:event]
    #  calling_method = caller[1][/`.*'/][1..-2].gsub('!', '').to_sym
    #  options[:event] = calling_method if state_events.include? calling_method
    #end

    run_callbacks "#{name}_change".to_sym, new_state do

      run_callbacks (options[:event] || "#{name}_non_event_change") do
        __send__("#{name}=", new_state)
        block.call if block

        ## if a specific persist method value was provided
        if options.has_key?(:persist_method)
          # call the method if one was provided
          __send__(options[:persist_method]) if options[:persist_method]
        # if no persist method option was provided than use the defaults
        else
          method = persist_methods.find {|m| respond_to?(m)}
          if method
            __send__(method)
          else
            true
          end
        end
      end
    end
  end

  protected "change_#{name}"
  protected "change_#{name}!"
  private :_change_state

  ## state events support:

  # provide a reader so that the current event being fired can be accessed
  attr_reader "#{name}_event".to_sym

  define_singleton_method "#{name}_event" do |event, &block|
    define_method(event) do
      instance_variable_set("@#{name}_change_method", "change_#{name}")
      instance_variable_set("@#{name}_event", event)
      begin
        result = instance_eval &block
      ensure
        instance_variable_set("@#{name}_change_method", nil)
        instance_variable_set("@#{name}_event", nil)
      end
      result
    end

    define_method("#{event}!") do
      instance_variable_set("@#{name}_change_method", "change_#{name}!")
      instance_variable_set("@#{name}_event", event)
      begin
        result = instance_eval &block
      ensure
        instance_variable_set("@#{name}_change_method", nil)
        instance_variable_set("@#{name}_event", nil)
      end
      result
    end
  end

  # define the transition_to_state method that works in conjunction with the state_event
  define_method "transition_to_#{name}" do |new_state, &block|
    event = __send__("#{name}_event")
    raise "transition_to_#{name} can only be called while a #{name} event is being called" unless event

    method = instance_variable_get("@#{name}_change_method")
    __send__(method, new_state, event, &block)
  end

  protected "transition_to_#{name}"

  define_method "can_transition_to_#{name}?" do |new_state|
    __send__("#{name}_info").can_transition_to?(new_state)
  end

  ## init and configure state info:

  init_state_info(name, options[:states])
  __send__("#{name}_infos").values.each do |info|
    info.expand_to_transitions

    define_method "#{options[:prefix]}#{info.name}?" do
      current_info = __send__("#{name}_info")
      !!(current_info && current_info.is?(info.name))
    end
  end

  define_state_attribute(options)

  # define the event callbacks
  events = (["#{name}_change".to_sym, "#{name}_non_event_change".to_sym] + options[:events])
  define_callbacks *events

  # define callback helpers
  events.each do |event|
    define_singleton_method "before_#{event}" do |method = nil, &block|
      set_callback(event, :before, method ? method : block)
    end

    define_singleton_method "after_#{event}" do |method = nil, &block|
      set_callback(event, :after, method ? method : block)
    end
  end
end

Protected Instance Methods

define_state_attribute(options) click to toggle source
# File lib/stateful.rb, line 172
def define_state_attribute(options)
  define_method options[:name] do
    instance_variable_get("@#{options[:name]}") || options[:default]
  end

  define_method "#{options[:name]}=" do |val|
    instance_variable_set("@#{options[:name]}", val)
  end
end

Private Instance Methods

init_state_info(name, values, parent = nil) click to toggle source
# File lib/stateful.rb, line 184
def init_state_info(name, values, parent = nil)
  values.each do |state_name, config|
    info = __send__("#{name}_infos")[state_name] = Stateful::StateInfo.new(self, name, parent, state_name, config)
    init_state_info(name, config, info) if info.is_group?
  end
end