Class: StateMachines::Machine
- Inherits:
-
Object
- Object
- StateMachines::Machine
- Extended by:
- ClassMethods
- Includes:
- EvalHelpers, ActionHooks, AsyncExtensions, Callbacks, Configuration, EventMethods, HelperGenerators, Integration, Parsing, Rendering, Scoping, StateMethods, Utilities, Validation, MatcherHelpers
- Defined in:
- lib/state_machines/machine.rb,
lib/state_machines/machine/parsing.rb,
lib/state_machines/machine/scoping.rb,
lib/state_machines/machine/callbacks.rb,
lib/state_machines/machine/rendering.rb,
lib/state_machines/machine/utilities.rb,
lib/state_machines/machine/validation.rb,
lib/state_machines/machine/integration.rb,
lib/state_machines/machine/action_hooks.rb,
lib/state_machines/machine/class_methods.rb,
lib/state_machines/machine/configuration.rb,
lib/state_machines/machine/event_methods.rb,
lib/state_machines/machine/state_methods.rb,
lib/state_machines/machine/async_extensions.rb,
lib/state_machines/machine/helper_generators.rb
Overview
Represents a state machine for a particular attribute. State machines consist of states, events and a set of transitions that define how the state changes after a particular event is fired.
A state machine will not know all of the possible states for an object unless they are referenced somewhere in the state machine definition. As a result, any unused states should be defined with the other_states
or state
helper.
Actions
When an action is configured for a state machine, it is invoked when an object transitions via an event. The success of the event becomes dependent on the success of the action. If the action is successful, then the transitioned state remains persisted. However, if the action fails (by returning false), the transitioned state will be rolled back.
For example,
class Vehicle
attr_accessor :fail, :saving_state
state_machine :initial => :parked, :action => :save do
event :ignite do
transition :parked => :idling
end
event :park do
transition :idling => :parked
end
end
def save
@saving_state = state
fail != true
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
vehicle.save # => true
vehicle.saving_state # => "parked" # The state was "parked" was save was called
# Successful event
vehicle.ignite # => true
vehicle.saving_state # => "idling" # The state was "idling" when save was called
vehicle.state # => "idling"
# Failed event
vehicle.fail = true
vehicle.park # => false
vehicle.saving_state # => "parked"
vehicle.state # => "idling"
As shown, even though the state is set prior to calling the save
action on the object, it will be rolled back to the original state if the action fails. Note that this will also be the case if an exception is raised while calling the action.
Indirect transitions
In addition to the action being run as the result of an event, the action can also be used to run events itself. For example, using the above as an example:
vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
vehicle.state_event = 'ignite'
vehicle.save # => true
vehicle.state # => "idling"
vehicle.state_event # => nil
As can be seen, the save
action automatically invokes the event stored in the state_event
attribute (:ignite
in this case).
One important note about using this technique for running transitions is that if the class in which the state machine is defined also defines the action being invoked (and not a superclass), then it must manually run the StateMachine hook that checks for event attributes.
For example, in ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel, the default action (save
) is already defined in a base class. As a result, when a state machine is defined in a model / resource, StateMachine can automatically hook into the save
action.
On the other hand, the Vehicle class from above defined its own save
method (and there is no save
method in its superclass). As a result, it must be modified like so:
def save
self.class.state_machines.transitions(self, :save).perform do
@saving_state = state
fail != true
end
end
This will add in the functionality for firing the event stored in the state_event
attribute.
Callbacks
Callbacks are supported for hooking before and after every possible transition in the machine. Each callback is invoked in the order in which it was defined. See StateMachines::Machine#before_transition and StateMachines::Machine#after_transition for documentation on how to define new callbacks.
Note that callbacks only get executed within the context of an event. As a result, if a class has an initial state when it’s created, any callbacks that would normally get executed when the object enters that state will not get triggered.
For example,
class Vehicle
state_machine initial: :parked do
after_transition all => :parked do
raise ArgumentError
end
...
end
end
vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked">
vehicle.save # => true (no exception raised)
If you need callbacks to get triggered when an object is created, this should be done by one of the following techniques:
-
Use a
before :create
or equivalent hook:class Vehicle before :create, :track_initial_transition state_machine do ... end end
-
Set an initial state and use the correct event to create the object with the proper state, resulting in callbacks being triggered and the object getting persisted (note that the
:pending
state is actually stored as nil):class Vehicle state_machine initial: :pending after_transition pending: :parked, do: :track_initial_transition event :park do transition pending: :parked end state :pending, value: nil end end vehicle = Vehicle.new vehicle.park
-
Use a default event attribute that will automatically trigger when the configured action gets run (note that the
:pending
state is actually stored as nil):class Vehicle < ActiveRecord::Base state_machine initial: :pending after_transition pending: :parked, do: :track_initial_transition event :park do transition pending: :parked end state :pending, value: nil end def initialize(*) super self.state_event = 'park' end end vehicle = Vehicle.new vehicle.save
Canceling callbacks
Callbacks can be canceled by throwing :halt at any point during the callback. For example,
...
throw :halt
...
If a before
callback halts the chain, the associated transition and all later callbacks are canceled. If an after
callback halts the chain, the later callbacks are canceled, but the transition is still successful.
These same rules apply to around
callbacks with the exception that any around
callback that doesn’t yield will essentially result in :halt being thrown. Any code executed after the yield will behave in the same way as after
callbacks.
Note that if a before
callback fails and the bang version of an event was invoked, an exception will be raised instead of returning false. For example,
class Vehicle
state_machine :initial => :parked do
before_transition any => :idling, :do => lambda {|vehicle| throw :halt}
...
end
end
vehicle = Vehicle.new
vehicle.park # => false
vehicle.park! # => StateMachines::InvalidTransition: Cannot transition state via :park from "idling"
Observers
Observers, in the sense of external classes and not Ruby’s Observable mechanism, can hook into state machines as well. Such observers use the same callback api that’s used internally.
Below are examples of defining observers for the following state machine:
class Vehicle
state_machine do
event :park do
transition idling: :parked
end
...
end
...
end
Event/Transition behaviors:
class VehicleObserver
def self.before_park(vehicle, transition)
logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
end
def self.after_park(vehicle, transition, result)
logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
end
def self.before_transition(vehicle, transition)
logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
end
def self.after_transition(vehicle, transition)
logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
end
def self.around_transition(vehicle, transition)
logger.info Benchmark.measure { yield }
end
end
Vehicle.state_machine do
before_transition :on => :park, :do => VehicleObserver.method(:before_park)
before_transition VehicleObserver.method(:before_transition)
after_transition :on => :park, :do => VehicleObserver.method(:after_park)
after_transition VehicleObserver.method(:after_transition)
around_transition VehicleObserver.method(:around_transition)
end
One common callback is to record transitions for all models in the system for auditing/debugging purposes. Below is an example of an observer that can easily automate this process for all models:
class StateMachineObserver
def self.before_transition(object, transition)
Audit.log_transition(object.attributes)
end
end
[Vehicle, Switch, Project].each do |klass|
klass.state_machines.each do |attribute, machine|
machine.before_transition StateMachineObserver.method(:before_transition)
end
end
Additional observer-like behavior may be exposed by the various integrations available. See below for more information on integrations.
Overriding instance / class methods
Hooking in behavior to the generated instance / class methods from the state machine, events, and states is very simple because of the way these methods are generated on the class. Using the class’s ancestors, the original generated method can be referred to via super
. For example,
class Vehicle
state_machine do
event :park do
...
end
end
def park(*args)
logger.info "..."
super
end
end
In the above example, the park
instance method that’s generated on the Vehicle class (by the associated event) is overridden with custom behavior. Once this behavior is complete, the original method from the state machine is invoked by simply calling super
.
The same technique can be used for state
, state_name
, and all other instance and class methods on the Vehicle class.
Method conflicts
By default state_machine does not redefine methods that exist on superclasses (including Object) or any modules (including Kernel) that were included before it was defined. This is in order to ensure that existing behavior on the class is not broken by the inclusion of state_machine.
If a conflicting method is detected, state_machine will generate a warning. For example, consider the following class:
class Vehicle
state_machine do
event :open do
...
end
end
end
In the above class, an event named “open” is defined for its state machine. However, “open” is already defined as an instance method in Ruby’s Kernel module that gets included in every Object. As a result, state_machine will generate the following warning:
Instance method "open" is already defined in Object, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.
Even though you may not be using Kernel’s implementation of the “open” instance method, state_machine isn’t aware of this and, as a result, stays safe and just skips redefining the method.
As with almost all helpers methods defined by state_machine in your class, there are generic methods available for working around this method conflict. In the example above, you can invoke the “open” event like so:
vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
vehicle.fire_events(:open) # => true
# This will not work
vehicle.open # => NoMethodError: private method `open' called for #<Vehicle:0xb72686b4 @state=nil>
If you want to take on the risk of overriding existing methods and just ignore method conflicts altogether, you can do so by setting the following configuration:
StateMachines::Machine.ignore_method_conflicts = true
This will allow you to define events like “open” as described above and still generate the “open” instance helper method. For example:
StateMachines::Machine.ignore_method_conflicts = true
class Vehicle
state_machine do
event :open do
...
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
vehicle.open # => true
By default, state_machine helps prevent you from making mistakes and accidentally overriding methods that you didn’t intend to. Once you understand this and what the consequences are, setting the ignore_method_conflicts
option is a perfectly reasonable workaround.
Integrations
By default, state machines are library-agnostic, meaning that they work on any Ruby class and have no external dependencies. However, there are certain libraries which expose additional behavior that can be taken advantage of by state machines.
This library is built to work out of the box with a few popular Ruby libraries that allow for additional behavior to provide a cleaner and smoother experience. This is especially the case for objects backed by a database that may allow for transactions, persistent storage, search/filters, callbacks, etc.
When a state machine is defined for classes using any of the above libraries, it will try to automatically determine the integration to use (Agnostic, ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, or Sequel) based on the class definition. To see how each integration affects the machine’s behavior, refer to all constants defined under the StateMachines::Integrations namespace.
Defined Under Namespace
Modules: ActionHooks, AsyncExtensions, Callbacks, ClassMethods, Configuration, EventMethods, HelperGenerators, Integration, Parsing, Rendering, Scoping, StateMethods, Utilities, Validation
Constant Summary
Constants included from Validation
Validation::DANGEROUS_PATTERNS
Instance Attribute Summary collapse
-
#action ⇒ Object
readonly
The action to invoke when an object transitions.
-
#callbacks ⇒ Object
readonly
The callbacks to invoke before/after a transition is performed.
-
#events ⇒ Object
readonly
The events that trigger transitions.
-
#name ⇒ Object
readonly
The name of the machine, used for scoping methods generated for the machine as a whole (not states or events).
-
#namespace ⇒ Object
readonly
An identifier that forces all methods (including state predicates and event methods) to be generated with the value prefixed or suffixed, depending on the context.
-
#owner_class ⇒ Object
readonly
The class that the machine is defined in.
-
#states ⇒ Object
readonly
A list of all of the states known to this state machine.
-
#use_transactions ⇒ Object
readonly
Whether the machine will use transactions when firing events.
Attributes included from ClassMethods
Instance Method Summary collapse
-
#action_hook?(self_only = false) ⇒ Boolean
Determines whether an action hook was defined for firing attribute-based event transitions when the configured action gets called.
-
#after_failure(*args, **options) ⇒ Object
Creates a callback that will be invoked after a transition failures to be performed so long as the given requirements match the transition.
-
#after_transition(*args, **options) ⇒ Object
Creates a callback that will be invoked after a transition is performed so long as the given requirements match the transition.
-
#around_transition(*args, **options) ⇒ Object
Creates a callback that will be invoked around a transition so long as the given requirements match the transition.
-
#before_transition(*args, **options) ⇒ Object
Creates a callback that will be invoked before a transition is performed so long as the given requirements match the transition.
-
#define_helper(scope, method, &block) ⇒ Object
Defines a new helper method in an instance or class scope with the given name.
- #draw ⇒ Object
-
#errors_for(_object) ⇒ Object
Gets a description of the errors for the given object.
-
#generate_message(name, values = []) ⇒ Object
Generates the message to use when invalidating the given object after failing to transition on a specific event.
-
#invalidate(_object, _attribute, _message, _values = []) ⇒ Object
Marks the given object as invalid with the given message.
- #renderer ⇒ Object
-
#reset(_object) ⇒ Object
Resets any errors previously added when invalidating the given object.
-
#within_transaction(object) ⇒ Object
Runs a transaction, rolling back any changes if the yielded block fails.
Methods included from ClassMethods
default_messages, default_messages=, find_or_create, replace_messages
Methods included from AsyncExtensions
#async_mode_enabled?, #configure_async_mode!, #read_safely, #run_callbacks_safely, #write_safely
Methods included from EventMethods
#event, #paths_for, #transition
Methods included from StateMethods
#dynamic_initial_state?, #initial_state, #initialize_state, #read, #state, #write
Methods included from Configuration
#attribute, #initial_state=, #initialize, #initialize_copy, #owner_class=
Methods included from MatcherHelpers
Methods included from EvalHelpers
#evaluate_method, #evaluate_method_with_event_args
Instance Attribute Details
#action ⇒ Object (readonly)
The action to invoke when an object transitions
469 470 471 |
# File 'lib/state_machines/machine.rb', line 469 def action @action end |
#callbacks ⇒ Object (readonly)
The callbacks to invoke before/after a transition is performed
Maps :before => callbacks and :after => callbacks
466 467 468 |
# File 'lib/state_machines/machine.rb', line 466 def callbacks @callbacks end |
#events ⇒ Object (readonly)
The events that trigger transitions. These are sorted, by default, in the order in which they were defined.
450 451 452 |
# File 'lib/state_machines/machine.rb', line 450 def events @events end |
#name ⇒ Object (readonly)
The name of the machine, used for scoping methods generated for the machine as a whole (not states or events)
446 447 448 |
# File 'lib/state_machines/machine.rb', line 446 def name @name end |
#namespace ⇒ Object (readonly)
An identifier that forces all methods (including state predicates and event methods) to be generated with the value prefixed or suffixed, depending on the context.
474 475 476 |
# File 'lib/state_machines/machine.rb', line 474 def namespace @namespace end |
#owner_class ⇒ Object (readonly)
The class that the machine is defined in
442 443 444 |
# File 'lib/state_machines/machine.rb', line 442 def owner_class @owner_class end |
#states ⇒ Object (readonly)
A list of all of the states known to this state machine. This will pull states from the following sources:
-
Initial state
-
State behaviors
-
Event transitions (:to, :from, and :except_from options)
-
Transition callbacks (:to, :from, :except_to, and :except_from options)
-
Unreferenced states (using
other_states
helper)
These are sorted, by default, in the order in which they were referenced.
461 462 463 |
# File 'lib/state_machines/machine.rb', line 461 def states @states end |
#use_transactions ⇒ Object (readonly)
Whether the machine will use transactions when firing events
477 478 479 |
# File 'lib/state_machines/machine.rb', line 477 def use_transactions @use_transactions end |
Instance Method Details
#action_hook?(self_only = false) ⇒ Boolean
Determines whether an action hook was defined for firing attribute-based event transitions when the configured action gets called.
1615 1616 1617 |
# File 'lib/state_machines/machine.rb', line 1615 def action_hook?(self_only = false) @action_hook_defined || (!self_only && owner_class.state_machines.any? { |_name, machine| machine.action == action && machine != self && machine.action_hook?(true) }) end |
#after_failure(*args, **options) ⇒ Object
Creates a callback that will be invoked after a transition failures to be performed so long as the given requirements match the transition.
See before_transition
for a description of the possible configurations for defining callbacks. Note however that you cannot define the state requirements in these callbacks. You may only define event requirements.
The callback
Failure callbacks get invoked whenever an event fails to execute. This can happen when no transition is available, a before
callback halts execution, or the action associated with this machine fails to succeed. In any of these cases, any failure callback that matches the attempted transition will be run.
For example,
class Vehicle
state_machine do
after_failure do |vehicle, transition|
logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
end
after_failure :on => :ignite, :do => :log_ignition_failure
...
end
end
1483 1484 1485 1486 1487 1488 1489 |
# File 'lib/state_machines/machine.rb', line 1483 def after_failure(*args, **, &) # Extract legacy positional arguments and merge with keyword options = parse_callback_arguments(args, ) StateMachines::OptionsValidator.assert_valid_keys!(, :on, :do, :if, :unless) add_callback(:failure, , &) end |
#after_transition(*args, **options) ⇒ Object
Creates a callback that will be invoked after a transition is performed so long as the given requirements match the transition.
See before_transition
for a description of the possible configurations for defining callbacks.
1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 |
# File 'lib/state_machines/machine.rb', line 1378 def after_transition(*args, **, &) # Extract legacy positional arguments and merge with keyword options = parse_callback_arguments(args, ) # Only validate callback-specific options, not state transition requirements = .slice(:do, :if, :unless, :bind_to_object, :terminator) StateMachines::OptionsValidator.assert_valid_keys!(, :do, :if, :unless, :bind_to_object, :terminator) add_callback(:after, , &) end |
#around_transition(*args, **options) ⇒ Object
Creates a callback that will be invoked around a transition so long as the given requirements match the transition.
The callback
Around callbacks wrap transitions, executing code both before and after. These callbacks are defined in the exact same manner as before / after callbacks with the exception that the transition must be yielded to in order to finish running it.
If defining around
callbacks using blocks, you must yield within the transition by directly calling the block (since yielding is not allowed within blocks).
For example,
class Vehicle
state_machine do
around_transition do |block|
Benchmark.measure { block.call }
end
around_transition do |vehicle, block|
logger.info "vehicle was #{state}..."
block.call
logger.info "...and is now #{state}"
end
around_transition do |vehicle, transition, block|
logger.info "before #{transition.event}: #{vehicle.state}"
block.call
logger.info "after #{transition.event}: #{vehicle.state}"
end
end
end
Notice that referencing the block is similar to doing so within an actual method definition in that it is always the last argument.
On the other hand, if you’re defining around
callbacks using method references, you can yield like normal:
class Vehicle
state_machine do
around_transition :benchmark
...
end
def benchmark
Benchmark.measure { yield }
end
end
See before_transition
for a description of the possible configurations for defining callbacks.
1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 |
# File 'lib/state_machines/machine.rb', line 1444 def around_transition(*args, **, &) # Extract legacy positional arguments and merge with keyword options = parse_callback_arguments(args, ) # Only validate callback-specific options, not state transition requirements = .slice(:do, :if, :unless, :bind_to_object, :terminator) StateMachines::OptionsValidator.assert_valid_keys!(, :do, :if, :unless, :bind_to_object, :terminator) add_callback(:around, , &) end |
#before_transition(*args, **options) ⇒ Object
Creates a callback that will be invoked before a transition is performed so long as the given requirements match the transition.
The callback
Callbacks must be defined as either an argument, in the :do option, or as a block. For example,
class Vehicle
state_machine do
before_transition :set_alarm
before_transition :set_alarm, all => :parked
before_transition all => :parked, :do => :set_alarm
before_transition all => :parked do |vehicle, transition|
vehicle.set_alarm
end
...
end
end
Notice that the first three callbacks are the same in terms of how the methods to invoke are defined. However, using the :do
can provide for a more fluid DSL.
In addition, multiple callbacks can be defined like so:
class Vehicle
state_machine do
before_transition :set_alarm, :lock_doors, all => :parked
before_transition all => :parked, :do => [:set_alarm, :lock_doors]
before_transition :set_alarm do |vehicle, transition|
vehicle.lock_doors
end
end
end
Notice that the different ways of configuring methods can be mixed.
State requirements
Callbacks can require that the machine be transitioning from and to specific states. These requirements use a Hash syntax to map beginning states to ending states. For example,
before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
In this case, the set_alarm
callback will only be called if the machine is transitioning from parked
to idling
or from idling
to parked
.
To help define state requirements, a set of helpers are available for slightly more complex matching:
-
all
- Matches every state/event in the machine -
all - [:parked, :idling, ...]
- Matches every state/event except those specified -
any
- An alias forall
(matches every state/event in the machine) -
same
- Matches the same state being transitioned from
See StateMachines::MatcherHelpers for more information.
Examples:
before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
before_transition all => :parked, :do => ... # Matches all states to parked
before_transition any => same, :do => ... # Matches every loopback
Event requirements
In addition to state requirements, an event requirement can be defined so that the callback is only invoked on specific events using the on
option. This can also use the same matcher helpers as the state requirements.
Examples:
before_transition :on => :ignite, :do => ... # Matches only on ignite
before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
Verbose Requirements
Requirements can also be defined using verbose options rather than the implicit Hash syntax and helper methods described above.
Configuration options:
-
:from
- One or more states being transitioned from. If none are specified, then all states will match. -
:to
- One or more states being transitioned to. If none are specified, then all states will match. -
:on
- One or more events that fired the transition. If none are specified, then all events will match. -
:except_from
- One or more states not being transitioned from -
:except_to
- One more states not being transitioned to -
:except_on
- One or more events that *did not* fire the transition
Examples:
before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
Conditions
In addition to the state/event requirements, a condition can also be defined to help determine whether the callback should be invoked.
Configuration options:
-
:if
- A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value. -
:unless
- A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
Examples:
before_transition :parked => :idling, :if => :moving?, :do => ...
before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
Accessing the transition
In addition to passing the object being transitioned, the actual transition describing the context (e.g. event, from, to) can be accessed as well. This additional argument is only passed if the callback allows for it.
For example,
class Vehicle
# Only specifies one parameter (the object being transitioned)
before_transition all => :parked do |vehicle|
vehicle.set_alarm
end
# Specifies 2 parameters (object being transitioned and actual transition)
before_transition all => :parked do |vehicle, transition|
vehicle.set_alarm(transition)
end
end
Note that the object in the callback will only be passed in as an argument if callbacks are configured to not be bound to the object involved. This is the default and may change on a per-integration basis.
See StateMachines::Transition for more information about the attributes available on the transition.
Usage with delegates
As noted above, state_machine uses the callback method’s argument list arity to determine whether to include the transition in the method call. If you’re using delegates, such as those defined in ActiveSupport or Forwardable, the actual arity of the delegated method gets masked. This means that callbacks which reference delegates will always get passed the transition as an argument. For example:
class Vehicle
extend Forwardable
delegate :refresh => :dashboard
state_machine do
before_transition :refresh
...
end
def dashboard
@dashboard ||= Dashboard.new
end
end
class Dashboard
def refresh(transition)
# ...
end
end
In the above example, Dashboard#refresh
must defined a transition
argument. Otherwise, an ArgumentError
exception will get raised. The only way around this is to avoid the use of delegates and manually define the delegate method so that the correct arity is used.
Examples
Below is an example of a class with one state machine and various types of before
transitions defined for it:
class Vehicle
state_machine do
# Before all transitions
before_transition :update_dashboard
# Before specific transition:
before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
# With conditional callback:
before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
# Using helpers:
before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
...
end
end
As can be seen, any number of transitions can be created using various combinations of configuration options.
1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 |
# File 'lib/state_machines/machine.rb', line 1362 def before_transition(*args, **, &) # Extract legacy positional arguments and merge with keyword options = parse_callback_arguments(args, ) # Only validate callback-specific options, not state transition requirements = .slice(:do, :if, :unless, :bind_to_object, :terminator) StateMachines::OptionsValidator.assert_valid_keys!(, :do, :if, :unless, :bind_to_object, :terminator) add_callback(:before, , &) end |
#define_helper(scope, method, &block) ⇒ Object
Defines a new helper method in an instance or class scope with the given name. If the method is already defined in the scope, then this will not override it.
If passing in a block, there are two side effects to be aware of
-
The method cannot be chained, meaning that the block cannot call
super
-
If the method is already defined in an ancestor, then it will not get overridden and a warning will be output.
Example:
# Instance helper
machine.define_helper(:instance, :state_name) do |machine, object|
machine.states.match(object).name
end
# Class helper
machine.define_helper(:class, :state_machine_name) do |machine, klass|
"State"
end
You can also define helpers using string evaluation like so:
# Instance helper
machine.define_helper :instance, " def state_name\n self.class.state_machine(:state).states.match(self).name\n end\n", __FILE__, __LINE__ + 1
# Class helper
machine.define_helper :class, " def state_machine_name\n \"State\"\n end\n", __FILE__, __LINE__ + 1
552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 |
# File 'lib/state_machines/machine.rb', line 552 def define_helper(scope, method, *, **, &block) helper_module = @helper_modules.fetch(scope) if block_given? if !self.class.ignore_method_conflicts && (conflicting_ancestor = owner_class_ancestor_has_method?(scope, method)) ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true." else name = self.name helper_module.class_eval do define_method(method) do |*args, **kwargs| block.call((scope == :instance ? self.class : self).state_machine(name), self, *args, **kwargs) end end end else helper_module.class_eval(method, __FILE__, __LINE__) end end |
#draw ⇒ Object
1609 1610 1611 |
# File 'lib/state_machines/machine.rb', line 1609 def draw(**) renderer.draw_machine(self, **) end |
#errors_for(_object) ⇒ Object
Gets a description of the errors for the given object. This is used to provide more detailed information when an InvalidTransition exception is raised.
1569 1570 1571 |
# File 'lib/state_machines/machine.rb', line 1569 def errors_for(_object) '' end |
#generate_message(name, values = []) ⇒ Object
Generates the message to use when invalidating the given object after failing to transition on a specific event
1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 |
# File 'lib/state_machines/machine.rb', line 1580 def (name, values = []) = [name] || self.class.[name] # Check whether there are actually any values to interpolate to avoid # any warnings if .scan(/%./).any? { |match| match != '%%' } % values.map(&:last) else end end |
#invalidate(_object, _attribute, _message, _values = []) ⇒ Object
Marks the given object as invalid with the given message.
By default, this is a no-op.
1564 |
# File 'lib/state_machines/machine.rb', line 1564 def invalidate(_object, _attribute, , _values = []); end |
#renderer ⇒ Object
1605 1606 1607 |
# File 'lib/state_machines/machine.rb', line 1605 def renderer self.class.renderer end |
#reset(_object) ⇒ Object
Resets any errors previously added when invalidating the given object.
By default, this is a no-op.
1576 |
# File 'lib/state_machines/machine.rb', line 1576 def reset(_object); end |
#within_transaction(object) ⇒ Object
Runs a transaction, rolling back any changes if the yielded block fails.
This is only applicable to integrations that involve databases. By default, this will not run any transactions since the changes aren’t taking place within the context of a database.
1597 1598 1599 1600 1601 1602 1603 |
# File 'lib/state_machines/machine.rb', line 1597 def within_transaction(object, &) if use_transactions transaction(object, &) else yield end end |