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.
- data/lib/simplefsm.rb +295 -0
- 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: []
|