This repository was archived by the owner on Mar 8, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinit.lua
More file actions
350 lines (285 loc) · 8.92 KB
/
Copy pathinit.lua
File metadata and controls
350 lines (285 loc) · 8.92 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
343
344
345
346
347
348
349
350
-- heart.lua - a micro web framework for luvit
-- pretty much just handles routing and http requests/responses
local url = require 'url'
local querystring = require 'querystring'
local table = require 'table'
local string = require 'string'
-- for static files
local fs = require 'fs'
local mime = require 'mime'
----- URL DISPATCHING
-- Supports the following sorts of syntax: (patterns are matched with string.find)
-- /
-- /hello/:name
-- /birthday/<name:identifier>/<age:int>
-- same as
-- /birthday/<name:%w%a*>/<age:%d+>
-- Route parser, turns a route into a string:find pattern, if necessary
-- Return values:
-- A pattern string
-- false if string:find is not necessary (exact comparison is enough) or a table of parameter names if it is
local route_parse_specialchars = '[%(%)%%%+%-%*%?%[%^%$]'
local function route_check(exists, route, i)
if not exists then
error('heart: route parser choked on ' .. route:sub(i))
end
end
local function route_parse(route)
local match = ''
local exact = true
local params = {}
local i = 1
while i <= #route do
local c = route:sub(i, i)
-- Check for parameters
if c == '<' then
-- This is a patterned or typed parameter
exact = false
local exists, len, name = route:sub(i):find("(%a%w*):")
route_check(exists, route, i)
i = i + len
table.insert(params, name)
-- Get the actual pattern
local exists, len, pattern = route:sub(i):find("([^>]+)")
route_check(exists, route, i)
-- some builtin utility types
if pattern == 'int' then
pattern = '%d+'
elseif pattern == 'identifier' then
pattern = '%w%a*'
end
match = match .. '(' .. pattern .. ')'
i = i + len + 1 -- skip >
elseif c == ':' then
-- This is a simple named parameter (matches everything up to next /)
exact = false
local exists, len, name = route:sub(i):find("(%a%w*)")
route_check(exists, route, i)
i = i + len
table.insert(params, name)
match = match .. '([^/]+)'
else
-- Not a parameter, match it directly
if c:find(route_parse_specialchars) then
-- Special character, escape it so string.find won't care
-- Also, we treat these as non-exact characters because it's easier
exact = false
match = match .. '%'
end
match = match .. c
i = i + 1
end
end
exact = exact and #params == 0
if exact then
params = false
else
match = match .. '$'
end
return match, params
end
-- Dispatcher
-- Inputs:
-- A path to be dispatched
-- A table containing simple URLs as keys to be compared exactly against the path
-- A table containing patterns as keys to be run against the path, along with parameters, like so
-- table[pattern] = { parameter_names, return_value }
-- If simple match is successful, returns the relevant value
-- If pattern match is successful, returns the relevant value in a parameters table like { value, parameter1 = value1 }
-- If not, returns false
-- Called against string.find to parse results into an array of names
local function pattern_results(names, a, b, ...)
-- If match was not successful, keep going
if a ~= 1 then return false end
local params = {}
local arg = { n = select('#', ...), ... }
for i, name in ipairs(names) do
params[name] = arg[i]
end
return params
end
local function dispatch(path, strings, patterns)
for k, ret in pairs(strings) do
if path == k then
return ret, false
end
end
for pattern, tbl in pairs(patterns) do
local params = tbl[2]
local result = pattern_results(params, path:find(pattern))
if result then
-- return handler function in existing table
return tbl[1], result
end
end
return false
end
----- REQUEST HANDLING
local emptyparamtable = {}
local function respond(app, req, res, code, headers, body)
if body then
headers['Content-Length'] = #body
end
if not headers['Content-Type'] then
headers['Content-Type'] = app.default_content_type
end
if code == 404 then
body = app.not_found(req)
headers['Content-Length'] = #body
end
app.log(code)
res:writeHead(code, headers)
res:finish(body)
end
local function sugary_response(app, req, res, code, body, headers)
-- Shortcut: return only a string or a string and headers
if type(code) == 'string' then
respond(app, req, res, 200, body or {}, code)
else
assert(type(code) == 'number')
-- If code == 0 then assume the handler has written the response
if code == nil then
return
end
headers = headers or {}
-- Special redirect handling
-- Allows us to do something like
-- return 301, '/other/url'
if code == 301 or code == 302 then
headers['Location'] = body
body = nil
end
respond(app, req, res, code, headers, body)
end
end
local function unescape(s)
s = string.gsub (s, "+", " ")
s = string.gsub (s, "%%(%x%x)", function(h) return string.char(tonumber(h,16)) end)
s = string.gsub (s, "\r\n", "\n")
return s
end
local function call_handler(app, handler, req, res)
local code, body, headers = handler(req, res)
-- Ignore nil return values, assume the handler has already sent a response
if code ~= nil then
sugary_response(app, req, res, code, body, headers)
end
end
local function request(app, req, res)
-- dispatch based on url
local path = url.parse(req.url).pathname
local method = req.method
local routes = app[method]
app.log(method .. ' ' .. path)
local handler, params
if routes then
handler, params = dispatch(path, routes.simple_routes, routes.pattern_routes)
if params then
for k, v in pairs(params) do
params[k] = unescape(v)
end
end
end
req.heart = { app = app, params = params or emptyparamtable }
res.heart = { respond = function(code, body, headers) sugary_response(app, req, res, code, body, headers) return 0 end }
if not handler then
app.log('Failed to dispatch URL')
respond(app, req, res, 404, {}, nil)
return
end
if method == 'POST' then
local body = ''
req:on('data', function(chunk) body = body .. chunk end)
req:on('end', function()
local pparams = querystring.parse(body)
if not params then params = {} end
for k,v in pairs(pparams) do
params[k] = v
end
req.heart.params = params
call_handler(app, handler, req, res)
end)
else
call_handler(app, handler, req, res)
end
end
local function mount(self, method, route, handler)
assert(self)
assert(type(self) == 'table' and 'you probably meant to call this as a method')
self.log('MOUNT ' .. tostring(route))
local pattern, params = route_parse(route)
if params then
method.pattern_routes[pattern] = { handler, params }
else
method.simple_routes[pattern] = handler
end
end
-- Somewhat confusingly, the routes containing tables for each method are also named after the method but with capital
-- letters
local function get(self, route, handler)
return mount(self, self.GET, route, handler)
end
local function post(self, route, handler)
return mount(self, self.POST, route, handler)
end
-- Default 404 message
local function not_found(req)
return "Heart couldn't find " .. req.url .. '.\n<br/>To customize this message, write something like <pre>app.not_found = function(req) return "Couldn\'t find " .. req.url end</pre>'
end
----- META SILLINESS
local appt = {
__call = request,
get = get,
post = post,
log = function(...) print('heart: ', ...) end,
not_found = not_found
}
appt.__index = appt
local function app()
local app = {
default_response_type = 'text/html',
GET = { simple_routes = {}, pattern_routes = {} },
POST = { simple_routes = {}, pattern_routes = {} },
middleware = {}
}
setmetatable(app, appt)
return app
end
----- SERVE STATIC FILES
local function serve_file(req, res, path)
local function respond(code, headers, body)
res:writeHead(code, headers)
res:finish(body)
end
req.heart.app.log('Serving static file ' .. path)
fs.open(path, 'r', function(err, fd)
if err then
if err.code == 'ENOENT' then
return respond(404, {}, 'Could not find ' .. path)
end
return respond(500, {}, err.message)
end
fs.fstat(fd, function(err, stat)
if not stat.is_file then
return respond(500, {}, 'Not a file')
end
headers = { ['Content-Type'] = mime.getType(path), ['Content-Length'] = stat.size }
res:writeHead(200, headers)
fs.createReadStream(nil, { fd = fd }):pipe(res)
end)
end)
end
local function static(dir)
return function(req, res)
local path = dir .. '/' .. req.heart.params.path
print ('da path ' .. path)
serve_file(req, res, path)
end
end
local function static_file(path)
return function(req, res)
serve_file(req, res, path)
end
end
----- MODULE
return { static = static, route_parse = route_parse, dispatch = dispatch, app = app, static_file = static_file, serve_file = serve_file }