-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathluafsm.lua
227 lines (191 loc) · 6.66 KB
/
luafsm.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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
--[[=================================================
Lua State Machine Library
----=================================================]]
module("luafsm", package.seeall)
VERSION = "2.3.2"
SUCCEEDED = 1 -- the event transitioned successfully from one state to another
NOTRANSITION = 2 -- the event was successfull but no state transition was necessary
CANCELLED = 3 -- the event was cancelled by the caller in a beforeEvent callback
PENDING = 4 -- the event is asynchronous and the caller is in control of when the transition occurs
INVALID_TRANSITION_ERROR = 'INVALID_TRANSITION_ERROR' -- caller tried to fire an event that was innapropriate in the current state
PENDING_TRANSITION_ERROR = 'PENDING_TRANSITION_ERROR' -- caller tried to fire an event while an async transition was still pending
INVALID_CALLBACK_ERROR = 'INVALID_CALLBACK_ERROR' -- caller provided callback function threw an exception
WILDCARD = '*'
ASYNC = 'async'
local function do_callback(fsm, func, event, params)
if type(func) == 'function' then
local success, ret = pcall(func, unpack(params))
if not success then
local err = ret
fsm:error(event, INVALID_CALLBACK_ERROR, err)
end
return ret
end
end
local function before_any_event(fsm, event, params)
return do_callback(fsm, fsm.onbeforeevent, event, params)
end
local function after_any_event(fsm, event, params)
return do_callback(fsm, fsm.onafterevent or fsm.onevent, event, params)
end
local function leave_any_state(fsm, event, params)
return do_callback(fsm, fsm.onleavestate, event, params)
end
local function enter_any_state(fsm, event, params)
return do_callback(fsm, fsm.onenterstate or fsm.onstate, event, params)
end
local function change_state(fsm, event, params)
return do_callback(fsm, fsm.onchangestate, event, params)
end
local function before_this_event(fsm, event, params)
return do_callback(fsm, fsm['onbefore' .. event.name], event, params)
end
local function after_this_event(fsm, event, params)
return do_callback(fsm, fsm['onafter' .. event.name] or fsm['on' .. event.name], event, params)
end
local function leave_this_state(fsm, event, params)
return do_callback(fsm, fsm['onleave' .. event.from], event, params)
end
local function enter_this_state(fsm, event, params)
return do_callback(fsm, fsm['onenter' .. event.to] or fsm['on' .. event.to], event, params)
end
local function before_event(fsm, event, params)
if before_this_event(fsm, event, params) == false or before_any_event(fsm, event, params) == false then
return false
end
end
local function after_event(fsm, event, params)
after_this_event(fsm, event, params)
after_any_event(fsm, event, params)
end
local function leave_state(fsm, event, params)
local specific = leave_this_state(fsm, event, params)
local general = leave_any_state(fsm, event, params)
if specific == false or general == false then
return false
elseif specific == ASYNC or general == ASYNC then
return ASYNC
end
end
local function enter_state(fsm, event, params)
enter_this_state(fsm, event, params)
enter_any_state(fsm, event, params)
end
local function build_event(name, entry)
return function(self, ...)
local from = self.current
local to = entry[from] or entry[WILDCARD] or from
local event = {
name = name,
from = from,
to = to,
}
local params = {self, event, ...}
if self.transition then
return self:error(event, PENDING_TRANSITION_ERROR, ('event %s inappropriate because previous transition did not complete'):format(name))
end
if self:cannot(name) then
return self:error(event, INVALID_TRANSITION_ERROR, ('event %s inappropriate in current state %s'):format(name, self.current))
end
if before_event(self, event, params) == false then
return CANCELLED
end
if from == to then
after_event(self, event, params)
return NOTRANSITION
end
-- prepare a transition method for use EITHER lower down,
-- or by caller if they want an async transition (indicated by an ASYNC return value from leaveState)
local fsm = self
self.transition = {
-- provide a way for caller to cancel async transition if desired
cancel = function()
fsm.transition = nil
after_event(fsm, event, params)
end
}
setmetatable(self.transition, {
__call = function()
fsm.transition = nil -- this method should only ever be called once
fsm.current = to
enter_state(fsm, event, params)
change_state(fsm, event, params)
after_event(fsm, event, params)
return SUCCEEDED
end
})
local leave = leave_state(fsm, event, params)
if leave == false then
self.transition = nil
return CANCELLED
elseif leave == ASYNC then
return PENDING
else
if self.transition then -- need to check in case user manually called transition() but forgot to return ASYNC
return self.transition()
end
end
end
end
function create(cfg, target)
assert(type(cfg) == 'table', 'cfg must be a table')
-- allow for a simple string, or an object with { state: = 'foo', event = 'setup', defer = true|false }
local initial = type(cfg.initial) == 'string' and { state = cfg.initial } or cfg.initial
local terminal = cfg.terminal or cfg.final
local fsm = target or cfg.target or {}
local events = cfg.events or {}
local callbacks = cfg.callbacks or {}
local map = {}
local function add(e)
-- allow 'wildcard' transition if 'from' is not specified
local from = type(e.from) == 'table' and e.from or (e.from and {e.from} or {WILDCARD})
local entry = map[e.name] or {}
map[e.name] = entry
for _, v in ipairs(from) do
entry[v] = e.to or v -- allow no-op transition if 'to' is not specified
end
end
if initial then
initial.event = initial.event or 'startup'
add { name = initial.event, from = 'none', to = initial.state }
end
for _, e in ipairs(events) do
add(e)
end
for k, v in pairs(map) do
fsm[k] = build_event(k, v)
end
for k, v in pairs(callbacks) do
fsm[k] = v
end
fsm.current = 'none'
fsm.is = function(self, state)
if type(state) == 'table' then
for _, s in ipairs(state) do
if s == self.current then
return true
end
end
return false
else
return self.current == state
end
end
fsm.can = function(self, event)
if (not self.transition) and map[event] and
(map[event][self.current] or map[event][WILDCARD]) then
return true
else
return false
end
end
fsm.cannot = function(self, event) return not self:can(event) end
-- default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired
fsm.error = cfg.error or function(self, event, error_code, err) error(error_code .. " " .. err) end
fsm.is_finished = function(self) return self:is(terminal) end
if initial and not initial.defer then
fsm[initial.event](fsm)
end
return fsm
end
return _M