Module: SimpleFSM
- Defined in:
- lib/simplefsm.rb
Overview
SimpleFSM - a DSL for finite state machines
This module provides a domain specific language (DSL) that can be used to model a finite state machine (FSM) for any domain, including complex communication applications based on the SIP protocol.
To utilize the DSL in a new class, the DSL module should be included into the class. The state machine the class is implementing is defined within the block of code after the fsm keyword.
- Authors
-
Edin Pjanic ([email protected]), Amer Hasanovic ([email protected])
- License
-
MIT License
Defined Under Namespace
Classes: TransitionFactory
Constant Summary collapse
- VERSION =
'0.2.3'
Class Method Summary collapse
-
.included(klass) ⇒ Object
injecting the class methods for FSM definition.
Instance Method Summary collapse
-
#initialize ⇒ Object
Instance and class methods that are to be injected to the host class.
-
#run(*args) ⇒ Object
start the machine only if it hasn’t been started.
- #state(*args) ⇒ Object
Class Method Details
.included(klass) ⇒ Object
injecting the class methods for FSM definition
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/simplefsm.rb', line 57 def self.included klass klass.class_eval do @@states ||= [] @@events ||= [] @@transitions ||= {} def self.fsm (&block) instance_eval(&block) #Events methods definition # - one method is defined for every event specified @@events.each do |ev| Kernel.send :define_method, ev do |*args| if args # If we have args here it must be an Array if args.class != Array return end else args = [] end if current_state(args).class == Hash st = current_state(args)[:state] else st = current_state(args) end statetrans = @@transitions[st] # uniquestates = [] if statetrans # Get all transitions for this event in the current state trans = statetrans.select{|t| !t.select{|k, v| k==:event and v == ev}.empty?} if trans and trans.size>0 # Index of the first transition that is triggered index_triggered = trans.index do |v| # Guard specifiers: # :guard - all must be true # :guard_not - all must be false # :guard_or - at least one must be true # All guard specifiers must evaluate to true # in order for transition to be triggered. guard_all = true guards_and = [] guards_or = [] guards_not = [] if v.has_key?(:guard) guards_and << v[:guard] guards_and.flatten! end if v.has_key?(:guard_or) guards_or << v[:guard_or] guards_or.flatten! end if v.has_key?(:guard_not) guards_not << v[:guard_not] guards_not.flatten! end # TODO: think again about those guards guard_all &&= guards_and.all? {|g| self.send(g, args) } if guards_and.size > 0 guard_all &&= guards_or.any? {|g| self.send(g, args) } if guards_or.size > 0 guard_all &&= !guards_not.any? {|g| self.send(g, args) } if guards_not.size > 0 guard_all end if index_triggered trans_triggered = trans[index_triggered] new_state = trans_triggered[:new] if trans_triggered.has_key?(:new) #START of :action keyword # Call procs for the current event # :do keyword - is not prefered # because it confuses source code editors # :action is a prefered one action_keys = ['do'.to_sym, :action] doprocs = [] action_keys.each do |key| doprocs << trans_triggered[key] if trans_triggered.has_key?(key) end doprocs.flatten! doprocs.each {|p| self.send(p, args)} if doprocs.size > 0 #END of :action keyword do_transform new_state, args end end end end end end ### FSM keywords: state, transitions_for ### # FSM state definition def self.state(sname, *data) state_data = {} symname = sname.to_sym state_data[:state] = symname if data state_data[:on] = data.first else state_data[:on] = {} end add_state_data symname, state_data end #FSM state transitions definition def self.transitions_for(sname, *trans, &block) return if !sname # return if sname is nil (no transition) sname = sname.to_sym @@transitions[sname] ||= [] #add state in case it haven't been defined add_state_data sname if block_given? tf = TransitionFactory.new tf.send :define_singleton_method, :yield_block, &block tf.yield_block trans << tf.transitions trans.flatten! end trans.each{ |t| check_transition t, sname } trans.each do |t| add_transition t, sname @@events << t[:event] if !@@events.any? { |e| t[:event] == e } end end # event keyword # returns transition that is to be added inside transitions_for method def self.event ev, args #, &block return {:event => ev}.merge!(args) end ## Private class methods ###################### # Check whether given transition is valid def self.check_transition tran, st='unknown' ev = tran[:event] if tran.is_a?(Hash) and tran.has_key?(:event) ev ||= "unknown" if !tran or !tran.is_a?(Hash) or !tran.has_key?(:event) or !tran.has_key?(:new) raise "Error in transition specification for event '#{ev}' of state '#{st}'.\n" + "\t-> Transition MUST be a Hash and at least MUST contain both keywords 'event' and 'new'.\n" + "\t-> Transition data: #{tran}.\n" return end end # Add transition to state's transitions if it does not exist def self.add_transition t, st if !@@transitions[st].any? {|v| v == t} @@transitions[st] << t #add the state to @@states if it does not exist add_state_data t[:new] end @@events << t[:event] if !@@events.any? { |e| t[:event] == e } end def self.add_state_data sname, data={:on=>nil}, overwrite=false return if !sname symname = sname.to_sym data.merge!({:state=>symname}) if !data.key?(symname) @@states.delete_if {|s| s[:state] == sname} if overwrite if !@@states.any?{|s| s[:state] == sname} @@states << data end end private_class_method :fsm, :state, :transitions_for, :event private_class_method :add_state_data , :add_transition, :check_transition end end |
Instance Method Details
#initialize ⇒ Object
Instance and class methods that are to be injected to the host class
37 38 39 40 41 |
# File 'lib/simplefsm.rb', line 37 def initialize # set_current_state nil # self.set_current_state {} super end |
#run(*args) ⇒ Object
start the machine only if it hasn’t been started
44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/simplefsm.rb', line 44 def run *args st = current_state args if !st st = @@states.first if st[:on] self.send(st[:on][:enter], args) if st[:on].has_key?(:enter) end set_current_state(st, args) end st end |
#state(*args) ⇒ Object
313 314 315 |
# File 'lib/simplefsm.rb', line 313 def state *args current_state(args)[:state] #.to_sym end |