-
Notifications
You must be signed in to change notification settings - Fork 10
/
mercury.lua
348 lines (295 loc) · 10.5 KB
/
mercury.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
require 'wsapi.request'
require 'wsapi.response'
require 'wsapi.util'
module('mercury', package.seeall)
local mercury_env = getfenv()
local route_env = setmetatable({ }, { __index = _G })
local route_table = { GET = {}, POST = {}, PUT = {}, DELETE = {} }
local application_methods = {
get = function(path, method, options) add_route('GET', path, method) end,
post = function(path, method, options) add_route('POST', path, method) end,
put = function(path, method, options) add_route('PUT', path, method) end,
delete = function(path, method, options) add_route('DELETE', path, method) end,
helper = function(name, method) set_helper(route_env, name, method) end,
helpers = function(helpers)
if type(helpers) == 'table' then
set_helpers(route_env, helpers)
elseif type(helpers) == 'function' then
local temporary_env = setmetatable({}, {
__newindex = function(e,k,v)
set_helper(route_env, k, v)
end,
})
setfenv(helpers, temporary_env)()
else
-- TODO: raise an error?
end
end,
}
function set_helpers(environment, methods)
for name, method in pairs(methods) do
set_helper(environment, name, method)
end
end
function set_helper(environment, name, method)
if type(method) ~= 'function' then
error('"' .. name .. '" is an invalid helper, only functions are allowed.')
end
environment[name] = setfenv(method, environment)
end
--
-- *** ext functions *** --
--
function merge_tables(...)
local numargs, out = select('#', ...), {}
for i = 1, numargs do
local t = select(i, ...)
if type(t) == "table" then
for k, v in pairs(t) do out[k] = v end
end
end
return out
end
--
-- *** route environment *** --
--
(function() setfenv(1, route_env)
-- This is a glorious trick to setup a different environment for routes since
-- Lua functions inherits the environment in which they are *created*! This
-- will not be compatible with Lua 5.2, for which the new _ENV should provide
-- a much more clean way to achieve a similar result.
-- TODO: Create a function like setup_route_environment(fun)
local templating_engines = {
haml = function(template, options, locals)
local haml = haml.new(options)
return function(env)
return haml:render(template, mercury_env.merge_tables(env, locals))
end
end,
cosmo = function(template, values)
return function(env)
return cosmo.fill(template, values)
end
end,
string = function(template, ...)
return function(env)
return string.format(template, unpack(arg))
end
end,
lp = function(template, values)
return function(env)
local lp = require 'lp'
return lp.fill(template, mercury_env.merge_tables(env, values))
end
end,
codegen = function(template, top, values)
local CodeGen = require 'CodeGen'
return function(env)
local tmpl = CodeGen(template, values, env)
return tmpl(top)
end
end,
}
local route_methods = {
pass = function()
coroutine.yield({ pass = true })
end,
-- Use a table to group template-related methods to prevent name clashes.
t = setmetatable({ }, {
__index = function(env, name)
local engine = templating_engines[name]
if type(engine) == nil then
error('cannot find template renderer "'.. name ..'"')
end
return function(...)
coroutine.yield({ template = engine(...) })
end
end
}),
}
for k, v in pairs(route_methods) do route_env[k] = v end
setfenv(1, mercury_env) end)()
--
-- *** application *** --
--
function application(application, fun)
if type(application) == 'string' then
application = { _NAME = application }
else
application = application or {}
end
for k, v in pairs(application_methods) do
application[k] = v
end
application.run = function(wsapi_env)
return run(application, wsapi_env)
end
local mt = { __index = _G }
if fun then
setfenv(fun, setmetatable(application, mt))()
else
setmetatable(application, mt)
end
return application
end
function add_route(verb, path, handler, options)
table.insert(route_table[verb], {
pattern = compile_url_pattern(path),
handler = setfenv(handler, route_env),
options = options,
})
end
function error_500(response, output)
response.status = 500
response.headers = { ['Content-type'] = 'text/html' }
response:write(
'<pre>An error has occurred while serving this page.\n\n' ..
'Error details:\n' .. output:gsub("\n", "<br/>") ..
'</pre>'
)
return response:finish()
end
function compile_url_pattern(pattern)
local compiled_pattern = {
original = pattern,
params = { },
}
-- Lua pattern matching is blazing fast compared to regular expressions,
-- but at the same time it is tricky when you need to mimic some of
-- their behaviors.
pattern = pattern:gsub("[%(%)%.%%%+%-%%?%[%^%$%*]", function(char)
if char == '*' then return ':*' else return '%' .. char end
end)
pattern = pattern:gsub(':([%w%*]+)(/?)', function(param, slash)
if param == '*' then
table.insert(compiled_pattern.params, 'splat')
return '(.-)' .. slash
else
table.insert(compiled_pattern.params, param)
return '([^/?&#]+)' .. slash
end
end)
if pattern:sub(-1) ~= '/' then pattern = pattern .. '/' end
compiled_pattern.pattern = '^' .. pattern .. '?$'
return compiled_pattern
end
function extract_parameters(pattern, matches)
local params = { }
for i,k in ipairs(pattern.params) do
if (k == 'splat') then
if not params.splat then params.splat = {} end
table.insert(params.splat, wsapi.util.url_decode(matches[i]))
else
params[k] = wsapi.util.url_decode(matches[i])
end
end
return params
end
function extract_post_parameters(request, params)
for k,v in pairs(request.POST) do
if not params[k] then params[k] = v end
end
end
function url_match(pattern, path)
local matches = { string.match(path, pattern.pattern) }
if #matches > 0 then
return true, extract_parameters(pattern, matches)
else
return false, nil
end
end
function prepare_route(route, request, response, params)
route_env.params = params
route_env.request = request
route_env.response = response
return route.handler
end
function router(application, state, request, response)
local verb, path = state.vars.REQUEST_METHOD, state.vars.PATH_INFO
return coroutine.wrap(function()
local routes = verb == "HEAD" and route_table["GET"] or route_table[verb]
for _, route in ipairs(routes) do
local match, params = url_match(route.pattern, path)
if match then
if verb == 'POST' then extract_post_parameters(request, params) end
coroutine.yield(prepare_route(route, request, response, params))
end
end
end)
end
function initialize(application, wsapi_env)
-- TODO: Taken from Orbit! It will change soon to adapt request
-- and response to a more suitable model.
local web = {
status = 200,
headers = { ["Content-Type"]= "text/html" },
cookies = {}
}
web.vars = wsapi_env
web.prefix = application.prefix or wsapi_env.SCRIPT_NAME
web.suffix = application.suffix
web.doc_root = wsapi_env.DOCUMENT_ROOT
if wsapi_env.APP_PATH == '' then
web.real_path = application.real_path or '.'
else
web.real_path = wsapi_env.APP_PATH
end
local wsapi_req = wsapi.request.new(wsapi_env)
local wsapi_res = wsapi.response.new(web.status, web.headers)
web.set_cookie = function(_, name, value)
wsapi_res:set_cookie(name, value)
end
web.delete_cookie = function(_, name, path)
wsapi_res:delete_cookie(name, path)
end
web.path_info = wsapi_req.path_info
if not wsapi_env.PATH_TRANSLATED == '' then
web.path_translated = wsapi_env.PATH_TRANSLATED
else
web.path_translated = wsapi_env.SCRIPT_FILENAME
end
web.script_name = wsapi_env.SCRIPT_NAME
web.method = string.lower(wsapi_req.method)
web.input = wsapi_req.params
web.cookies = wsapi_req.cookies
return web, wsapi_req, wsapi_res
end
function run(application, wsapi_env)
local state, request, response = initialize(application, wsapi_env)
for route in router(application, state, request, response) do
local coroute = coroutine.create(route)
local success, output = coroutine.resume(coroute)
if not success then
return error_500(response, output)
end
if not output then
-- render an empty body
return response:finish()
end
local output_type = type(output)
if output_type == 'function' then
-- First attempt at streaming responses using coroutines.
return response.status, response.headers, coroutine.wrap(output)
elseif output_type == 'string' then
response:write(output)
return response:finish()
elseif output.template then
response:write(output.template(getfenv(route)) or 'template rendered an empty body')
return response:finish()
else
if not output.pass then
return error_500(response, output)
end
end
end
local function emit_no_routes_matched()
coroutine.yield('<html><head><title>ERROR</title></head><body>')
coroutine.yield('Sorry, no route found to match ' .. request.path_info .. '<br /><br/>')
if application.debug_mode then
coroutine.yield('<code><b>REQUEST DATA:</b><br/>' .. tostring(request) .. '<br/><br/>')
coroutine.yield('<code><b>RESPONSE DATA:</b><br/>' .. tostring(response) .. '<br/><br/>')
end
coroutine.yield('</body></html>')
end
return 404, { ['Content-type'] = 'text/html' }, coroutine.wrap(emit_no_routes_matched)
end