module StateMachines
# Represents a collection of state machines for a class
class MachineCollection < Hash
# Initializes the state of each machine in the given object. This can allow
# states to be initialized in two groups: static and dynamic. For example:
#
# machines.initialize_states(object) do
# # After static state initialization, before dynamic state initialization
# end
#
# If no block is provided, then all states will still be initialized.
#
# Valid configuration options:
# * :static - Whether to initialize static states. Unless set to
# false, the state will be initialized regardless of its current value.
# Default is true.
# * :dynamic - Whether to initialize dynamic states. If set to
# :force, the state will be initialized regardless of its current value.
# Default is true.
# * :to - A hash to write the initialized state to instead of
# writing to the object. Default is to write directly to the object.
def initialize_states(object, options = {}, attributes = {})
options.assert_valid_keys( :static, :dynamic, :to)
options = {static: true, dynamic: true}.merge(options)
result = yield if block_given?
each_value do |machine|
unless machine.dynamic_initial_state?
force = options[:static] == :force || !attributes.keys.map(&:to_sym).include?(machine.attribute)
machine.initialize_state(object, force: force, to: options[:to])
end
end if options[:static]
each_value do |machine|
machine.initialize_state(object, force: options[:dynamic] == :force, to: options[:to]) if machine.dynamic_initial_state?
end if options[:dynamic]
result
end
# Runs one or more events in parallel on the given object. See
# StateMachines::InstanceMethods#fire_events for more information.
def fire_events(object, *events)
run_action = [true, false].include?(events.last) ? events.pop : true
# Generate the transitions to run for each event
transitions = events.collect do |event_name|
# Find the actual event being run
event = nil
detect { |name, machine| event = machine.events[event_name, :qualified_name] }
raise(InvalidEvent.new(object, event_name)) unless event
# Get the transition that will be performed for the event
unless (transition = event.transition_for(object))
event.on_failure(object)
end
transition
end.compact
# Run the events in parallel only if valid transitions were found for
# all of them
if events.length == transitions.length
TransitionCollection.new(transitions, {use_transactions: resolve_use_transactions, actions: run_action}).perform
else
false
end
end
# Builds the collection of transitions for all event attributes defined on
# the given object. This will only include events whose machine actions
# match the one specified.
#
# These should only be fired as a result of the action being run.
def transitions(object, action, options = {})
transitions = map do |name, machine|
machine.events.attribute_transition_for(object, true) if machine.action == action
end
AttributeTransitionCollection.new(transitions.compact, {use_transactions: resolve_use_transactions}.merge(options))
end
protected
def resolve_use_transactions
use_transactions = nil
each_value do |machine|
# Determine use_transactions setting for this set of transitions. If from multiple state_machines, the settings must match.
raise 'Encountered mismatched use_transactions configurations for multiple state_machines' if !use_transactions.nil? && use_transactions != machine.use_transactions
use_transactions = machine.use_transactions
end
use_transactions
end
end
end