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

Instance Method Summary collapse

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

#initializeObject

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