The idea of that particular state machine is to embed validation declaration inside the state.
state :orange do
validate :validate_core
end
The configuration above will perform the validation :validate_core whenever the object is transitioning to orange.
event :orangify do
transition all => :orange
end
I understand your concern about the rollback, but keep in mind that the rollback is performed in a transaction, thus it's quite cheap.
record.orangify!
Moreover, remember you can also use the non bang version that don't use exceptions.
> c.orangify
(0.3ms) BEGIN
(0.3ms) ROLLBACK
=> false
That said, if you want to use a different approach based on the before transition, then you only have to know that if the callback returns false, the transition is halted.
before_transition do
false
end
> c.orangify!
(0.2ms) BEGIN
(0.2ms) ROLLBACK
StateMachine::InvalidTransition: Cannot transition state via :cancel from :purchased (Reason(s): Transition halted)
Note that a transaction is always started, but it's likely no query will be performed if the callback is at the very beginning.
The before_transaction accepts some params. You can yield the object and the transaction instance.
before_transition do |object, transaction|
object.validate_core
end
and indeed you can restrict it by event
before_transition all => :orange do |object, transaction|
object.validate_core # => false
end
In this case, validate_core however is supposed to be a simple method that returns true/false. If you want to use the defined validation chain, then what comes to my mind is to invoke valid? on the model itself.
before_transition all => :orange do |object, transaction|
object.valid?
end
However, please note that you can't run a transaction outside the scope of a transaction. In fact, if you inspect the code for perform, you will see that callbacks are inside the transaction.
# Runs each of the collection's transitions in parallel.
#
# All transitions will run through the following steps:
# 1. Before callbacks
# 2. Persist state
# 3. Invoke action
# 4. After callbacks (if configured)
# 5. Rollback (if action is unsuccessful)
#
# If a block is passed to this method, that block will be called instead
# of invoking each transition's action.
def perform(&block)
reset
if valid?
if use_event_attributes? && !block_given?
each do |transition|
transition.transient = true
transition.machine.write(object, :event_transition, transition)
end
run_actions
else
within_transaction do
catch(:halt) { run_callbacks(&block) }
rollback unless success?
end
end
end
# ...
end
To skip the transaction, you should monkey patch state_machine so that transition methods (such as orangify!) check whether the record is valid before transitioning.
Here's an example of what you should achieve
# Override orangify! state machine action
# If the record is valid, then perform the actual transition,
# otherwise return early.
def orangify!(*args)
return false unless self.valid?
super
end
Of course, you can't do that manually for each method, that's why you should monkey patch the library to achieve this result.