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
-- load our template engine
local tirtemplate = require('tirtemplate')
-- Load redis
local redis = require "resty.redis"
-- Set the content type
ngx.header.content_type = 'text/html';
-- use nginx $root variable for template dir
TEMPLATEDIR = ngx.var.root .. 'luanew/';
-- the db global
red = nil
--
-- Index view
--
local function index()
-- increment index counter
local counter, err = red:incr("index_visist_counter")
-- load template
local page = tirtemplate.tload('index.html')
local context = {counter = tostring(counter) }
-- render template with counter as context
-- and return it to nginx
ngx.print( page(context) )
end
--
-- the about view
--
local function about()
-- increment about counter
local counter, err = red:incr("about_visist_counter")
-- load template
local page = tirtemplate.tload('about.html')
local context = {counter = tostring(counter) }
-- render template with counter as context
-- and return it to nginx
ngx.print( page(context) )
end
--
-- hello world view
--
local function hello()
ngx.print( tirtemplate.tload('hello.html'){} )
end
--
-- Initialise db
--
local function init_db()
-- Start redis connection
red = redis:new()
local ok, err = red:connect("unix:/var/run/redis/redis.sock")
if not ok then
ngx.say("failed to connect: ", err)
return
end
end
--
-- End db, we could close here, but park it in the pool instead
--
local function end_db()
-- put it into the connection pool of size 100,
-- with 0 idle timeout
local ok, err = red:set_keepalive(0, 100)
if not ok then
ngx.say("failed to set keepalive: ", err)
return
end
end
-- mapping patterns to views
local routes = {
['^/$'] = index,
['^/about$'] = about,
['^/hello$'] = hello,
}
-- iterate route patterns and find view
for pattern, view in pairs(routes) do
if ngx.re.match(pattern, ngx.var.uri) then
init_db()
view()
end_db()
-- return OK, since we called a view
ngx.exit( ngx.HTTP_OK )
end
end
-- no match, return 404
ngx.exit( ngx.HTTP_NOT_FOUND )
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.