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.

Source

Find the complete source at github.