simplefsm 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/simplefsm.rb +295 -0
  2. metadata +50 -0
data/lib/simplefsm.rb ADDED
@@ -0,0 +1,295 @@
1
+ # SimpleFSM - a DSL for finite state machines
2
+ #
3
+ #
4
+ # This module provides a domain specific language (DSL) that
5
+ # can be used to model a finite state machine (FSM) for any domain,
6
+ # including complex communication applications
7
+ # based on the SIP protocol.
8
+ #
9
+ # To utilize the DSL in a new class, the DSL module
10
+ # should be included into the class. The state machine the
11
+ # class is implementing is defined within the block of code
12
+ # after the fsm keyword.
13
+ #
14
+ # Authors:: Edin Pjanic (mailto:edin.pjanic@untz.com), Amer Hasanovic (mailto:amer.hasanovic@untz.com)
15
+ # License:: MIT License
16
+
17
+ module SimpleFSM
18
+ VERSION = '0.1.2'
19
+
20
+ def initialize
21
+ @state ||= {}
22
+ super
23
+ end
24
+
25
+ # start the machine
26
+ def run
27
+ @state = @@states.first
28
+ if @state[:on]
29
+ send(@state[:on][:enter], nil) if @state[:on].has_key?(:enter)
30
+ end
31
+ end
32
+
33
+ # get the current FSM state name
34
+ def state
35
+ @state[:state] #.to_sym
36
+ end
37
+
38
+ # injecting the class methods for FSM definition
39
+ def self.included klass
40
+ klass.class_eval do
41
+ @@states ||= []
42
+ @@events ||= []
43
+ @@transitions ||= {}
44
+ @@current_state_setup = nil
45
+
46
+ def self.fsm (&block)
47
+ instance_eval(&block)
48
+
49
+ #Events methods definition
50
+ # - one method is defined for every event specified
51
+
52
+ @@events.each do |ev|
53
+ Kernel.send :define_method, ev do |*args|
54
+ fsm_prepare_state args
55
+
56
+ if args
57
+ if args.class != Array
58
+ return
59
+ end
60
+ else
61
+ args = []
62
+ end
63
+
64
+ if @state.class == Hash
65
+ st = @state[:state]
66
+ else
67
+ st = @state
68
+ end
69
+
70
+ statetrans = @@transitions[st]
71
+ uniquestates = []
72
+
73
+ if statetrans
74
+ trans = statetrans.select{|t| !t.select{|k, v| k==:event and v == ev}.empty?}
75
+
76
+ if trans and trans.size>0
77
+ newstates = trans.select do |v|
78
+ if v.has_key?(:guard)
79
+ send(v[:guard], args)
80
+ else
81
+ true
82
+ end
83
+ end
84
+
85
+ newstates.each do |a|
86
+ uniquestates << a[:new] if a.has_key?(:new)
87
+ end
88
+ uniquestates.uniq!
89
+ numstates = uniquestates.size
90
+
91
+ if numstates > 1
92
+ raise "Error in transition (event #{ev}, state #{st}): More than 1 (#{numstates}) new state (#{uniquestates.inspect})."
93
+ return
94
+ elsif numstates < 1
95
+ return
96
+ end
97
+
98
+ #:do keyword => call proc for event
99
+ doprocs = []
100
+ newstates.each do |a|
101
+ doprocs << a[:do] if a.has_key?(:do)
102
+ end
103
+ doprocs.uniq!
104
+
105
+ if doprocs.size > 0
106
+ doprocs.each do |p|
107
+ send(p, args)
108
+ end
109
+ end
110
+ #end :do keyword
111
+
112
+ do_transform uniquestates.first, args
113
+ end
114
+ end
115
+ fsm_save_state args
116
+ end
117
+ end
118
+ end
119
+
120
+
121
+ ### FSM keywords: state, transitions_for ###
122
+
123
+ # FSM state definition
124
+ def self.state(sname, *data)
125
+ state_data = {}
126
+ symname = sname.to_sym
127
+ state_data[:state] = symname
128
+
129
+ if data
130
+ state_data[:on] = data.first
131
+ else
132
+ state_data[:on] = {}
133
+ end
134
+
135
+ add_state_data symname, state_data
136
+ end
137
+
138
+ #FSM state transitions definition
139
+ def self.transitions_for(sname, *trans, &block)
140
+ return if !sname # return if sname is nil (no transition)
141
+ sname = sname.to_sym
142
+ @@transitions[sname] ||= []
143
+
144
+ #add state in case it haven't been defined
145
+ add_state_data sname
146
+
147
+ erroneous_trans = trans.select{|a| !a.has_key?(:event) or !a.has_key?(:new)}
148
+
149
+ if !erroneous_trans.empty?
150
+ raise "Error in transitions for :#{sname}." +
151
+ "Transition MUST contain keys :event and :new.\n" +
152
+ "In: " + erroneous_trans.inspect
153
+ return
154
+ end
155
+
156
+ trans.each do |t|
157
+ if t.class != Hash
158
+ raise "Error in transitions for :#{sname} in \'#{t.inspect}\'." +
159
+ "Transition must be a Hash Array."
160
+ return
161
+ end
162
+
163
+ add_transition sname, t
164
+
165
+ @@events << t[:event] if !@@events.any? { |e| t[:event] == e }
166
+ end
167
+
168
+ # if events block is given
169
+ if block_given?
170
+ @@current_state_setup = sname
171
+ yield
172
+ @@current_state_setup = nil
173
+ end
174
+
175
+ end
176
+
177
+ # the event keyword
178
+ def self.event ev, args
179
+ if !args or !args.is_a?(Hash)
180
+ raise "Error in event description for event: #{ev}." +
181
+ "Transition MUST be a Hash and at least MUST contain key :new.\n"
182
+ return
183
+
184
+ end
185
+ if !args.has_key?(:new)
186
+ raise "Error in transitions for :#{sname}." +
187
+ "Transition MUST contain keys :event and :new.\n" +
188
+ "In: " + erroneous_trans.inspect
189
+ return
190
+ end
191
+
192
+ t = {:event => ev}.merge!(args)
193
+
194
+ if !@@current_state_setup
195
+ t
196
+ else
197
+ add_transition @@current_state_setup, t
198
+ end
199
+ end
200
+
201
+ ## private class methods ######################
202
+
203
+ # add transition to state's transitions if it does not exist
204
+ def self.add_transition st, t
205
+ if !@@transitions[st].any? {|v| v == t}
206
+ @@transitions[st] << t
207
+ add_state_data t[:new]
208
+ end
209
+
210
+ @@events << t[:event] if !@@events.any? { |e| t[:event] == e }
211
+ end
212
+
213
+ def self.add_state_data sname, data={:on=>nil}, overwrite=false
214
+ return if !sname
215
+ symname = sname.to_sym
216
+ data.merge!({:state=>symname}) if !data.key?(symname)
217
+
218
+ @@states.delete_if {|s| s[:state] == sname} if overwrite
219
+ if !@@states.any?{|s| s[:state] == sname}
220
+ @@states << data
221
+ end
222
+ end
223
+
224
+
225
+ private_class_method :fsm, :state, :transitions_for, :event
226
+
227
+ private_class_method :add_state_data, :add_transition
228
+
229
+ end
230
+ end
231
+
232
+ private
233
+
234
+ # perform transition from current to the next state
235
+ def do_transform sname, args
236
+ # if new state is nil => don't change state
237
+ return if !sname
238
+
239
+ symname = sname.to_sym
240
+ newstate = @@states.select{|a| a[:state] == symname}.first
241
+ if !newstate
242
+ raise "New state (#{sname}) is empty."
243
+ return
244
+ end
245
+
246
+ onexit = @state[:on][:exit] if @state.has_key?(:on) and @state[:on] and @state[:on].has_key?(:exit)
247
+ if newstate[:on]
248
+ onenter = newstate[:on][:enter] if newstate[:on].has_key?(:enter)
249
+ end
250
+
251
+ send(onexit, args) if onexit
252
+ @state = newstate
253
+ send(onenter, args) if onenter
254
+ end
255
+
256
+ def fsm_responds_to? ev
257
+ if @@events
258
+ return @@events.any?{|v| v == ev}
259
+ else
260
+ return false
261
+ end
262
+ end
263
+
264
+ def fsm_state_responds_to? st, ev
265
+ get_state_events(st).any?{|e| e == ev}
266
+ end
267
+
268
+ def get_state_events state
269
+ ev = []
270
+ tr = @@transitions[state[:state]]
271
+ if tr
272
+ ts = tr.each{|t| ev << t[:event]}
273
+ ev.uniq
274
+ else
275
+ []
276
+ end
277
+ end
278
+
279
+ # The following methods should be overriden according to the application.
280
+ #
281
+ # They are called when any event is fired in order
282
+ # to perform state loading/saving if the state is saved in
283
+ # an external database or similar facility.
284
+ #
285
+ # fsm_prepare_state method is called before and
286
+ # fsm_save_state method is called after
287
+ # actual state transition and all consequent actions.
288
+ def fsm_prepare_state args
289
+ @state
290
+ end
291
+
292
+ def fsm_save_state args
293
+ @state
294
+ end
295
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simplefsm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Edin Pjanic
9
+ - Amer Hasanovic
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-02-15 00:00:00.000000000 Z
14
+ dependencies: []
15
+ description: A simple and lightweight domain specific language (DSL) for modeling finite state machines (FSM).
16
+ email:
17
+ - edin@ictlab.com.ba
18
+ - amer@ictlab.com.ba
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - lib/simplefsm.rb
24
+ homepage: http://ictlab.com.ba
25
+ licenses: []
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: !binary |-
35
+ MA==
36
+ none: false
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: !binary |-
42
+ MA==
43
+ none: false
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 1.8.24
47
+ signing_key:
48
+ specification_version: 3
49
+ summary: SimpleFSM - a Ruby DSL for FSMs
50
+ test_files: []