Fullmoon es un marco web rápido y minimalista basado en RedBean: un servidor web portátil distribuible de un solo archivo.
Todo lo necesario para el desarrollo y la distribución viene en un solo archivo sin dependencias externas y después del empaque con RedBean se ejecuta en Windows, Linux o MacOS. El siguiente es un ejemplo completo de una aplicación 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 () Después de que se empaqueta con RedBean, se puede lanzar usando ./redbean.com , que inicia un servidor que devuelve "Hello, World" a una solicitud HTTP (S) enviada a http: // localhost: 8080/hello/world.
RedBean es un servidor web multiplataforma distribuible de un solo archivo con cualidades únicas y potentes. Si bien hay varios marcos web basados en LUA (Lapis, LOR, Sailor, Pegasus y otros), ninguno de ellos se integra con RedBean (aunque hay un marco experimental ANPAN).
Fullmoon es un marco web liviano y minimalista que se escribe desde la perspectiva de mostrar todas las capacidades que RedBean proporciona al extenderlas y aumentarlas de la manera más simple y eficiente. Se ejecuta rápidamente y viene con baterías incluidas (rutas, plantillas, generación JSON y más).
Fullmoon sigue la filosofía de Lua y proporciona un conjunto mínimo de herramientas para combinar según sea necesario y usar como base para construir.
fork multiplataforma, socket , memoria compartida y másDescargue una copia de RedBean ejecutando los siguientes comandos (omita el segundo si ejecuta estos comandos en Windows):
curl -o redbean.com https://redbean.dev/redbean-2.2.com
chmod +x redbean.comEl último número de versión se puede recuperar con la siguiente solicitud:
curl https://redbean.dev/latest.txtOtra opción es construir RedBean desde la fuente siguiendo instrucciones para la compilación de la fuente.
fullmoon.lua a .lua/ Directorio.init.lua (por ejemplo, el código LUA que se muestra en la descripción). Otra opción es colocar el código de aplicación en un archivo separado (por ejemplo, .lua/myapp.lua ) y agregar require "myapp" a .init.lua . Así es como se presentan todos los ejemplos incluidos.
zip redbean.com .init.lua .lua/fullmoon.lua Si el código de aplicación se almacena en un archivo LUA separado, como se describió anteriormente, asegúrese de colocarlo dentro del directorio .lua/ Directorio y Zip también ese archivo.
./redbean.comSi este comando se ejecuta en Linux y lanza un error al no encontrar intérprete, debe solucionarse ejecutando el siguiente comando (aunque tenga en cuenta que puede no sobrevivir a un reinicio del sistema):
sudo sh -c " echo ':APE:M::MZqFpD::/bin/sh:' >/proc/sys/fs/binfmt_misc/register "Si este comando produce errores desconcertantes en WSL o vino cuando se usa RedBean 2.x, se pueden solucionar deshabilitando binfmt_misc:
sudo sh -c ' echo -1 >/proc/sys/fs/binfmt_misc/status 'Inicie un navegador que apunte a http: // localhost: 8080/hello/world y debería devolver "hola, mundo" (suponiendo que la aplicación esté utilizando el código que se muestra en la introducción o el de la sección de uso).
El ejemplo más simple debe (1) cargar el módulo, (2) configurar una ruta y (3) ejecutar la aplicación:
local fm = require " fullmoon " -- (1)
fm . setRoute ( " /hello " , function ( r ) return " Hello, world " end ) -- (2)
fm . run () -- (3) Esta solicitud responde a cualquier solicitud para /hello URL con el contenido de "hola, mundo" (y el código de estado 200) y responde con el código de estado 404 para todas las demás solicitudes.
setRoute(route[, action]) : registra una ruta. Si route es una cadena, entonces se usa como una expresión de ruta para comparar la ruta de solicitud. Si es una tabla, entonces sus elementos son cadenas que se usan como rutas y sus valores hash son condiciones en las que se verifican las rutas. Si el segundo parámetro es una función, entonces se ejecuta si se satisfacen todas las condiciones. Si es una cadena, entonces se usa como una expresión de ruta y la solicitud se procesa como si se envíe en la ruta especificada (actúa como una redirección interna). Si no se cumple alguna condición, entonces se verifica la siguiente ruta. La expresión de ruta puede tener múltiples parámetros y piezas opcionales. El controlador de acción acepta una tabla de solicitudes que proporciona acceso a los parámetros de solicitud y ruta, así como encabezados, cookies y sesión.
setTemplate(name, template[, parameters]) : registra una plantilla con el nombre especificado o un conjunto de plantillas de un directorio. Si template es una cadena, se compila en un controlador de plantilla. Si se trata de una función, se almacena y se llama cuando se solicita la representación de la plantilla. Si es una tabla, entonces su primer elemento es una plantilla o una función y el resto se usan como opciones. Por ejemplo, especificar ContentType como una de las opciones establece el encabezado Content-Type para el contenido generado. Varias plantillas ( 500 , json y otras) se proporcionan por defecto y pueden sobrescribirse. parameters son una tabla con parámetros de plantilla almacenados como pares de nombre/valor (referenciados como variables en la plantilla).
serveResponse(status[, headers][, body]) : envía una respuesta HTTP utilizando status , headers y los valores body proporcionados. headers es una tabla opcional poblada con pares de nombre/valor del encabezado HTTP. Si se proporciona, este conjunto de encabezados elimina todos los demás encabezados establecidos anteriormente durante el manejo de la misma solicitud. Los nombres de los encabezados son insensibles al caso , pero los alias proporcionados para los nombres de encabezado con guiones son sensibles al caso : {ContentType = "foo"} es una forma alternativa para {["Content-Type"] = "foo"} . body es una cadena opcional.
serveContent(name, parameters) : hace una plantilla utilizando los parámetros proporcionados. name es una cadena que nombra la plantilla (según lo establecido por una llamada setTemplate ) y parameters son una tabla con parámetros de plantilla almacenados como pares de nombre/valor (referenciados como variables en la plantilla).
run([options]) : ejecuta el servidor usando rutas configuradas. Por defecto, el servidor escucha en LocalHost y el puerto 8080. Estos valores se pueden cambiar configurando los valores addr y port en la tabla options .
Ejecutar ejemplos requiere que incluya una declaración require en el archivo .init.lua , que carga el módulo con cada código de ejemplo, por lo que para el ejemplo de exhibición implementado en showcase.lua , .init.lua incluye lo siguiente:
-- 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`El ejemplo de Showcase demuestra varias características de luna completa:
serveAsset )serveRedirect )Se deben agregar los siguientes archivos al ejecutable/archivo de RedBean:
.init.lua - requiere "escaparate" .lua/fullmoon.lua .lua/showcase.lua
El ejemplo de TechEmPower implementa varios tipos de prueba para los puntos de referencia del marco web utilizando Fullmoon y una base de datos SQLite en memoria.
Este ejemplo demuestra varias características Fullmoon/RedBean:
Se deben agregar los siguientes archivos al ejecutable/archivo de RedBean:
.init.lua - requiere "techbench" .lua/fullmoon.lua .lua/techbench.lua
El ejemplo de la placa HTMX demuestra una aplicación simple que genera fragmentos HTML entregados al cliente utilizando la biblioteca HTMX.
Este ejemplo demuestra varias características Fullmoon/RedBean:
Se deben agregar los siguientes archivos al ejecutable/archivo de RedBean:
.init.lua - requiere "htmxboard" .lua/fullmoon.lua .lua/htmxboard.lua activos/estilos.css TMPL/* - Todos los archivos de ejemplos/htmxboard/tmpl directorio
Nota 1: Dado que todos los datos se almacenan en la memoria, este ejemplo se ejecuta en el modo uniprocesos.
Nota 2: Estos ejemplos recupera HTMX, HyperScript y bibliotecas ordenables de recursos externos, pero estas bibliotecas también pueden almacenarse como activos locales, proporcionando así un paquete de distribución portátil completamente autosuficiente.
El ejemplo de SSE HTMX demuestra una forma de generar eventos (SSE) de servidor de servidor que se pueden transmitir a un cliente (que muestra resultados utilizando la biblioteca HTMX y su extensión SSE).
Este ejemplo demuestra varias características Fullmoon/RedBean:
streamContent )Se deben agregar los siguientes archivos al ejecutable/archivo de RedBean:
.init.lua - requiere "htmxsse" .lua/fullmoon.lua .lua/htmxsse.lua
Cada aplicación Fullmoon sigue el mismo flujo básico con cinco componentes principales:
Veamos cada uno de los componentes a partir del enrutamiento de la solicitud.
Fullmoon maneja cada solicitud HTTP utilizando el mismo proceso:
false o nil regresa del controlador de acción (y continúa el proceso de lo contrario) En general, las definiciones de ruta vinculan las URL de solicitud (y un conjunto de condiciones) a los manejadores de acción (que son la función LUA regular). Todas las condiciones se verifican en un orden aleatorio para cada URL que coincida con la definición de ruta. Tan pronto como falla cualquier condición, el procesamiento de la ruta se aborta y la siguiente ruta se verifica con una excepción : cualquier condición puede establecer el valor otherwise , lo que desencadena una respuesta con el código de estado especificado.
Si ninguna ruta coincide con la solicitud, se activa el procesamiento 404 predeterminado, que se puede personalizar registrando una plantilla 404 personalizada ( fm.setTemplate("404", "My 404 page...") ).
Cada ruta toma un camino que coincide exactamente, por lo que la ruta "/hello" coincide con las solicitudes de /hello y no coincide /hell , /hello-world , o /hello/world . La ruta a continuación responde con "¡Hola, mundo!" Para todas las solicitudes dirigidas a la ruta /hello y devuelve 404 para todas las demás solicitudes.
fm . setRoute ( " /hello " , function ( r ) return " Hello, World! " end ) Para que coincida con una ruta donde /hello es solo una parte de ella, se pueden usar parámetros opcionales y splat.
Además de las rutas fijas, cualquier ruta puede incluir marcadores de posición para parámetros, que se identifican por A : seguidos inmediatamente por el nombre del parámetro:
fm . setRoute ( " /hello/:name " ,
function ( r ) return " Hello, " .. ( r . params . name ) end ) Cada parámetro coincide con uno o más caracteres excepto / , por lo que la ruta "/hello/:name" coincide /hello/alice , /hello/bob , /hello/123 y no coincide /hello/bob/and/alice (debido a las barras delanteras no coincidentes) o /hello/ (porque la longitud del fragmento de emparejado es cero).
Los nombres de parámetros solo pueden incluir caracteres alfanuméricos y _ .
Se puede acceder a los parámetros utilizando la tabla de solicitudes y su tabla params , de modo que r.params.name se puede usar para obtener el valor del parámetro de name del ejemplo anterior.
Cualquier fragmento o parámetro de ruta especificado puede declararse como opcional envolviéndolo en paréntesis:
fm . setRoute ( " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end ) En el ejemplo anterior, se aceptan tanto /hello y /hello/Bob , pero no /hello/ , ya que el corte de arrastre es parte del fragmento opcional y :name todavía espera uno o más personajes.
Cualquier parámetro opcional inigualable se vuelve false como su valor, por lo que en el caso anterior "¡Hola, mundo!" se devuelve para la URL de solicitud /hello .
Se pueden especificar más de un parámetro opcional y se pueden anidar los fragmentos opcionales, por lo que tanto "/posts(/:pid/comments(/:cid))" y "/posts(/:pid)/comments(/:cid)" son valores de ruta válidos.
Hay otro tipo de parámetro llamado Splat que se escribe como * y coincide con cero o más caracteres, incluida una barra de avance ( / ). El Splat también se almacena en la tabla de params debajo del nombre splat . Por ejemplo, la ruta "/download/*" coincide /download/my/file.zip file.zip y el splat obtiene el valor de my/file.zip . Si se necesitan múltiples salpicaduras en la misma ruta, entonces se pueden asignar nombres de SPLATS similares a otros parámetros: /download/*path/*fname.zip (aunque el mismo resultado se puede lograr usando /download/*path/:fname.zip , como el primer Splat captura todas las partes de la ruta excepto el nombre de archivo).
Todos los parámetros (incluido el Splat) pueden aparecer en cualquier parte de la ruta y pueden estar rodeados de otro texto, que debe coincidir exactamente. Esto significa que la ruta "/download/*/:name.:ext" coincide /download/my/path/file.zip y params.name obtiene file , params.ext obtiene zip y params.splat obtiene my/path .
Otra razón para usar Splat es permitir múltiples rutas con la misma ruta que se registra en el sistema. La implementación actual sobrescribe las rutas con el mismo nombre y para evitar que se pueda usar un Splat con nombre para crear rutas únicas. Por ejemplo,
fm . setRoute ( " /*dosomething1 " , function ( r ) return " something 1 " end )
fm . setRoute ( " /*dosomething2 " , function ( r ) return " something 2 " end )Esto se puede usar en situaciones cuando hay un conjunto de condiciones que deben verificarse en el controlador de acción y, aunque puede ser posible combinar ambas rutas en una, a veces es más limpio mantenerlas separadas.
El valor predeterminado para los parámetros son todos los caracteres (excepto / ) de longitud uno o más. Para especificar un conjunto diferente de caracteres válidos, se puede agregar al final del nombre de la variable; Por ejemplo, usando :id[%d] en lugar de :id cambia el parámetro para que coincida solo con dígitos.
fm . setRoute ( " /hello(/:id[%d]) " ,
function ( r ) return " Hello, " .. ( r . params . id or " World! " ) end ) Se respaldan las siguientes clases de caracteres de Lua: %w , %d , %a , %l , %u y %x ; Cualquier carácter de puntuación (incluidos % y ] ) también se puede escapar con % . Las clases negativas (escritas en Lua como %W ) no son compatibles , pero la sintaxis no en el set es compatible, por lo que [^%d] coincide con un parámetro que no incluye ningún dígito.
Tenga en cuenta que el número de repeticiones no se puede cambiar (entonces :id[%d]* no es una forma válida de aceptar dígitos cero o más), ya que solo se permiten conjuntos y los valores aún aceptan uno o más caracteres. Si se necesita más flexibilidad en la descripción de formatos aceptables, entonces los validadores personalizados se pueden usar para extender la lógica coincidente.
Se pueden acceder a los parámetros de consulta y formulario de la misma manera que los parámetros de ruta utilizando la tabla de params en la tabla request que se pasa a cada manejador de acción. Tenga en cuenta que si hay un conflicto entre los nombres de parámetro y consulta/formulario, entonces los nombres de los parámetros tienen prioridad .
Hay un caso especial que puede dar como resultado una tabla devuelta en lugar de un valor de cadena: si el nombre del parámetro de consulta/formulario termina en [] , entonces todos los resultados coincidentes (uno o más) se devuelven como una tabla. Por ejemplo, para una cadena de consulta a[]=10&a[]&a[]=12&a[]= El valor de params["a[]"] es {10, false, 12, ""} .
Como escribir estos nombres de parámetros puede requerir varios soportes, params.a se puede usar como un atajo para params["a[]"] con ambas formas que devuelven la misma tabla.
Los parámetros multipart también se procesan cuando se solicitan y se puede acceder de la misma manera que el resto de los parámetros utilizando la tabla params . Por ejemplo, los parámetros con nombres simple y more se pueden recuperar de un mensaje con tipo de contenido multipart/form-data usando params.simple y params.more .
Como algunos de los contenidos multiparte pueden incluir encabezados y parámetros adicionales dentro de esos encabezados, se puede acceder a ellos con el campo multipart de la tabla de params :
fm . setRoute ({ " /hello " , simple = " value " }, function ( r )
return " Show " .. r . params . simple .. " " .. r . params . multipart . more . data )
end ) La tabla multipart incluye todas las partes del mensaje multipart (por lo que se puede iterar sobre el uso ipairs ), pero también permite el acceso usando nombres de parámetros ( params.multipart.more ). Cada uno de los elementos también es una tabla que incluye los siguientes campos:
nil si no.nil si no. Este procesamiento multipart consume cualquier subtipo multipart y maneja mensajes de multipartes recursivos. También inserta una parte con el valor Content-ID que coincide con el parámetro start en la primera posición.
A pesar de todos los ejemplos anteriores que muestran una sola ruta, rara vez es el caso en aplicaciones reales; Cuando hay múltiples rutas presentes, siempre se evalúan en el orden en que están registrados .
Una llamada setRoute también puede establecer múltiples rutas cuando tienen el mismo conjunto de condiciones y compartir el mismo controlador de acción:
fm . setRoute ({ " /route1 " , " /route2 " }, handler )Esto es equivalente a dos llamadas que establecen cada ruta individualmente:
fm . setRoute ( " /route1 " , handler )
fm . setRoute ( " /route2 " , handler )Dado que las rutas se evalúan en el orden en que se establecen, las rutas más selectivas deben establecerse primero, de lo contrario, es posible que no tengan la oportunidad de ser evaluados:
fm . setRoute ( " /user/bob " , handlerBob )
fm . setRoute ( " /user/:name " , handlerName ) Si las rutas se establecen en el orden opuesto, /user/bob nunca se verifica siempre que el controlador de acción "/user/:name" devuelva algún resultado no false .
Como se describió anteriormente, si ninguna de las rutas coincide, se devuelve una respuesta con un código de estado 404. Puede haber casos en los que esto no sea deseable; Por ejemplo, cuando la aplicación incluye scripts LUA para manejar solicitudes que no se registran explícitamente como rutas. En esos casos, se puede agregar una ruta de atrapar todo que implementa el procesamiento predeterminado de redBean (el nombre del parámetro Splat solo se usa para desambiguar esta ruta contra otras rutas /* que se pueden usar en otro lugar):
fm . setRoute ( " /*catchall " , fm . servePath ) Cada ruta se puede proporcionar con un nombre opcional, que es útil para hacer referencia a esa ruta cuando su URL debe generarse en función de valores de parámetros específicos. La función proporcionada makePath acepta un nombre de ruta o una URL de ruta en sí, así como la tabla de parámetros y devuelve una ruta con marcadores de posición de parámetros poblados:
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 oneSi dos rutas usan el mismo nombre, entonces el nombre está asociado con el que se registró por última vez, pero ambas rutas aún están presentes.
El nombre de la ruta también se puede usar con rutas externas/estáticas que solo se usan para la generación de URL.
Si la ruta solo se usa para la generación de rutas, entonces ni siquiera necesita tener un manejador de ruta:
fm . setRoute ({ " https://youtu.be/:videoid " , routeName = " youtube " })
fm . makePath ( " youtube " , { videoid = " abc " }) -- > https://youtu.be/abcSe omite una ruta sin ningún controlador de acción durante el proceso de coincidencia de ruta.
Las rutas internas permiten la redirección de un conjunto de URL a una diferente. La URL objetivo puede apuntar a un recurso estático o un script .lua . Por ejemplo, si las solicitudes de una ubicación deben redirigirse a otra, la siguiente configuración redirige las solicitudes de cualquier recurso en /blog/ URL a aquellos bajo /new-blog/ URL / URL siempre que exista el recurso de destino:
fm . setRoute ( " /blog/* " , " /new-blog/* " ) Esta ruta acepta una solicitud para /blog/post1 y sirve /new-blog/post1 como su respuesta, siempre que exista el activo /new-blog/post1 . Si el activo no existe, entonces se verifica la siguiente ruta. Del mismo modo, el uso de fm.setRoute("/static/*", "/*") hace que las solicitudes de /static/help.txt sean atendidas /help.txt .
Ambas URL pueden incluir parámetros que se completan si se resuelven:
fm . setRoute ( " /blog/:file " , " /new-blog/:file.html " ) -- <<-- serve "nice" URLs
fm . setRoute ( " /new-blog/:file.html " , fm . serveAsset ) -- <<-- serve original URLs Este ejemplo resuelve las URL "agradables" que sirven sus versiones "HTML". Tenga en cuenta que esto no se desencadena la redirección del lado del cliente al devolver el código de estado 3xx , sino que maneja la redacción internamente. También tenga en cuenta que la segunda regla es necesaria para servir a las URL "originales", ya que la primera regla no las maneja, porque si la solicitud es para /blog/mylink.html , entonces la URL redirigida es /new-blog/mylink.html.html , que probablemente no existe, por lo que se omite la ruta y la siguiente se verifica. Si también se requiere manejo de separadores de ruta, entonces *path se puede usar en lugar de :file , como * permite separadores de ruta.
Si una aplicación necesita ejecutar diferentes funciones dependiendo de valores específicos de atributos de solicitud (por ejemplo, un método), esta biblioteca proporciona dos opciones principales: (1) Verifique el valor de atributo un controlador de acción (por ejemplo, usando request.method == "GET" verificación) y (2) Agregue una condición que filme las solicitudes de tal manera que solo las solicitudes utilizan el valor de atributo especificado alcanzar el controlador de acción. Esta sección describe la segunda opción con más detalle.
Cada ruta registrada de forma predeterminada responde a todos los métodos HTTP (obtener, poner, post, etc.), pero es posible configurar cada ruta para responder solo a métodos HTTP específicos:
fm . setRoute ( fm . GET " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end ) En este caso, la sintaxis fm.GET"/hello(/:name)" configura la ruta para aceptar solo las solicitudes GET . Esta sintaxis es equivalente a pasar una tabla con la ruta y cualquier condición de filtrado adicional:
fm . setRoute ({ " /hello(/:name) " , method = " GET " },
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )Si se necesita especificar más de un método, entonces se puede pasar una tabla con una lista de métodos en lugar de un valor de cadena:
fm . setRoute ({ " /hello(/:name) " , method = { " GET " , " POST " }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end ) Cada ruta que permite una solicitud GET también (implícitamente) permite una solicitud HEAD y esa solicitud se maneja devolviendo todos los encabezados sin enviar el cuerpo en sí. Si por alguna razón este manejo implícito no es deseable, entonces agregar HEAD = false a la tabla de métodos lo deshabilita (como en method = {"GET", "POST", HEAD = false} ).
Tenga en cuenta que las solicitudes con métodos no coincidentes no se rechazan, sino que se caen para ser verificados por otras rutas y activar el código de estado 404 devuelto si no se combinan (con una excepción).
Además del method , se pueden aplicar otras condiciones utilizando host , clientAddr , serverAddr , scheme , encabezados de solicitud y parámetros. Por ejemplo, especificar name = "Bob" como una de las condiciones garantiza que el valor del parámetro name sea "Bob" para que se llame al controlador de acción.
Cualquier encabezado de solicitud se puede verificar utilizando el nombre del encabezado como clave, por lo que ContentType = "multipart/form-data" se cumple si el valor del encabezado Content-Type es multipart/form-data . Tenga en cuenta que el valor del encabezado puede incluir otros elementos (un límite o un charset como parte del valor Content-Type ) y solo se compara el tipo de medio real.
Dado que los nombres para encabezados, parámetros y propiedades pueden superponerse, se verifican en el siguiente orden:
ContentType ,method , port , host , etc.) y El encabezado Host también se verifica primero (a pesar de ser una sola palabra), por lo que hace referencia a los filtros Host basados en el Host del encabezado, mientras hace referencia a los filtros host basados en el host de la propiedad.
Los valores de cadena no son los únicos valores que se pueden usar en rutas condicionales. Si es aceptable más de un valor, pasar una tabla permite proporcionar una lista de valores aceptables. Por ejemplo, si Bob y Alice son valores aceptables, entonces name = {Bob = true, Alice = true} expresa esto como una condición.
Dos valores especiales pasados en una tabla permiten aplicar una regex o una validación de patrones :
regex : acepta una cadena que tiene una expresión regular. Por ejemplo, name = {regex = "^(Bob|Alice)$"} tiene el mismo resultado que la verificación hash que se muestra anteriormente en esta secciónpattern : acepta una cadena con una expresión de patrón LUA. Por ejemplo, name = {pattern = "^%u%l+$"} acepta valores que comienzan con un carácter en mayúsculas seguidos de uno o más caracteres minúsculas. Estas dos verificaciones se pueden combinar con la verificación de existencia de la tabla: name = {Bob = true, regex = "^Alice$"} acepta los valores Bob y Alice . Si la primera verificación de existencia de tabla falla, entonces se devuelven los resultados de la expresión regex o pattern .
El último tipo de validador personalizado es una función. La función proporcionada recibe el valor para validar y su resultado se evalúa como false o true . Por ejemplo, pasar id = tonumber asegura que el valor id sea un número. Como otro ejemplo, clientAddr = fm.isLoopbackIp asegura que la dirección del cliente sea una dirección IP de bucleback.
fm . setRoute ({ " /local-only " , clientAddr = fm . isLoopbackIp },
function ( r ) return " Local content " end )Como la función de validador se puede generar dinámicamente, esto también funciona:
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 )Es importante tener en cuenta que la función de validador realmente devuelve una función que se llama durante una solicitud para aplicar el cheque. En el ejemplo anterior, la función devuelta acepta un valor de encabezado y lo compara con el límite aprobado durante su creación.
En algunos casos, no satisfacer una condición es una razón suficiente para devolver alguna respuesta al cliente sin verificar otras rutas. En un caso como este, establecer un valor otherwise en un número o una función devuelve una respuesta con el estado especificado o el resultado de la función:
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 ) En este ejemplo, el motor de enrutamiento coincide con la ruta y luego valida las dos condiciones que comparan el valor del método con POST y el valor del encabezado Content-Length con el resultado de la función isLessThan . Si una de las condiciones no coincide, el código de estado especificado por el valor otherwise se devuelve con el resto de la respuesta.
Si la condición otherwise solo debe aplicarse a la verificación ContentLength , entonces el valor otherwise junto con la función de validador se puede mover a una tabla asociada con la verificación ContentLength :
fm . setRoute ( fm . POST { " /upload " ,
ContentLength = { isLessThan ( 100000 ), otherwise = 413 }
}, function ( r ) ... handle the upload ... end ) La diferencia entre los dos últimos ejemplos es que en este ejemplo solo la falla de verificación ContentLength desencadena la respuesta 413 (y todos los demás métodos caen a otras rutas), mientras que en la anterior, tanto method como las fallas de verificación ContentLength activan la misma respuesta 413.
Tenga en cuenta que cuando el valor marcado es nil , se considera válida la verificación contra una tabla y la ruta se acepta. Por ejemplo, una comprobación de un parámetro opcional realizado en una cadena ( name = "Bo" ) falla si el valor de params.name es nil , pero pasa si la misma verificación se realiza en una tabla ( name = {Bo=true, Mo=true} ), incluyendo verificaciones de reglas/patrones. Si esto no es deseable, una función de validador personalizada puede verificar explícitamente el valor esperado.
Considere el siguiente ejemplo:
fm . setRoute ({ " /hello(/:name) " ,
method = { " GET " , " POST " , otherwise = 405 }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end ) En este caso, si se accede a este punto final con el método PUT , entonces en lugar de verificar otras rutas (porque la condición method no se cumple), se devuelve el código de estado 405, según lo configurado con el valor especificado otherwise . Como se documenta en otro lugar, esta ruta también acepta una solicitud HEAD (incluso cuando no se enumera), como se acepta una solicitud GET .
Cuando se devuelve el código de estado 405 (método malo) y no se establece el encabezado Allow , se establece en la lista de métodos permitidos por la ruta. En el caso anterior, está configurado para GET, POST, HEAD, OPTIONS , ya que esos son los métodos permitidos por esta configuración. Si el valor otherwise es una función (en lugar de un número), entonces devolver un resultado adecuado y establecer el encabezado Allow es responsabilidad de esta función.
El valor otherwise también se puede establecer en una función, que proporciona más flexibilidad que solo configurar un código de estado. Por ejemplo, la configuración otherwise = fm.serveResponse(413, "Payload Too Large") desencadena una respuesta con el código de estado especificado y el mensaje.
El manejo de la validación del formulario a menudo requiere especificar un conjunto de condiciones para el mismo parámetro y un mensaje de error personalizado que es posible que deba devolver cuando las condiciones no se cumplan y estos son proporcionados por validadores especiales devueltos por la función 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 )En este ejemplo, el validador está configurado para verificar dos parámetros: "nombre" y "contraseña", para sus longitudes min y máximas y devolver un mensaje cuando uno de los parámetros falla la verificación.
Dado que la comprobación de falla hace que la ruta se saltea, siempre que el valor otherwise permita que el error se devuelva como parte de la respuesta:
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 ,
} En este caso, el controlador otherwise recibe el mensaje de error (o una tabla con mensajes si se solicita al pasar la opción all cubierta a continuación) que se puede proporcionar como un parámetro de plantilla y devuelto al cliente.
Otra opción es llamar a la función de validador directamente en un controlador de acción y devolver sus resultados:
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 ) En este ejemplo, el validador se llama directamente y se pasa una tabla ( r.params ) con todos los valores de parámetros para permitir que la función de validador verifique los valores con las reglas especificadas.
La función de validador luego devuelve true al éxito de la señal o nil, error para indicar una falla en verificar una de las reglas. Esto permite que la llamada de validador se envuelva en una assert si el script necesita devolver un error de inmediato:
assert ( validator ( r . params )) -- throw an error if validation fails
return fm . serveRedirect ( 307 , " / " ) -- return redirect in other casesLas siguientes comprobaciones de validador están disponibles:
minlen : (entero) verifica la longitud mínima de una cadena.maxlen : (entero) Verifica la longitud máxima de una cadena.test : (función) llama a una función que se pasa un parámetro y se espera que devuelva true o nil | false [, error] .oneof : ( value | { table of values to be compared against } ) verifica si el parámetro coincide con uno de los valores proporcionados.pattern : (cadena) Comprueba si el parámetro coincide con una expresión de patrón LUA.Además de los cheques, las reglas pueden incluir opciones:
optional : (bool) hace que un parámetro sea opcional cuando es nil . Todos los parámetros se requieren de forma predeterminada, por lo que esta opción permite que las reglas se omitan cuando no se proporciona el parámetro. Todas las reglas aún se aplican si el parámetro no es nulo.msg : (cadena) Agrega un mensaje del cliente para esto si una de sus comprobaciones falla, lo que sobrescribe mensajes de cheques individuales. El mensaje puede incluir un marcador de posición ( %s ), que será reemplazado por un nombre de parámetro.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. Por ejemplo,
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 . serve413It 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.