A web API toolkit playground for the Lua programming language.
- Lua Web API Toolkit
- Table of Contents
- Installation
- Overview
- Setup
- Run
- HTTP API Reference
- Validation API Reference
- Security API Reference
- Web API Reference
- Tools
luarocks install --server=http://luarocks.org/dev lucid
... or use docker images.
Using a function:
local http = require 'http'
local app = http.app.new()
app:use(http.middleware.routing)
app:get('', function(w, req)
return w:write('Hello World!\n')
end)
return app()
... or a metaclass:
local class = require 'core.class'
local web = require 'web'
local WelcomeHandler = class {
get = function(self)
self.w:write('Hello World!\n')
end
}
local all_urls = {
{'', WelcomeHandler}
}
local options = {
urls = all_urls
}
return web.app({web.middleware.routing}, options)
see more here.
Install development dependencies:
sudo make debian
make env nginx
make test qa
eval "$(env/bin/luarocks path --bin)"
alternative environments:
make env LUA_VERSION=5.2.4
make env LUA_IMPL=luajit LUA_VERSION=2.0.5
make env LUA_IMPL=luajit LUA_VERSION=2.1.0-beta2
Check from the command line:
lurl -v demos/http/hello.lua /
Serve files with a web server:
export app=demos.http.hello ; make run
curl -v http://localhost:8080
Use docker:
docker run -it --rm -p 8080:8080 -v `pwd`/demos:/app \
-e app=http.hello akorn/lucid:dev-luajit2.1-alpine
Open your browser at http://localhost:8080
The app
object by convention corresponds to HTTP application. Create it by
calling http.app.new
function exported by the http
module:
local http = require 'http'
local app = http.app.new()
It also accepts optional table options
that affects how the application
behaves:
local app = http.app.new {
root_path = '/api/v1/'
}
Later options can be accessed as app.options
.
The same options are shared as a parameter to middleware initialization and available in HTTP request object as
req.options
.
The app
object has methods for configuring middleware:
app:use(http.middleware.routing)
and routing HTTP requests:
app:get('', function(w, req)
return w:write('Hello World!\n')
end)
Here app:get
function corresponds to HTTP GET verb, app:post
to HTTP
POST, etc.
Finally you call app
to build url mapping, chain middlewares and run it
through initialization step.
return app()
This call returns the first middleware registered with app:use
.
The table of adapted HTTP verbs can be accessed as
http.app.http_verbs
,
which is an association between a function name and HTTP verb, e.g.
post = POST
, etc. The association happens during application initialization
only, thus does not affect runtime.
HTTP verb OPTIONS is not in
http.app.http_verbs
.
Use HTTP verb in upper case if is not in http.app.http_verbs
.
app:OPTIONS('', function(w, req)
end)
The app.options
table has properties that are specific for the application.
app.options.root_path
-- '/'
These options remain throughout the life of the application. You can access
options
during middleware initialization and in HTTP request object as
req.options
.
The
app
object is not supposed to be shared with request handlers. Usereq.options
instead.
The following table describes the properties of the options
object.
Property | Description | Default |
---|---|---|
urls | Keeps url path mapping to request handler | {} |
The mounted
event is fired on a sub-app, when it is mounted on a
parent app. The parent
app is passed to the function.
Sub-app will:
- Not inherit the value of
options
of the parent application.- Keep own
options
unchanged.- Use any values from the
parent
application as necessary.
The following example shows the use of mounted
event.
local http = require 'http'
local greetings = http.app.new()
greetings:on('mounted', function(parent)
print('mounted')
end)
greetings:get('hi', function(w)
return w:write('hi')
end)
local app = http.app.new()
app:use(http.middleware.routing)
app:add('greetings/', greetings)
return app()
Mounts specified sub_app
at the pattern
: the application handles all
requests that match url path according to pattern.
This allows you to build modular or composable applications.
Example: composable application
Here is child application that we could reuse later.
-- child.lua
local http = require 'http'
local app = http.app.new()
app:get('hi', function(w, req)
return w:write('Hello World!\n')
end)
return app
The child app object is returned without a call for initialization.
The parent app builds url mapping for sub_app
app.
The middlewares registered in
sub_app
are ignored.
The route that matches any path that follows its path immediately after "/greetings/" will be handed to child application.
-- main.lua
local http = require 'http'
local app = http.app.new()
app:add('greetings/', require 'child')
app:use(http.middleware.routing)
return app()
The child application handles requests to /greetings/hi.
The main application can extend child application routing as necessary.
app:get('greetings/hallo', function(w, req)
return w:write('Hallo Welt!\n')
end)
By default the main application can not override patterns (the routing middleware treats this as an error, unless allow_path_override option).
local app = http.app.new {
allow_path_override = true
}
app:add('greetings/', require 'child')
app:get('greetings/hi', function(w, req)
return w:write('hey!\n')
end)
Routes an HTTP request regardless HTTP verb.
app:all('hi', function(w)
return w:write('hi')
end)
The actual HTTP verb can be obtained from request, e.g.:
app:all('hi', function(w, req)
return w:write(req.method)
end)
The following table describes the arguments.
Argument | Description |
---|---|
pattern | The path for which the middleware function is invoked, it can be a string representing a path or a regular expression pattern. |
route_name | The name used to address this route in reverse URL lookup. Optional. |
function (following, options) | Middleware function. See below. |
function (w, req) | Request handler function. See below. |
Middleware function (interceptor) can be used to impose pre-conditions on a route handler.
local function middleware(following, options)
return function(w, req)
return following(w, req)
end
end
The following
object by convention corresponds to the next route handler,
which is a function(w, req)
; options
is a table used to initialize
application and holds properties that are specific to application and shared
across.
The middleware function is called only once during application initialization, while returning function for route handling on each request routed.
The middleware function does not influence routing, that means if a call to
following
has not been made, the processing is still considered successful,
an attempt to find a next matching route is not performed.
A return value of middleware function is ignored, however
return following(w, req)
enables Lua's tail call, thus generally preferred.
Example: middleware function
Here is an example that shows the use of multiple middleware functions:
local function interceptor1(following, options)
return function(w, req)
print('before1')
following(w, req)
print('after1')
end
end
local function interceptor2(following, options)
return function(w, req)
print('before2')
following(w, req)
print('after2')
end
end
app:all('hi', interceptor1, interceptor2, function(w, req)
print('hi')
return w:write(req.method)
end)
The interceptor1
and interceptor2
middleware functions intercept all
calls (one after another) to corresponding route handler function.
You can use multiple middleware functions, the execution order is from left to right. The above example prints:
before1
before2
hi
after2
after1
HTTP route / request handler function processes application logic and writes response if any.
local function handler(w, req)
return w:write('hi')
end
The w object by convention corresponds to HTTP response
writer, req to HTTP request. If req
object is not used it can be
safely omitted.
local function handler(w)
return w:write('hi')
end
A return value of HTTP route handler function is ignored, however
return following(w, req)
enables Lua's tail call, thus generally
preferred.
Returns an instance of a route, which can be used to further handle HTTP verbs.
app:route('hi')
:get(function(w, req)
-- respond to HTTP GET request
end)
:post(function(w)
-- respond to HTTP POST request
end)
Use app:route()
to specify HTTP verb that does not have a valid map in
http.app.http_verbs
.
app:route('hi')['OPTIONS'](function(w)
end)
Use app:route()
to avoid duplicate routing patterns and potential typo
errors.
Mounts the specified middleware function. The middleware function is executed for each request served by the application.
local http = require 'http'
local app = http.app.new()
app:use(http.middleware.routing)
The req
object represents the HTTP request and has properties for the
request method, path, HTTP headers, and so on. By convention, the object is
always referred to as req
.
This property holds a reference to the instance of the application app.options.
Contains a string corresponding to the HTTP method of the request: GET, POST, PATCH, and so on.
Contains the path part of the request URL.
-- http://blog.example.com/api/v1/posts?q=lua
req.path
-- "/api/v1/posts"
This property is a table containing properties mapped to the named route “arguments” set by routing middleware.
app:get('{locale}/user/{user_id:i}', function(w, req)
return w:write(req:route_args.user_id)
end)
-- path: /en/user/123
req.route_args
-- {["locale"] = "en", ["user_id"] = "123"}
The req.route_args
can have a reserved property route_name
if there is an associated name with the request handler.
app:get('{locale}/user/{user_id:i}', 'user', function(w, req)
end)
-- path: /en/user/123
req.route_args.route_name
-- "user"
If there is no route arguments, it is the empty table, {}
.
This property is a table containing a property for each query string parameter (case-sensitive) in the route.
The value is a
string
for a single occurrence of query parameter or atable
for multiple values.
-- http://blog.example.com/api/v1/posts?q=lua&page=2
req.query
-- {["q"] = "lua", ["page" = "2"]}
req.query.q
-- "lua"
req.query.page
-- "2"
Use req.query
or req:parse_query()
.
app:get('', function(w, req)
local qs = req.query or req:parse_query()
-- ...
end)
If there is no query string, it is the empty table, {}
.
-- http://blog.example.com/api/v1/posts
req.query
-- {}
The
req.query
table isnil
unless you callreq:parse_query()
first.
This property is a table containing a property for each HTTP header name (lowercase).
The value is a
string
for a single occurrence of HTTP header or atable
for multiple values.
Use req.headers
or req:parse_headers()
.
app:get('', function(w, req)
local headers = req.headers or req:parse_headers()
-- ...
end)
The
req.headers
table isnil
unless you callreq:parse_headers()
first.
Use req.headers
to determine whenever the request is AJAX.
local headers = self.headers or self:parse_headers()
local is_ajax = headers['x-requested-with'] == 'XMLHttpRequest'
This property is a table that contains HTTP cookies (case-sensitive).
Use req.cookies
or req:parse_cookie()
.
app:get('', function(w, req)
local cookies = req.cookies or req:parse_cookie()
-- ...
end)
The req:parse_cookie()
uses an empty name if not specified.
-- Cookie:
req.cookies
-- {['']=''}
-- Cookie: abc
req.cookies
-- {['']='abc'}
Returns a table with mapped cookie name to value.
-- Cookie: a=1
req.cookies
-- {['a']='1'}
-- Cookie: a=1; b=2
req.cookies
-- {a='1', b='2'}
Supports cookie value with spaces.
-- Cookie: c1=a b; c2= ; c3=a ; c4= b
req.cookies
-- {['c1']='a b', ['c2']=' ', ['c3']='a ', ['c4']=' b'}
If there is no cookies, it is the empty table, {}
.
The
req.cookies
table isnil
unless you callreq:parse_cookies()
first.
This property type depends on MIME type of incoming HTTP request.
MIME Type | Value |
---|---|
application/x-www-form-urlencoded | a table that contains all the current request POST query arguments. |
application/json | a table that corresponds to parsed JSON object, either array or map. |
multipart/form-data | a string, in-memory request body data. |
Use req.body
or req:parse_body()
.
local values = req.body or req:parse_body()
To force in-memory request bodies, set client_body_buffer_size to the same size value in client_max_body_size.
This function returns nil
if the request body has zero size.
Parses HTTP query string.
See req.query.
Parses HTTP headers.
See req.headers.
Parses HTTP cookie header.
See req.cookies.
Parses HTTP body.
See req.body.
Returns multiple values representing various server parts.
-- http://blog.example.com/api/v1/posts?q=lua
local scheme, host, port = req:server_parts()
-- "http", "blog.example.com", "80"
Use req.server_parts()
to build request URL authority part.
local authority = scheme .. '://' .. host
.. (port == '80' and '' or ':' .. port)
-- http://blog.example.com
The w
object represents the HTTP response and has properties for the
response HTTP headers. By convention, the object is always referred to as w
.
This property is a table that contains HTTP headers. The keys of the returned table are the header names (case-insensitive) and the values are the respective header values.
The value is a
string
for a single occurrence of HTTP header or atable
for multiple values.
-- w.headers['X-Request-Count'] = '100'
w.headers['X-Request-Count']
-- "100"
w.headers['x-request-count']
-- "100"
Use table to set multiple values.
-- w.headers['Set-Cookie'] = {'a=1', 'b=2'}
w.headers['set-cookie']
-- {"a=1", "b=2"}
Use value
nil
to remove corresponding HTTP response header.
Returns the status code which was sent to the client.
nil
value indicates successful HTTP response.
Controls the HTTP status code that will be sent to the client when the headers get flushed.
w:set_status_code(403)
Sends a chunk c
of the response body. This method may be called multiple
times to provide successive parts of the body.
The response body is omitted when the request is a HEAD request. The 204 and 304 responses must not include a body.
The behavior of this method depends on adapter in use.
Adapter | Behavior |
---|---|
buffered | The w:write() calls are buffered. The headers and body is sent to the client when application will finish processing request. |
stream | The first time w:write() is called, it will send the headers and the first chunk of the body to the client. |
This method sends the raw HTTP body and do not perform any body encodings.
Flushes response output to the client asynchronously.
This method returns immediately without waiting for output data to be written into the system send buffer.
This function has no effect in case of buffered adapter in use.
Adds a single string value for HTTP header.
w:addHeader('Set-Cookie', 'c=100')
If this header already exists it will add value so multiple headers withthe same name will be sent.
Sets specified cookie string by adding Set-Cookie
HTTP header.
w:set_cookie(http.cookie.dump {
name = 'c', value = '100', http_only = true
})
w.headers['Set-Cookie']
-- c=100; HttpOnly
Use this method to delete cookie.
w:set_cookie(http.cookie.delete {name = 'c'})
w.headers['Set-Cookie']
-- c=; Expires=Thu, 01 Jan 1970 00:00:00 GMT
Redirects to the absolute URL with specified status that corresponds to an HTTP status code. If not specified, status defaults to “302 “Found”.
w:redirect('http://example.com')
Use this method together with routing mixin to redirect to named routes.
local mixin = require 'core.mixin'
local http = require 'http'
mixin(http.Request, http.mixins.routing)
app:get('', function(w, req)
return w:redirect(req:absolute_url_for('welcome'))
end)
app:get('welcome', 'welcome', function(w, req)
return w:write('Hello World!\n')
end)
An HTTP cookie (browser cookie) is a small piece of data that a server sends to the user's web browser. The browser may store it and send it back with the next request to the same server.
The http.cookie
module represents the HTTP cookie.
Returns a string that represents the HTTP cookie per options
provided.
http.cookie.dump {name='a', value='1'}
-- "a=1"
http.cookie.dump {name='a', value='1', path='/abc/'}
-- "a=1; Path=/abc/"
http.cookie.dump {name='a', value='1', domain='example.com'}
-- "a=1; Domain=example.com"
http.cookie.dump {name='a', value='1', expires=1423473707}
-- "a=1; Expires=Mon, 09 Feb 2015 09:21:47 GMT"
http.cookie.dump {name='a', value='1', max_age=600}
-- "a=1; Max-Age=600"
http.cookie.dump {name='a', value='1', same_site='Strict'}
-- "a=1; SameSite=Strict"
http.cookie.dump {name='a', value='1', http_only=true}
-- "a=1; HttpOnly"
http.cookie.dump {name='a', value='1', secure=true}
-- "a=1; Secure"
The following table describes the options.
Property | Type | Description |
---|---|---|
name | string | Name of the cookie. |
value | string | Cookie value. |
path | string | Path for the cookie. |
domain | string | Domain name for the cookie. |
expires | number | Expiry date of the cookie in GMT. If not specified creates a session cookie. |
max_age | number | The expiry time relative to the current time in milliseconds. |
same_site | string | Can be used to disable third-party usage for a specific cookie. Either Lax or Strict. |
http_only | boolean | Flags the cookie to be accessible only by the web server. |
secure | boolean | Marks the cookie to be used with HTTPS only. |
Returns a string that corresponds to an empty expired cookie.
http.cookie.delete {name='a'}
-- a=; Expires=Thu, 01 Jan 1970 00:00:00 GMT
http.cookie.delete {name='a', path='/abc/'}
-- a=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/abc/
See options in http.cookie.dump.
Middleware functions are functions that have
access to the
response writer object (w
),
the request object (req
), the
following function, and the application
options in the application’s request processing lifecycle.
The following
function is a function, when invoked, executes the
middleware succeeding the current one.
If the current middleware function does not end the request processing cycle, it must call
following(w, req)
to pass control to the next middleware function.
Configuration
Middleware function receives application options during initialization. However sometimes it is useful to override some settings per middleware without affecting application options.
Use http.middleware.opt
to override any middleware specific options over
those in application options.
local http = require 'http'
local opt = require 'http.middleware.opt'
local app = http.app.new {
option1 = 'A'
}
local function my_middleware(following, options)
local option1 = options.option1
-- option1 == 'B'
return function(w, req)
return following(w, req)
end
end
app:use(opt(my_middleware, {
option1 = 'B' -- this will override default in app.options
}))
app:use(http.middleware.routing)
app:get('', function(w, req)
-- req.options.option1 == 'A'
end)
return app()
Authentication cookie middleware implements creation of signed and encrypted authentication session cookie.
Use response writer w.principal
property to set or delete the security
principal associated with request authentication.
local http = require 'http'
local authcookie = http.middleware.authcookie
app:get('signin', authcookie, function(w, req)
w.principal = {id = 'john.smith', roles = {admin = true}}
end)
app:get('signout', authcookie, function(w, req)
w.principal = nil
end)
Read more about a principal object in security section.
The authentication cookie is set only on successful status code (2XX).
Use app.options
to configure middleware.
local ticket = require 'security.crypto.ticket'
local digest = require 'security.crypto.digest'
local cipher = require 'security.crypto.cipher'
local http = require 'http'
local app = http.app.new {
ticket = ticket.new {
-- digest = digest.new('sha256'),
digest = digest.hmac('ripemd160', 'b`*>Z!P4pf99%p,)'),
cipher = cipher.new {
cipher = 'aes128',
key = 'DK((-x=e[.2cLq]f',
iv = 'b#KXN>H9"j><f2N`'
}
},
auth_cookie = {
name = '_a'
},
principal = require 'security.principal'
}
The following table describes the configuration options.
Property | Type | Description |
---|---|---|
ticket | table | Required. An object that provides secure string encoding, ticket:encode(s) . |
auth_cookie | table | Optional. Defaults to {name='_a'} . The http_only is always true . If path is not specified, fallbacks to options.root_path or / . See more here. |
principal | table | Optional. An object that provides function principal.dump(p) , where p is a table like: {id='', roles={role1=true}, alias='', extra=''} . Defaults to require 'security.principal' . |
Use
auth_cookie.max_age
to control timeout before authentication cookie expires.
Authorization middleware implements verification of signed and encrypted authentication session cookie.
Use request req.principal
property to get a security
principal associated during request authentication.
app:get('secure', authorize, function(w, req)
if not req.principal.roles['admin'] then
return w:set_status_code(403)
end
-- req.principal.id
end)
The middleware responds with HTTP status code 401 (Unauthorized) in case the authentication cookie is not in the request or cannot be decoded.
The authentication cookie lifetime is automatically extended once cookie time left is less than a half of cookie max age.
See configuration options in authcookie middleware.
TODO
This middleware implements cross-origin resource sharing.
local http = require 'http'
app:use(http.middleware.cors)
Use app.options
to configure middleware.
local app = http.app.new {
cors = http.cors.new {
allow_credentials = true,
allowed_origins = {'*'},
allowed_methods = {'GET', 'HEAD', 'POST', 'PUT', 'DELETE'},
allowed_headers = {'content-type', 'x-requested-with'},
exposed_headers = {'content-length', 'etag'},
max_age = 180
}
}
The following table describes the configuration options.
Property | Type | Description |
---|---|---|
allow_credentials | boolean | Indicates whether the client is allowed to send credentials (cookies, authorization headers, etc) to the server. Defaults to false . |
allowed_origins | table | Required. Indicates whether the response can be shared with resources with the given origin. For requests without credentials, specify "*" as a wildcard, thereby allowing any origin to access the resource. Otherwise, specify a URI that may access the resource. |
allowed_methods | table | Specifies the methods allowed when accessing the resource in response to a preflight request. Defaults to {'GET', 'HEAD'} . |
allowed_headers | table | Used in response to a preflight request to indicate which HTTP headers will be available via Access-Control-Expose-Headers when making the actual request. |
exposed_headers | table | Indicates which headers can be exposed as part of the response. Only simple response headers are exposed. Use this property to allow clients to access other headers. |
max_age | number | How long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached. |
Routing refers to the definition of endpoints (URI paths) and how they respond to client requests.
local http = require 'http'
app:use(http.middleware.routing)
A route method is derived from one of the HTTP methods and is attached to the
app
object. Routing supports the following methods that correspond to HTTP
verbs: get
, head
, post
, patch
, put
, delete
.
Route path, in combination with a request method, define the endpoint at which requests can be dispatched. Route path can be string pattern.
app:post('signin', function(w, req)
end)
There is a special routing method, app:all()
, which is used to respond to all
request methods.
app:all('', function(w, req)
end)
Route arguments are named URL path segments that used to capture the values
specified at their position in the URL path. The captured values are populated in
the req.route_args
table, with the name of the route arguments specified in
the URL path as the respective keys in the table.
-- URL: /en/user/123
-- route path: {locale}/user/{user_id:i}
-- req.route_args: {['locale'] = "en", ['user_id'] = "123"}
Use route arguments in the path.
app:get('{locale}/user/{user_id:i}', function(w, req)
end)
Because routing supports string patterns, specify type of the matching path segment.
Name | Pattern | Description |
---|---|---|
i , int , number , digits |
%d+ |
One or more digits. |
w , word |
%w+ |
One or more word characters. |
s , segment , part |
[^/]+ |
Everything until / . |
* , a , any , rest |
.+ |
Any match. |
Use named route to build URL from name.
-- URL: /en/user/123
-- route path: {locale}/user/{user_id:i}
req:path_for('user', {locale='de', user_id='1'})
-- "/de/user/1"
req:path_for('user', {user_id='1'})
-- "/en/user/2"
req:path_for('user')
-- "/en/user/123"
req:absolute_url_for('user')
-- "http://localhost:8080/en/user/123"
If route argument is not specified, it is inherited from
req.route_args
.
Use routing mixin to build URL from name.
local mixin = require 'core.mixin'
local http = require 'http'
mixin(http.Request, http.mixins.routing)
app:get('{locale}/user/{user_id:i}', 'user', function(w, req)
-- req:path_for('user', {user_id='1'})
-- req:absolute_url_for('user')
end)
Use app:route
to chain multiple request handlers to the same URL path.
app:route('users')
:get(function(w, req)
end)
:post(function(w, req)
end)
The following table describes the configuration options.
Property | Type | Description |
---|---|---|
root_path | string | The URL path on which a router instance to be mounted. Defaults to / . |
router | object | Defaults to require 'routing.router' . |
urls | table | Holds all URL mapping for the application. |
allow_path_override | boolean | Specifies whenever allowed to override path that is already defined. Defaults to false . |
The routing middleware responds with HTTP status 404 (Not Found) in case there
is not match for req.path
.
Provides integration with lua-resty-websocket library.
Usage example:
local http = require 'http'
local websocket = require 'http.middleware.websocket'
local app = http.app.new {
websocket = {
timeout = 30000
}
}
app:use(http.middleware.routing)
app:get('echo', websocket, function(ws, req)
ws:on('text', function(message)
ws:send_text(message)
end)
ws:on('timeout', function()
ws:close()
end)
ws:loop()
end)
return app()
The following table describes the configuration options.
Property | Type | Description |
---|---|---|
timeout | number | The network timeout threshold in milliseconds. |
max_payload_len | number | The maximal length of payload allowed when sending and receiving WebSocket frames. Defaults to 65535 . |
send_masked | boolean | Specifies whether to send out masked WebSocket frames. When it is true , masked frames are always sent. Default to false . |
The websocket middleware responds with HTTP status 400 (Bad Request) in case there is an error.
A mixin is a special kind of multiple inheritance. Specifically, it is used to provide optional features or reuse of particular feature in different classes.
The mixins are not made to stand on their own. Usually, mixins assume some context which they extend.
local mixin = require 'core.mixin'
Extends response writer (w
) with ability to send JSON.
Use mixin
to extend response writer:
local mixin = require 'core.mixin'
local http = require 'http'
mixin(http.ResponseWriter, http.mixins.json)
Sends a JSON response. This method sends a response (with the content-type
application/json) that is the obj
parameter converted to a JSON string
using core.encoding
module.
app:get('', function(w, req)
return w:json({message = 'Hello World!'})
end)
The parameter can be any JSON type, including table
, string
, boolean
,
or number
, and nil
.
Extends request (req
) with ability to resolve named routes.
Use mixin
to extend request:
local mixin = require 'core.mixin'
local http = require 'http'
mixin(http.Request, http.mixins.routing)
Returns URL path part for route name
, optionally substituting path named
segments with args
table.
-- URL: /en/user/123
-- route path: {locale}/user/{user_id:i}
req:path_for('user', {user_id='1'})
-- "/en/user/1"
Any route argument not provided by
args
is taken fromreq.route_args
.
Returns URL for route name
.
-- URL: http://localhost:8080/en/user/123
-- route path: {locale}/user/{user_id:i}
req:path_for('user', {user_id='1'})
-- "http://localhost:8080/en/user/1"
See req:path_for.
Provides integration with Nginx HTTP service using lua-nginx-module.
This adapter buffers each chunk of w:write()
, flushes once application finishes processing HTTP request.
http {
init_by_lua '
local adapter = require "http.adapters.nginx.buffered"
main = adapter(require(os.getenv("app") or "demos.http.hello"))
';
server {
listen 8080;
server_name 127.0.0.1;
location / {
default_type 'text/plain';
content_by_lua 'main(ngx)';
}
}
}
This adapter asynchronously writes data and will return immediately without waiting for all the data to be written into the system send buffer.
http {
init_by_lua '
local adapter = require "http.adapters.nginx.stream"
main = adapter(require(os.getenv("app") or "demos.http.hello"))
';
server {
listen 8080;
server_name 127.0.0.1;
location / {
default_type 'text/plain';
content_by_lua 'main(ngx)';
}
}
}
Data validation ensures data quality, that they are both correct and useful.
The model binder converts a table of string values to corresponding Lua type
per target model attribute type. For example, if there is a model with an
attribute message_id
and it defaults to numeric value 0, the model binder
attempts to convert source value to number.
Example: binding a message model
local binder = require 'validation.binder'
app:put('', function(w, req)
local m = {message_id=0, message=''}
local b = binder.new()
local values = req.body or req:parse_body()
if not b:bind(m, values) then
w:set_status_code(400)
return w:json(b.errors)
end
end)
There are several model value adapters accessible in
validation.model.adapters
table.
The following table describes behavior of corresponding model value adapter.
Name | Description |
---|---|
boolean | The nil and empty string "" values are converted to nil . If source value type is not boolean , the adapter converts it to string and checks whenever it equals to 1 or true . |
number | The nil and empty string "" values are converted to nil . Uses lua tonumber function for conversion. Adds an error message in case the input is not in numeric format. |
string | The nil returned as nil . If source value type is not string , the adapter uses tostring function for conversion. Trims returned value. |
If source value is a table with multiple values, the last one is used.
Model value adapter is a function that satisfies the following contract:
local function my_adapter(value, translations)
if value == nil then
return nil, translations:gettext("My error message.")
end
return value
end
Where value
- the original value, can be a table or a string or any JSON
compatible type.
The model can define specific adapters by using adapters
attribute, which is a table per
attribute to adapt.
Any attributes that have no corresponding name in the model are ignored.
The b
object by convention corresponds to an intance of the model binder.
Create it by calling binder.new
function exported by the validation.binder
module.
local binder = require 'validation.binder'
app:post('', function(w, req)
local b = binder.new()
end)
It also accepts an optional parameter translations
.
The binder object cannot be shared between request because it maintains its state in the
errors
property.
This property is a table containing any errors reported either by model value adapters or validators.
The data contract for errors
table uses attribute name as a key and a table
of strings for multiple error messages.
{
"message": ["Required field cannot be left blank."]
}
Example: report binding errors in JSON
app:post('', function(w, req)
local m = {message=''}
local b = binder.new()
local values = req.body or req:parse_body()
if not b:bind(m, values) then
w:set_status_code(400)
return w:json(b.errors)
end
end)
Binds values
to model
. Returns a boolean
value indicating whenever the
binding succeeds or not. In the later case the b.errors
table contains any
errors reported.
The
b.errors
table is populated with all errors per model attributes.
Validates model
using validator
. Returns a boolean
value if validation is
passed or not. Similar to bind
method, the b.errors
table contains
validation errors.
Example: bind model to request body and validate
local binder = require 'validation.binder'
local validator = require 'validation.validator'
local length = require 'validation.rules.length'
local required = require 'validation.rules.required'
local greeting_validator = validator.new {
author = {required, length{max=20}},
message = {required, length{min=5}, length{max=512}}
}
-- ...
app:post('', function(w, req)
local m = {author='', message=''}
local b = binder.new()
local values = req.body or req:parse_body()
if not b:bind(m, values) or
not b:validate(m, greeting_validator) then
w:set_status_code(400)
return w:json(b.errors)
end
end)
Combine several calls to
b.validate
to reuse validators between models.
The object with suffix _validator
by convention corresponds to the model
validator. Create it by calling validator.new
function exported by the
validation.validator
module:
local validator = require 'validation.validator'
local greeting_validator = validator.new {
}
It accepts the table mapping
which designates which model attributes to be
validated by particular validation rules.
Example: greeting validator
local validator = require 'validation.validator'
local length = require 'validation.rules.length'
local required = require 'validation.rules.required'
local greeting_validator = validator.new {
message = {required, length{min=5}, length{max=512}}
}
You can reuse domain validation rules between validators.
local message_rules = {required, length{min=5}, length{max=512}}
local greeting_validator = validator.new {
message = message_rules
}
Use validator object by passing it to binder validate
method.
local b = binder.new()
if not b:validate(m, greeting_validator) then
-- see b.errors
end
The validator object is stateless and can be reused.
**Example: composite validation
local greeting_validator = validator.new {
author = validator.new {
name = {required}
}
}
The composite validation does not stack model attributes, for example in case author name is empty, the
errors
object will contain validation error for attributename
, notauthor.name
.
Extends with ability to set validation error, assumes self.errors
.
Example: create account
local mixin = require 'core.mixin'
local validation = require 'validation'
local MembershipService = mixin({}, validation.mixins.set_error)
function MembershipService:create_account(r)
if self:has_account(r.username) then
return self:set_error(
'The user with such username is already registered.',
'username')
end
if not self:add_account(r) then
return self:set_error(
'The system was unable to create an account for you.')
end
return true
end
The self.errors
is populated with the field error in the first case.
{
"username": ["The user with such username is already registered."]
}
and with general error in the later one.
{
"__ERROR__": ["The system was unable to create an account for you."]
}
The use of __ERROR__
key is solely by convention.
The
self.errors
table can be encoded to json so UI can display the error to the user. For example, the field error displayed below the input box, while general error as a message at the top part of the input form.
Extends with ability to perform model validation, assumes self.errors
and
self:get_locale()
.
local class = require 'core.class'
local mixin = require 'core.mixin'
local web = require 'web'
mixin(BaseHandler, web.mixins.validation)
local MessageHandler = class(BaseHandler, {
post = function(self)
local m = {author='', message=''}
self.errors = {}
if not self:update_model(m) or
not self:validate(m, greeting_validator) then
return self:json_errors()
end
return self:json(m)
end
})
see a complete example here.
Validation rule is a module that satisfies the following contract:
local mt = {__index = {
msg = 'My validation error message.',
validate = function(self, value, model, translations)
if not value then
return translations:gettext(self.msg)
end
return nil
end
}}
return function (options)
return setmetatable(options, mt)
end
Where value
- the value to be validated, like string, number, etc. Returns
nil
if there is no error, otherwise an error message.
Use an optional parameter msg
in table options
to override the default
error message.
Combine several validation rules using a table.
local greeting_validator = validator.new {
message = {required, length{min=5}, length{max=512}}
}
Validation rules are checked from left to right until a first fail.
Ensures if all of the provided rules validates the field.
This rule is analogue to logical and, it checks each rule until a first fails.
Ensures if any of the provided rules validates the field.
local validator = require 'validation.validator'
local anyof = require 'validation.rules.anyof'
local message_validator = validator.new {
nbr = {anyof{range{min=0, max=5}, range{min=10, max=20}}}
}
This rule is analogue to logical or, it checks each rule until a first succeeds. If none of the rules succeeds returns the first error.
The length of the raw byte string
value of the model attribute must match the
specified boundaries.
local validator = require 'validation.validator'
local bytes = require 'validation.rules.bytes'
local message_validator = validator.new {
message = {bytes{max=512}}
}
Use one of the optional min
or max
to specify the boundaries.
Compares value of the model with the one named as an argument.
local validator = require 'validation.validator'
local compare = require 'validation.rules.compare'
local password_validator = validator.new {
password = {compare{equal='confirm_password'}}
}
Use one of the optional equal
or not_equal
to specify the model attribute
to compare.
Checks whenever the value corresponds to a valid email address.
local validator = require 'validation.validator'
local email = require 'validation.rules.email'
local password_validator = validator.new {
alternate_email = {email()}
}
Use an optional parameter msg
to override the error message.
The value must be an empty string.
Use an optional parameter msg
to override the error message.
The model must contain only specified fields.
local validator = require 'validation.validator'
local fields = require 'validation.rules.fields'
local place_validator = validator.new {
__ERROR__ = {fields {'x', 'y'}}
}
Use an optional parameter msg
to override the error message.
local place_validator = validator.new {
__ERROR__ = {
fields {allowed = {'x', 'y'}, msg = 'Unknown field %s.'}
}
}
The rule stops on a first unknown field encountered. The field name is truncated to the first 9 characters.
The value must be of type table, the rules are applied to each item.
local validator = require 'validation.validator'
local typeof = require 'validation.rules.typeof'
local items = require 'validation.rules.items'
local coords_validator = validator.new {
coords = {items{typeof 'number'}}
}
The rule stops on a first error.
The length of the UTF8 string
value of the model attribute must match the
specified boundaries.
local validator = require 'validation.validator'
local length = require 'validation.rules.length'
local message_validator = validator.new {
message = {length{max=512}}
}
Use one of the optional min
or max
to specify the boundaries.
The value must be nil.
Use an optional parameter msg
to override the error message.
The value must not be an empty string.
local validator = require 'validation.validator'
local nonempty = require 'validation.rules.nonempty'
local user_validator = validator.new {
username = {nonempty}
}
Use an optional parameter msg
to override the error message.
The value must be either nil or all rules must succeed.
local validator = require 'validation.validator'
local optional = require 'validation.rules.nonempty'
local user_validator = validator.new {
username = {optional{length{min=6, max=20}}}
}
Use an optional parameter msg
to override the error message.
The value must match the Lua pattern expression.
local validator = require 'validation.validator'
local pattern = require 'validation.rules.pattern'
local id_validator = validator.new {
id = {pattern{"%s+"}}
}
Use one of the optional plain
or negated
to specify desired behavior.
The numeric value must match the specified boundaries.
local validator = require 'validation.validator'
local range = require 'validation.rules.range'
local age_validator = validator.new {
age = {range{min=21}}
}
Use one of the optional min
or max
to specify the boundaries.
The value must not be null.
local validator = require 'validation.validator'
local required = require 'validation.rules.required'
local user_validator = validator.new {
username = {required}
}
Use an optional parameter msg
to override the error message.
Call a custom function for validation.
local validator = require 'validation.validator'
local rule = require 'validation.rules.required'
local coords_validator = validator.new {
coords = {
rule(function(value)
if value % 2 ~= 0 then
return 'Must be an even number.'
end
end)
}
}
Always succeeds regardless value supplied.
The value must not be of given type.
local validator = require 'validation.validator'
local typeof = require 'validation.rules.typeof'
local user_validator = validator.new {
username = {typeof 'string'},
age = {typeof {'integer', msg = 'Must be an integer number.'}},
agreed = {typeof {type = 'boolean', msg = 'You must agree to terms.'}}
}
Use an optional parameter msg
to override the error message.
This validator besides standard Lua types also supports integer type.
Provides integration with luaossl library.
Returns a new cipher instance. cipher
is a string suitable for passing to the
OpenSSL, typically of a form similar to "AES-128-CBC", "aes256", etc. key
and iv
are optional binary strings with lengths equal to that required by
the cipher.
local cipher = require 'security.crypto.cipher'
local c = cipher.new {
cipher = 'aes128',
key = 'DK((-x=e[.2cLq]f',
iv = 'b#KXN>H9"j><f2N`'
}
To get a list of available cipher algorithms use the following.
openssl list -cipher-algorithms
Returns the encrypted string on success, or nil
and an error message on
failure.
local s, err = c:encrypt('test')
Returns the decrypted string on success, or nil
and an error message on
failure.
local msg, err = c:decrypt(s)
Returns a new digest instance using the specified algorithm type. digest_type
is a string suitable for passing to the OpenSSL, typically of form "SHA1",
"ripemd160", etc.
local digest = require 'security.crypto.digest'
local md5 = digest.new 'md5'
Returns the final message digest as a binary string.
local s = md5('test')
To get a list of available digest algorithms use the following.
openssl list -digest-algorithms
Returns a new instance that represents a cryptographic HMAC algorithm using
the specified digest_type
and key
.
local hmac = require 'security.crypto.hmac'
local d = digest.hmac('ripemd160', '6xZxzaP)C2d5LRnw')
Returns the final message digest as a binary string.
local s = d('test')
Returns count
cryptographically-strong bytes as a single string.
local rand = require 'security.crypto.rand'
local r = rand.bytes(16)
Returns a cryptographically strong uniform random integer in the interval
[0, n). If n
is omitted, the interval is [0, 2^64 − 1].
local rand = require 'security.crypto.rand'
local i = rand.uniform(100)
The number returned is in range from zero (including) to n (exclusive).
Provides access to the ticket used to cryptographically secure sensitive information.
Returns a new instance that represents a cryptographically secure ticket. The
content of the ticket is secured using cipher
and signed by digest
(supplied either as a string or a function), encoder
(a table with functions
encode and decode) specifies character encoding to apply to raw string and
defaults to base64 encoding. The ticket lifetime is limited per max_age
and
defaults to 900 seconds from the time of encoding.
local cipher = require 'security.crypto.cipher'
local digest = require 'security.crypto.digest'
local ticket = require 'security.crypto.ticket'
local t = ticket.new {
--digest = 'sha256',
--digest = digest.new 'sha256',
digest = digest.hmac('ripemd160', '6xZxzaP)C2d5LRnw'),
cipher = cipher.new {
cipher = 'aes128',
key = 'DK((-x=e[.2cLq]f',
iv = 'b#KXN>H9"j><f2N`'
},
encoder = require 'core.encoding.base64',
max_age = 900
}
Returns a secured string on success, or nil
and an error message on
failure.
local secured, err = t:encode('some secret string')
Returns a decoded string on success, or nil
and an error message on
failure.
local text, err = t:decode(secured)
A principal table represents the security context of the user on whose behalf the code is running, including that user's identity (name) and any roles to which the user belongs.
local user = {
id = 'bob',
roles = {
staff = true,
operator = true
},
alias = 'Bob',
extra = 'any arbitrary string'
}
Returns a principle table parsed out from string.
Dumps a principle table into a string representation.
lurl is a tool that allows sending a request to an application and display response.
Usage: lurl [options...] <app> <path>
Options:
-X COMMAND Specify request command to use, e.g. POST
-I Fetch the headers only
-H LINE Pass custom header LINE, e.g. 'Accept: application/json'
-d DATA Request body data, e.g. '{"msg":"hello"}', 'msg=hello'
-b Issue a number of requests through iterations
-v Make the operation more talkative
Basic usage:
lurl -v demos/http/hello.lua /
Output:
req: {
["body"] = {},
["headers"] = {
["accept"] = "*/*",
["host"] = "localhost:8080",
["user-agent"] = "lurl/scm-0"
},
["method"] = "GET",
["path"] = "/",
["query"] = {},
["route_args"] = {}
}
w: {
["buffer"] = {
"Hello World!\
"
},
["headers"] = {}
}