Fullmoonは、Redbeanに基づく高速で最小限のWebフレームワークです。これは、ポータブルで単一ファイル分散型Webサーバーです。
開発と配布に必要なものはすべて、外部依存関係のない単一のファイルにあり、Windows、Linux、またはMacOでRedbeanを走行した後に梱包後に行われます。以下は、フルムーンアプリケーションの完全な例です。
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 ()
RedBeanでパッケージ化された後、 ./redbean.com
を使用して起動できます。これは、http:// localhost:8080/hello/worldに送信されたhttp(s)リクエストに「hello、world」を返すサーバーを起動します。
RedBeanは、ユニークで強力な品質を備えた単一ファイル分散クロスプラットフォームWebサーバーです。 LuaベースのWebフレームワーク(Lapis、Lor、Sailor、Pegasusなど)がいくつかありますが、それらのどれもRedbeanと統合されていません(ただし、実験的なフレームワークAnpanはあります)。
Fullmoonは、RedBeanが最も簡単で最も効率的な方法で拡張および増強することにより、RedBeanが提供するすべての機能を紹介するという観点から書かれた軽量でミニマルなWebフレームワークです。速く動作し、バッテリーが付属しています(ルート、テンプレート、JSON世代など)。
FullmoonはLua哲学に従い、必要に応じて組み合わせて構築する基礎として使用する最小限のツールセットを提供します。
fork
、 socket
、共有メモリなど次のコマンドを実行してRedBeanのコピーをダウンロードします(これらのコマンドをWindowsで実行する場合は2番目のコマンドをスキップします):
curl -o redbean.com https://redbean.dev/redbean-2.2.com
chmod +x redbean.com
最新のバージョン番号は、次のリクエストで取得できます。
curl https://redbean.dev/latest.txt
別のオプションは、ソースビルドの指示に従って、ソースからレッドビーンを構築することです。
fullmoon.lua
を.lua/
ディレクトリにコピーします.init.lua
という名前のファイルに保存します(たとえば、説明に示されているLUAコード)。別のオプションは、アプリケーションコードを別のファイル( .lua/myapp.lua
など)に配置し、 .init.lua
にrequire "myapp"
とすることです。これは、含まれているすべての例がどのように表示されるかです。
zip redbean.com .init.lua .lua/fullmoon.lua
上記のように、アプリケーションコードが別のLUAファイルに保存されている場合は、 .lua/
ディレクトリ内に配置し、そのファイルもzipしてください。
./redbean.com
このコマンドがLinuxで実行され、通訳が見つからないというエラーをスローする場合、次のコマンドを実行して修正する必要があります(ただし、システムの再起動に耐えられない可能性があることに注意してください)。
sudo sh -c " echo ':APE:M::MZqFpD::/bin/sh:' >/proc/sys/fs/binfmt_misc/register "
このコマンドがRedbean 2.xを使用するときにWSLまたはワインに不可解なエラーを生成する場合、binfmt_miscを無効にすることで修正される場合があります。
sudo sh -c ' echo -1 >/proc/sys/fs/binfmt_misc/status '
http:// localhost:8080/hello/worldを指すブラウザを起動すると、「hello、world」を返す必要があります(アプリケーションが導入部に示されているコードまたは使用セクションにあるコードを使用していると仮定します)。
最も単純な例では、(1)モジュールをロードし、(2)1つのルートを構成し、(3)アプリケーションを実行する必要があります。
local fm = require " fullmoon " -- (1)
fm . setRoute ( " /hello " , function ( r ) return " Hello, world " end ) -- (2)
fm . run () -- (3)
このアプリケーションは、「hello、world」コンテンツ(および200ステータスコード)を返して、 /hello
urlのリクエストに応答し、他のすべてのリクエストに対して404ステータスコードで応答します。
setRoute(route[, action])
:ルートを登録します。 route
が文字列の場合、要求パスを比較するためのルート式として使用されます。テーブルの場合、その要素はルートとして使用される文字列であり、ハッシュ値はルートがチェックされる条件です。 2番目のパラメーターが関数の場合、すべての条件が満たされた場合に実行されます。文字列の場合、ルート式として使用され、リクエストは指定されたルートで送信されるかのように処理されます(内部リダイレクトとして機能します)。条件が満たされていない場合は、次のルートがチェックされます。ルート式には、複数のパラメーターとオプションの部分を持つことができます。アクションハンドラーは、ヘッダー、Cookie、セッションだけでなく、リクエストおよびルーティングパラメーターへのアクセスを提供するリクエストテーブルを受け入れます。
setTemplate(name, template[, parameters])
:指定された名前またはディレクトリからテンプレートのセットを使用してテンプレートを登録します。 template
が文字列の場合、テンプレートハンドラーにコンパイルされます。関数の場合、テンプレートのレンダリングが要求されるときに保存および呼び出されます。テーブルの場合、最初の要素はテンプレートまたは関数であり、残りはオプションとして使用されます。たとえば、 ContentType
オプションの1つとして指定すると、生成されたコンテンツのContent-Type
ヘッダーが設定されます。いくつかのテンプレート( 500
、 json
、その他)はデフォルトで提供され、上書きできます。 parameters
は、名前/値のペアとして保存されたテンプレートパラメーターを備えたテーブルです(テンプレート内の変数として参照)。
serveResponse(status[, headers][, body])
:提供されたstatus
、 headers
、およびbody
値を使用してHTTP応答を送信します。 headers
HTTPヘッダー名/値ペアが入力されたオプションのテーブルです。提供されている場合、このヘッダーのセットは、同じリクエストの処理中に早期に設定された他のすべてのヘッダーを削除します。ヘッダー名{["Content-Type"] = "foo"}
ケース非感受性ですが、ダッシュを備えたヘッダー名のエイリアスを提供することは{ContentType = "foo"}
ケースセンシティブです。 body
オプションの文字列です。
serveContent(name, parameters)
:提供されたパラメーターを使用してテンプレートをレンダリングします。 name
は、テンプレートに名前を付ける文字列( setTemplate
呼び出しで設定)であり、 parameters
名前/値ペア(テンプレートの変数として参照)として保存されたテンプレートパラメーターを備えたテーブルです。
run([options])
:構成されたルートを使用してサーバーを実行します。デフォルトでは、サーバーはLocalHostとポート8080でリッスンします。これらの値は、 options
テーブルにaddr
とport
値を設定することで変更できます。
実行の例では、各サンプルコードをモジュールにロードする.init.lua
ファイルにrequire
ステートメントを含む必要があります。したがって、 showcase.lua
、 .init.lua
に実装されたショーケースの例には、以下が含まれます。
-- 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`
ショーケースの例は、いくつかのフルムーン機能を示しています。
serveAsset
を使用)serveRedirect
を使用)次のファイルをRedBean実行可能ファイル/アーカイブに追加する必要があります。
.init.lua-「showcase」が必要です .lua/fullmoon.lua .lua/showcase.lua
TechMpowerの例は、FullMoonとIn-Memory SQLiteデータベースを使用して、Webフレームワークベンチマークのさまざまなテストタイプを実装しています。
この例は、いくつかのフルムーン/レッドビーンの機能を示しています。
次のファイルをRedBean実行可能ファイル/アーカイブに追加する必要があります。
.init.lua-「Techbench」が必要です .lua/fullmoon.lua .lua/techbench.lua
HTMXボードの例は、HTMXライブラリを使用してクライアントに配信されるHTMLフラグメントを生成する簡単なアプリケーションを示しています。
この例は、いくつかのフルムーン/レッドビーンの機能を示しています。
次のファイルをRedBean実行可能ファイル/アーカイブに追加する必要があります。
.init.lua-「htmxboard」が必要です .lua/fullmoon.lua .lua/htmxboard.lua Assets/Styles.css tmpl/* - Examples/htmxboard/tmplディレクトリからのすべてのファイル
注1:すべてのデータがメモリに保存されるため、この例はユニプロセスモードで実行されます。
注2:この例は、外部リソースからHTMX、ハイパースクリプト、およびソート可能なライブラリを取得しますが、これらのライブラリはローカル資産として保存することもできます。
HTMX SSEの例は、クライアントにストリーミングできるサーバーセントイベント(SSE)を生成する方法を示しています(HTMXライブラリとそのSSE拡張機能を使用して結果を示します)。
この例は、いくつかのフルムーン/レッドビーンの機能を示しています。
streamContent
を使用)次のファイルをRedBean実行可能ファイル/アーカイブに追加する必要があります。
.init.lua-「htmxsse」が必要です .lua/fullmoon.lua .lua/htmxsse.lua
各フルムーンアプリケーションは、5つの主要なコンポーネントを持つ同じ基本フローに従います。
リクエストルーティングから始まる各コンポーネントを見てみましょう。
FullMoonは、同じプロセスを使用して各HTTP要求を処理します。
false
またはnil
以外のものがアクションハンドラーから返された場合に応答を提供します(そして、それ以外のプロセスを継続します)一般に、ルート定義は、アクションハンドラー(これは通常のLUA関数です)にリクエストURL(および一連の条件)をバインドします。すべての条件は、ルート定義に一致する各URLのランダムな順序でチェックされます。条件が失敗するとすぐに、ルート処理が中止され、次のルートが1つの例外でチェックされます。任意の条件は、指定されたステータスコードで応答をトリガーotherwise
値を設定できます。
リクエストと一致するルートがない場合、デフォルトの404処理がトリガーされます。これは、カスタム404テンプレート( fm.setTemplate("404", "My 404 page...")
)を登録することでカスタマイズできます。
各ルートは正確に一致するパスを使用するため、ルート"/hello"
は/hello
のリクエストと一致し、 /hell
、 /hello-world
、または/hello/world
と一致しません。以下のルートは、「こんにちは、ワールド!」で応答します。 /hello
Pathに向けられたすべてのリクエストについて、他のすべてのリクエストに対して404を返します。
fm . setRoute ( " /hello " , function ( r ) return " Hello, World! " end )
/hello
その一部に過ぎないパスを一致させるには、オプションのパラメーターとSPLATを使用できます。
固定ルートに加えて、任意のパスにはパラメーターのプレースホルダーが含まれる場合があります:
パラメーターは次のとおりです。
fm . setRoute ( " /hello/:name " ,
function ( r ) return " Hello, " .. ( r . params . name ) end )
各パラメーターは/
除く1つ以上の文字と一致するため、ルート"/hello/:name"
mates /hello/alice
、 /hello/bob
、 /hello/123
で、 /hello/bob/and/alice
(一致していないフォワードスラッシュ)または/hello/
(一致していないフラグメントの長さがゼロであるため)。
パラメーター名には、英数字の文字と_
を含めることができます。
パラメーターには、リクエストテーブルとそのparams
テーブルを使用してアクセスできます。そのため、 r.params.name
使用して、以前の例からname
パラメーターの値を取得できます。
指定されたルートフラグメントまたはパラメーターは、括弧内に包むことでオプションとして宣言できます。
fm . setRoute ( " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
上記の例では、 /hello
:name
/hello/Bob
両方が受け入れられますが、 /hello/
ではなく受け入れられます。
比類のないオプションのパラメーターは、その値としてfalse
になるため、上記の「Hello、World!」 /hello
request urlに対して返されます。
複数のオプションのパラメーターを指定し、オプションのフラグメントをネストできるため、 "/posts(/:pid/comments(/:cid))"
と"/posts(/:pid)/comments(/:cid)"
ネストできます。有効なルート値です。
Splatと呼ばれる別の種類のパラメーターがあり、 *
と書かれており、フォワードスラッシュ( /
)を含むゼロ以上の文字に一致します。スプラットは、 splat
名の下にparams
テーブルにも保存されます。たとえば、ルート"/download/*"
Matches /download/my/file.zip
file.zipとSplatはmy/file.zip
の値を取得します。同じルートで複数のスプラットが必要な場合、スプラットは他のパラメーターと同様の名前を割り当てることができます: /download/*path/*fname.zip
(ただし同じ結果は/download/*path/:fname.zip
を使用して達成できます。 、最初のSplatがファイル名を除くすべてのパス部分をキャプチャするように)。
すべてのパラメーター(SPLATを含む)は、パスの任意の部分に表示され、他のテキストに囲まれます。これは正確に一致する必要があります。これは、ルート"/download/*/:name.:ext"
Matches /download/my/path/file.zip
およびparams.name
file
を取得し、 params.ext
zip
を取得し、 params.splat
my/path
値を取得することを意味します。
Splatを使用するもう1つの理由は、同じパスを持つ複数のルートをシステムに登録できるようにすることです。現在の実装では、同じ名前のルートを上書きし、名前付きのスプラットを使用して一意のパスを作成できることを避けるためです。例えば、
fm . setRoute ( " /*dosomething1 " , function ( r ) return " something 1 " end )
fm . setRoute ( " /*dosomething2 " , function ( r ) return " something 2 " end )
これは、アクションハンドラーでチェックする必要がある条件のセットがあり、両方のルートを1つに組み合わせることができる場合に使用できます。
パラメーターのデフォルト値は、長さ1以上のすべての文字( /
除く)です。異なる有効な文字のセットを指定するには、変数名の最後に追加できます。たとえば、使用:id[%d]
の代わりに:id
パラメーターを変更して数字のみに一致します。
fm . setRoute ( " /hello(/:id[%d]) " ,
function ( r ) return " Hello, " .. ( r . params . id or " World! " ) end )
次のLUA文字クラスがサポートされています: %w
、 %d
、 %a
、 %l
、 %u
、および%x
。句読点( %
および]
を含む)は、 %
で逃げることもできます。ネガティブクラス(LUAで%W
として記述)はサポートされていませんが、セットではない構文はサポートされているため、 [^%d]
数字を含まないパラメーターと一致します。
繰り返しの数は変更できないことに注意してください(so :id[%d]*
ゼロまたは数桁を受け入れる有効な方法ではありません)。セットのみが許可され、値は1つ以上の文字を受け入れるためです。許容できる形式を説明する柔軟性が必要な場合は、カスタムバリエーターを使用して一致するロジックを拡張できます。
クエリおよびフォームパラメーターは、各アクションハンドラーに渡されるrequest
テーブルのparams
テーブルを使用して、パスパラメーターと同じ方法でアクセスできます。パラメーターとクエリ/フォーム名の間に競合がある場合、パラメーター名が優先されることに注意してください。
文字列値の代わりにテーブルが返される可能性のある特別なケースが1つあります。クエリ/フォームパラメーター名が[]
で終了する場合、すべての一致する結果(1つ以上)がテーブルとして返されます。たとえば、クエリ文字列a[]=10&a[]&a[]=12&a[]=
params["a[]"]
の値は{10, false, 12, ""}
です。
これらのパラメーター名を書くことにはいくつかのブラケットが必要になる場合があるため、 params.a
、両方のフォームが同じテーブルを返すparams["a[]"]
のショートカットとして使用できます。
マルチパートパラメーターも要求されたときに処理され、 params
テーブルを使用して他のパラメーターと同じ方法でアクセスできます。たとえば、名前がsimple
で、 more
パラメーターは、 params.simple
およびparams.more
使用してmultipart/form-data
コンテンツタイプを使用してメッセージから取得できます。
マルチパートコンテンツの一部には、それらのヘッダー内に追加のヘッダーとパラメーターが含まれる可能性があるため、 params
テーブルのmultipart
フィールドでアクセスできます。
fm . setRoute ({ " /hello " , simple = " value " }, function ( r )
return " Show " .. r . params . simple .. " " .. r . params . multipart . more . data )
end )
multipart
テーブルには、MultiPartメッセージのすべての部分が含まれています(そのため、 ipairs
使用して繰り返すことができます)が、パラメーター名( params.multipart.more
)を使用してアクセスすることもできます。各要素は、次のフィールドを含むテーブルでもあります。
nil
。nil
。このマルチパート処理は、マルチパートサブタイプとハンドルの再帰的マルチパートメッセージを消費します。また、 start
パラメーターを最初の位置に一致させるContent-ID
値を持つ部品を挿入します。
単一のルートを示すすべての以前の例にもかかわらず、実際のアプリケーションではめったにケースではありません。複数のルートが存在する場合、それらは常に登録されている順序で評価されます。
1つのsetRoute
コールは、同じ条件セットがある場合に複数のルートを設定し、同じアクションハンドラーを共有することもできます。
fm . setRoute ({ " /route1 " , " /route2 " }, handler )
これは、各ルートを個別に設定する2つの呼び出しに相当します。
fm . setRoute ( " /route1 " , handler )
fm . setRoute ( " /route2 " , handler )
ルートが設定されている順序で評価されていることを考えると、より選択的なルートを最初に設定する必要があります。そうしないと、評価する機会が得られない場合があります。
fm . setRoute ( " /user/bob " , handlerBob )
fm . setRoute ( " /user/:name " , handlerName )
ルートが反対の順序で設定されている場合、 /user/bob
"/user/:name"
アクションハンドラーがfalse
結果を返している限り、チェックされることはありません。
前述のように、ルートが一致しない場合、404ステータスコードの応答が返されます。これが望ましくない場合がある場合があります。たとえば、アプリケーションにルートとして明示的に登録されていないリクエストを処理するLUAスクリプトが含まれている場合。そのような場合、デフォルトのRedbean処理を実装するキャッチオールルートを追加できます(Splatパラメーターの名前は、他の場所で使用できる他の/*
ルートに対してこのルートを乱用するためにのみ使用されます):
fm . setRoute ( " /*catchall " , fm . servePath )
各ルートには、オプションの名前を付けることができます。これは、特定のパラメーター値に基づいてURLを生成する必要がある場合にそのルートを参照するのに役立ちます。 makePath
関数は、ルート名またはルートURL自体とパラメーターテーブルのいずれかを受け入れ、人口のあるパラメータープレースホルダーを使用してパスを返します。
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
2つのルートが同じ名前を使用している場合、名前は最後に登録されたルートに関連付けられていますが、両方のルートがまだ存在します。
ルート名は、URL生成にのみ使用される外部/静的ルートでも使用できます。
ルートがパス生成にのみ使用されている場合、ルートハンドラーを持つ必要さえありません。
fm . setRoute ({ " https://youtu.be/:videoid " , routeName = " youtube " })
fm . makePath ( " youtube " , { videoid = " abc " }) -- > https://youtu.be/abc
アクションハンドラーのないルートは、ルートマッチングプロセス中にスキップされます。
内部ルートにより、1つのURLを別のURLにリダイレクトすることができます。ターゲットURLは、静的リソースまたは.lua
スクリプトを指すことができます。たとえば、ある場所のリクエストを別の場所にリダイレクトする必要がある場合、ターゲットリソースが存在する限り、 /new-blog/
urlの下にある / /blog/
urlのリソースのリソースを次の構成にリダイレクトします。
fm . setRoute ( " /blog/* " , " /new-blog/* " )
このルートは、 /new-blog / /blog/post1
/new-blog/post1
のリクエストを受け入れ、その応答として/new-blog/post1
を提供します。資産が存在しない場合、次のルートがチェックされます。同様に、 fm.setRoute("/static/*", "/*")
を使用すると、 /static/help.txt
がリソース/help.txt
にリクエストを行います。
両方のURLには、解決された場合に記入されるパラメーターを含めることができます。
fm . setRoute ( " /blog/:file " , " /new-blog/:file.html " ) -- <<-- serve "nice" URLs
fm . setRoute ( " /new-blog/:file.html " , fm . serveAsset ) -- <<-- serve original URLs
この例は、「HTML」バージョンを提供する「素敵な」URLを解決します。これにより、 3xx
ステータスコードを返すことでクライアント側のリダイレクトがトリガーされないが、代わりに内部で再ルーティングを処理することに注意してください。また、リクエストが/blog/mylink.html
の場合、リダイレクトされたURLが/new-blog/mylink.html.html
であるため、最初のルールによって処理されないため、 「元の」URLを提供するには2番目のルールが必要であることに注意してください。 /new-blog/mylink.html.html
は存在しない可能性が高いため、ルートがスキップされ、次のルートがチェックされます。パスセパレーターの処理も必要な場合は、 *
:file
代わりに*path
使用できます。
アプリケーションがリクエスト属性の特定の値(たとえば、 request.method == "GET"
)に応じて異なる関数を実行する必要がある場合、このライブラリは次の2つの主なオプションを提供します。 request.method == "GET"
check)および(2)指定された属性値を使用してアクションハンドラーに到達するようにリクエストをフィルタリングする条件を追加します。このセクションでは、2番目のオプションについて詳しく説明します。
デフォルトで登録された各ルートは、すべてのHTTPメソッド(取得、配置、投稿など)に応答しますが、特定のHTTPメソッドのみに応答するように各ルートを構成することができます。
fm . setRoute ( fm . GET " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
この場合、Syntax fm.GET"/hello(/:name)"
GET
Requestsのみを受け入れるようにルートを構成します。この構文は、ルートと追加のフィルタリング条件でテーブルを渡すのと同等です。
fm . setRoute ({ " /hello(/:name) " , method = " GET " },
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
複数のメソッドを指定する必要がある場合、1つの文字列値ではなく、メソッドのリストを持つテーブルを渡すことができます。
fm . setRoute ({ " /hello(/:name) " , method = { " GET " , " POST " }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
(暗黙的に) GET
リクエストを許可するすべてのルートは、 HEAD
リクエストを許可し、そのリクエストは、ボディ自体を送信せずにすべてのヘッダーを返すことによって処理されます。何らかの理由でこの暗黙の取り扱いが望ましくない場合、メソッドテーブルにHEAD = false
追加することはそれを無効にします( method = {"GET", "POST", HEAD = false}
のように)。
非一致方法を使用したリクエストは拒否されることはありませんが、他のルートで確認され、一致しない場合に返される404ステータスコードをトリガーするために繰り返されることに注意してください(1つの例外を除きます)。
method
に加えて、 host
、 clientAddr
、 serverAddr
、 scheme
、リクエストヘッダー、およびパラメーターを使用して、他の条件を適用できます。たとえば、 name = "Bob"
条件の1つとして指定すると、 name
パラメーターの値がアクションハンドラーが呼び出される「bob」になります。
ヘッダー名をキーとして使用して任意のリクエストヘッダーをチェックできるため、 Content-Type
ContentType = "multipart/form-data"
multipart/form-data
れます。ヘッダー値には、他の要素( Content-Type
値の一部として境界またはcharset)が含まれ、実際のメディアタイプのみが比較されることに注意してください。
ヘッダー、パラメーター、プロパティの名前は重複できるため、次の順序でチェックされます。
ContentType
などの複数の単語で構成されるヘッダーをリクエストしますmethod
、 port
、 host
など)をリクエストし、 Host
ヘッダーも最初にチェックされます(単一の単語にもかかわらず)ので、ヘッダーHost
に基づいてHost
フィルターを参照しながら、プロパティhost
に基づいてhost
フィルターを参照します。
文字列値は、条件付きルートで使用できる値だけではありません。複数の値が許容できる場合、テーブルを渡すと、許容値のリストを提供できます。たとえば、 Bob
とAlice
許容値である場合、 name = {Bob = true, Alice = true}
これを条件として表現します。
テーブルに渡された2つの特別な値により、正規表現またはパターン検証を適用できます。
regex
:正規表現を持つ文字列を受け入れます。たとえば、 name = {regex = "^(Bob|Alice)$"}
このセクションの前半に示すハッシュチェックと同じ結果を持っていますpattern
:LUAパターン式の文字列を受け入れます。たとえば、 name = {pattern = "^%u%l+$"}
は、大文字の文字と1つ以上の小文字が続く値から始まる値を受け入れます。これらの2つのチェックは、テーブルの存在チェックと組み合わせることができます: name = {Bob = true, regex = "^Alice$"}
Bob
とAlice
両方の値を受け入れます。最初のテーブル存在チェックが失敗した場合、 regex
またはpattern
発現の結果が返されます。
カスタムバリーターの最後のタイプは関数です。提供された関数は、検証する値を受け取り、その結果はfalse
またはtrue
として評価されます。たとえば、 id = tonumber
を渡すと、 id
値が数字であることが保証されます。別の例として、 clientAddr = fm.isLoopbackIp
クライアントアドレスがループバックIPアドレスであることを保証します。
fm . setRoute ({ " /local-only " , clientAddr = fm . isLoopbackIp },
function ( r ) return " Local content " end )
バリデーター関数は動的に生成できるため、これも機能します。
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 )
Balidator関数は、チェックを適用するリクエスト中に呼び出される関数を実際に返すことに留意することが重要です。前の例では、返された関数はヘッダー値を受け入れ、作成中に渡された制限と比較します。
場合によっては、条件を満たさないことは、他のルートをチェックせずにクライアントに返信するのに十分な理由です。このような場合、 otherwise
数値に値を設定するか、関数は指定されたステータスの応答または関数の結果を返します。
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 )
この例では、ルーティングエンジンがルートと一致し、メソッド値とPOST
とContent-Length
ヘッダーの値をisLessThan
関数の結果と比較する2つの条件を検証します。条件のいずれかが一致しない場合、 otherwise
値で指定されたステータスコードは、応答の残りの部分で返されます。
otherwise
条件がContentLength
Checkにのみ適用する必要がある場合、 otherwise
値とともにValidator関数とともに、 ContentLength
Checkに関連付けられたテーブルに移動できます。
fm . setRoute ( fm . POST { " /upload " ,
ContentLength = { isLessThan ( 100000 ), otherwise = 413 }
}, function ( r ) ... handle the upload ... end )
最後の2つの例の違いは、この例では、 ContentLength
Check障害のみが413の応答をトリガーする(および他のすべての方法が他のルートに分類される)、前の方法では同じ413応答をトリガーするmethod
とContentLength
Check障害の両方がトリガーされることです。
チェック値がnil
の場合、テーブルに対するチェックは有効であるとみなされ、ルートが受け入れられることに注意してください。たとえば、文字列に対して行われたオプションのパラメーターのチェック( name = "Bo"
)は、 params.name
の値がnil
の場合に失敗しますが、テーブルに対して同じチェックが行われた場合に渡されます( name = {Bo=true, Mo=true}
)、regex/パターンチェックを含む。これが望ましくない場合、カスタムバリーター関数は、期待値を明示的に確認できます。
次の例を考えてみましょう。
fm . setRoute ({ " /hello(/:name) " ,
method = { " GET " , " POST " , otherwise = 405 }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
この場合、このエンドポイントがPUT
メソッドでアクセスされる場合、他のルートをチェックする代わりに( method
条件が満たされないため)、指定されたotherwise
値で構成されているように、405ステータスコードが返されます。他の場所で文書化されているように、このルートは、 GET
リクエストが受け入れられているため、 HEAD
リクエストも(リストされていない場合でも)受け入れます。
405(悪い方法)ステータスコードが返され、 Allow
ヘッダーが設定されていない場合、ルートで許可されているメソッドのリストに設定されます。上記のケースでは、この構成で許可されている方法であるため、 GET, POST, HEAD, OPTIONS
値を取得するように設定されています。 otherwise
値が(数字ではなく)関数である場合、適切な結果を返してAllow
ヘッダーを設定することがこの関数の責任です。
otherwise
値は関数に設定することもできます。これは、ステータスコードを設定するよりも柔軟性を提供します。たとえば、 otherwise = fm.serveResponse(413, "Payload Too Large")
設定は、指定されたステータスコードとメッセージで応答をトリガーします。
フォーム検証の処理には、同じパラメーターの条件のセットと、条件が満たされておらず、これらが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 )
この例では、VALIBARTARは、最小および最大の長さについて「名前」と「パスワード」という2つのパラメーターをチェックし、パラメーターのいずれかがチェックに失敗したときにメッセージを返すように構成されています。
失敗したチェックによりルートがスキップされるため、 otherwise
値を提供することで、応答の一部としてエラーを返すことができます。
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 ,
}
この場合、 otherwise
場合は、ハンドラーはエラーメッセージ(または、下にカバーされているall
オプションを渡すことで要求された場合、メッセージ付きのテーブル)を受信します。これは、テンプレートパラメーターとして提供され、クライアントに返される可能性があります。
別のオプションは、アクションハンドラーでバリデーター関数を直接呼び出して結果を返すことです。
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 )
この例では、バリデーターは直接呼び出され、すべてのパラメーター値を備えたテーブル( r.params
)に渡され、VALIDATOR関数が指定されたルールに対して値を確認できるようにします。
次に、VALIDATOR関数は、Signal Successまたはnil, error
にtrue
返します。エラーは、ルールのいずれかを確認できないことを信号します。これにより、スクリプトがすぐにエラーを返す必要がある場合、バリデーターコールをassert
にラップすることができます。
assert ( validator ( r . params )) -- throw an error if validation fails
return fm . serveRedirect ( 307 , " / " ) -- return redirect in other cases
次のバリデーターチェックが利用可能です。
minlen
:(整数)文字列の最小限の長さをチェックします。maxlen
:(整数)文字列の最大長をチェックします。test
:(関数)1つのパラメーターに渡され、 true
またはnil | false [, error]
。oneof
:( value | { table of values to be compared against }
)パラメーターが提供された値の1つと一致するかどうかを確認します。pattern
:(文字列)パラメーターがLUAパターン式と一致するかどうかを確認します。チェックに加えて、ルールにはオプションが含まれる場合があります。
optional
:( bool)は、 nil
の場合、パラメーターをオプションにします。すべてのパラメーターはデフォルトで必要なため、このオプションを使用すると、パラメーターが提供されていないときにルールをスキップできます。パラメーターがゼロではない場合、すべてのルールがまだ適用されます。msg
:(String)チェックの1つが失敗した場合、このために顧客メッセージを追加し、個々のチェックからメッセージを上書きします。メッセージには、プレースホルダー( %s
)が含まれる場合があり、パラメーター名に置き換えられます。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.例えば、
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.
次の例を考えてみましょう。
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.