Home

Awesome

Waffle

Waffle is a fast, asynchronous, express-inspired web framework for Lua/Torch built on top of ASyNC.

Waffle's performance is impressive. On this test, given in examples/fib.lua, Waffle reaches over 20,000 requests/sec (2-4 x Node+express, 1/2 x multithreaded Go). With automatic caching enabled, Waffle can reach over 26,000 requests/sec, equaling single-threaded Go.

This project depends on htmlua for HTML templating.

Installation

> (sudo) luarocks install https://raw.githubusercontent.com/benglard/htmlua/master/htmlua-scm-1.rockspec
> (sudo) luarocks install https://raw.githubusercontent.com/benglard/waffle/master/waffle-scm-1.rockspec

Hello World

local app = require('waffle')

app.get('/', function(req, res)
   res.send('Hello World!')
end)

app.listen()

Requests

app.get('/', function(req, res)
   res.send('Getting...')
end)

app.post('/', function(req, res)
   res.send('Posting...')
end)

app.put('/', function(req, res)
   res.send('Putting...')
end)

app.delete('/', function(req, res)
   res.send('Deleting...')
end)

URL Parameters

app.get('/user/(%d+)', function(req, res)
   local userId = tonumber(req.params[1])
   local users = {
      [1] = 'Lua',
      [2] = 'JavaScript',
      [3] = 'Python'
   }
   res.send(string.format('Hello, %s', users[userId] or 'undefined'))
end)

app.get('/user/(%a+)', function(req, res)
   local name = req.params[1]
   res.send(string.format('Hello, %s', name))
end)

HTML Rendering

There are two options for html rendering. The first involves writing actual html and using the string interp utility provided, ${variable-name}.

<html>
<head></head>
<body>
  <h3>Welcome, ${name}</h3>
  <p>Time: ${time}</p>
</body>
</html>
app.get('/render/(%a+)', function(req, res)
   res.render('./examples/template.html', {
      name = req.params[1],
      time = os.time()
   })
end)

The second, preferable, more powerful way involves writing htmlua scripts, either as separate template files, or inline in view functions.

-- luatemp.html
local base = extends 'examples/baseluatemp.html'
return block(base, 'content'){
   h3 'Welcome, ${name}',
   p 'Time: ${time}',
   ul(each([[${users}]], li)),
   img {
      src = 'https://www.google.com/images/srpr/logo11w.png'
   }
}
-- htmlua.lua
-- Template
app.get('/', function(req, res)
   res.htmlua('luatemp.html', {
      name = 'waffle',
      time = os.time(),
      users = {'lua', 'python', 'javascript'}
   })
end)

-- Inline
app.get('/i', function(req, res)
   res.send(html {
      head { title 'Title' },
      body { p 'Hello World!' }
   })
end)

The htmlua page provides further documentation and examples.

Form Parsing

app.get('/m', function(req, res)
   res.send(html { body { form {
      action = '/m',
      method = 'POST',
      enctype = 'multipart/form-data',
      p { input {
         type = 'text',
         name = 'firstname',
         placeholder = 'First Name'
      }},
      p { input {
         type = 'text',
         name = 'lastname',
         placeholder = 'Last Name'
      }},
      p { input {
         type = 'file',
         name = 'file' 
      }},
      p { input {
         type = 'submit',
         'Upload'
      }}
   }}})
end)

app.post('/m', function(req, res)
   local name = string.format('%s %s', req.form.firstname, req.form.lastname)
   local path = paths.add(os.getenv('HOME'), req.form.file.filename)
   req.form.file:save{path=path}
   res.send('Saved to ' .. path)
end)

You can easily transform an uploaded image into a typical torch tensor like so:

app.post('/', function(req, res)
   local img = req.form.file:toImage()
   local m = img:mean()
   res.send('Image mean: ' .. m)
end)

Websockets

To implement a websocket server, call app.ws with a url path and a function accepting a single table. You can then define checkorigin, onopen, onmessage, onpong, and onclose for that table, to control the server-side websocket connection.

Benchmarking websockets is tricky, but on first attempts, waffle seems competitive with similar node libraries.

local app = require('waffle')
local js = [[
var ws = new WebSocket("ws://127.0.0.1:8080/ws/");
function print() { console.log(ws.readyState); }
ws.onopen = function() {
   console.log("opened");
   print();
   ws.send("Hello");
}

ws.onmessage = function(msg) {
   console.log(msg);
   setTimeout(function() { ws.close(); }, 1000);
}

ws.onclose = function(event) {
   console.log(event);
   console.log("closed");
   print();
}
]]

