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.

Source

Find the complete source at github.