forked from EvandroLG/pegasus.lua
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrouter.lua
More file actions
342 lines (295 loc) · 10.7 KB
/
router.lua
File metadata and controls
342 lines (295 loc) · 10.7 KB
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
--- Module `pegasus.plugins.router`
--
-- A plugin that routes requests based on path and method, with support for
-- path parameters and pre/post hooks at both router and path levels.
--
-- @module pegasus.plugins.router
--
-- Supports path parameters.
--
-- The `routes` table to configure the router is a hash-table where the keys are the path, and
-- the value is another hash-table. The second hash-table has the method as the key, and the
-- callbacks as the values.
-- Both hash-tables can have "`preFunction`" and "`postFunction`" entries,
-- which should have callbacks as values.
--
-- There are 5 callbacks (called in this order);
--
-- * the router `preFunction` callback is called first when there is a `prefix` match. It can
-- be used to do some validations, like path parameters, etc. It is defined on `router` level.
--
-- * the path `preFunction` callback is called when there is a `path` match. It can
-- be used to do some validations, like path parameters, etc. It is defined on `path` level.
--
-- * the `METHOD` (eg. `GET, `POST`, etc) this callback implements the specific method. It is defined
-- once for each supported method on the path. The special case is method "`*`" which is a catch-all.
-- The catch-all will be used for any method that doesn't have its own handler defined.
-- If omitted, the default catch-all will return a "405 Method Not Allowed" error.
--
-- * the path `postFunction` is called after the `METHOD` callback. This one is defined on `path` level.
--
-- * the router `postFunction` is called last. This one is defined on `router` level.
--
-- The callbacks have the following function signature; `stop = function(request, response)`.
-- If `stop` is truthy, request handling is terminated, no further callbacks will be called.
--
-- Path parameters can be defined in the path in curly braces; "`{variableName}`", and they will match
-- a single path segment. The values will be made available on the Request object as
-- `request.pathParameters.variableName`.
--
-- The API sub-path (without the prefix) is available as on the Request object as `request.routerPath`.
--
-- Route matching is based on a complete match (not prefix). And the order is based on the number
-- of path-parameters defined. Least number of parameters go first, such that static paths have
-- precedence over variables.
-- @usage
-- local routes = {
-- preFunction = function(req, resp)
-- local stop = false
-- -- this gets called before any path specific callback
--
-- if some_error then
-- resp:writeDefaultErrorMessage(400)
-- stop = true
-- end
-- return stop
-- end,
--
--
-- ["/my/{accountNumber}/{param2}/endpoint"] = { -- define path parameters
--
-- preFunction = function(req, resp)
-- local stop = false
-- -- this gets called before any method specific callback,
-- -- but after the path-preFunction
-- return stop
-- end,
--
-- GET = function(req, resp)
-- local stop = false
-- -- this implements the main GET logic
-- return stop
-- end,
--
-- POST = function(req, resp)
-- local stop = false
-- -- this implements the main POST logic
-- return stop
-- end,
--
-- ["*"] = function(req, resp)
-- local stop = false
-- -- this implements the wildcard, will handle any method except for the
-- -- GET/POST ones defined above.
--
-- -- If the wildcard is not defined, then a default one will be added which
-- -- only returns a "405 Method Not Allowed" error.
-- return stop
-- end,
--
-- postFunction = function(req, resp)
-- local stop = false
-- -- this gets called before after the method specific (or wildcard)
-- -- callback.
--
-- return stop
-- end,
-- },
--
-- ["/my/endpoint"] = function(req, resp)
-- local stop = false
-- -- this is a shortcut to create a wildcard-method, one callback
-- -- to handle any method for this path. Identical to:
-- -- ["/my/endpoint"] = { ["*"] = function(req, resp) ... end }
-- return stop
-- end,
--
-- postFunction = function(req, resp)
-- local stop = false
-- -- this gets called last.
-- return stop
-- end,
-- }
--
-- local router = Router:new {
-- prefix = "/api/1v0/",
-- routes = routes,
-- }
local xpcall = xpcall
-- Test if xpcall supports extra arguments (Lua 5.2+, LuaJIT), fix if not (Lua 5.1)
do
local _, result = xpcall(function(arg) return arg == "test" end, function() end, "test")
if not result then
-- Lua 5.1: wrap xpcall to support extra arguments
local original_xpcall = xpcall
xpcall = function(f, err, ...)
local args = {...}
local n = select('#', ...)
return original_xpcall(
function() return f(unpack(args, 1, n)) end,
err
)
end
end
end
--- Router plugin instance.
--
-- Options passed to `Router:new{ ... }`:
-- - `prefix` (string, optional): base path for all routes
-- - `routes` (table, required): route definitions (see module docs)
--
-- Methods invoked by the handler:
-- - `newRequestResponse(request, response)`
--
-- @type Router
---@class Router
local Router = {}
Router.__index = Router
local noOpCallback = function()
return false
end
local function methodNotAllowed(req, resp)
resp:writeDefaultErrorMessage(405)
return true -- "stop"
end
-- Parse the routes table.
local function parseRoutes(self, routes, prefix)
local rts = {}
local routerPreFunction = routes.preFunction
local routerPostFunction = routes.postFunction
for path, methods in pairs(routes) do
if path ~= "preFunction" and path ~= "postFunction" then
assert(path:sub(1,1) == "/", "paths must start with '/', got: " .. path)
if type(methods) == "function" then
methods = { ["*"] = methods } -- turn a single-function-shortcut into a table
end
methods["*"] = methods["*"] or methodNotAllowed
local m = {}
for method, callback in pairs(methods) do
assert(type(callback) == "function", "expected callback to be a function, got: " .. type(callback))
if method ~= "preFunction" and method ~= "postFunction" then
assert(method == method:upper(), "expected method to be allcaps, got: " .. tostring(method))
if method ~= "*" then
m[method] = callback
else
-- a "catch all"; '*', so add metamethod to return the catch all
setmetatable(m, {
__index = function(self, key)
return callback
end
})
end
end
end
local params = {}
local pattern = path:gsub("{(%w+)}", function(name)
params[#params+1] = name
return "([^/]+)"
end)
pattern = "^" .. pattern .. "$"
-- create and store the route
rts[#rts+1] = {
pattern = pattern,
params = params,
methods = m,
routerPreFunction = routerPreFunction or noOpCallback,
preFunction = methods.preFunction or noOpCallback,
postFunction = methods.postFunction or noOpCallback,
routerPostFunction = routerPostFunction or noOpCallback,
}
end
end
-- sort by number of parameters in the path, least go first
table.sort(rts, function(a,b) return #a.params < #b.params end)
return rts
end
-- the errorhandler signature is: message_to_log = function(request, response, errobj)
local function error_handler(request, response, errobj)
-- an error occurred, return a 500 if still possible
if not response.closed then
pcall(response.writeDefaultErrorMessage, response, 500)
end
-- return a stacktrace for logging
local err = debug.traceback(tostring(errobj), 3)
return err -- no tailcall since LuaJIT will eat a stack-level
end
--- Creates a new Router plugin instance.
-- @tparam options table the options table with the following fields;
-- @tparam[opt] options.prefix string the base path for all underlying routes.
-- @tparam options.routes table route definitions to be handled by this router plugin instance.
-- @tparam[opt] options.errorHandler function an optional error handler function with signature
-- `message_to_log = function(request, response, errobj)`. The default handler will send a 500 error
-- response, and return a stack trace for logging.
-- @return the new plugin
---@param options table|nil
---@return Router
function Router:new(options)
options = options or {}
local plugin = {}
local prefix = "/" .. (options.prefix or "") .. "/"
while prefix:find("//") do
prefix = prefix:gsub("//", "/")
end
plugin.prefix = prefix:sub(1, -2) -- drop trailing slash
plugin.routes = parseRoutes(plugin, options.routes)
plugin.errorHandler = options.errorHandler or error_handler
setmetatable(plugin, Router)
return plugin
end
local function newRequestResponse(self, request, response)
local stop = false
local path = request:path()
if path:sub(1, #self.prefix) ~= self.prefix then
return stop
end
path = path:sub(#self.prefix + 1, -1)
for _, route in ipairs(self.routes) do
local matches = { path:match(route.pattern) }
if matches[1] ~= nil then
-- we have a match
local p = {}
for i, paramName in ipairs(route.params) do
p[paramName] = matches[i]
end
request.pathParameters = p
request.routerPath = path -- the request path without the prefix
stop = route.routerPreFunction(request, response)
if stop then break end
stop = route.preFunction(request, response)
if stop then break end
stop = route.methods[request:method()](request, response)
if stop then break end
stop = route.postFunction(request, response)
if stop then break end
route.routerPostFunction(request, response)
stop = true
break
end
end
return stop
end
--- Route the request to the matching path/method callback.
-- Populates `request.pathParameters` and `request.routerPath` upon match.
-- Executes callbacks in order: router pre, path pre, method, path post, router post.
-- @tparam table request
-- @tparam table response
-- @treturn boolean stop whether request handling should stop
---@param request table
---@param response table
---@return boolean
function Router:newRequestResponse(request, response)
local errh = function(...) -- error function that injects request/response objects
local errobj = self.errorHandler(request, response, ...)
return errobj -- no tailcall since LuaJIT will eat a stack-level
end
local ok, stop = xpcall(newRequestResponse, errh, self, request, response)
if not ok then
if stop ~= nil then
-- 'stop' is now the error object returned from the error handler, log an error message
request.log:error('Request for: %s %s, failed: %s', request:method(), request:path(), tostring(stop))
end
stop = true
end
return stop
end
return Router