Class ActionFlow::Base
In: lib/action_flow/base.rb
Parent: ActionController::Base

The ActionFlow framework turns an ActionController into a flow capable controller. It can handle event trigerring, the back button and any user defined step. Here are the main concepts.

Flow

A flow is a logical procedure which defines how to handle different steps and guide the user through them.

Step

A step is a single unit of processing included inside a flow. All steps have to inherit from ActionFlow::FlowStep. They can be used to define one of the following :

 - ViewStep : displays a view and then relays the execution to methods
              or steps according to the trigered event.

 - ActionStep : calls a method of an ActionFlow controller and then relays the
                execution to methods or steps according to the trigered event.

More step types can be created to extend the ActionFlow framework.

Event

An event is nothing more than a symbol which triggers the execution of methods or steps, depending of the mapping.

Special event names are reserved to tell the framework to do specific tasks.

Special events

These event names are reserved and serve the following purposes.

 - render : Tells the framework that we must return the control to the view
            parser, since one of the 'render' method has been called and we
            are now ready to display something.

Methods

Constants

Event_prefix = '_event_'   Defines the symbols name prefix which are used to identify the next events. As an example, in a view_state, the button used to trigger the save event would be named ‘_event_save’. If we were in an action_state, to trigger the success event, we would ‘return :_event_success‘
Event_input_name = '_actionflow_event'   Defines the name of the parameter submitted which contains the next event name.
Flow_execution_key_id = 'flow_exec_key'   Defines the name of the variable which holds the flow unique state id. As an example, a view_state would add a hidden field named ‘flow_exec_key’, which would contain the key used to store a state in the session.

We could then restore the state by accessing it with : session[:flow_data-1234abcd].fetch( params[:flow_exec_key] )

See the SessionHandler for more details

Public Class methods

Makes sure that the Event_input_name constant is available to the view

[Source]

# File lib/action_flow/base.rb, line 106
    def self::event_input_name
        Event_input_name
    end

Makes sure that the Event_prefix constant is available to the view

[Source]

# File lib/action_flow/base.rb, line 96
    def self::event_prefix
        Event_prefix
    end

Makes sure that the Flow_execution_key_id constant is available to the view

[Source]

# File lib/action_flow/base.rb, line 101
    def self::flow_execution_key_id
        Flow_execution_key_id
    end

Initializes the class instance

[Source]

# File lib/action_flow/base.rb, line 112
    def initialize
      
      # Initializes the step registry of the current controller instance
      step_registry_value = Hash.new
      
      # Makes sure that the nil value is in the @start_step instance variable upon init
      start_step_value = nil
      
      # Makes sure that the nil value is in the @end_step instance variable upon init
      end_step_value = nil
    
    end

Protected Class methods

Called in a mapping to define the end point of the flow. Once this step finishes it‘s execution, the flow data is deleted. Pass it a symbol which bears the same name as a defined step. ie.

  end_step :defined_step_name

would point to the definition :

  view_state :defined_step_name do
    whatever
  end

[Source]

# File lib/action_flow/base.rb, line 262
      def self::end_step(step_name)
        @end_step = step_name.to_s
      end

Makes the end_step instance variable visible to the execute and index methods

[Source]

# File lib/action_flow/base.rb, line 238
      def self::end_step_value
        return @end_step
      end

Adds a step name to the registry and associates it to an object which will be used to handle a given step when necessary.

[Source]

# File lib/action_flow/base.rb, line 206
      def self::register(step_name, step)
      
        self.step_registry_value.store step_name.to_s, step
      
      end

Called in a mapping to define the starting point of the flow. Pass it a symbol which bears the same name as a defined step. ie.

  start_step :defined_step_name

would point to the definition :

  view_state :defined_step_name do
    whatever
  end

[Source]

# File lib/action_flow/base.rb, line 226
      def self::start_step(step_name)
        @start_step = step_name.to_s
      end

Makes the start_step instance variable visible to the execute and index methods

[Source]

# File lib/action_flow/base.rb, line 233
      def self::start_step_value
        return @start_step
      end

Makes the step_registry instance variable visible to the execute and index methods

[Source]

# File lib/action_flow/base.rb, line 243
      def self::step_registry_value
        return @step_registry ||= {}
      end

Public Instance methods

Default method to handle the requests. All the dispatching is done here.

[Source]

