Rotas


Manual

The instanciation : app = Rotas([ table ])

The instanciation of a dispatcher is done by the call of Rotas with an optional table parameter. The instance inherits of this optional parameter which could carry some contexts.

The registration : app.METH[matcher] = function

The allowed HTTP methods are stored in the array Rotas.http_methods (which contains by default : DELETE, HEAD, GET, OPTIONS, PATCH, POST, PUT and TRACE). The pseudo method ALL could be also used.

The matcher is typically an instance of Silva, or a string used to build a matcher function.

The variable Rotas.matcher contains the name of module used to transform a string to a matcher function (by default Silva.template, see RFC 6570 - URI Template).

The matching : fn, capture = app(METH, uri)

If a pattern matches the uri for the given HTTP method, the registered function is returned, plus the parameters captured by the matcher.

If no match, nil is returned.

Examples

Hello, World

First, the app created with Rotas

-- hello.lua

local rotas = require'Rotas'
local app = rotas()

app.GET['/hello'] = function ()
    return "Hello, World"
end

app.GET['/hello{/name}'] = function (params)
    return "Hello, " .. params.name or ''
end

return app

Here, the nginx.conf used by an OpenResty server.

worker_processes  1;

events {
    worker_connections 1024;
}

http {
    server {
        listen 8080;

        location /hello {
            content_by_lua_block {

                local fn, capture = require'hello'(ngx.req.get_method(), ngx.var.request_uri)
                if not fn then
                    ngx.status = ngx.HTTP_NOT_FOUND
                else
                    ngx.status = ngx.HTTP_OK
                    ngx.header['Content-Type'] = 'text/plain'
                    ngx.say(fn(capture))
                end

            }
        }
    }
}

And now, some tests:

    $ curl "http://localhost:8080/hello"
    Hello, World
    $ curl "http://localhost:8080/hello/bob"
    Hello, bob

a calculator

First, the service calc created with Rotas.

The parameters are validated by lua-LIVR.

-- calc.lua

local livr = require'LIVR.Validator'

local function livr_helper (spec, fn)
    local validator = livr.new(spec)
    return function (capture)
        local params, err = validator:validate(capture)
        if err then
            return { errmsg = err }
        else
            return fn(params)
        end
    end
end

local uri = require'Silva.template'
local rotas = require'Rotas'
local calc = rotas()

calc.GET[uri'/calc/mul{?x,y}'] = livr_helper({
    x = { 'required', 'decimal' },
    y = { 'required', 'decimal' },
}, function (params)
    return { ret = params.x * params.y }
end)

calc.GET[uri'/calc/div{?x,y}'] = livr_helper({
    x = { 'required', 'decimal' },
    y = { 'required', 'decimal' },
}, function (params)
    if params.y == 0 then
        return { errmsg = "Division by zero" }
    end
    return { ret = params.x / params.y }
end)

return calc

Here, the specific Xavante part.

The responses are formated in JSON by dkjson.

-- calc_xavante.lua

local xavante = require'xavante'
local encode = require'dkjson'.encode

local function json_fmt (req, res, obj, status)
    if status then
        res.statusline = 'HTTP/1.1 ' .. status
        obj = obj or { errmsg = status }
    elseif obj and obj.errmsg then
        res.statusline = 'HTTP/1.1 400 Bad Request'
    elseif req.cmd_mth == 'POST' then
        res.statusline = 'HTTP/1.1 201 Created'
    else
        res.statusline = 'HTTP/1.1 200 OK'
    end
    if obj then
        res.headers['Content-Type'] = 'application/json'
        res.content = encode(obj)
    end
    return res
end

local function handler (req, res, router, formatter)
    print(req.cmd_mth, req.cmd_url)
    local fn, capture = router(req.cmd_mth, req.cmd_url)
    if not fn then
        return formatter(req, res, nil, '404 Not Found')
    else
        return formatter(req, res, fn(capture))
    end
end

xavante.HTTP{
    server = { host = '*', port = 8080 },
    defaultHost = {
        rules = {
            {
                match = '^/calc/',
                with = function (req, res) return handler(req, res, require'calc', json_fmt) end,
            }
        }
    }
}

xavante.start()

And now, some tests with a server:

    $ curl "http://localhost:8080/calc/div?x=3.0&y=2.0"
    {"ret":1.5}
    $ curl "http://localhost:8080/calc/div?x=3.0&y=0.0"
    {"errmsg":"Division by zero"}
    $ curl "http://localhost:8080/calc/div?x=a&y=b"
    {"errmsg":{"y":"NOT_DECIMAL","x":"NOT_DECIMAL"}}
    $ curl "http://localhost:8080/calc/div"
    {"errmsg":{"y":"REQUIRED","x":"REQUIRED"}}
    $ curl "http://localhost:8080/calc/idiv"
    {"errmsg":"404 Not Found"}
    $ curl "http://localhost:8080/calc/div?z=top"
    {"errmsg":"404 Not Found"}

