-
Notifications
You must be signed in to change notification settings - Fork 53
/
handlers.lua
384 lines (342 loc) · 13.4 KB
/
handlers.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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
--- The Handlers library provides a flexible way to manage and execute a series of handlers based on patterns. Each handler consists of a pattern function, a handle function, and a name. This library is suitable for scenarios where different actions need to be taken based on varying input criteria. Returns the handlers table.
-- @module handlers
--- The handlers table
-- @table handlers
-- @field _version The version number of the handlers module
-- @field list The list of handlers
-- @field coroutines The coroutines of the handlers
-- @field onceNonce The nonce for the once handlers
-- @field utils The handlers-utils module
-- @field generateResolver The generateResolver function
-- @field receive The receive function
-- @field once The once function
-- @field add The add function
-- @field append The append function
-- @field prepend The prepend function
-- @field remove The remove function
-- @field evaluate The evaluate function
local handlers = { _version = "0.0.5" }
local coroutine = require('coroutine')
local utils = require('.utils')
handlers.utils = require('.handlers-utils')
-- if update we need to keep defined handlers
if Handlers then
handlers.list = Handlers.list or {}
handlers.coroutines = Handlers.coroutines or {}
else
handlers.list = {}
handlers.coroutines = {}
end
handlers.onceNonce = 0
--- Given an array, a property name, and a value, returns the index of the object in the array that has the property with the value.
-- @lfunction findIndexByProp
-- @tparam {table[]} array The array to search through
-- @tparam {string} prop The property name to check
-- @tparam {any} value The value to check for in the property
-- @treturn {number | nil} The index of the object in the array that has the property with the value, or nil if no such object is found
local function findIndexByProp(array, prop, value)
for index, object in ipairs(array) do
if object[prop] == value then
return index
end
end
return nil
end
--- Given a name, a pattern, and a handle, asserts that the arguments are valid.
-- @lfunction assertAddArgs
-- @tparam {string} name The name of the handler
-- @tparam {table | function | string} pattern The pattern to check for in the message
-- @tparam {function} handle The function to call if the pattern matches
-- @tparam {number | string | nil} maxRuns The maximum number of times the handler should run, or nil if there is no limit
local function assertAddArgs(name, pattern, handle, maxRuns)
assert(
type(name) == 'string' and
(type(pattern) == 'function' or type(pattern) == 'table' or type(pattern) == 'string'),
'Invalid arguments given. Expected: \n' ..
'\tname : string, ' ..
'\tpattern : Action : string | MsgMatch : table,\n' ..
'\t\tfunction(msg: Message) : {-1 = break, 0 = skip, 1 = continue},\n' ..
'\thandle(msg : Message) : void) | Resolver,\n' ..
'\tMaxRuns? : number | "inf" | nil')
end
--- Given a resolver specification, returns a resolver function.
-- @function generateResolver
-- @tparam {table | function} resolveSpec The resolver specification
-- @treturn {function} A resolver function
function handlers.generateResolver(resolveSpec)
return function(msg)
-- If the resolver is a single function, call it.
-- Else, find the first matching pattern (by its matchSpec), and exec.
if type(resolveSpec) == "function" then
return resolveSpec(msg)
else
for matchSpec, func in pairs(resolveSpec) do
if utils.matchesSpec(msg, matchSpec) then
return func(msg)
end
end
end
end
end
--- Given a pattern, returns the next message that matches the pattern.
-- This function uses Lua's coroutines under-the-hood to add a handler, pause,
-- and then resume the current coroutine. This allows us to effectively block
-- processing of one message until another is received that matches the pattern.
-- @function receive
-- @tparam {table | function} pattern The pattern to check for in the message
function handlers.receive(pattern)
local self = coroutine.running()
handlers.once(pattern, function (msg)
-- If the result of the resumed coroutine is an error then we should bubble it up to the process
local _, success, errmsg = coroutine.resume(self, msg)
if not success then
error(errmsg)
end
end)
return coroutine.yield(pattern)
end
--- Given a name, a pattern, and a handle, adds a handler to the list.
-- If name is not provided, "_once_" prefix plus onceNonce will be used as the name.
-- Adds handler with maxRuns of 1 such that it will only be called once then removed from the list.
-- @function once
-- @tparam {string} name The name of the handler
-- @tparam {table | function | string} pattern The pattern to check for in the message
-- @tparam {function} handle The function to call if the pattern matches
function handlers.once(...)
local name, pattern, handle
if select("#", ...) == 3 then
name = select(1, ...)
pattern = select(2, ...)
handle = select(3, ...)
else
name = "_once_" .. tostring(handlers.onceNonce)
handlers.onceNonce = handlers.onceNonce + 1
pattern = select(1, ...)
handle = select(2, ...)
end
handlers.prepend(name, pattern, handle, 1)
end
--- Given a name, a pattern, and a handle, adds a handler to the list.
-- @function add
-- @tparam {string} name The name of the handler
-- @tparam {table | function | string} pattern The pattern to check for in the message
-- @tparam {function} handle The function to call if the pattern matches
-- @tparam {number | string | nil} maxRuns The maximum number of times the handler should run, or nil if there is no limit
function handlers.add(...)
local name, pattern, handle, maxRuns
local args = select("#", ...)
if args == 2 then
name = select(1, ...)
pattern = select(1, ...)
handle = select(2, ...)
maxRuns = nil
elseif args == 3 then
name = select(1, ...)
pattern = select(2, ...)
handle = select(3, ...)
maxRuns = nil
else
name = select(1, ...)
pattern = select(2, ...)
handle = select(3, ...)
maxRuns = select(4, ...)
end
assertAddArgs(name, pattern, handle, maxRuns)
handle = handlers.generateResolver(handle)
-- update existing handler by name
local idx = findIndexByProp(handlers.list, "name", name)
if idx ~= nil and idx > 0 then
-- found update
handlers.list[idx].pattern = pattern
handlers.list[idx].handle = handle
handlers.list[idx].maxRuns = maxRuns
else
-- not found then add
table.insert(handlers.list, { pattern = pattern, handle = handle, name = name, maxRuns = maxRuns })
end
return #handlers.list
end
--- Appends a new handler to the end of the handlers list.
-- @function append
-- @tparam {string} name The name of the handler
-- @tparam {table | function | string} pattern The pattern to check for in the message
-- @tparam {function} handle The function to call if the pattern matches
-- @tparam {number | string | nil} maxRuns The maximum number of times the handler should run, or nil if there is no limit
function handlers.append(...)
local name, pattern, handle, maxRuns
local args = select("#", ...)
if args == 2 then
name = select(1, ...)
pattern = select(1, ...)
handle = select(2, ...)
maxRuns = nil
elseif args == 3 then
name = select(1, ...)
pattern = select(2, ...)
handle = select(3, ...)
maxRuns = nil
else
name = select(1, ...)
pattern = select(2, ...)
handle = select(3, ...)
maxRuns = select(4, ...)
end
assertAddArgs(name, pattern, handle, maxRuns)
handle = handlers.generateResolver(handle)
-- update existing handler by name
local idx = findIndexByProp(handlers.list, "name", name)
if idx ~= nil and idx > 0 then
-- found update
handlers.list[idx].pattern = pattern
handlers.list[idx].handle = handle
handlers.list[idx].maxRuns = maxRuns
else
table.insert(handlers.list, { pattern = pattern, handle = handle, name = name, maxRuns = maxRuns })
end
end
--- Prepends a new handler to the beginning of the handlers list.
-- @function prepend
-- @tparam {string} name The name of the handler
-- @tparam {table | function | string} pattern The pattern to check for in the message
-- @tparam {function} handle The function to call if the pattern matches
-- @tparam {number | string | nil} maxRuns The maximum number of times the handler should run, or nil if there is no limit
function handlers.prepend(...)
local name, pattern, handle, maxRuns
local args = select("#", ...)
if args == 2 then
name = select(1, ...)
pattern = select(1, ...)
handle = select(2, ...)
maxRuns = nil
elseif args == 3 then
name = select(1, ...)
pattern = select(2, ...)
handle = select(3, ...)
maxRuns = nil
else
name = select(1, ...)
pattern = select(2, ...)
handle = select(3, ...)
maxRuns = select(4, ...)
end
assertAddArgs(name, pattern, handle, maxRuns)
handle = handlers.generateResolver(handle)
-- update existing handler by name
local idx = findIndexByProp(handlers.list, "name", name)
if idx ~= nil and idx > 0 then
-- found update
handlers.list[idx].pattern = pattern
handlers.list[idx].handle = handle
handlers.list[idx].maxRuns = maxRuns
else
table.insert(handlers.list, 1, { pattern = pattern, handle = handle, name = name, maxRuns = maxRuns })
end
end
--- Returns an object that allows adding a new handler before a specified handler.
-- @function before
-- @tparam {string} handleName The name of the handler before which the new handler will be added
-- @treturn {table} An object with an `add` method to insert the new handler
function handlers.before(handleName)
assert(type(handleName) == 'string', 'Handler name MUST be a string')
local idx = findIndexByProp(handlers.list, "name", handleName)
return {
add = function (name, pattern, handle, maxRuns)
assertAddArgs(name, pattern, handle, maxRuns)
handle = handlers.generateResolver(handle)
if idx then
table.insert(handlers.list, idx, { pattern = pattern, handle = handle, name = name, maxRuns = maxRuns })
end
end
}
end
--- Returns an object that allows adding a new handler after a specified handler.
-- @function after
-- @tparam {string} handleName The name of the handler after which the new handler will be added
-- @treturn {table} An object with an `add` method to insert the new handler
function handlers.after(handleName)
assert(type(handleName) == 'string', 'Handler name MUST be a string')
local idx = findIndexByProp(handlers.list, "name", handleName)
return {
add = function (name, pattern, handle, maxRuns)
assertAddArgs(name, pattern, handle, maxRuns)
handle = handlers.generateResolver(handle)
if idx then
table.insert(handlers.list, idx + 1, { pattern = pattern, handle = handle, name = name, maxRuns = maxRuns })
end
end
}
end
--- Removes a handler from the handlers list by name.
-- @function remove
-- @tparam {string} name The name of the handler to be removed
function handlers.remove(name)
assert(type(name) == 'string', 'name MUST be string')
if #handlers.list == 1 and handlers.list[1].name == name then
handlers.list = {}
end
local idx = findIndexByProp(handlers.list, "name", name)
if idx ~= nil and idx > 0 then
table.remove(handlers.list, idx)
end
end
--- Evaluates each handler against a given message and environment. Handlers are called in the order they appear in the handlers list.
-- Return 0 to not call handler, -1 to break after handler is called, 1 to continue
-- @function evaluate
-- @tparam {table} msg The message to be processed by the handlers.
-- @tparam {table} env The environment in which the handlers are executed.
-- @treturn The response from the handler(s). Returns a default message if no handler matches.
function handlers.evaluate(msg, env)
local handled = false
assert(type(msg) == 'table', 'msg is not valid')
assert(type(env) == 'table', 'env is not valid')
for _, o in ipairs(handlers.list) do
if o.name ~= "_default" then
local match = utils.matchesSpec(msg, o.pattern)
if not (type(match) == 'number' or type(match) == 'string' or type(match) == 'boolean') then
error("Pattern result is not valid, it MUST be string, number, or boolean")
end
-- handle boolean returns
if type(match) == "boolean" and match == true then
match = -1
elseif type(match) == "boolean" and match == false then
match = 0
end
-- handle string returns
if type(match) == "string" then
if match == "continue" then
match = 1
elseif match == "break" then
match = -1
else
match = 0
end
end
if match ~= 0 then
if match < 0 then
handled = true
end
-- each handle function can accept, the msg, env
local status, err = pcall(o.handle, msg, env)
if not status then
error(err)
end
-- remove handler if maxRuns is reached. maxRuns can be either a number or "inf"
if o.maxRuns ~= nil and o.maxRuns ~= "inf" then
o.maxRuns = o.maxRuns - 1
if o.maxRuns == 0 then
handlers.remove(o.name)
end
end
end
if match < 0 then
return handled
end
end
end
-- do default
if not handled then
local idx = findIndexByProp(handlers.list, "name", "_default")
handlers.list[idx].handle(msg,env)
end
end
return handlers