require 'rubygems' require 'workflow/specification' require 'workflow/adapters/active_record' require 'workflow/adapters/remodel' # See also README.markdown for documentation module Workflow module ClassMethods attr_reader :workflow_spec def workflow_column(column_name=nil) #I guess we want to preserve the api??? @workflow_state_column_name ||= column_name if @workflow_state_column_name.nil? && superclass.respond_to?(:workflow_column) @workflow_state_column_name = superclass.workflow_column end @workflow_state_column_name ||= :workflow_state end def workflow(column=nil,&specification) column = workflow_column(column) assign_workflow Specification.new(Hash.new, &specification) inject_setter_for_state end private #allow setting of workflow_state by name, and interpolate the value def inject_setter_for_state define_method("#{@workflow_state_column_name}=") do |val| matching_state = spec.states.select{|k,v| v.name.to_s == val.to_s}.values.first val = matching_state.value if matching_state super(val) end end # Creates the convinience methods like `my_transition!` def assign_workflow(specification_object) # Merging two workflow specifications can **not** be done automically, so # just make the latest specification win. Same for inheritance - # definition in the subclass wins. if respond_to? :inherited_workflow_spec # undefine methods defined by the old workflow_spec inherited_workflow_spec.states.values.each do |state| state_name = state.name module_eval do undef_method "#{state_name}?" end state.events.flat.each do |event| event_name = event.name module_eval do undef_method "#{event_name}!".to_sym undef_method "can_#{event_name}?" end end end end @workflow_spec = specification_object @workflow_spec.states.values.each do |state| state_name = state.name module_eval do define_method "#{state_name}?" do state_name == current_state.name end end state.events.flat.each do |event| event_name = event.name module_eval do define_method "#{event_name}!".to_sym do |*args| process_event!(event_name, *args) end define_method "can_#{event_name}?" do return !!current_state.events.first_applicable(event_name, self) end end end end end end module InstanceMethods def current_state loaded_state_value = load_workflow_state if loaded_state_value loaded_state_name = spec.states.select{|k,v| v.value.to_s == loaded_state_value.to_s}.keys.first end res = spec.states[loaded_state_name.to_sym] if loaded_state_name res || spec.initial_state end # See the 'Guards' section in the README # @return true if the last transition was halted by one of the transition callbacks. def halted? @halted end # @return the reason of the last transition abort as set by the previous # call of `halt` or `halt!` method. def halted_because @halted_because end def process_event!(name, *args) event = current_state.events.first_applicable(name, self) if event.nil? return run_on_unavailable_transition(current_state, name, *args) end @halted_because = nil @halted = false check_transition(event) from = current_state to_state = spec.states[event.transitions_to] to_value = to_state.value run_before_transition(from, to_state, name, *args) return false if @halted begin return_value = run_action(event.action, *args) || run_action_callback(event.name, *args) rescue StandardError => e run_on_error(e, from, to_state, name, *args) end return false if @halted run_on_transition(from, to_state, name, *args) run_on_exit(from, to_state, name, *args) transition_value = persist_workflow_state to_value run_on_entry(to_state, from, name, *args) run_after_transition(from, to_state, name, *args) return_value.nil? ? transition_value : return_value end def halt(reason = nil) @halted_because = reason @halted = true end def halt!(reason = nil) @halted_because = reason @halted = true raise TransitionHalted.new(reason) end def spec # check the singleton class first class << self return workflow_spec if workflow_spec end c = self.class # using a simple loop instead of class_inheritable_accessor to avoid # dependency on Rails' ActiveSupport until c.workflow_spec || !(c.include? Workflow) c = c.superclass end c.workflow_spec end private def check_transition(event) # Create a meaningful error message instead of # "undefined method `on_entry' for nil:NilClass" # Reported by Kyle Burton if !spec.states[event.transitions_to] raise WorkflowError.new("Event[#{event.name}]'s " + "transitions_to[#{event.transitions_to}] is not a declared state.") end end def run_before_transition(from, to, event, *args) instance_exec(from.name, to.name, event, *args, &spec.before_transition_proc) if spec.before_transition_proc end def run_on_error(error, from, to, event, *args) if spec.on_error_proc instance_exec(error, from.name, ( to.name rescue nil ), event, *args, &spec.on_error_proc) halt(error.message) else raise error end end def run_on_unavailable_transition(from, to_name, *args) if !spec.on_unavailable_transition_proc || !instance_exec(from.name, to_name.to_sym, *args, &spec.on_unavailable_transition_proc) run_on_error(NoTransitionAllowed.new( "There is no event #{to_name.to_sym} defined for the #{current_state} state"), current_state, nil, to_name, *args) return false end end def run_on_transition(from, to, event, *args) instance_exec(from.name, to.name, event, *args, &spec.on_transition_proc) if spec.on_transition_proc end def run_after_transition(from, to, event, *args) instance_exec(from.name, to.name, event, *args, &spec.after_transition_proc) if spec.after_transition_proc end def run_action(action, *args) instance_exec(*args, &action) if action end def has_callback?(action) # 1. public callback method or # 2. protected method somewhere in the class hierarchy or # 3. private in the immediate class (parent classes ignored) action = action.to_sym self.respond_to?(action) or self.class.protected_method_defined?(action) or self.private_methods(false).map(&:to_sym).include?(action) end def run_action_callback(action_name, *args) action = action_name.to_sym self.send(action, *args) if has_callback?(action) end def run_on_entry(state, prior_state, triggering_event, *args) if state.on_entry instance_exec(prior_state.name, triggering_event, *args, &state.on_entry) else hook_name = "on_#{state}_entry" self.send hook_name, prior_state, triggering_event, *args if has_callback?(hook_name) end end def run_on_exit(state, new_state, triggering_event, *args) if state if state.on_exit instance_exec(new_state.name, triggering_event, *args, &state.on_exit) else hook_name = "on_#{state}_exit" self.send hook_name, new_state, triggering_event, *args if has_callback?(hook_name) end end end # load_workflow_state and persist_workflow_state # can be overriden to handle the persistence of the workflow state. # # Default (non ActiveRecord) implementation stores the current state # in a variable. # # Default ActiveRecord implementation uses a 'workflow_state' database column. def load_workflow_state @workflow_state if instance_variable_defined? :@workflow_state end def persist_workflow_state(new_value) @workflow_state = new_value end end def self.included(klass) klass.send :include, InstanceMethods # backup the parent workflow spec, making accessible through #inherited_workflow_spec if klass.superclass.respond_to?(:workflow_spec, true) klass.module_eval do # see http://stackoverflow.com/a/2495650/111995 for implementation explanation pro = Proc.new { klass.superclass.workflow_spec } singleton_class = class << self; self; end singleton_class.send(:define_method, :inherited_workflow_spec) do pro.call end end end klass.extend ClassMethods # Look for a hook; otherwise detect based on ancestor class. if klass.respond_to?(:workflow_adapter) klass.send :include, klass.workflow_adapter else if Object.const_defined?(:ActiveRecord) && klass < ActiveRecord::Base klass.send :include, Adapter::ActiveRecord end if Object.const_defined?(:Remodel) && klass < Adapter::Remodel::Entity klass.send :include, Adapter::Remodel::InstanceMethods end end end end