But, unit tests of the service are also feasible:

-- test_calc.lua

require 'Test.More'

plan(10)

local calc = require'calc'

local fn, capture, res
fn, capture = calc('GET', '/calc/div?x=3.0&y=2.0')
res = fn(capture)
is( res.ret, 1.5 )

fn, capture = calc('GET', '/calc/div?x=3.0&y=0.0')
res = fn(capture)
is( res.errmsg, "Division by zero" )

fn, capture = calc('GET', '/calc/div?x=a&y=b')
res = fn(capture)
is( res.errmsg.x, 'NOT_DECIMAL' )
is( res.errmsg.y, 'NOT_DECIMAL' )

fn, capture = calc('GET', '/calc/div')
res = fn(capture)
is( res.errmsg.x, 'REQUIRED' )
is( res.errmsg.y, 'REQUIRED' )

fn, capture = calc('GET', '/calc/idiv')
nok( fn )
nok( capture )

fn, capture = calc('GET', '/calc/div?z=top')
nok( fn )
nok( capture )

two levels of dispatch

A first level of dispatch is done between the two previous services hello and calc.

Here, the library lua-http is used in order to build a server.

-- 2level_http.lua

local http_server = require'http.server'
local http_headers = require'http.headers'
local encode = require'dkjson'.encode
local re = require'Silva.lua'
local rotas = require'Rotas'
local app = rotas()

local function json_fmt (stream, req_method, obj, status)
    local res_headers = http_headers.new()
    res_headers:append('access-control-allow-origin', '*')
    if status then
        res_headers:append(':status', status)
    elseif obj and obj.errmsg then
        res_headers:append(':status', '400')    -- Bad Request
    elseif req_method == 'POST' then
        res_headers:append(':status', '201')    -- Created
    else
        res_headers:append(':status', '200')    -- OK
    end
    if obj then
        res_headers:append('cache-control', 'max-age=0, must-revalidate, no-cache, no-store, private')
        res_headers:append('content-type', 'application/json')
        res_headers:append('x-content-type-options', 'nosniff')
        stream:write_headers(res_headers, false)
        stream:write_body_from_string(encode(obj))
    else
        stream:write_headers(res_headers, true)
    end
end

app.ALL[re'^/calc/'] = function (stream, req_headers)
    local req_method = req_headers:get':method'
    local req_path = req_headers:get':path'
    print(req_method, req_path)
    local fn, capture = require'calc'(req_method, req_path)
    if not fn then
        json_fmt(stream, req_method, { errmsg = '404 Not Found' }, '404')
    else
        json_fmt(stream, req_method, fn(capture))
    end
end

app.ALL[re'^/hello'] = function (stream, req_headers)
    local req_method = req_headers:get':method'
    local req_path = req_headers:get':path'
    print(req_method, req_path)
    local fn, capture = require'hello'(req_method, req_path)
    local res_headers = http_headers.new()
    if not fn then
        res_headers:append(':status', '404')
        stream:write_headers(res_headers, true)
    else
        local txt = fn(capture)
        res_headers:append(':status', '200')
        res_headers:append('cache-control', 'max-age=0, must-revalidate, no-cache, no-store, private')
        res_headers:append('content-type', 'text/plain')
        res_headers:append('x-content-type-options', 'nosniff')
        stream:write_headers(res_headers, false)
        stream:write_body_from_string(txt)
    end
end

local myserver = http_server.listen{
    host = 'localhost',
    port = '8080',
    onstream = function (server, stream)
        local req_headers = stream:get_headers()
        local hdl = app(req_headers:get':method', req_headers:get':path')
        if not hdl then
            local res_headers = http_headers.new()
            res_headers:append(':status', '404')
            stream:write_headers(res_headers, true)
        else
            hdl(stream, req_headers)
        end
    end,
    onerror = function (server, context, op, err, errno)
        local msg = op .. ' on ' .. tostring(context) .. ' failed'
        if err then
            msg = msg .. ': ' .. tostring(err)
        end
        io.stderr:write(msg, "\n")
    end,
}

assert(myserver:listen())
io.stderr:write(string.format("Now listening on port %d\n", select(3, myserver:localname())))
assert(myserver:loop())

WebDAV extension

The WebDAV methods could be easily added to Rotas.

local m = require'Rotas'

m.http_methods = {
    'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE',        -- HTTP
    'COPY', 'LOCK', 'MKCOL', 'MOVE', 'PROPFIND', 'PROPPATCH', 'UNLOCK',         -- WebDAV
}

return m