FullMoon adalah kerangka web yang cepat dan minimalis berdasarkan Redbean-server web portabel yang dapat didistribusikan.
Segala sesuatu yang diperlukan untuk pengembangan dan distribusi datang dalam satu file tanpa dependensi eksternal dan setelah kemasan dengan Redbean berjalan di Windows, Linux, atau MacOS. Berikut ini adalah contoh lengkap dari aplikasi fullmoon:
local fm = require " fullmoon "
fm . setTemplate ( " hello " , " Hello, {%& name %} " )
fm . setRoute ( " /hello/:name " , function ( r )
return fm . serveContent ( " hello " , { name = r . params . name })
end )
fm . run ()
Setelah dikemas dengan Redbean, dapat diluncurkan menggunakan ./redbean.com
, yang memulai server yang mengembalikan "Halo, Dunia" ke permintaan HTTP (s) yang dikirim ke http: // localhost: 8080/hello/world.
Redbean adalah server web lintas-platform yang dapat didistribusikan dengan file tunggal dengan kualitas yang unik dan kuat. Sementara ada beberapa kerangka kerja web yang berbasis di LUA (Lapis, Lor, Sailor, Pegasus, dan lainnya), tidak ada yang terintegrasi dengan Redbean (meskipun ada anpan kerangka kerja eksperimental).
Fullmoon adalah kerangka web yang ringan dan minimalis yang ditulis dari perspektif menampilkan semua kemampuan yang disediakan Redbean dengan memperluas dan menambahnya dengan cara yang paling sederhana dan paling efisien. Ini berjalan cepat dan disertakan dengan baterai (rute, templat, generasi JSON dan banyak lagi).
Fullmoon mengikuti filosofi Lua dan menyediakan serangkaian alat minimal untuk menggabungkan sesuai kebutuhan dan digunakan sebagai dasar untuk membangun.
fork
lintas platform, socket
, memori bersama, dan banyak lagiUnduh salinan Redbean dengan menjalankan perintah berikut (lewati yang kedua jika menjalankan perintah ini di windows):
curl -o redbean.com https://redbean.dev/redbean-2.2.com
chmod +x redbean.com
Nomor versi terbaru dapat diambil dengan permintaan berikut:
curl https://redbean.dev/latest.txt
Pilihan lain adalah membangun redbean dari sumber dengan mengikuti instruksi untuk pembangunan sumber.
fullmoon.lua
ke .lua/
direktori.init.lua
(misalnya, kode LUA yang ditampilkan dalam deskripsi). Opsi lain adalah menempatkan kode aplikasi ke dalam file terpisah (misalnya, .lua/myapp.lua
) dan tambahkan require "myapp"
ke .init.lua
. Beginilah semua contoh yang disertakan disajikan.
zip redbean.com .init.lua .lua/fullmoon.lua
Jika kode aplikasi disimpan dalam file LUA terpisah, seperti dijelaskan di atas, pastikan untuk menempatkannya di dalam .lua/
direktori dan zip file itu juga.
./redbean.com
Jika perintah ini dieksekusi di Linux dan melempar kesalahan tentang tidak menemukan interpreter, itu harus diperbaiki dengan menjalankan perintah berikut (walaupun perhatikan bahwa itu mungkin tidak selamat dari sistem restart):
sudo sh -c " echo ':APE:M::MZqFpD::/bin/sh:' >/proc/sys/fs/binfmt_misc/register "
Jika perintah ini menghasilkan kesalahan membingungkan pada WSL atau anggur saat menggunakan Redbean 2.x, mereka mungkin diperbaiki dengan menonaktifkan BINFMT_MISC:
sudo sh -c ' echo -1 >/proc/sys/fs/binfmt_misc/status '
Luncurkan browser yang menunjuk ke http: // localhost: 8080/hello/world dan itu harus mengembalikan "halo, dunia" (dengan asumsi aplikasi menggunakan kode yang ditunjukkan dalam pengantar atau yang ada di bagian penggunaan).
Contoh paling sederhana perlu (1) memuat modul, (2) mengkonfigurasi satu rute, dan (3) menjalankan aplikasi:
local fm = require " fullmoon " -- (1)
fm . setRoute ( " /hello " , function ( r ) return " Hello, world " end ) -- (2)
fm . run () -- (3)
Aplikasi ini menanggapi permintaan /hello
URL dengan mengembalikan konten "halo, dunia" (dan 200 kode status) dan merespons dengan kode status 404 untuk semua permintaan lainnya.
setRoute(route[, action])
: Mendaftarkan rute. Jika route
adalah string, maka digunakan sebagai ekspresi rute untuk membandingkan jalur permintaan dengan. Jika itu adalah tabel, maka elemennya adalah string yang digunakan sebagai rute dan nilai hashnya adalah kondisi yang diperiksa rute. Jika parameter kedua adalah fungsi, maka dieksekusi jika semua kondisi terpenuhi. Jika itu adalah string, maka digunakan sebagai ekspresi rute dan permintaan diproses seolah -olah dikirim pada rute yang ditentukan (bertindak sebagai pengalihan internal). Jika ada kondisi yang tidak terpenuhi, maka rute berikutnya diperiksa. Ekspresi rute dapat memiliki beberapa parameter dan bagian opsional. Penangan tindakan menerima tabel permintaan yang menyediakan akses ke parameter permintaan dan rute, serta header, cookie, dan sesi.
setTemplate(name, template[, parameters])
: Mendaftarkan templat dengan nama yang ditentukan atau satu set templat dari direktori. Jika template
adalah string, maka itu dikompilasi menjadi penangan template. Jika itu adalah fungsi, disimpan dan dipanggil ketika rendering templat diminta. Jika itu tabel, maka elemen pertama adalah templat atau fungsi dan sisanya digunakan sebagai opsi. Misalnya, menentukan ContentType
sebagai salah satu opsi yang mengatur header Content-Type
untuk konten yang dihasilkan. Beberapa templat ( 500
, json
, dan lainnya) disediakan secara default dan dapat ditimpa. parameters
adalah tabel dengan parameter templat yang disimpan sebagai pasangan nama/nilai (dirujuk sebagai variabel dalam templat).
serveResponse(status[, headers][, body])
: Mengirim respons HTTP menggunakan status
, headers
, dan nilai body
yang disediakan. headers
adalah tabel opsional yang dihuni dengan nama header header HTTP. Jika disediakan, set header ini menghapus semua header lain yang ditetapkan lebih awal selama penanganan permintaan yang sama. Nama header tidak sensitif , tetapi disediakan alias untuk nama header dengan tanda hubung adalah case-sensitive : {ContentType = "foo"}
adalah formulir alternatif untuk {["Content-Type"] = "foo"}
. body
adalah string opsional.
serveContent(name, parameters)
: membuat templat menggunakan parameter yang disediakan. name
adalah string yang memberi nama templat (sebagaimana ditetapkan oleh panggilan setTemplate
) dan parameters
adalah tabel dengan parameter template yang disimpan sebagai pasangan nama/nilai (direferensikan sebagai variabel dalam templat).
run([options])
: Menjalankan server menggunakan rute yang dikonfigurasi. Secara default server mendengarkan LocalHost dan port 8080. Nilai -nilai ini dapat diubah dengan mengatur nilai addr
dan port
di tabel options
.
Menjalankan contoh yang diperlukan termasuk pernyataan require
dalam file .init.lua
, yang memuat modul dengan masing -masing kode contoh, jadi untuk contoh showcase yang diimplementasikan di showcase.lua
, .init.lua
termasuk yang berikut:
-- this is the content of .init.lua
require " showcase "
-- this loads `showcase` module from `.lua/showcase.lua` file,
-- which also loads its `fullmoon` dependency from `.lua/fullmoon.lua`
Contoh showcase menunjukkan beberapa fitur fullmoon:
serveAsset
)serveRedirect
)File -file berikut perlu ditambahkan ke Redbean Executable/Archive:
.init.lua - membutuhkan "showcase" .lua/fullmoon.lua .lua/showcase.lua
Contoh TechEMPower mengimplementasikan berbagai jenis pengujian untuk tolok ukur kerangka kerja web menggunakan fullmoon dan database SQLite dalam memori.
Contoh ini menunjukkan beberapa fitur fullmoon/redbean:
File -file berikut perlu ditambahkan ke Redbean Executable/Archive:
.init.lua - membutuhkan "TechBench" .lua/fullmoon.lua .lua/techbench.lua
Contoh papan HTMX menunjukkan aplikasi sederhana yang menghasilkan fragmen HTML yang dikirimkan ke klien menggunakan pustaka HTMX.
Contoh ini menunjukkan beberapa fitur fullmoon/redbean:
File -file berikut perlu ditambahkan ke Redbean Executable/Archive:
.init.lua - membutuhkan "htmxboard" .lua/fullmoon.lua .lua/htmxboard.lua Asset/Styles.CSS TMPL/* - Semua file dari Contoh/Direktori HTMXBOARD/TMPL
Catatan 1: Karena semua data disimpan dalam memori, contoh ini dijalankan dalam mode uniprocess.
Catatan 2: Contoh ini mengambil htmx, hyperscript, dan perpustakaan yang dapat diurutkan dari sumber daya eksternal, tetapi perpustakaan ini juga dapat disimpan sebagai aset lokal, sehingga menyediakan paket distribusi portabel yang sepenuhnya mandiri.
Contoh HTMX SSE menunjukkan cara untuk menghasilkan peristiwa server-sent (SSE) yang dapat dialirkan ke klien (yang menunjukkan hasil menggunakan perpustakaan HTMX dan ekstensi SSE-nya).
Contoh ini menunjukkan beberapa fitur fullmoon/redbean:
streamContent
)File -file berikut perlu ditambahkan ke Redbean Executable/Archive:
.init.lua - membutuhkan "htmxsse" .lua/fullmoon.lua .lua/htmxsse.lua
Setiap aplikasi fullmoon mengikuti aliran dasar yang sama dengan lima komponen utama:
Mari kita lihat masing -masing komponen mulai dari perutean permintaan.
Fullmoon menangani setiap permintaan HTTP menggunakan proses yang sama:
false
atau nil
yang dikembalikan dari penangan tindakan (dan melanjutkan proses sebaliknya) Secara umum, definisi rute mengikat URL permintaan (dan satu set kondisi) untuk penangan tindakan (yang merupakan fungsi LUA biasa). Semua kondisi diperiksa dalam urutan acak untuk setiap URL yang cocok dengan definisi rute. Segera setelah kondisi apa pun gagal, pemrosesan rute dibatalkan dan rute berikutnya diperiksa dengan satu pengecualian : kondisi apa pun dapat mengatur nilai otherwise
, yang memicu respons dengan kode status yang ditentukan.
Jika tidak ada rute yang cocok dengan permintaan, maka pemrosesan 404 default dipicu, yang dapat disesuaikan dengan mendaftarkan Template 404 khusus ( fm.setTemplate("404", "My 404 page...")
).
Setiap rute mengambil jalan yang cocok dengan persis, jadi rute "/hello"
cocok dengan permintaan untuk /hello
dan tidak cocok /hell
, /hello-world
, atau /hello/world
. Rute di bawah ini merespons dengan "Halo, Dunia!" Untuk semua permintaan yang diarahkan di /hello
Path dan mengembalikan 404 untuk semua permintaan lainnya.
fm . setRoute ( " /hello " , function ( r ) return " Hello, World! " end )
Untuk mencocokkan jalur di mana /hello
hanyalah bagian dari itu, parameter opsional dan percikan dapat digunakan.
Selain rute tetap, jalur apa pun dapat menyertakan placeholder untuk parameter, yang diidentifikasi oleh A :
diikuti segera dengan nama parameter:
fm . setRoute ( " /hello/:name " ,
function ( r ) return " Hello, " .. ( r . params . name ) end )
Setiap parameter cocok dengan satu atau lebih karakter kecuali /
, jadi rute "/hello/:name"
cocok /hello/alice
, /hello/bob
, /hello/123
dan tidak cocok /hello/bob/and/alice
(karena Slash forward yang tidak cocok) atau /hello/
(karena panjang fragmen yang akan dicocokkan adalah nol).
Nama parameter hanya dapat menyertakan karakter alfanumerik dan _
.
Parameter dapat diakses menggunakan tabel permintaan dan tabel params
-nya, sehingga r.params.name
dapat digunakan untuk mendapatkan nilai parameter name
dari contoh sebelumnya.
Fragmen atau parameter rute yang ditentukan dapat dinyatakan sebagai opsional dengan membungkusnya menjadi tanda kurung:
fm . setRoute ( " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Dalam contoh di atas, keduanya /hello
dan /hello/Bob
diterima, tetapi tidak /hello/
, karena slash trailing adalah bagian dari fragmen opsional dan :name
masih mengharapkan satu atau lebih karakter.
Parameter opsional yang tidak tertandingi menjadi false
sebagai nilainya, jadi dalam kasus di atas "Halo, dunia!" dikembalikan untuk URL permintaan /hello
.
Lebih dari satu parameter opsional dapat ditentukan dan fragmen opsional dapat bersarang, jadi keduanya "/posts(/:pid/comments(/:cid))"
dan "/posts(/:pid)/comments(/:cid)"
adalah nilai rute yang valid.
Ada jenis parameter lain yang disebut Splat yang ditulis sebagai *
dan cocok dengan nol atau lebih karakter, termasuk slash maju ( /
). Splat juga disimpan dalam tabel params
di bawah nama splat
. Misalnya, rute "/download/*"
cocok /download/my/file.zip
file.zip dan splat mendapatkan nilai my/file.zip
. Jika beberapa percikan diperlukan di rute yang sama, maka percikan dapat diberi nama yang mirip dengan parameter lain: /download/*path/*fname.zip
(meskipun hasil yang sama dapat dicapai dengan menggunakan /download/*path/:fname.zip
, karena percikan pertama menangkap semua bagian jalur kecuali nama file).
Semua parameter (termasuk percikan) dapat muncul di bagian mana pun dari jalur dan dapat dikelilingi oleh teks lain, yang perlu dicocokkan dengan tepat. Ini berarti bahwa rute "/download/*/:name.:ext"
cocok /download/my/path/file.zip
dan params.name
mendapatkan file
, params.ext
mendapatkan zip
dan params.splat
mendapatkan nilai my/path
.
Alasan lain untuk menggunakan Splat adalah untuk memungkinkan beberapa rute dengan jalur yang sama terdaftar dalam sistem. Implementasi saat ini menimpa rute dengan nama yang sama dan untuk menghindari bahwa percikan bernama dapat digunakan untuk membuat jalur yang unik. Misalnya,
fm . setRoute ( " /*dosomething1 " , function ( r ) return " something 1 " end )
fm . setRoute ( " /*dosomething2 " , function ( r ) return " something 2 " end )
Ini dapat digunakan dalam situasi ketika ada serangkaian kondisi yang perlu diperiksa dalam penangan aksi dan sementara dimungkinkan untuk menggabungkan kedua rute menjadi satu, kadang -kadang lebih bersih untuk memisahkannya.
Nilai default untuk parameter adalah semua karakter (kecuali /
) panjang satu atau lebih. Untuk menentukan set karakter yang valid yang berbeda, itu dapat ditambahkan di akhir nama variabel; Misalnya, menggunakan :id[%d]
alih -alih :id
mengubah parameter yang hanya mencocokkan digit.
fm . setRoute ( " /hello(/:id[%d]) " ,
function ( r ) return " Hello, " .. ( r . params . id or " World! " ) end )
Kelas karakter LUA berikut didukung: %w
, %d
, %a
, %l
, %u
, dan %x
; Karakter tanda baca apa pun (termasuk %
dan ]
) juga dapat diloloskan dengan %
. Kelas negatif (ditulis dalam LUA as %W
) tidak didukung , tetapi sintaks yang tidak disetel didukung, jadi [^%d]
cocok dengan parameter yang tidak termasuk digit apa pun.
Perhatikan bahwa jumlah pengulangan tidak dapat diubah (jadi :id[%d]*
bukan cara yang valid untuk menerima angka nol-atau-lebih), karena hanya set yang diizinkan dan nilainya masih menerima satu atau lebih karakter. Jika lebih banyak fleksibilitas dalam menggambarkan format yang dapat diterima diperlukan, maka validator khusus dapat digunakan untuk memperpanjang logika yang cocok.
Parameter kueri dan bentuk dapat diakses dengan cara yang sama seperti parameter jalur menggunakan tabel params
di tabel request
yang diteruskan ke setiap penangan tindakan. Perhatikan bahwa jika ada konflik antara parameter dan nama kueri/bentuk, maka nama parameter diutamakan .
Ada satu kasus khusus yang dapat mengakibatkan tabel yang dikembalikan alih -alih nilai string: Jika nama parameter kueri/formulir berakhir di []
, maka semua hasil yang cocok (satu atau lebih) dikembalikan sebagai tabel. Misalnya, untuk string kueri a[]=10&a[]&a[]=12&a[]=
nilai params["a[]"]
adalah {10, false, 12, ""}
.
Karena menulis nama parameter ini mungkin memerlukan beberapa tanda kurung, params.a
dapat digunakan sebagai pintasan untuk params["a[]"]
dengan kedua formulir mengembalikan tabel yang sama.
Parameter multipart juga diproses ketika diminta dan dapat diakses dengan cara yang sama seperti parameter lainnya menggunakan tabel params
. Misalnya, parameter dengan nama simple
dan more
dapat diambil dari pesan dengan tipe konten multipart/form-data
menggunakan params.simple
dan params.more
.
Karena beberapa konten multipart dapat menyertakan header dan parameter tambahan dalam header tersebut, mereka dapat diakses dengan bidang multipart
dari tabel params
:
fm . setRoute ({ " /hello " , simple = " value " }, function ( r )
return " Show " .. r . params . simple .. " " .. r . params . multipart . more . data )
end )
Tabel multipart
mencakup semua bagian dari pesan multipart (sehingga dapat diulangi menggunakan ipairs
), tetapi juga memungkinkan akses menggunakan nama parameter ( params.multipart.more
). Masing -masing elemen juga merupakan tabel yang mencakup bidang -bidang berikut:
nil
jika tidak.nil
jika tidak. Pemrosesan multipart ini mengkonsumsi sub-jenis multipart dan menangani pesan multipart rekursif. Ini juga menyisipkan bagian dengan nilai Content-ID
yang mencocokkan parameter start
ke posisi pertama.
Terlepas dari semua contoh sebelumnya yang menunjukkan satu rute, jarang terjadi dalam aplikasi nyata; Ketika beberapa rute hadir, mereka selalu dievaluasi dalam urutan di mana mereka terdaftar .
Satu panggilan setRoute
juga dapat mengatur beberapa rute ketika mereka memiliki set kondisi yang sama dan berbagi penangan tindakan yang sama:
fm . setRoute ({ " /route1 " , " /route2 " }, handler )
Ini setara dengan dua panggilan yang mengatur setiap rute secara individual:
fm . setRoute ( " /route1 " , handler )
fm . setRoute ( " /route2 " , handler )
Mengingat bahwa rute dievaluasi dalam urutan di mana mereka ditetapkan, rute selektif yang lebih perlu ditetapkan terlebih dahulu, jika tidak mereka mungkin tidak mendapatkan kesempatan untuk dievaluasi:
fm . setRoute ( " /user/bob " , handlerBob )
fm . setRoute ( " /user/:name " , handlerName )
Jika rute diatur dalam urutan yang berlawanan, /user/bob
mungkin tidak akan pernah diperiksa selama "/user/:name"
Action Handler mengembalikan beberapa hasil yang tidak false
.
Seperti yang dijelaskan sebelumnya, jika tidak ada rute yang cocok, respons dengan kode status 404 dikembalikan. Mungkin ada kasus ketika ini tidak diinginkan; Misalnya, ketika aplikasi menyertakan skrip LUA untuk menangani permintaan yang tidak secara eksplisit terdaftar sebagai rute. Dalam kasus-kasus tersebut, rute tangkapan-semua dapat ditambahkan yang mengimplementasikan pemrosesan redbean default (nama parameter Splat hanya digunakan untuk menghilangkan rute ini terhadap rute /*
lain yang dapat digunakan di tempat lain):
fm . setRoute ( " /*catchall " , fm . servePath )
Setiap rute dapat disediakan dengan nama opsional, yang berguna dalam merujuk rute itu ketika URL -nya perlu dihasilkan berdasarkan nilai parameter tertentu. Fungsi makePath
yang disediakan menerima nama rute atau URL rute itu sendiri serta tabel parameter dan mengembalikan jalur dengan placeholder parameter populasi:
fm . setRoute ( " /user/:name " , handlerName )
fm . setRoute ({ " /post/:id " , routeName = " post " }, handlerPost )
fm . makePath ( " /user/:name " , { name = " Bob " }) -- > /user/Bob
fm . makePath ( " /post/:id " , { id = 123 }) -- > /post/123
fm . makePath ( " post " , { id = 123 }) -- > /post/123, same as the previous one
Jika dua rute menggunakan nama yang sama, maka nama tersebut dikaitkan dengan nama yang terdaftar terakhir, tetapi kedua rute masih ada.
Nama rute juga dapat digunakan dengan rute eksternal/statis yang hanya digunakan untuk generasi URL.
Jika rute hanya digunakan untuk pembuatan jalur, maka ia bahkan tidak perlu memiliki rute handler:
fm . setRoute ({ " https://youtu.be/:videoid " , routeName = " youtube " })
fm . makePath ( " youtube " , { videoid = " abc " }) -- > https://youtu.be/abc
Rute tanpa Action Handler dilewati selama proses pencocokan rute.
Rute internal memungkinkan pengalihan satu set URL ke yang berbeda. URL target dapat menunjuk ke sumber daya statis atau skrip .lua
. Misalnya, jika permintaan untuk satu lokasi perlu dialihkan ke yang lain, konfigurasi berikut mengarahkan permintaan untuk sumber daya apa pun di bawah /blog/
URL ke yang di bawah /new-blog/
url selama sumber daya target ada:
fm . setRoute ( " /blog/* " , " /new-blog/* " )
Rute ini menerima permintaan untuk /blog/post1
dan melayani /new-blog/post1
sebagai tanggapannya, selama /new-blog/post1
ada. Jika aset tidak ada, maka rute berikutnya diperiksa. Demikian pula, menggunakan fm.setRoute("/static/*", "/*")
menyebabkan permintaan untuk /static/help.txt
disajikan sumber daya /help.txt
.
Kedua URL dapat mencakup parameter yang diisi jika diselesaikan:
fm . setRoute ( " /blog/:file " , " /new-blog/:file.html " ) -- <<-- serve "nice" URLs
fm . setRoute ( " /new-blog/:file.html " , fm . serveAsset ) -- <<-- serve original URLs
Contoh ini menyelesaikan URL "bagus" yang melayani versi "html" mereka. Perhatikan bahwa ini tidak memicu pengalihan sisi klien dengan mengembalikan kode status 3xx
, tetapi sebaliknya menangani rute ulang secara internal. Perhatikan juga bahwa aturan kedua diperlukan untuk melayani URL "asli", karena mereka tidak ditangani oleh aturan pertama, karena jika permintaan itu untuk /blog/mylink.html
, maka URL yang dialihkan adalah /new-blog/mylink.html.html
, yang tidak mungkin ada, sehingga rute dilewati dan yang berikutnya diperiksa. Jika penanganan pemisah jalur juga diperlukan, maka *path
dapat digunakan sebagai pengganti :file
, karena *
memungkinkan pemisah jalur.
Jika aplikasi perlu menjalankan fungsi yang berbeda tergantung pada nilai spesifik atribut permintaan (misalnya, suatu metode), pustaka ini menyediakan dua opsi utama: (1) Periksa nilai atribut penangan tindakan (misalnya, menggunakan request.method == "GET"
Periksa) dan (2) Tambahkan kondisi yang memfilter permintaan sedemikian rupa sehingga hanya permintaan menggunakan nilai atribut yang ditentukan mencapai penangan tindakan. Bagian ini menjelaskan opsi kedua secara lebih rinci.
Setiap rute yang terdaftar secara default menanggapi semua metode HTTP (Get, Put, Post, dll.), Tetapi dimungkinkan untuk mengonfigurasi setiap rute untuk hanya menanggapi metode HTTP tertentu:
fm . setRoute ( fm . GET " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Dalam hal ini, sintaks fm.GET"/hello(/:name)"
mengkonfigurasi rute untuk hanya menerima permintaan GET
. Sintaks ini setara dengan melewati tabel dengan rute dan kondisi penyaringan tambahan:
fm . setRoute ({ " /hello(/:name) " , method = " GET " },
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Jika lebih dari satu metode perlu ditentukan, maka tabel dengan daftar metode dapat diteruskan alih -alih satu nilai string:
fm . setRoute ({ " /hello(/:name) " , method = { " GET " , " POST " }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Setiap rute yang memungkinkan permintaan GET
juga (secara implisit) memungkinkan permintaan HEAD
dan permintaan itu ditangani dengan mengembalikan semua header tanpa mengirim tubuh itu sendiri. Jika karena alasan tertentu penanganan implisit ini tidak diinginkan, maka menambahkan HEAD = false
ke tabel metode menonaktifkannya (seperti dalam method = {"GET", "POST", HEAD = false}
).
Perhatikan bahwa permintaan dengan metode non-pencocokan tidak ditolak, melainkan jatuh untuk diperiksa oleh rute lain dan memicu kode status 404 yang dikembalikan jika tidak dicocokkan (dengan satu pengecualian).
Selain method
, kondisi lain dapat diterapkan menggunakan host
, clientAddr
, serverAddr
, scheme
, header permintaan, dan parameter. Misalnya, menentukan name = "Bob"
sebagai salah satu kondisi memastikan nilai parameter name
menjadi "bob" untuk penangan tindakan dipanggil.
Header permintaan apa pun dapat diperiksa menggunakan nama header sebagai kunci, sehingga ContentType = "multipart/form-data"
dipenuhi jika nilai header Content-Type
adalah multipart/form-data
. Perhatikan bahwa nilai header dapat mencakup elemen lain (batas atau charset sebagai bagian dari nilai Content-Type
) dan hanya tipe media aktual yang dibandingkan.
Karena nama untuk header, parameter dan properti dapat tumpang tindih, mereka diperiksa dalam urutan berikut:
ContentType
,method
, port
, host
, dll.), dan Host
Header juga diperiksa terlebih dahulu (meskipun merupakan satu kata), jadi merujuk filter Host
berdasarkan Host
header, sementara merujuk filter host
berdasarkan host
properti.
Nilai string bukan satu -satunya nilai yang dapat digunakan dalam rute bersyarat. Jika lebih dari satu nilai dapat diterima, melewati tabel memungkinkan untuk memberikan daftar nilai yang dapat diterima. Misalnya, jika Bob
dan Alice
adalah nilai yang dapat diterima, maka name = {Bob = true, Alice = true}
mengungkapkan ini sebagai kondisi.
Dua nilai khusus yang dilewati dalam tabel memungkinkan untuk menerapkan regex atau validasi pola :
regex
: menerima string yang memiliki ekspresi reguler. Misalnya, name = {regex = "^(Bob|Alice)$"}
memiliki hasil yang sama dengan pemeriksaan hash yang ditunjukkan sebelumnya di bagian inipattern
: menerima string dengan ekspresi pola LUA. Misalnya, name = {pattern = "^%u%l+$"}
menerima nilai yang dimulai dengan karakter huruf besar diikuti oleh satu atau lebih karakter kecil. Kedua cek ini dapat dikombinasikan dengan pemeriksaan keberadaan tabel: name = {Bob = true, regex = "^Alice$"}
menerima nilai Bob
dan Alice
. Jika pemeriksaan eksistensi tabel pertama gagal, maka hasil dari regex
atau ekspresi pattern
dikembalikan.
Jenis terakhir dari validator khusus adalah fungsi. Fungsi yang disediakan menerima nilai untuk divalidasi dan hasilnya dievaluasi sebagai false
atau true
. Misalnya, melewati id = tonumber
memastikan bahwa nilai id
adalah angka. Sebagai contoh lain, clientAddr = fm.isLoopbackIp
memastikan bahwa alamat klien adalah alamat IP loopback.
fm . setRoute ({ " /local-only " , clientAddr = fm . isLoopbackIp },
function ( r ) return " Local content " end )
Karena fungsi validator dapat dihasilkan secara dinamis, ini berfungsi juga:
local function isLessThan ( n )
return function ( l ) return tonumber ( l ) < n end
end
fm . setRoute ( fm . POST { " /upload " , ContentLength = isLessThan ( 100000 )},
function ( r ) ... handle the upload ... end )
Penting untuk diingat bahwa fungsi validator benar -benar mengembalikan fungsi yang dipanggil selama permintaan untuk menerapkan cek. Dalam contoh sebelumnya, fungsi yang dikembalikan menerima nilai header dan membandingkannya dengan batas yang dilewati selama pembuatannya.
Dalam beberapa kasus, gagal memenuhi kondisi adalah alasan yang cukup untuk mengembalikan beberapa respons kembali ke klien tanpa memeriksa rute lain. Dalam kasus seperti ini, mengatur nilai otherwise
ke angka atau fungsi mengembalikan respons dengan status yang ditentukan atau hasil fungsi:
local function isLessThan ( n )
return function ( l ) return tonumber ( l ) < n end
end
fm . setRoute ( fm . POST { " /upload " ,
ContentLength = isLessThan ( 100000 ), otherwise = 413
}, function ( r ) ... handle the upload ... end )
Dalam contoh ini mesin routing cocok dengan rute dan kemudian memvalidasi dua kondisi yang membandingkan nilai metode dengan POST
dan nilai header Content-Length
dengan hasil fungsi isLessThan
. Jika salah satu kondisi tidak cocok, kode status yang ditentukan oleh nilai otherwise
dikembalikan dengan sisa respons.
Jika kondisi otherwise
hanya perlu diterapkan pada pemeriksaan ContentLength
, maka nilai otherwise
bersama dengan fungsi validator dapat dipindahkan ke tabel yang terkait dengan ContentLength
Check:
fm . setRoute ( fm . POST { " /upload " ,
ContentLength = { isLessThan ( 100000 ), otherwise = 413 }
}, function ( r ) ... handle the upload ... end )
Perbedaan antara dua contoh terakhir adalah bahwa dalam contoh ini hanya kegagalan cek ContentLength
memicu respons 413 (dan semua metode lain jatuh ke rute lain), sedangkan pada method
sebelumnya dan kegagalan cek ContentLength
memicu respons 413 yang sama.
Perhatikan bahwa ketika nilai yang diperiksa adalah nil
, cek terhadap tabel dianggap valid dan rute diterima. Misalnya, cek untuk parameter opsional yang dibuat terhadap string ( name = "Bo"
) gagal jika nilai params.name
adalah nil
, tetapi lulus jika cek yang sama dibuat terhadap tabel ( name = {Bo=true, Mo=true}
), termasuk cek regex/pola. Jika ini tidak diinginkan, maka fungsi validator khusus dapat secara eksplisit memeriksa nilai yang diharapkan.
Pertimbangkan contoh berikut:
fm . setRoute ({ " /hello(/:name) " ,
method = { " GET " , " POST " , otherwise = 405 }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Dalam hal ini, jika titik akhir ini diakses dengan metode PUT
, maka alih -alih memeriksa rute lain (karena kondisi method
tidak terpenuhi), kode status 405 dikembalikan, seperti yang dikonfigurasi dengan nilai yang ditentukan otherwise
. Seperti yang didokumentasikan di tempat lain, rute ini juga menerima permintaan HEAD
(bahkan ketika tidak terdaftar), karena permintaan GET
diterima.
Ketika kode status 405 (metode buruk) dikembalikan dan header Allow
tidak diatur, itu diatur ke daftar metode yang diizinkan oleh rute. Dalam kasus di atas diatur untuk GET, POST, HEAD, OPTIONS
, karena itu adalah metode yang diizinkan oleh konfigurasi ini. Jika nilai otherwise
adalah fungsi (bukan angka), maka mengembalikan hasil yang tepat dan mengatur header Allow
adalah tanggung jawab fungsi ini.
Nilai otherwise
juga dapat diatur ke fungsi, yang memberikan lebih banyak fleksibilitas daripada hanya mengatur kode status. Misalnya, pengaturan otherwise = fm.serveResponse(413, "Payload Too Large")
memicu respons dengan kode status dan pesan yang ditentukan.
Penanganan Formulir Validasi Seringkali memerlukan menentukan serangkaian kondisi untuk parameter yang sama dan pesan kesalahan khusus yang mungkin perlu dikembalikan ketika kondisinya tidak terpenuhi dan ini disediakan oleh validator khusus yang dikembalikan oleh fungsi makeValidator
:
local validator = fm . makeValidator {
{ " name " , minlen = 5 , maxlen = 64 , msg = " Invalid %s format " },
{ " password " , minlen = 5 , maxlen = 128 , msg = " Invalid %s format " },
}
fm . setRoute ( fm . POST { " /signin " , _ = validator }, function ( r )
-- do something useful with name and password
return fm . serveRedirect ( 307 , " / " )
end )
Dalam contoh ini, validator dikonfigurasi untuk memeriksa dua parameter - "nama" dan "kata sandi" - untuk panjang min dan maksimal dan mengembalikan pesan ketika salah satu parameter gagal cek.
Karena pemeriksaan yang gagal menyebabkan rute dilewati, memberikan nilai otherwise
memungkinkan kesalahan dikembalikan sebagai bagian dari respons:
local validator = fm . makeValidator {
{ " name " , minlen = 5 , maxlen = 64 , msg = " Invalid %s format " },
{ " password " , minlen = 5 , maxlen = 128 , msg = " Invalid %s format " },
otherwise = function ( error )
return fm . serveContent ( " signin " , { error = error })
end ,
}
Dalam hal ini penangan otherwise
menerima pesan kesalahan (atau tabel dengan pesan jika diminta dengan meneruskan all
opsi yang dibahas di bawah) yang kemudian dapat disediakan sebagai parameter templat dan dikembalikan ke klien.
Opsi lain adalah memanggil fungsi validator secara langsung di penangan tindakan dan mengembalikan hasilnya:
local validator = fm . makeValidator {
{ " name " , minlen = 5 , maxlen = 64 , msg = " Invalid %s format " },
{ " password " , minlen = 5 , maxlen = 128 , msg = " Invalid %s format " },
}
fm . setRoute ( fm . POST { " /signin " }, function ( r )
local valid , error = validator ( r . params )
if valid then
return fm . serveRedirect ( " / " ) -- status code is optional
else
return fm . serveContent ( " signin " , { error = error })
end
end )
Dalam contoh ini validator dipanggil secara langsung dan dilewatkan tabel ( r.params
) dengan semua nilai parameter untuk memungkinkan fungsi validator untuk memeriksa nilai terhadap aturan yang ditentukan.
Fungsi validator kemudian mengembalikan true
ke sinyal keberhasilan atau nil, error
untuk memberi sinyal kegagalan untuk memeriksa salah satu aturan. Ini memungkinkan panggilan validator untuk dibungkus menjadi assert
jika skrip perlu mengembalikan kesalahan segera:
assert ( validator ( r . params )) -- throw an error if validation fails
return fm . serveRedirect ( 307 , " / " ) -- return redirect in other cases
Pemeriksaan validator berikut tersedia:
minlen
: (Integer) Memeriksa panjang minimal string.maxlen
: (Integer) Memeriksa panjang maksimal string.test
: (Fungsi) memanggil fungsi yang dilewatkan satu parameter dan diharapkan untuk mengembalikan true
atau nil | false [, error]
.oneof
: ( value | { table of values to be compared against }
) memeriksa apakah parameter cocok dengan salah satu nilai yang disediakan.pattern
: (String) Memeriksa apakah parameter cocok dengan ekspresi pola LUA.Selain cek, aturan mungkin termasuk opsi:
optional
: (bool) membuat parameter opsional saat ini nil
. Semua parameter diperlukan secara default, sehingga opsi ini memungkinkan aturan untuk dilewati ketika parameter tidak disediakan. Semua aturan masih diterapkan jika parameter bukan nol.msg
: (String) Menambahkan pesan pelanggan untuk ini jika salah satu ceknya gagal, yang menimpa pesan dari cek individual. The message may include a placeholder ( %s
), which is going to be replaced by a parameter name.The validator itself also accepts several options that modify how the generated errors are returned or handled:
otherwise
: (function) sets an error handler that is called when one of the checks fails. The function receives the error(s) triggered by the checks.all
: (bool) configures the validator to return all errors instead of just the first one. By default only one (first) error is returned as a string, so if all errors are requested, they are returned as a table with each error being a separate item.key
: (bool) configures the validator to return error(s) as values in a hash table (instead of element) where the keys are parameter names. This is useful to pass the table with errors to a template that can then display errors.name
and errors.password
error messages next to their input fields. An action handler receives all incoming HTTP requests filtered for a particular route. Each of the examples shown so far includes an action handler, which is passed as a second parameter to the setRoute
method.
Multiple action handlers can be executed in the course of handling one request and as soon as one handler returns a result that is evaluated as a non- false
value, the route handling process ends. Returning false
or nil
from an action handler continues the processing, which allows implementing some common processing that applies to multiple routes (similar to what is done using "before" filters in other frameworks):
local uroute = " /user/:id "
fm . setRoute ({ uroute .. " /* " , method = { " GET " , " POST " , otherwise = 405 }},
function ( r )
-- retrieve user information based on r.params.id
-- and store in r.user (as one of the options);
-- return error if user is not found
return false -- continue handling
end )
fm . setRoute ( fm . GET ( uroute .. " /view " ), function ( r ) ... end )
fm . setRoute ( fm . GET ( uroute .. " /edit " ), function ( r ) ... end )
fm . setRoute ( fm . POST ( uroute .. " /edit " ), function ( r ) ... end )
In this example, the first route can generate three outcomes:
method
check) is not matched, then the 405 status code is returned.false
, which continues processing with other routes, or fails to retrieve the user and returns an error.In general, an action handler can return any of the following values:
true
: this stops any further processing, sets the headers that have been specified so far, and returns the generated or set response body.false
or nil
: this stops the processing of the current route and proceeds to the next one.Content-Type
is set based on the body content (using a primitive heuristic) if not set explicitly.serve*
methods): this executes the requested method and returns an empty string or true
to signal the end of the processing.true
is returned (and a warning is logged). Normally any processing that results in a Lua error is returned to the client as a server error response (with the 500 status code). To assist with local debugging, the error message includes a stack trace, but only if the request is sent from a loopback or private IP (or if redbean is launched with the -E
command line option).
It may be desirable to return a specific response through multiple layers of function calls, in which case the error may be triggered with a function value instead of a string value. For example, executing error(fm.serve404)
results in returning the 404 status code, which is similar to using return fm.serve404
, but can be executed in a function called from an action handler (and only from inside an action handler).
Here is a more complex example that returns the 404 status code if no record is fetched (assuming there is a table test
with a field id
):
local function AnyOr404(res, err)
if not res then error(err) end
-- serve 404 when no record is returned
if res == db.NONE then error(fm.serve404) end
return res, err
end
fm.setRoute("/", function(r)
local row = AnyOr404(dbm:fetchOne("SELECT id FROM test"))
return row.id
end)
This example uses the serve404
function, but any other serve* method can also be used.
Each action handler accepts a request table that includes the following attributes:
method
: request HTTP method (GET, POST, and others).host
: request host (if provided) or the bind address.serverAddr
: address to which listening server socket is bound.remoteAddr
: client ip4 address encoded as a number. This takes into consideration reverse proxy scenarios. Use formatIp
function to convert to a string representing the address.scheme
: request URL scheme (if any).path
: request URL path that is guaranteed to begin with /
.authority
: request URL with scheme, host, and port present.url
: request URL as an ASCII string with illegal characters percent encoded.body
: request message body (if present) or an empty string.date
: request date as a Unix timestamp.time
: current time as a Unix timestamp with 0.0001s precision.The request table also has several utility functions, as well as headers, cookies, and session tables that allow retrieving request headers, cookies, and session and setting of headers and cookies that are included with the response.
The same request table is given as a parameter to all (matched) action handlers, so it can be used as a mechanism to pass values between those action handlers, as any value assigned as a field in one handler is available in all other action handlers .
The headers
table provides access to the request headers. For example, r.headers["Content-Type"]
returns the value of the Content-Type
header. This form of header access is case-insensitive. A shorter form is also available ( r.headers.ContentType
), but only for registered headers and is case-sensitive with the capitalization preserved.
The request headers can also be set using the same syntax. For example, r.headers.MyHeader = "value"
sets MyHeader: value
response header. As the headers are set at the end of the action handler processing, headers set earlier can also be removed by assigning a nil
value.
Repeatable headers can also be assigned with values separated by commas: r.headers.Allow = "GET, POST"
.
The cookies
table provides access to the request cookies. For example, r.cookies.token
returns the value of the token
cookie.
The cookies can also be set using the same syntax. For example, r.cookies.token = "new value"
sets token
cookie to new value
. If the cookie needs to have its attributes set as well, then the value and the attributes need to be passed as a table: r.cookies.token = {"new value", secure = true, httponly = true}
.
The following cookie attributes are supported:
expires
: sets the maximum lifetime of the cookie as an HTTP-date timestamp. Can be specified as a date in the RFC1123 (string) format or as a UNIX timestamp (number of seconds).maxage
: sets number of seconds until the cookie expires. A zero or negative number expires the cookie immediately. If both expires
and maxage
are set, maxage
has precedence.domain
: sets the host to which the cookie is going to be sent.path
: sets the path that must be present in the request URL, or the client is not going to send the Cookie header.secure
: (bool) requests the cookie to be only send to the server when a request is made with the https: scheme.httponly
: (bool) forbids JavaScript from accessing the cookie.samesite
: ( Strict
, Lax
, or None
) controls whether a cookie is sent with cross-origin requests, providing some protection against cross-site request forgery attacks. Note that httponly
and samesite="Strict"
are set by default; a different set of defaults can be provided using cookieOptions
passed to the run method. Any attributes set with a table overwrite the default , so if Secure
needs to be enabled, make sure to also pass httponly
and samesite
options.
To delete a cookie, set its value to false
: for example, r.cookies.token = false
deletes the value of the token
cookie.
The session
table provides access to the session table that can be used to set or retrieve session values. For example, r.session.counter
returns the counter
value set previously. The session values can also be set using the same syntax. For example, r.session.counter = 2
sets the counter
value to 2
.
The session allows storing of nested values and other Lua values. If the session needs to be removed, it can be set to an empty table or a nil
value. Each session is signed with an application secret, which is assigned a random string by default and can be changed by setting session options.
The following functions are available as both request functions (as fields in the request table) and as library functions:
makePath(route[, parameters])
: creates a path from either a route name or a path string by populating its parameters using values from the parameters table (when provided). The path doesn't need to be just a path component of a URL and can be a full URL as well. Optional parts are removed if they include parameters that are not provided.makeUrl([url,] options)
: creates a URL using the provided value and a set of URL parameters provided in the options
table: scheme, user, pass, host, port, path, and fragment. The url
parameter is optional; the current request URL is used if url
is not specified. Any of the options can be provided or removed (using false
as the value). For example, makeUrl({scheme="https"})
sets the scheme for the current URL to https
.escapeHtml(string)
: escapes HTML entities ( &><"'
) by replacing them with their HTML entity counterparts ( &><"'
).escapePath(path)
: applies URL encoding ( %XX
) escaping path unsafe characters (anything other than -.~_@:!$&'()*+,;=0-9A-Za-z/
).formatHttpDateTime(seconds)
: converts UNIX timestamp (in seconds) to an RFC1123 string ( Mon, 21 Feb 2022 15:37:13 GMT
).Templates provide a simple and convenient way to return a predefined and parametrized content instead of generating it piece by piece.
The included template engine supports mixing an arbitrary text with Lua statements/expressions wrapped into {% %}
tags. All the code in templates uses a regular Lua syntax, so there is no new syntax to learn. There are three ways to include some Lua code:
{% statement %}
: used for Lua statements . For example, {% if true then %}Hello{% end %}
renders Hello
.{%& expression %}
: used for Lua expressions rendered as HTML-safe text. For example, {%& '2 & 2' %}
renders 2 & 2
.{%= expression %}
: used for Lua expressions rendered as-is (without escaping). For example, {%= 2 + 2 %}
renders 4
. Be careful, as HTML is not escaped with {%= }
, this should be used carefully due to the potential for XSS attacks.The template engine provides two main functions to use with templates:
setTemplate(name, text[, parameters])
: registers a template with the provided name and text (and uses parameters
as its default parameters). There are special cases where name
or text
parameters may not be strings, with some of those cases covered in the Loading templates section. parameters
is a table with template parameters as name/value pairs (referenced as variables in the template).render(name, parameters)
: renders a registered template using the parameters
table to set values in the template (with key/value in the table assigned to name/value in the template).There is only one template with a given name, so registering a template with an existing name replaces this previously registered template. This is probably rarely needed, but can be used to overwrite default templates.
Here is an example that renders Hello, World!
to the output buffer:
fm . setTemplate ( " hello " , " Hello, {%& title %}! " )
fm . render ( " hello " , { title = " World " })
Rendering statements using the expression syntax or expressions using the statement syntax is a syntax error that is reported when the template is registered. Function calls can be used with either syntax.
Any template error (syntax or run-time) includes a template name and a line number within the template. For example, calling fm.setTemplate("hello", "Hello, {%& if title then end %}!")
results in throwing hello:1: unexpected symbol near 'if'
error (as it inserts a Lua statement using the expression syntax).
Templates can also be loaded from a file or a directory using the same setTemplate
function, which is described later in the Loading templates section.
There are several aspects worth noting, as they may differ from how templates are processed in other frameworks:
json
and sse
templates are implemented using this approach.Each template accepts parameters that then can be used in its rendering logic. Parameters can be passed in two ways: (1) when the template is registered and (2) when the template is rendered. Passing parameters during registration allows to set default values that are used if no parameter is provided during rendering. Misalnya,
fm . setTemplate ( " hello " , " Hello, {%& title %}! " , { title = " World " })
fm . render ( " hello " ) -- renders `Hello, World!`
fm . render ( " hello " , { title = " All " }) -- renders `Hello, All!`
nil
or false
values are rendered as empty strings without throwing any error, but any operation on a nil
value is likely to result in a Lua error. For example, doing {%& title .. '!' %}
(without title
set) results in attempt to concatenate a nil value (global 'title')
error.
There is no constraint on what values can be passed to a template, so any Lua value can be passed and then used inside a template.
In addition to the values that can be passed to templates, there are two special tables that provide access to cross-template values :
vars
: provides access to values registered with setTemplateVar
, andblock
: provides access to template fragments that can be overwritten by other templates. Any value registered with setTemplateVar
becomes accessible from any template through the vars
table. In the following example, the vars.title
value is set by the earlier setTemplateVar('title', 'World')
call:
fm . setTemplateVar ( ' title ' , ' World ' )
fm . setTemplate ( " hello " , " Hello, {%& vars.title %}! " )
fm . render ( " hello " ) -- renders `Hello, World!`
While undefined values are rendered as empty string by default (which may be convenient in most cases), there are still situations when it is preferrable to not allow undefined values to be silently handled. In this a special template variable ( if-nil
) can be set to handle those cases to throw an error or to log a message. For example, the following code throws an error, as the missing
value is undefined, which triggers if-nil
handler:
fm . setTemplateVar ( ' if-nil ' , function () error " missing value " end )
fm . setTemplate ( " hello " , " Hello, {%& vars.missing %}! " )
fm . render ( " hello " ) -- throws "missing value" error
Templates can be also rendered from other templates by using the render
function, which is available in every template:
fm . setTemplate ( " hello " , " Hello, {%& title %}! " )
fm . setTemplate ( " header " , " <h1>{% render('hello', {title = title}) %}</h1> " )
---- -----------------------------└──────────────────────────────┘----------
fm . render ( " header " , { title = ' World ' }) -- renders `<h1>Hello, World!</h1>`
There are no limits on how templates can be rendered from other templates, but no checks for loops are made either, so having circular references in template rendering (when a template A renders a template B, which in turn renders A again) is going to cause a Lua error.
It's worth noting that the render
function doesn't return the value of the template it renders, but instead puts it directly into the output buffer.
This ability to render templates from other templates allows producing layouts of any complexity. There are two ways to go about it:
To dynamically choose the template to use at render time, the template name itself can be passed as a parameter:
fm . setTemplate ( " hello " , " Hello, {%& title %}! " )
fm . setTemplate ( " bye " , " Bye, {%& title %}! " )
fm . setTemplate ( " header " , " <h1>{% render(content, {title = title}) %}</h1> " )
fm . render ( " header " , { title = ' World ' , content = ' hello ' })
This example renders either <h1>Hello, World!</h1>
or <h1>Bye, World!</h1>
depending on the value of the content
parameter.
Using blocks allows defining template fragments that can (optionally) be overwritten from other templates (usually called "child" or "inherited" templates). The following example demonstrates this approach:
fm . setTemplate ( " header " , [[
<h1>
{% function block.greet() %} -- define a (default) block
Hi
{% end %}
{% block.greet() %}, -- render the block
{%& title %}!
</h1>
]] )
fm . setTemplate ( " hello " , [[
{% function block.greet() %} -- overwrite the `header` block (if any)
Hello
{% end %}
{% render('header', {title=title}) %}!
]] )
fm . setTemplate ( " bye " , [[
{% function block.greet() %} -- overwrite the `header` block (if any)
Bye
{% end %}
{% render('header', {title=title}) %}!
]] )
-- normally only one of the three `render` calls is needed,
-- so all three are shown for illustrative purposes only
fm . render ( " hello " , { title = ' World ' }) -- renders <h1>Hello, World!</h1>
fm . render ( " bye " , { title = ' World ' }) -- renders `<h1>Bye, World!</h1>`
fm . render ( " header " , { title = ' World ' }) -- renders `<h1>Hi, World!</h1>`
In this example the header
template becomes the "layout" and defines the greet
block with Hi
as its content. The block is defined as a function in the block
table with the content it needs to produce. It's followed by a call to the block.greet
function to include its content in the template.
This is important to emphasize, as in addition to defining a block, it also needs to be called from the base/layout template at the point where it is expected to be rendered.
The hello
template also defines block.greet
function with a different content and then renders the header
template. When the header
template is rendered, it uses the content of the block.greet
function as defined in the hello
template. In this way, the child template "redefines" the greet
block with its own content, inserting it into the appropriate place into the parent template.
It works the same way for the bye
and header
templates. There is nothing special about these "block" functions other than the fact that they are defined in the block
table.
This concepts is useful for template composition at any depth. For example, let's define a modal template with a header and a footer with action buttons:
fm . setTemplate ( " modal " , [[
<div class="modal">
<div class="modal-title">
{% function block.modal_title() %}
Details
{% end %}
{% block.modal_title() %}
</div>
<div class="modal-content">
{% block.modal_content() %}
</div>
<div class="modal-actions">
{% function block.modal_actions() %}
<button>Cancel</button>
<button>Save</button>
{% end %}
{% block.modal_actions() %}
</div>
</div>
]] )
Now, in a template that renders the modal, the blocks can be overwritten to customize the content:
fm . setTemplate ( " page " , [[
{% function block.modal_title() %}
Insert photo
{% end %}
{% function block.modal_content() %}
<div class="photo-dropzone">Upload photo here</div>
{% end %}
{% render('modal') %}
]] )
This enables easily building composable layouts and components, such as headers and footers, cards, modals, or anything else that requires the ability to dynamically customize sections in other templates.
Here is an example to illustrate how nested blocks work together:
-- base/layout template
{ % function block . greet () % } -- 1. defines default "greet" block
Hi
{ % end % }
{ % block . greet () % } -- 2. calls "greet" block
-- child template
{ % function block . greet () % } -- 3. defines "greet" block
Hello
{ % end % }
{ % render ( ' base ' ) % } -- 4. renders "base" template
-- grandchild template
{ % function block . greet () % } -- 5. defines "greet" block
Bye
{ % end % }
{ % render ( ' child ' ) % } -- 6. renders "child" template
In this example the "child" template "extends" the base template and any block.greet
content defined in the child template is rendered inside the "base" template (when and where the block.greet()
function is called). The default block.greet
block doesn't need to be defined in the base template, but when it is present (step 1), it sets the content to be rendered (step 2) if the block is not overwritten in a child template and needs to be defined before block.greet
function is called.
Similarly, block.greet
in the child template needs to be defined before (step 3) the base template is rendered (step 4) to have a desired effect.
If one of the templates in the current render tree doesn't define the block, then the later defined block is going to be used. For example, if the grandchild template doesn't define the block in step 5, then the greet
block from the child template is going to be used when the grandchild template is rendered.
If none of the block.greet
functions is defined, then block.greet()
fails (in the base
template). To make the block optional , just check the function before calling. For example, block.greet and block.greet()
.
In those cases where the "overwritten" block may still need to be rendered, it's possible to reference that block directly from the template that defines it, as shown in the following example:
fm . setTemplate ( " header " , [[
<h1>
{% function block.greet() %}
Hi
{% end %}
{% block.greet() %}, {%& title %}!
</h1>
]] )
fm . setTemplate ( " bye " , [[
{% block.header.greet() %},
{% function block.greet() %}
Bye
{% end %}
{% render('header', {title=title}) %}!
]] )
fm . render ( " bye " , { title = ' World ' }) -- renders `<h1>Hi, Bye, World!</h1>`
In this case, {% block.header.greet() %}
in the bye
template renders the greet
block from the header
template. This only works with the templates that are currently being rendered and is intended to simulate the "super" reference (albeit with explicit template references). The general syntax of this call is block.<templatename>.<blockname>()
.
As blocks are simply regular Lua functions, there are no restrictions on how blocks can be nested into other blocks or how blocks are defined relative to template fragments or other Lua statements included in the templates.
In addition to registering templates from a string, the templates can be loaded and registered from a file or a directory using the same setTemplate
function, but passing a table with the directory and a list of mappings from file extensions to template types to load. For example, calling fm.setTemplate({"/views/", tmpl = "fmt"})
loads all *.tmpl
files from the /views/
directory (and its subdirectories) and registers each of them as the fmt
template, which is the default template type. Only those files that match the extension are loaded and multiple extension mappings can be specified in one call.
Each loaded template gets its name based on the full path starting from the specified directory: the file /views/hello.tmpl
is registered as a template with the name "hello" (without the extension), whereas the file /views/greet/bye.tmpl
is registered as a template with the name "greet/bye" (and this is the exact name to use to load the template).
There are two caveats worth mentioning, both related to the directory processing. The first one is related to the trailing slash in the directory name passed to setTemplate
. It's recommended to provide one, as the specified value is used as a prefix, so if /view
is specified, it's going to match both /view/
and /views/
directories (if present), which may or may not be the intended result .
The second caveat is related to how external directories are used during template search. Since redbean allows access to external directories when configured using the -D
option or directory
option (see Running application for details), there may be multiple locations for the same template available. The search for the template follows these steps:
setTemplate
call); This allows to have a working copy of a template to be modified and processed from the file system (assuming the -D
option is used) during development without modifying its copy in the archive.
Even though using fm.render
is sufficient to get a template rendered, for consistency with other serve* functions, the library provides the serveContent
function, which is similar to fm.render
, but allows the action handler to complete after serving the content:
fm . setTemplate ( " hello " , " Hello, {%& name %} " )
fm . setRoute ( " /hello/:name " , function ( r )
return fm . serveContent ( " hello " , { name = r . params . name })
end )
There is also one subtle difference between render
and serveContent
methods that comes into play when serving static templates . It may be tempting to directly render a static template in response to a route with something like this:
fm . setTemplate ( " hello " , " Hello, World! " )
-- option 1:
fm . setRoute ( " /hello " , fm . render ( " hello " ))
---- ---------------------└─────┘-------- not going to work
-- option 2:
fm . setRoute ( " /hello " , fm . serveContent ( " hello " ))
---- ---------------------└───────────┘-- works as expected
The first approach is not going to work, as the call to fm.render
is going to be made when setRoute
is called (and the route is only being set up) and not when a request is being handled. When the serveContent
method is using (the second option), it's implemented in a way that delays the processing until the request is handled, thus avoiding the issue. If the template content depends on some values in the request, then the serverContent
call has to be wrapped into a function to accept and pass those variables (as shown in the earlier /hello/:name
route example).
Most of the time, the library configuration is focused on handling of incoming requests, but in some cases it may be desirable to trigger and handle internal events. The library supports job scheduling using cron syntax, with configured jobs executed at the scheduled time (as long as the redbean instance is running). A new schedule can be registered using the setSchedule
method:
---- ----------- ┌─────────── minute (0-59)
---- ----------- │ ┌───────── hour (0-23)
---- ----------- │ │ ┌─────── day of the month (1-31)
---- ----------- │ │ │ ┌───── month (1-12 or Jan-Dec)
---- ----------- │ │ │ │ ┌─── day of the week (0-6 or Sun-Mon)
---- ----------- │ │ │ │ │ --
---- ----------- │ │ │ │ │ --
fm . setSchedule ( " * * * * * " , function () fm . logInfo ( " every minute " ) end )
All the standard and some non-standard cron expressions are supported:
*
: describes any values in the allowed range.,
: uses to form a list of items, for example, 1,2,3
.-
: creates an (inclusive) range; for example, 1-3
is equivalent to 1,2,3
. Open ranges are allowed as well, so -3
is equivalent to 1-3
for months and 0-3
for minutes and hours./
: describes a step for ranges. It selects a subset of the values in the range, using the step value; for example, 2-9/3
is equivalent to 2,5,8
(it starts with 2, then adds a step value to get 5 and 8). Non-numeric values are supported for months ( Jan-Dec
) and days of week ( Sun-Mon
) in any capitalization. Using 7
for Sun
is supported too.
By default all functions are executed in a separate (forked) process. If the execution within the same process is needed, then setSchedule
can be passed a third parameter (a table) to set sameProc
value as one of the options: {sameProc = true}
.
Some of the caveats to be aware of:
OnServerHeartbeat
hook, so a version of Redbean that provides that (v2.0.16+) should be used.and
(instead of an or
), so when both are specified, the job is executed when both are satisfied (and not when both or either are specified). In other words, * * 13 * Fri
is only valid on Friday the 13th and not on any Friday. If the or
behavior is needed, then the schedule can be split into two to handle each condition separately.sameProc = true
option to avoid forking.Sun
available on both ends (as 0 or 7), so it's better to use closed ranges in this case to avoid ambiguity.6-100
for months is corrected to 6-12
.Each action handler generates some sort of response to send back to the client. In addition to strings, the application can return the following results:
serveResponse
),serveContent
),serveRedirect
),serveAsset
),serveError
),serveIndex
), andservePath
). Each of these methods can be used as the return value from an action handler. serveAsset
, servePath
, and serveIndex
methods can also be used as action handlers directly:
fm . setRoute ( " /static/* " , fm . serveAsset )
fm . setRoute ( " /blog/ " , fm . serveIndex ( " /new-blog/ " ))
The first route configures all existing assets to be served from /static/*
location; the second route configures /blog/
URL to return the index ( index.lua
or index.html
resource) from /new-blog/
directory.
serveResponse(status[, headers][, body])
: sends an HTTP response using provided status
, headers
, and body
values. headers
is an optional table populated with HTTP header name/value pairs. If provided, this set of headers removes all other headers set earlier during the handling of the same request. Similar to the headers set using the request.headers
field, the names are case-insensitive , but provided aliases for header names with dashes are case-sensitive : {ContentType = "foo"}
is an alternative form for {["Content-Type"] = "foo"}
. body
is an optional string.
Consider the following example:
return fm . serveResponse ( 413 , " Payload Too Large " )
This returns the 413 status code and sets the body of the returned message to Payload Too Large
(with the header table not specified).
If only the status code needs to be set, the library provides a short form using the serve###
syntax:
return fm . serve413
It can also be used as the action handler itself:
fm . setRoute ( fm . PUT " /status " , fm . serve402 )
serveContent(name, parameters)
renders a template using provided parameters. name
is a string that names the template (as set by a setTemplate
call) and parameters
is a table with template parameters (referenced as variables in the template).
Fullmoon's function makeStorage
is a way to connect to, and use a SQLite3
database. makeStorage
returns a database management table which contains a rich set of functions to use with the connected database.
The run
method executes the configured application. By default the server is launched listening on localhost and port 8080. Both of these values can be changed by passing addr
and port
options:
fm . run ({ addr = " localhost " , port = 8080 })
The following options are supported; the default values are shown in parentheses and options marked with mult
can set multiple values by passing a table:
addr
: sets the address to listen on (mult)brand
: sets the Server
header value ( "redbean/v# fullmoon/v#"
)cache
: configures Cache-Control
and Expires
headers (in seconds) for all static assets served. A negative value disables the headers. Zero value means no cache.certificate
: sets the TLS certificate value (mult)directory
: sets local directory to serve assets from in addition to serving them from the archive within the executable itself (mult)headers
: sets default headers added to each response by passing a table with HTTP header name/value pairslogMessages
: enables logging of response headerslogBodies
: enables logging of request bodies (POST/PUT/etc.)logPath
: sets the log file path on the local file systempidPath
: sets the pid file path on the local file systemport
: sets the port number to listen on (8080)privateKey
: sets the TLS private key value (mult)sslTicketLifetime
: sets the duration (in seconds) of the ssl ticket (86400)trustedIp
: configures IP address to trust (mult). This option accepts two values (IP and CIDR values), so they need to be passed as a table within a table specifying multiple parameters: trustedIp = {{ParseIp("103.31.4.0"), 22}, {ParseIp("104.16.0.0"), 13}}
tokenBucket
: enables DDOS protection. This option accepts zero to 5 values (passed as a table within a table); an empty table can be passed to use default values: tokenBucket = {{}}
Each option can accept a simple value ( port = 80
), a list of values ( port = {8080, 8081}
) or a list of parameters. Since both the list of values and the list of parameters are passed as tables, the list of values takes precedence, so if a list of parameters needs to be passed to an option (like trustedIp
), it has to be wrapped into a table: trustedIp = {{ParseIp("103.31.4.0"), 22}}
. If only one parameter needs to be passed, then both trustedIp = {ParseIp("103.31.4.0")}
and trustedIp = ParseIp("103.31.4.0")
can work.
The key
and certificate
string values can be populated using the getAsset
method that can access both assets packaged within the webserver archive and those stored in the file system.
There are also default cookie and session options that can be assigned using cookieOptions
and sessionOptions
tables described below.
cookieOptions
sets default options for all cookie values assigned using request.cookie.name = value
syntax ( {httponly=true, samesite="Strict"}
). It is still possible to overwrite default values using table assignment: request.cookie.name = {value, secure=false}
.
sessionOptions
sets default options for the session value assigned using request.session.attribute = value
syntax ( {name="fullmoon_session", hash="SHA256", secret=true, format="lua"}
). If the secret
value is set to true
, then a random key is assigned each time the server is started ; if verbose logging is enabled (by either adding -v
option for Redbean or by using fm.setLogLevel(fm.kLogVerbose)
call), then a message is logged explaining how to apply the current random value to make it permanent.
Setting this value to false
or an empty string applies hashing without a secret key.
The results shown are from runs in the same environment and on the same hardware as the published redbean benchmark (thanks to @jart for executing the tests!). Even though these tests are using pre-1.5 version of redbean and 0.10 version of Fullmoon, the current versions of redbean/Fullmoon are expected to deliver similar performance.
The tests are using exactly the same code that is shown in the introduction with one small change: using {%= name %}
instead of {%& name %}
in the template, which skips HTML escaping. This code demonstrates routing, parameter handling and template processing.
$ wrk -t 12 -c 120 http://127.0.0.1:8080/user/paul Running 10s test @ http://127.0.0.1:8080/user/paul 12 threads and 120 connections Thread Stats Avg Stdev Max +/- Stdev Latency 312.06us 4.39ms 207.16ms 99.85% Req/Sec 32.48k 6.69k 71.37k 82.25% 3913229 requests in 10.10s, 783.71MB read Requests/sec: 387477.76 Transfer/sec: 77.60MB
The following test is using the same configuration, but redbean is compiled with MODE=optlinux
option:
$ wrk -t 12 -c 120 http://127.0.0.1:8080/user/paul Running 10s test @ http://127.0.0.1:8080/user/paul 12 threads and 120 connections Thread Stats Avg Stdev Max +/- Stdev Latency 346.31us 5.13ms 207.31ms 99.81% Req/Sec 36.18k 6.70k 90.47k 80.92% 4359909 requests in 10.10s, 0.85GB read Requests/sec: 431684.80 Transfer/sec: 86.45MB
The following two tests demonstrate the latency of the request handling by Fullmoon and by redbean serving a static asset (no concurrency):
$ wrk -t 1 -c 1 http://127.0.0.1:8080/user/paul Running 10s test @ http://127.0.0.1:8080/user/paul 1 threads and 1 connections Thread Stats Avg Stdev Max +/- Stdev Latency 15.75us 7.64us 272.00us 93.32% Req/Sec 65.54k 589.15 66.58k 74.26% 658897 requests in 10.10s, 131.96MB read Requests/sec: 65241.45 Transfer/sec: 13.07MB
The following are the results from redbean itself on static compressed assets:
$ wrk -H 'Accept-Encoding: gzip' -t 1 -c 1 htt://10.10.10.124:8080/tool/net/demo/index.html Running 10s test @ htt://10.10.10.124:8080/tool/net/demo/index.html 1 threads and 1 connections Thread Stats Avg Stdev Max +/- Stdev Latency 7.40us 1.95us 252.00us 97.05% Req/Sec 129.66k 3.20k 135.98k 64.36% 1302424 requests in 10.10s, 1.01GB read Requests/sec: 128963.75 Transfer/sec: 102.70MB
Berwyn Hoyt included Redbean results in his lua server benchmark results, which shows redbean outperforming a comparable nginx/openresty implementation.
Highly experimental with everything being subject to change.
The core components are more stable and have been rarely updated since v0.3. Usually, the documented interfaces are much more stable than undocumented ones. Those commits that modified some of the interfaces are marked with COMPAT
label, so can be easily identified to review for any compatibility issues.
Some of the obsolete methods are still present (with a warning logged when used) to be removed later.
Paul Kulchenko ([email protected])
See LICENSE.