FullMoon est un cadre Web rapide et minimaliste basé sur Redbean - un serveur Web distribuable portable et à un seul fichier.
Tout ce qui est nécessaire pour le développement et la distribution est disponible dans un seul fichier sans dépendances externes et après l'emballage avec Redbean exécute sur Windows, Linux ou MacOS. Ce qui suit est un exemple complet d'une application 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 ()
Après avoir été emballé avec Redbean, il peut être lancé en utilisant ./redbean.com
, qui démarre un serveur qui renvoie "Hello, World" à une demande HTTP (s) envoyée à http: // localhost: 8080 / bonjour / monde.
Redbean est un serveur Web multiplateforme distribuable unique avec des qualités uniques et puissantes. Bien qu'il existe plusieurs cadres Web basés sur LUA (Lapis, Lor, Sailor, Pegasus et autres), aucun d'entre eux ne s'intègre à Redbean (bien qu'il existe un cadre expérimental ANPAN).
Fullmoon est un cadre Web léger et minimaliste qui est écrit du point de vue de la présentation de toutes les capacités que Redbean fournit en les étendant et en les augmentant de la manière la plus simple et la plus efficace. Il fonctionne rapidement et est livré avec des batteries incluses (itinéraires, modèles, génération JSON et plus).
Fullmoon suit la philosophie LUA et fournit un ensemble minimal d'outils pour combiner selon les besoins et utiliser comme base sur laquelle construire.
fork
multiplateforme, socket
, mémoire partagée et plusTéléchargez une copie de Redbean en exécutant les commandes suivantes (sautez la seconde si l'exécution de ces commandes sur Windows):
curl -o redbean.com https://redbean.dev/redbean-2.2.com
chmod +x redbean.com
Le dernier numéro de version peut être récupéré avec la demande suivante:
curl https://redbean.dev/latest.txt
Une autre option consiste à construire Redbean à partir de Source en suivant les instructions pour la construction source.
fullmoon.lua
à .lua/
répertoire.init.lua
(par exemple, le code LUA affiché dans la description). Une autre option consiste à placer le code d'application dans un fichier séparé (par exemple, .lua/myapp.lua
) et à ajouter require "myapp"
à .init.lua
. C'est ainsi que tous les exemples inclus sont présentés.
zip redbean.com .init.lua .lua/fullmoon.lua
Si le code d'application est stocké dans un fichier LUA distinct, comme décrit ci-dessus, assurez-vous de le placer à l'intérieur du .lua/
répertoire et ziptez également ce fichier.
./redbean.com
Si cette commande est exécutée sur Linux et lance une erreur de ne pas trouver d'interprète, il doit être corrigé en exécutant la commande suivante (bien qu'il ne soit pas survivre à un redémarrage du système):
sudo sh -c " echo ':APE:M::MZqFpD::/bin/sh:' >/proc/sys/fs/binfmt_misc/register "
Si cette commande produit des erreurs déroutantes sur le WSL ou le vin lors de l'utilisation de Redbean 2.x, ils peuvent être corrigées en désactivant Binfmt_Misc:
sudo sh -c ' echo -1 >/proc/sys/fs/binfmt_misc/status '
Lancez un navigateur pointant sur http: // localhost: 8080 / hello / world et il devrait retourner "Bonjour, monde" (en supposant que l'application utilise le code indiqué dans l'introduction ou celui de la section d'utilisation).
L'exemple le plus simple doit (1) charger le module, (2) configurer une route et (3) exécuter l'application:
local fm = require " fullmoon " -- (1)
fm . setRoute ( " /hello " , function ( r ) return " Hello, world " end ) -- (2)
fm . run () -- (3)
Cette application répond à toute demande d'URL /hello
avec le contenu "Hello, World" (et le code d'état 200) et répond avec le code d'état 404 pour toutes les autres demandes.
setRoute(route[, action])
: enregistre un itinéraire. Si route
est une chaîne, il est utilisé comme une expression de route pour comparer le chemin de demande. S'il s'agit d'un tableau, ses éléments sont des chaînes qui sont utilisées comme itinéraires et ses valeurs de hachage sont des conditions contre lesquelles les itinéraires sont vérifiés. Si le deuxième paramètre est une fonction, il est exécuté si toutes les conditions sont remplies. S'il s'agit d'une chaîne, il est utilisé comme une expression de route et la demande est traitée comme s'il était envoyée sur l'itinéraire spécifié (agit comme une redirection interne). Si une condition n'est pas satisfaite, l'itinéraire suivant est vérifié. L'expression de l'itinéraire peut avoir plusieurs paramètres et parties en option. Le gestionnaire d'action accepte un tableau de demande qui donne l'accès aux paramètres de demande et d'itinéraire, ainsi que des en-têtes, des cookies et de la session.
setTemplate(name, template[, parameters])
: enregistre un modèle avec le nom spécifié ou un ensemble de modèles à partir d'un répertoire. Si template
est une chaîne, il est compilé dans un gestionnaire de modèles. S'il s'agit d'une fonction, il est stocké et appelé lorsque le rendu du modèle est demandé. S'il s'agit d'une table, son premier élément est un modèle ou une fonction et les autres sont utilisés comme options. Par exemple, la spécification de ContentType
comme l'une des options définit l'en-tête Content-Type
pour le contenu généré. Plusieurs modèles ( 500
, json
et autres) sont fournis par défaut et peuvent être écrasés. parameters
sont un tableau avec des paramètres de modèle stockés sous forme de paires de nom / valeur (référencés sous forme de variables dans le modèle).
serveResponse(status[, headers][, body])
: envoie une réponse HTTP en utilisant status
fourni, headers
et les valeurs body
. headers
sont une table en option remplie de paires de nom / valeur d'en-tête HTTP. S'il est fourni, cet ensemble d'en-têtes supprime tous les autres en-têtes définis plus tôt lors du traitement de la même demande. Les noms d'en-tête sont insensibles au cas , mais les alias fournis pour les noms d'en-tête avec des tirets sont sensibles à la casse : {ContentType = "foo"}
est une forme alternative pour {["Content-Type"] = "foo"}
. body
est une chaîne facultative.
serveContent(name, parameters)
: rend un modèle à l'aide de paramètres fournis. name
est une chaîne qui nomme le modèle (tel que défini par un appel setTemplate
) et parameters
est une table avec des paramètres de modèle stockés sous forme de paires de nom / valeur (référencés sous forme de variables dans le modèle).
run([options])
: exécute le serveur à l'aide de routes configurées. Par défaut, le serveur écoute LocalHost et Port 8080. Ces valeurs peuvent être modifiées en définissant les valeurs addr
et port
dans le tableau options
.
Les exemples d'exécution nécessitent l'inclusion d'une instruction require
dans le fichier .init.lua
, qui charge le module avec chaque exemple de code, donc pour l'exemple de vitrine implémenté dans showcase.lua
, .init.lua
inclut les éléments suivants:
-- 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`
L'exemple de vitrine montre plusieurs fonctionnalités de Fullmoon:
serveAsset
)serveRedirect
)Les fichiers suivants doivent être ajoutés à Redbean Executable / Archive:
.init.lua - nécessite "la vitrine" .lua / fullmoon.lua .lua / showcase.lua
L'exemple TecheMpower met en œuvre divers types de tests pour les repères du framework Web à l'aide de FullMoon et une base de données SQLite en mémoire.
Cet exemple montre plusieurs caractéristiques Fullmoon / Redbean:
Les fichiers suivants doivent être ajoutés à Redbean Executable / Archive:
.init.lua - nécessite "Techbench" .lua / fullmoon.lua .lua / Techbench.lua
L'exemple de la carte HTMX démontre une application simple qui génère des fragments HTML livrés au client à l'aide de la bibliothèque HTMX.
Cet exemple montre plusieurs caractéristiques Fullmoon / Redbean:
Les fichiers suivants doivent être ajoutés à Redbean Executable / Archive:
.init.lua - nécessite "Htmxboard" .lua / fullmoon.lua .lua / htmxboard.lua actifs / styles.css TMPL / * - Tous les fichiers à partir des exemples / htmxboard / TMPL répertoire
Remarque 1: Étant donné que toutes les données sont stockées en mémoire, cet exemple est exécuté en mode uniprocess.
Remarque 2: Ces exemples récupèrent les bibliothèques HTMX, hyperscript et triables à partir de ressources externes, mais ces bibliothèques peuvent également être stockées en tant qu'actifs locaux, fournissant ainsi un package de distribution portable complètement autosuffisant.
L'exemple HTMX SSE montre un moyen de générer des événements de serveur (SSE) qui peuvent être diffusés à un client (qui montre des résultats à l'aide de la bibliothèque HTMX et de son extension SSE).
Cet exemple montre plusieurs caractéristiques Fullmoon / Redbean:
streamContent
)Les fichiers suivants doivent être ajoutés à Redbean Executable / Archive:
.init.lua - nécessite "htmxsse" .lua / fullmoon.lua .lua / htmxsse.lua
Chaque application FullMoon suit le même flux de base avec cinq composants principaux:
Regardons chacun des composants à partir du routage de la demande.
FullMoon gère chaque demande HTTP en utilisant le même processus:
false
ou nil
est retourné du gestionnaire d'action (et continue le processus autrement) En général, les définitions d'itinéraire lient les URL de la demande (et un ensemble de conditions) aux gestionnaires d'action (qui sont une fonction LUA régulière). Toutes les conditions sont vérifiées dans un ordre aléatoire pour chaque URL qui correspond à la définition de l'itinéraire. Dès que toute condition échoue, le traitement de l'itinéraire est interdit et l'itinéraire suivant est vérifié à une exception : toute condition peut définir la valeur otherwise
, qui déclenche une réponse avec le code d'état spécifié.
Si aucune route ne correspond à la demande, le traitement 404 par défaut est déclenché, qui peut être personnalisé en enregistrant un modèle 404 personnalisé ( fm.setTemplate("404", "My 404 page...")
).
Chaque itinéraire prend un chemin qui correspond exactement, de sorte que l'itinéraire "/hello"
correspond aux demandes pour /hello
et ne correspond pas /hell
, /hello-world
, ou /hello/world
. L'itinéraire ci-dessous répond avec "Bonjour, monde!" Pour toutes les demandes dirigées sur le chemin /hello
et renvoie 404 pour toutes les autres demandes.
fm . setRoute ( " /hello " , function ( r ) return " Hello, World! " end )
Pour correspondre à un chemin où /hello
n'est qu'une partie de celui-ci, les paramètres facultatifs et SPLAT peuvent être utilisés.
En plus des itinéraires fixes, tout chemin peut inclure des espaces réservés pour les paramètres, qui sont identifiés par A :
suivis immédiatement par le nom du paramètre:
fm . setRoute ( " /hello/:name " ,
function ( r ) return " Hello, " .. ( r . params . name ) end )
Chaque paramètre correspond à un ou plusieurs caractères sauf /
, de sorte que l'itinéraire "/hello/:name"
Matches /hello/alice
, /hello/bob
, /hello/123
et ne correspond pas /hello/bob/and/alice
(à cause de Les obstacles avant non appariés) ou /hello/
(car la longueur du fragment de correspondance est nulle).
Les noms de paramètres ne peuvent inclure que des caractères alphanumériques et _
.
Les paramètres sont accessibles à l'aide de la table de demande et de sa table params
, de sorte que r.params.name
peut être utilisé pour obtenir la valeur du paramètre name
de l'exemple précédent.
Tout fragment ou paramètre d'itinéraire spécifié peut être déclaré facultatif en l'emballage entre parenthèses:
fm . setRoute ( " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Dans l'exemple ci-dessus, les deux /hello
et /hello/Bob
sont acceptés, mais pas /hello/
, car la barre de fuite fait partie du fragment facultatif et :name
attend toujours un ou plusieurs personnages.
Tout paramètre facultatif inégalé devient false
comme sa valeur, donc dans le cas ci-dessus "Bonjour, monde!" est retourné pour l'URL de la demande /hello
.
Plus d'un paramètre facultatif peut être spécifié et les fragments facultatifs peuvent être imbriqués, donc à la fois "/posts(/:pid/comments(/:cid))"
et "/posts(/:pid)/comments(/:cid)"
sont des valeurs de route valides.
Il existe un autre type de paramètre appelé Splat qui est écrit comme *
et correspond à zéro ou plus de caractères, y compris une barre oblique avant ( /
). Le SPLAT est également stocké dans la table params
sous le nom splat
. Par exemple, la route "/download/*"
correspond /download/my/file.zip
file.zip et le splat obtient la valeur de my/file.zip
. Si plusieurs splates sont nécessaires dans la même route, alors Splats peut se voir attribuer des noms similaires à d'autres paramètres: /download/*path/*fname.zip
(bien que le même résultat puisse être obtenu en utilisant /download/*path/:fname.zip
, comme le premier SPLAT capture toutes les parties de chemin à l'exception du nom de fichier).
Tous les paramètres (y compris le SPLAT) peuvent apparaître dans n'importe quelle partie du chemin et peuvent être entourés d'un autre texte, qui doit être égalé exactement. Cela signifie que la route "/download/*/:name.:ext"
Matches /download/my/path/file.zip
et params.name
obtient file
, params.ext
obtient zip
et params.splat
obtient my/path
.
Une autre raison d'utiliser SPLAT est de permettre à plusieurs itinéraires avec le même chemin d'être enregistrés dans le système. L'implémentation actuelle écrase les voies avec le même nom et pour éviter qu'un SPLAT nommé puisse être utilisé pour créer des chemins uniques. Par exemple,
fm . setRoute ( " /*dosomething1 " , function ( r ) return " something 1 " end )
fm . setRoute ( " /*dosomething2 " , function ( r ) return " something 2 " end )
Cela peut être utilisé dans des situations lorsqu'il existe un ensemble de conditions qui doivent être vérifiés dans le gestionnaire d'action et bien qu'il puisse être possible de combiner les deux itinéraires en un seul, il est parfois plus propre de les séparer.
La valeur par défaut des paramètres est tous les caractères (sauf /
) de longueur une ou plusieurs. Pour spécifier un ensemble différent de caractères valides, il peut être ajouté à la fin du nom de la variable; Par exemple, l'utilisation :id[%d]
au lieu de :id
modifie le paramètre pour correspondre uniquement aux chiffres.
fm . setRoute ( " /hello(/:id[%d]) " ,
function ( r ) return " Hello, " .. ( r . params . id or " World! " ) end )
Les classes de caractères LUA suivantes sont prises en charge: %w
, %d
, %a
, %l
, %u
et %x
; Tout caractère de ponctuation (y compris %
et ]
) peut également être échappé avec %
. Les classes négatives (écrites en Lua en %W
) ne sont pas prises en charge , mais la syntaxe non dans le set est prise en charge, donc [^%d]
correspond à un paramètre qui n'inclut aucun chiffre.
Notez que le nombre de répétitions ne peut pas être modifié (donc :id[%d]*
n'est pas un moyen valable d'accepter des chiffres zéro ou plus), car les seuls ensembles sont autorisés et les valeurs acceptent toujours un ou plusieurs caractères. Si plus de flexibilité pour décrire des formats acceptables est nécessaire, les validateurs personnalisés peuvent être utilisés pour étendre la logique de correspondance.
Les paramètres de requête et de formulaire sont accessibles de la même manière que les paramètres de chemin à l'aide du tableau params
dans la table request
qui est transmise à chaque gestionnaire d'action. Notez que s'il existe un conflit entre les noms de paramètre et de requête / formulaire, les noms de paramètres ont la priorité .
Il existe un cas spécial qui peut entraîner un tableau renvoyé au lieu d'une valeur de chaîne: si le nom du paramètre Query / Form se termine par []
, tous les résultats correspondants (un ou plusieurs) sont renvoyés en tant que table. Par exemple, pour une chaîne de requête a[]=10&a[]&a[]=12&a[]=
la valeur de params["a[]"]
est {10, false, 12, ""}
.
Comme l'écriture de ces noms de paramètres peut nécessiter plusieurs supports, params.a
peuvent être utilisés comme raccourci pour params["a[]"]
avec les deux formulaires renvoyant le même tableau.
Les paramètres en multiparcules sont également traités lorsqu'ils sont demandés et sont accessibles de la même manière que le reste des paramètres à l'aide de la table params
. Par exemple, les paramètres avec des noms simple
et more
peuvent être récupérés à partir d'un message avec le type de contenu multipart/form-data
à l'aide de params.simple
et params.more
.
Comme une partie du contenu en multipar en multiples peut inclure des en-têtes et des paramètres supplémentaires au sein de ces en-têtes, ils sont accessibles avec un champ multipart
du tableau params
:
fm . setRoute ({ " /hello " , simple = " value " }, function ( r )
return " Show " .. r . params . simple .. " " .. r . params . multipart . more . data )
end )
La table multipart
comprend toutes les parties du message en multiparte (il peut donc être itéré à l'aide ipairs
), mais il permet également d'accéder à l'utilisation de noms de paramètres ( params.multipart.more
). Chacun des éléments est également un tableau qui comprend les champs suivants:
nil
sinon.nil
sinon. Ce traitement en multipar en multiples consomme tous les sous-types en plusieurs types et gère les messages en multipar en multiples. Il insère également une pièce avec une valeur Content-ID
correspondant au paramètre start
dans la première position.
Malgré tous les exemples antérieurs montrant un seul itinéraire, c'est rarement le cas dans les applications réelles; Lorsque plusieurs itinéraires sont présents, ils sont toujours évalués dans l'ordre dans lequel ils sont enregistrés .
Un appel setRoute
peut également définir plusieurs itinéraires lorsqu'ils ont le même ensemble de conditions et partager le même gestionnaire d'action:
fm . setRoute ({ " /route1 " , " /route2 " }, handler )
Cela équivaut à deux appels définissant chaque itinéraire individuellement:
fm . setRoute ( " /route1 " , handler )
fm . setRoute ( " /route2 " , handler )
Étant donné que les itinéraires sont évalués dans l'ordre dans lequel ils sont définis, les itinéraires plus sélectifs doivent être définis en premier, sinon ils peuvent ne pas avoir la chance d'être évalués:
fm . setRoute ( " /user/bob " , handlerBob )
fm . setRoute ( " /user/:name " , handlerName )
Si les itinéraires sont définis dans l'ordre opposé, /user/bob
peut ne jamais être vérifié tant que le gestionnaire d'action "/user/:name"
renvoie un résultat non false
.
Comme décrit précédemment, si aucune des itinéraires ne correspond, une réponse avec un code d'état 404 est renvoyée. Il peut y avoir des cas où cela n'est pas souhaitable; Par exemple, lorsque l'application comprend des scripts LUA pour gérer les demandes qui ne sont pas explicitement enregistrées en tant que routes. Dans ces cas, un itinéraire de fourre-tout peut être ajouté qui implémente le traitement Redbean par défaut (le nom du paramètre SPLAT n'est utilisé que pour désambiguïter cette voie sur d'autres routes /*
qui peuvent être utilisées ailleurs):
fm . setRoute ( " /*catchall " , fm . servePath )
Chaque itinéraire peut être fourni avec un nom facultatif, qui est utile pour référencer cette voie lorsque son URL doit être générée en fonction de valeurs de paramètres spécifiques. La fonction makePath
fournie accepte soit un nom de route ou une URL de route elle-même ainsi que la table des paramètres et renvoie un chemin avec des espaces réservés de paramètres peuplés:
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
Si deux itinéraires utilisent le même nom, le nom est associé à celui qui a été enregistré en dernier, mais les deux itinéraires sont toujours présents.
Le nom de la route peut également être utilisé avec des routes externes / statiques qui ne sont utilisées que pour la génération d'URL.
Si l'itinéraire n'est utilisé que pour la génération de chemins, il n'a même pas besoin d'avoir un gestionnaire de routes:
fm . setRoute ({ " https://youtu.be/:videoid " , routeName = " youtube " })
fm . makePath ( " youtube " , { videoid = " abc " }) -- > https://youtu.be/abc
Un itinéraire sans aucun gestionnaire d'action est ignoré pendant le processus de correspondance de l'itinéraire.
Les voies internes permettent de rediriger un ensemble d'URL vers une autre. L'URL cible peut indiquer une ressource statique ou un script .lua
. Par exemple, si les demandes d'un emplacement doivent être redirigées vers une autre, la configuration suivante redirige les demandes de ressources sous /blog/
URL vers les personnes sous /new-blog/
URL tant que la ressource cible existe:
fm . setRoute ( " /blog/* " , " /new-blog/* " )
Cet itinéraire accepte une demande de /blog/post1
et sert /new-blog/post1
comme réponse, tant que l'actif /new-blog/post1
existe. Si l'actif n'existe pas, la route suivante est vérifiée. De même, l'utilisation fm.setRoute("/static/*", "/*")
provoque des demandes pour /static/help.txt
à servir la ressource /help.txt
.
Les deux URL peuvent inclure des paramètres remplis si résolus:
fm . setRoute ( " /blog/:file " , " /new-blog/:file.html " ) -- <<-- serve "nice" URLs
fm . setRoute ( " /new-blog/:file.html " , fm . serveAsset ) -- <<-- serve original URLs
Cet exemple résout les URL "belles" desservant leurs versions "HTML". Notez que cela ne déclenche pas la redirection côté client en renvoyant le code d'état 3xx
, mais gère plutôt le réécoulement en interne. Notez également que la deuxième règle est nécessaire pour servir les URL "d'origine", car ils ne sont pas gérés par la première règle, car si la demande est pour /blog/mylink.html
, alors l'URL redirigé est /new-blog/mylink.html.html
, qui n'existe pas probablement, donc l'itinéraire est ignoré et le suivant est vérifié. Si la manipulation des séparateurs de chemin est également requise, *path
peut être utilisé à la place :file
, car *
permet des séparateurs de chemin.
Si une application doit exécuter différentes fonctions en fonction des valeurs spécifiques des attributs de demande (par exemple, une méthode), cette bibliothèque fournit deux options principales: (1) Vérifiez la valeur d'attribut un gestionnaire d'action (par exemple, en utilisant request.method == "GET"
chèque) et (2) Ajouter une condition qui filtre les demandes de telle sorte que seules les demandes en utilisant la valeur d'attribut spécifiée atteignent le gestionnaire d'action. Cette section décrit plus en détail la deuxième option.
Chaque itinéraire enregistré par défaut répond à toutes les méthodes HTTP (obtenir, mettre, publier, etc.), mais il est possible de configurer chaque itinéraire pour répondre uniquement à des méthodes HTTP spécifiques:
fm . setRoute ( fm . GET " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Dans ce cas, la syntaxe fm.GET"/hello(/:name)"
configure l'itinéraire pour accepter uniquement les demandes GET
. Cette syntaxe équivaut à passer une table avec l'itinéraire et à toute condition de filtrage supplémentaire:
fm . setRoute ({ " /hello(/:name) " , method = " GET " },
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Si plusieurs méthodes doivent être spécifiées, une table avec une liste de méthodes peut être transmise au lieu d'une valeur de chaîne:
fm . setRoute ({ " /hello(/:name) " , method = { " GET " , " POST " }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Chaque itinéraire qui permet également une demande GET
(implicitement) permet une demande HEAD
et cette demande est traitée en renvoyant tous les en-têtes sans envoyer le corps lui-même. Si pour une raison quelconque, cette manipulation implicite n'est pas souhaitable, alors ajouter HEAD = false
à la table de méthode le désactive (comme dans method = {"GET", "POST", HEAD = false}
).
Notez que les demandes avec les méthodes non correspondantes ne sont pas rejetées, mais passent plutôt pour être vérifiées par d'autres itinéraires et déclencher le code d'état 404 renvoyé s'ils ne sont pas appariés (à une exception).
En plus de method
, d'autres conditions peuvent être appliquées à l'aide host
, de la clientèle clientAddr
, serverAddr
, scheme
, des en-têtes de demande et des paramètres. Par exemple, la spécification name = "Bob"
comme l'une des conditions garantit que la valeur du paramètre name
est "bob" pour que le gestionnaire d'action soit appelé.
Tout en-tête de demande peut être vérifié à l'aide du nom d'en-tête comme clé, donc ContentType = "multipart/form-data"
est satisfait si la valeur de l'en-tête Content-Type
est multipart/form-data
. Notez que la valeur d'en-tête peut inclure d'autres éléments (une limite ou un charme dans le cadre de la valeur Content-Type
) et seul le type de support réel est comparé.
Étant donné que les noms pour les en-têtes, les paramètres et les propriétés peuvent se chevaucher, ils sont vérifiés dans l'ordre suivant:
ContentType
,method
, port
, host
, etc.) et L'en-tête Host
est également vérifié en premier (bien qu'il soit un seul mot), ce qui fait référence à des filtres Host
basés sur l' Host
d'en-tête, tout en faisant référence aux filtres host
basés sur l' host
de la propriété.
Les valeurs de chaîne ne sont pas les seules valeurs qui peuvent être utilisées dans les routes conditionnelles. Si plus d'une valeur est acceptable, le passage d'un tableau permet de fournir une liste de valeurs acceptables. Par exemple, si Bob
et Alice
sont des valeurs acceptables, alors name = {Bob = true, Alice = true}
l'exprime comme une condition.
Deux valeurs spéciales passées dans un tableau permettent d'appliquer une validation regex ou de modèle :
regex
: accepte une chaîne qui a une expression régulière. Par exemple, name = {regex = "^(Bob|Alice)$"}
a le même résultat que le chèque de hachage affiché plus tôt dans cette sectionpattern
: accepte une chaîne avec une expression de motif LUA. Par exemple, name = {pattern = "^%u%l+$"}
accepte des valeurs qui commencent par un caractère majuscule suivi d'un ou plusieurs caractères minuscules. Ces deux vérifications peuvent être combinées avec le contrôle de la table d'existence: name = {Bob = true, regex = "^Alice$"}
accepte les valeurs Bob
et Alice
. Si la première vérification de l'existence du tableau échoue, les résultats de l'expression regex
ou pattern
sont renvoyés.
Le dernier type de validateur personnalisé est une fonction. La fonction fournie reçoit la valeur à valider et son résultat est évalué comme false
ou true
. Par exemple, Passing id = tonumber
garantit que la valeur id
est un nombre. Comme autre exemple, clientAddr = fm.isLoopbackIp
garantit que l'adresse client est une adresse IP de bouclage.
fm . setRoute ({ " /local-only " , clientAddr = fm . isLoopbackIp },
function ( r ) return " Local content " end )
Comme la fonction Validator peut être générée dynamiquement, cela fonctionne également:
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 )
Il est important de garder à l'esprit que la fonction Validator renvoie réellement une fonction qui est appelée lors d'une demande d'application du chèque. Dans l'exemple précédent, la fonction renvoyée accepte une valeur d'en-tête et la compare à la limite transmise pendant sa création.
Dans certains cas, ne pas satisfaire à une condition est une raison suffisante pour renvoyer une réponse au client sans vérifier d'autres itinéraires. Dans un cas comme celui-ci, la définition de la valeur otherwise
sur un nombre ou une fonction renvoie une réponse avec l'état spécifié ou le résultat de la fonction:
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 )
Dans cet exemple, le moteur de routage correspond à l'itinéraire, puis valide les deux conditions en comparant la valeur de la méthode avec POST
et la valeur de l'en-tête Content-Length
avec le résultat de la fonction isLessThan
. Si l'une des conditions ne correspond pas, le code d'état spécifié par la valeur otherwise
est renvoyé avec le reste de la réponse.
Si la condition otherwise
doit s'appliquer uniquement à la vérification ContentLength
, la valeur otherwise
avec la fonction Validator peut être déplacée vers un tableau associé à la vérification ContentLength
:
fm . setRoute ( fm . POST { " /upload " ,
ContentLength = { isLessThan ( 100000 ), otherwise = 413 }
}, function ( r ) ... handle the upload ... end )
La différence entre les deux derniers exemples est que dans cet exemple, seul la rupture de vérification ContentLength
déclenche la réponse 413 (et toutes les autres méthodes tombent sur d'autres itinéraires), tandis que dans la method
précédente et les échecs de vérification ContentLength
, déclenchent la même réponse 413.
Notez que lorsque la valeur vérifiée est nil
, le contrôle par rapport à un tableau est considéré comme valide et l'itinéraire est accepté. Par exemple, une vérification d'un paramètre facultatif réalisé contre une chaîne ( name = "Bo"
) échoue si la valeur de params.name
est nil
, mais passe si le même chèque est effectué par rapport à un tableau ( name = {Bo=true, Mo=true}
), y compris les vérifications regex / motifs. Si cela n'est pas souhaitable, une fonction de validatrice personnalisée peut explicitement vérifier la valeur attendue.
Considérez l'exemple suivant:
fm . setRoute ({ " /hello(/:name) " ,
method = { " GET " , " POST " , otherwise = 405 }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Dans ce cas, si ce point de terminaison est accessible avec la méthode PUT
, alors au lieu de vérifier d'autres itinéraires (car la condition method
n'est pas satisfaite), le code d'état 405 est renvoyé, comme configuré avec la valeur otherwise
spécifiée. Comme documenté ailleurs, cet itinéraire accepte également une demande HEAD
(même lorsqu'il n'est pas répertorié), car une demande GET
est acceptée.
Lorsque le code d'état 405 (mauvaise méthode) est renvoyé et que l'en-tête Allow
n'est pas défini, il est défini sur la liste des méthodes autorisées par l'itinéraire. Dans le cas ci-dessus, il est défini pour GET, POST, HEAD, OPTIONS
, car ce sont les méthodes autorisées par cette configuration. Si la valeur otherwise
est une fonction (plutôt qu'un nombre), renvoyer un résultat approprié et définir l'en-tête Allow
est la responsabilité de cette fonction.
La valeur otherwise
peut également être définie sur une fonction, ce qui offre plus de flexibilité que de simplement définir un code d'état. Par exemple, la définition otherwise = fm.serveResponse(413, "Payload Too Large")
déclenche une réponse avec le code d'état et le message spécifié.
Le traitement de la validation du formulaire nécessite souvent de spécifier un ensemble de conditions pour le même paramètre et un message d'erreur personnalisé qui peut avoir besoin d'être renvoyé lorsque les conditions ne sont pas remplies et celles-ci sont fournies par des validateurs spéciaux renvoyés par makeValidator
Fonction:
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 )
Dans cet exemple, le validateur est configuré pour vérifier deux paramètres - "nom" et "mot de passe" - pour leurs longueurs min et max et renvoyer un message lorsque l'un des paramètres échoue le chèque.
Étant donné que le chèque d'échec fait sauter l'itinéraire, fournissant la valeur otherwise
permet de retourner l'erreur dans le cadre de la réponse:
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 ,
}
Dans ce cas, le gestionnaire otherwise
reçoit le message d'erreur (ou une table avec des messages si elle est demandée en passant l'option all
couvertes ci-dessous) qui peut être fournie comme paramètre de modèle et renvoyée au client.
Une autre option consiste à appeler la fonction Validator directement dans un gestionnaire d'action et à renvoyer ses résultats:
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 )
Dans cet exemple, le validateur est appelé directement et a passé un tableau ( r.params
) avec toutes les valeurs de paramètres pour permettre à la fonction validatrice de vérifier les valeurs par rapport aux règles spécifiées.
La fonction Validator renvoie ensuite true
dans le succès du signal ou nil, error
pour signaler un incapacité à vérifier l'une des règles. Cela permet à l'appel de validateur d'être enveloppé dans une assert
si le script doit renvoyer une erreur immédiatement:
assert ( validator ( r . params )) -- throw an error if validation fails
return fm . serveRedirect ( 307 , " / " ) -- return redirect in other cases
Les vérifications de validateur suivantes sont disponibles:
minlen
: (entier) vérifie la longueur minimale d'une chaîne.maxlen
: (entier) vérifie la longueur maximale d'une chaîne.test
: (Fonction) appelle une fonction qui passe un paramètre et devrait renvoyer true
ou nil | false [, error]
.oneof
: ( value | { table of values to be compared against }
) vérifie si le paramètre correspond à l'une des valeurs fournies.pattern
: (String) vérifie si le paramètre correspond à une expression de motif LUA.En plus des chèques, les règles peuvent inclure des options:
optional
: (bool) rend un paramètre facultatif lorsqu'il est nil
. Tous les paramètres sont requis par défaut, donc cette option permet de sauter les règles lorsque le paramètre n'est pas fourni. Toutes les règles sont toujours appliquées si le paramètre n'est pas nulle.msg
: (String) ajoute un message client pour cela si l'un de ses chèques échoue, qui écrase les messages des chèques individuels. Le message peut inclure un espace réservé ( %s
), qui va être remplacé par un nom de paramètre.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. Par exemple,
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.