Changing state using before_transition/after_failure callbacks?

111 views
Skip to first unread message

Jan Hecking

unread,
May 9, 2013, 6:31:43 AM5/9/13
to pluginaw...@googlegroups.com
Is it possible to use event callbacks to trigger another transition to a different state?

E.g. I have an event authorize that transitions an object from the new to the authorized state. There is a before_transition callback defined for the event that queries some remote web service. If the query succeeds the transition to authorized should complete. But if it fails I would like to transition the object to the failed state instead.

Here is simplified example:

class StateTest

  attr_accessor :state

  state_machine :initial => :new, :use_transactions => false do
    state :new
    state :authorized
    state :failed

    event :authorize do
      transition :new => :authorized
    end
    event :error do
      transition any => :failed
    end

    around_transition :do => :log_transition
    before_transition :on => :authorize, :do => :handle_authorization
    after_failure :do => :handle_error
  end

  def handle_authorization(transition)
    result = false # authorize_with_some_remote_service()
    if !result
      throw :halt
    end
  end

  def handle_error(transition)
    obj = transition.object
    obj.error unless transition.to == :failed
  end

  def log_transition(transition)
    puts "starting transition #{transition.from} -> #{transition.to}"
    yield
    completed = true
    puts "completed transition #{transition.from} -> #{transition.to}"
  ensure
    puts "aborted transition #{transition.from} -> #{transition.to}" unless completed
  end
end


When executing the authorize event this is what the log_transition method outputs:


1.9.3p392 :287 > s = StateTest.new
 => #<StateTest:0x007fb18e8be3d0 @state="new">
1.9.3p392 :288 > s.authorize
starting transition new -> authorized
aborted transition new -> authorized
starting transition new -> failed
completed transition new -> failed
 => false
1.9.3p392 :289 > s
 => #<StateTest:0x007fb18e8be3d0 @state="new">


So even though the new -> failed transition completed the object is still in the new state afterwards. I tried this example with and without using transactions but it does not make a difference. I've tried various other variations as well, such as setting the state property directly instead of using the error event. But whatever I tried, at the end of the authorized event the state was reverted back to new.

Is it possible to automatically trigger another transition if a transition fails?


One way I could think of solving this is to change my event name from authorize to authorization_success and turn authorize into a regular instance method that handles the web service query and then either triggers the authorization_success or error events:


class StateTest2

  attr_accessor :state

  state_machine :initial => :new, :use_transactions => false do
    state :new
    state :authorized
    state :failed

    event :authorization_success do
      transition :new => :authorized
    end
    event :error do
      transition any => :failed
    end

    around_transition :do => :log_transition
  end

  def authorize
    result = false # authorize_with_some_remote_service()
    if result
      authorization_success
    else
      error
    end
    result
  end

  def log_transition(transition)
    puts "starting transition #{transition.from} -> #{transition.to}"
    yield
    completed = true
    puts "completed transition #{transition.from} -> #{transition.to}"
  ensure
    puts "aborted transition #{transition.from} -> #{transition.to}" unless completed
  end
end

This works as expected:

1.9.3p392 :001 > s = StateTest2.new
 => #<StateTest2:0x007f87e2335880 @state="new">
1.9.3p392 :002 > s.authorize
starting transition new -> failed
completed transition new -> failed
 => true
1.9.3p392 :003 > s
 => #<StateTest2:0x007f87e2335880 @state="failed">


However, the first approach has several nice properties that I would like to retain:
  • I can call obj.can_authorize? to check whether a transition to the authorized state is possible. In the second approach I would have to call obj.can_authorization_success? which is not as nice or intuitive.
  • I loose the automatic logging through the log_transition callback for the actual authorization process.

Any solutions?

Jan

Greg Vaughn

unread,
May 24, 2013, 7:13:04 PM5/24/13
to pluginaw...@googlegroups.com
I'm fighting with the same sort of thing. I'm new to the gem and the list, but I don't see any replies. Was there a resolution to the problem posted somewhere else?

-Greg Vaughn

Jan Hecking

unread,
Jun 3, 2013, 12:17:34 PM6/3/13
to pluginaw...@googlegroups.com
Hi Greg,

Sorry that I haven't responded to your private mail earlier - it's been a busy week here...

Anyway, the way I eventually decided to address this issue was by overriding the event method generated by state_machine - StateTest#authorize in my prev. example. In that method I handle some remote API calls first then then either call super() to transition to the authorized state of else I transition to a failed state instead. E.g. something like this:

class StateTest

  attr_accessor :state

  state_machine :initial => :new do
    state :new
    state :authorized
    state :failed

    event :authorize do
      transition :new => :authorized
    end
    event :fail do
      transition any => :failed
    end
  end

  def authorize
    result = false # authorize_with_some_remote_service()
    if result
      super()
    else
      fail()
    end
  end
end

I still would have preferred a solution using before and after call-backs to control the transitions as per my orig. example. It just feels more elegant and I don't need to override any automatically generated methods. But state_machine's transaction handling is preventing this. So this solution is working for us now even if not quite as nice.

Hope this helps!
Jan



--
You received this message because you are subscribed to the Google Groups "PluginAWeek: Talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pluginaweek-ta...@googlegroups.com.
To post to this group, send email to pluginaw...@googlegroups.com.
Visit this group at http://groups.google.com/group/pluginaweek-talk?hl=en.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Reply all
Reply to author
Forward
0 new messages