# File lib/action_flow/base.rb, line 128
    def index
    
      # Tells which was the last state
      @flow_id = params[Flow_execution_key_id]
      
      # Flag used to know if we reached the end_step
      @end_step_reached = false
        
      # Check if the flow has already started
      unless @flow_id
      
        # Make sure there's a start_step defined
        raise (ActionFlowError.new, "Your controller must declare a start step name. Use 'start_step :step_name' and define this step in the mapping.") if self.class.start_step_value.nil?
        
        # Make sure there's an end_step defined
        raise (ActionFlowError.new, "Your controller must declare an end step name. Use 'end_step :step_name' and define this step in the mapping. I suggest using a view step which could redirect if you don't want to create a 'thank you' screen.") if self.class.end_step_value.nil?
        
        # Start a new flow session storage
        start_new_flow_session_storage
        
        # Execute the start_step
        execute_step self.class.start_step_value
      
      else # We have to resume a flow with a given id
      
        # Make sure there's data associated to this flow
        raise (ActionFlowError.new, "No flow data could be found for the given flow key. Your session doesn't exist.") unless validate_flow_existence
        
        # Get the event which was trigered and make sure that
        # the submit button had a value prefixed by ActionFlow::Base.event_prefix
        # The event name is put into 'event_name' variable.
        params[Event_input_name].to_s =~ /^#{Event_prefix}([a-zA-Z]+[a-zA-Z0-9_]*)$/ ? event_name = $1 : raise(ActionFlowError.new, "Your view did not submit properly an event name. The event name '#{params[Event_input_name]}' is not valid")
        
        # We need to know where we came from
        last_step = fetch_last_step_name
        
        # Make sure that the step has an outcome defined for this event
        raise (ActionFlowError.new, "No outcome has been defined for the event '#{event_name}' on the step named '#{last_step}' as specified in the submitted data. Use the 'on' method on your mapped step or make sure that you are submitting valid data.") unless self.class.step_registry_value.fetch(last_step).has_an_outcome_for?(event_name)
        
        # Before doing anything, we create a new state so we can restore the previous one with
        # the back button.
        serialize
        
        # Call the resulting step associated to this event
        execute_step self.class.step_registry_value.fetch(last_step).outcome(event_name)
        
      end
       
      # Check if we continue
      if @end_step_reached
      
        # We clean all data on this flow in the session
        terminate
        
      end
      
      # Cleanup the flows which have been hanging too long in the session placeholder
      cleanup
    
    end

Protected Instance methods

Launches the execution of a method on the controller. Encountered errors will be managed as defined in the mapping.

[Source]

# File lib/action_flow/base.rb, line 271
      def execute_step(step_name)
      
        step_name = step_name.to_s
        
        # Make sure that this step is registered
        raise (ActionFlowError.new, "The desired step ( #{step_name} ) is not present in the mapping.") unless self.class.step_registry_value.has_key? step_name
        
        # Validate that the controller will answer to the message
        raise (ActionFlowError.new, "Your controller doesn't have a method called '#{step_name}' as defined in your mapping.") if !respond_to?(step_name) && self.class.step_registry_value.fetch(step_name).definition_required?
        
        # Everything is ready for execution, we just have to memorize that this step
        # was caled.
        persist_last_step_name step_name
        
        # Encapsulate the execution in a begin block to manage
        # the errors. Runtime errors are not managed. Only subclasses
        # of StandardError will be.
        begin
        
          # Execute the step and keep the returned value
          return_value = self.class.step_registry_value.fetch(step_name).send :execute, self
       
          # Check if this was the end step, if so, cut the execution short
          if self.class.end_step_value.to_s == fetch_last_step_name
            
            # Raise that flag
            @end_step_reached = true
            
            # Terminate execution chain
            return
          end
          
          # Make sure that the step returned an event object
          raise (ActionFlowError.new, "Your step definition named '#{step_name}' didn't return an ActionFlow::Event object. All steps must return either events or errors.") unless return_value.kind_of? ActionFlow::Event
                    
          # If the event name is 'render', we have to get back to the view
          unless return_value.name =~ Regexp.new( /^render$/, 'i' )
          
            # Make sure that the step has an outcome defined for this event
            raise (ActionFlowError.new, "No outcome has been defined for the event '#{return_value.name}'. Use the 'on' method on your mapped step.") unless self.class.step_registry_value.fetch(step_name).has_an_outcome_for?(return_value.name.to_s)
                      
            # Call recursively the step mapped to the returned event
            execute_step self.class.step_registry_value.fetch(step_name).outcome(return_value.name)
            
          end
          
        # Rescue any subclass of StandardError
        rescue StandardError => error
        
          # Ask the step if he handles this error
          if self.class.step_registry_value.fetch(step_name).handles? error.class.to_s
          
            # Get the handler step name
            handler_name = self.class.step_registry_value.fetch(step_name).handler(error.class.to_s)
            
            # Make sure this handler is in the registry
            raise (ActionFlowError.new, "The error handling step ( #{handler_name} ) is not present in the mapping.") unless self.class.step_registry_value.has_key? handler_name
          
            # Execute the error handling step recursively
            execute_step handler_name
          
          else
          
            # Well, we've tried. It's now an official 'Someone Else Problem'
            raise error
            
          end
          
        end
        
        
      
      end

[Validate]