module FMM
Public Instance Methods
# File lib/fmm.rb, line 107 def aliases_for(state) state[:_machine][:aliases] ? state[:_machine][:aliases][current(state)] : nil end
from most to least specific, as this is the order in which we will resolve available transitions and run callbacks
# File lib/fmm.rb, line 114 def all_names_for(state) [ current(state), aliases_for(state), :* ].flatten end
# File lib/fmm.rb, line 101 def callbacks(state) state[:_machine][:callbacks] || {} rescue => err raise InvalidMachine, '#callbacks' end
talk to the machine object
# File lib/fmm.rb, line 87 def current(state) state[:_machine][:current] rescue => err # reraise as an explicit InvalidMachine; # get the orig out of #cause raise InvalidMachine, '#current' end
# File lib/fmm.rb, line 125 def events(state) transitions(state).keys end
# File lib/fmm.rb, line 133 def machine_states(state) transitions(state).values.map(&:to_a).flatten.uniq end
# File lib/fmm.rb, line 95 def transitions(state) state[:_machine][:transitions] rescue => err raise InvalidMachine, '#transitions' end
trigger state changes
# File lib/fmm.rb, line 75 def trigger(state, event, payload = nil) payload.freeze if payload.respond_to?(:freeze) trigger?(state, event) and change(state, event, payload) end
# File lib/fmm.rb, line 80 def trigger!(state, event, payload = nil) trigger(state, event, payload) or raise InvalidState, "Event '#{event}' not valid from state :'#{current(state)}'" end
# File lib/fmm.rb, line 118 def trigger?(state, event) unless transitions(state).has_key?(event) raise InvalidEvent, "no such event #{event}" end resolve_next_state_name(state, event) ? true : false end
# File lib/fmm.rb, line 129 def triggerable_events(state) events(state).select { |event| trigger?(state, event) } end
validate a state machine; defacto specification; no state for which this method returns true should ever crash with an InvalidMachine
error
# File lib/fmm.rb, line 11 def validate!(state) # The state is a Hash-like object (HLO) with a value # at key :_machine unless state[:_machine] raise InvalidMachine, "no state machine found: #{state}" end # The machine has a current state unless state[:_machine][:current] raise InvalidMachine, "no current state: #{state}" end # The machine specifies a set of transitions (and hence states) unless state[:_machine][:transitions] raise InvalidMachine, "you must specify some transitions: #{state}" end # The transitions table must be a HLO... unless state[:_machine][:transitions].is_a?(Hash) raise InvalidMachine, "transitions must be a hash: #{state}" end # ...all of whose values are also HLOs unless state[:_machine][:transitions].values.map { |v| v.is_a?(Hash) }.inject(:&) raise InvalidMachine, "transitions must be a hash of hashes: #{state}" end # Callbacks (which are all post-transition; see below) are optional, # but if they exist... if state[:_machine][:callbacks] # ...they must be in a HLO... unless state[:_machine][:callbacks].is_a?(Hash) raise InvalidMachine, "callbacks must be a hash: #{state}" end # ...whose values are either callables or collections thereof valid_callbacks = state[:_machine][:callbacks].values.flatten.map do |v| v.respond_to?(:call) end.inject(:&) unless valid_callbacks raise InvalidMachine, "callbacks must be callables or arrays thereof: #{state}" end end # Aliases are optional, but if they exist... if state[:_machine][:aliases] # ...they must be in a HLO. This is all we can actually # say about aliases, other than this: The keys of this # HLO correspond to states, but can be of any type; # nonexistent states simply won't be consulted. Similarly, # the values are all either names of states or collections # thereof, but that doesn't actually place any type limitation # on what the values _are_, other than not letting them be # arrays, because they will be flattened. unless state[:_machine][:aliases].is_a?(Hash) raise InvalidMachine, "aliases must be a hash: #{state}" end end true end
Private Instance Methods
the elbow grease
there was a whole one-person debate here about the point/utility of pre-transition callbacks; if they actually prove to be something useful, we can add them without breaking existing machines. ([:_machine], at which point, we can just construe [:_machine] as synonymous with [:post]) for now, i don't really see what purpose they serve in this sort of application other than being able to veto state changes, maybe; for now i say poo on them
# File lib/fmm.rb, line 152 def change(state, event, payload) state = update_machine_state(state, resolve_next_state_name(state, event)) # post callbacks; these we very much want resolve_callbacks(state).each do |callback| state = callback.call(state, event, payload) end state rescue => err raise InvalidMachine.new('#change') end
# File lib/fmm.rb, line 163 def resolve_callbacks(state) all_names_for(state).map { |n| callbacks(state)[n] }.flatten.compact end
# File lib/fmm.rb, line 167 def resolve_next_state_name(state, event) all_names_for(state).map { |n| transitions(state)[event][n] }.compact.first end
# File lib/fmm.rb, line 171 def update_machine_state(state, target) raise InvalidState, "nil target" unless target new_machine = state[:_machine].merge({ current: target }) state.merge({ _machine: new_machine }) end