Module: FMM

Extended by:
FMM
Included in:
FMM
Defined in:
lib/fmm.rb

Defined Under Namespace

Classes: InvalidEvent, InvalidMachine, InvalidState

Instance Method Summary collapse

Instance Method Details

#aliases_for(state) ⇒ Object



107
108
109
# File 'lib/fmm.rb', line 107

def aliases_for(state)
  state[:_machine][:aliases] ? state[:_machine][:aliases][current(state)] : nil
end

#all_names_for(state) ⇒ Object

from most to least specific, as this is the order in which we will resolve available transitions and run callbacks



114
115
116
# File 'lib/fmm.rb', line 114

def all_names_for(state)
  [ current(state), aliases_for(state), :* ].flatten
end

#callbacks(state) ⇒ Object



101
102
103
104
105
# File 'lib/fmm.rb', line 101

def callbacks(state)
  state[:_machine][:callbacks]  || {}
rescue => err
  raise InvalidMachine, '#callbacks'
end

#current(state) ⇒ Object

talk to the machine object



87
88
89
90
91
92
93
# 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

#events(state) ⇒ Object



125
126
127
# File 'lib/fmm.rb', line 125

def events(state)
  transitions(state).keys
end

#machine_states(state) ⇒ Object



133
134
135
# File 'lib/fmm.rb', line 133

def machine_states(state)
  transitions(state).values.map(&:to_a).flatten.uniq
end

#transitions(state) ⇒ Object



95
96
97
98
99
# File 'lib/fmm.rb', line 95

def transitions(state)
  state[:_machine][:transitions]
rescue => err
  raise InvalidMachine, '#transitions'
end

#trigger(state, event, payload = nil) ⇒ Object

trigger state changes



75
76
77
78
# 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

#trigger!(state, event, payload = nil) ⇒ Object



80
81
82
83
# 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

#trigger?(state, event) ⇒ Boolean

Returns:

  • (Boolean)


118
119
120
121
122
123
# 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

#triggerable_events(state) ⇒ Object



129
130
131
# File 'lib/fmm.rb', line 129

def triggerable_events(state)
  events(state).select { |event| trigger?(state, event) }
end

#validate!(state) ⇒ Object

validate a state machine; defacto specification; no state for which this method returns true should ever crash with an InvalidMachine error



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# 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