app.get('/', function(req, res)
   res.send(html { body {
      p 'Hello, World',
      script { type='text/javascript', js }
   }})
end)

app.ws('/ws', function(ws)
   ws.checkorigin = function(origin) return origin == 'http://127.0.0.1:8080' end
   ws.onopen = function(req) print('/ws/opened') end
   ws.onmessage = function(data)
      print(data)
      ws:write('World')
      ws:ping('test')
   end
   ws.onpong = function(data) print(data) end
   ws.onclose = function(req) print('/ws/closed') end
end)

You can broadcast a message to all open websockets on a url like this:

ws:broadcast('message')

or like this:

app.ws.broadcast(req.url.path, 'message')

Query Paramaters

app.get('/search', function(req, res)
   local search = req.url.args.q
   res.redirect('https://www.google.com/search?q=' .. search)
end)

Static Files

local app = require('waffle')
app.set('public', '.')
app.listen()

Error Handling

app.error(404, function(description, req, res)
   local url = string.format('%s%s', req.headers.host, req.url.path)
   res.status(404).send('No page found at ' .. url)
end)

app.error(500, function(description, req, res)
   if app.debug then
      res.status(500).send(description)
   else
      res.status(500).send('500 Error')
   end
end)

Cookies

app.get('/cookie', function(req, res)
   local c = req.cookies.counter or -1
   res.cookie('counter', tonumber(c) + 1)
   res.send('#' .. c)
end)

Sessions

Waffle has both in-memory and redis sessions using redis-async.

local app = require('../waffle').CmdLine()

app.get('/', function(req, res)
   if app.session.type == 'memory' then
      local n = app.session.n or 0
      res.send('#' .. n)
      if n > 19 then app.session.n = nil
      else app.session.n = n + 1 end
   else
      app.session:get('n', function(n)
         res.send('#' .. n)
         if n > 19 then app.session:delete('n')
         else app.session.n = n + 1 end
      end, 0)
   end
end)

app.listen()

JSON

app.get('/', function(req, res)
   res.json{test=true}
end)

urlfor and Modules

-- Add a name parameter, e.g. 'test'
app.get('/test', function(req, res) res.send('Hello World!') end, 'test')

-- Retreive url corresponding to route named 'test'
local url = app.urlfor('test')

Modules let you group routes together by url and name (really by function)

app.module('/', 'home') -- Home Routes
   .get('',     function(req, res) res.send 'Home' end, 'index')
   .get('test', function(req, res) res.send 'Test' end, 'test')

app.module('/auth', 'auth') -- Authentication Routes
   .get('', function(req, res) res.redirect(app.urlfor('auth.login'))
      end, 'index')
   .get('/login',  function(req, res) res.send 'Login'  end, 'login')
   .get('/signup', function(req, res) res.send 'Signup' end, 'signup')

Command Line Options

Allows you to write every currently possible waffle application property as a command line option, and have it handled seamlessly.

local app = require('waffle').CmdLine()
> th examples/cmdline.lua --debug --print --port 3000 --templates examples/

Async Debugging

app = require('waffle')
a = 1
b = 2
c = 3
app.repl()
app.listen()
th> async = require 'async'
                                                                      [0.0133s]  
th> async.repl.connect({host='127.0.0.1', port=8081})
                                                                      [0.0005s]  
th> async.go()
127.0.0.1:8081> a
1  
127.0.0.1:8081> b
2  
127.0.0.1:8081> c
3  
127.0.0.1:8081> app
{ ... }
127.0.0.1:8081> _G
{ ... }

wafflemaker (executable)

The wafflemaker executable can be used:

wafflemaker --create name_of_project
cd folder/i/want/to/serve
wafflemaker --serve

Larger example (with autocache)

When autocache is set to true, waffle will automatically store the response body, headers, and status code, and reuse them when a request is sent to the same http method/url. So, for instance, when a request is sent to GET/10 in the example below, it will only have to compute fib(10) once. Note that app.urlCache is set by default to cache the data of the last 20 method/url requests.

local app = require('../waffle') {
   debug = true,
   autocache = true
}

fib = function(n)
   if n == 0 then return 0
   elseif n == 1 then return 1
   else return fib(n-1) + fib(n-2)
   end
end

app.get('/(%d+)', function(req, res)
   local n = req.params[1]
   local result = fib(tonumber(n))
   res.header('Content-Type', 'text/html')
   res.send('ASyNC + Waffle<hr> fib(' .. n .. '): ' .. result)
end)

app.listen()

TODO