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
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