About my lua micro framework proof of concept
This page is just a document to describe my attempt at testing the lua features of nginx. The software below is just a proof of concept, and is not intended to be production ready. The project consists of 3 parts: nginx+openresty, my lua-app for db, routing, and loading templates and finally Tir's template engine
Nginx (openresty)
The unusual part about this project is it's usage of nginx as the "app server". This is possible using lua, since there is a nginx lua module that enables you to call lua from the nginx conf. Have a look at openresty's site for more about what it enables you to do.
Here follows the first part, the nginx configuration to run the lua app and also serve static files:
nginx.conf
lua_package_path '/home/www/lua/?.lua;;';
server {
listen 80;
server_name example.no www.example.no;
set $root /home/www/;
root $root;
# Serve static if file exist, or send to lua
location / { try_files $uri @lua; }
# Lua app
location @lua {
content_by_lua_file $root/lua/index.lua;
}
}
The app
Here follows the most simplest of apps. It uses a redis connection for a simple counter, just as a demo, and three views, with a route each.
index.lua
indexdemo.lua
Templating
The simple temlate engine is from the Tir micro framework. You can read about it here
A simple template would look like this:
index.html
{( "header.html" )}
<div class="">
Hello from lua!
</div>
{( "footer.html" )}
This is what the template engine looks like:
tirtemplate.lua
module('tirtemplate', package.seeall)
-- Simplistic HTML escaping.
function escape(s)
if s == nil then return '' end
local esc, i = s:gsub('&', '&'):gsub('<', '<'):gsub('>', '>')
return esc
end
-- Simplistic Tir template escaping, for when you need to show lua code on web.
function tirescape(s)
if s == nil then return '' end
local esc, i = s:gsub('{', '{'):gsub('}', '}')
return tirtemplate.escape(esc)
end
-- Helper function that loads a file into ram.
function load_file(name)
local intmp = assert(io.open(name, 'r'))
local content = intmp:read('*a')
intmp:close()
return content
end
-- Used in template parsing to figure out what each {} does.
local VIEW_ACTIONS = {
['{%'] = function(code)
return code
end,
['{{'] = function(code)
return ('_result[#_result+1] = %s'):format(code)
end,
['{('] = function(code)
return ([[
local tirtemplate = require('tirtemplate')
if not _children[%s] then
_children[%s] = tirtemplate.tload(%s)
end
_result[#_result+1] = _children[%s](getfenv())
]]):format(code, code, code, code)
end,
['{<'] = function(code)
return ('local tirtemplate = require("tirtemplate") _result[#_result+1] = tirtemplate.escape(%s)'):format(code)
end,
}
-- Takes a view template and optional name (usually a file) and
-- returns a function you can call with a table to render the view.
function compile_view(tmpl, name)
local tmpl = tmpl .. '{}'
local code = {'local _result, _children = {}, {}\n'}
for text, block in string.gmatch(tmpl, "([^{]-)(%b{})") do
local act = VIEW_ACTIONS[block:sub(1,2)]
local output = text
if act then
code[#code+1] = '_result[#_result+1] = [[' .. text .. ']]'
code[#code+1] = act(block:sub(3,-3))
elseif #block > 2 then
code[#code+1] = '_result[#_result+1] = [[' .. text .. block .. ']]'
else
code[#code+1] = '_result[#_result+1] = [[' .. text .. ']]'
end
end
code[#code+1] = 'return table.concat(_result)'
code = table.concat(code, '\n')
local func, err = loadstring(code, name)
if err then
assert(func, err)
end
return function(context)
assert(context, "You must always pass in a table for context.")
setmetatable(context, {__index=_G})
setfenv(func, context)
return func()
end
end
function tload(name)
name = TEMPLATEDIR .. name
if false then
local tempf = load_file(name)
return compile_view(tempf, name)
else
return function (params)
local tempf = load_file(name)
assert(tempf, "Template " .. name .. " does not exist.")
return compile_view(tempf, name)(params)
end
end
end
Tir uses a BSD 3-clause license.