simplefsm 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []