Fullmoon은 휴대용 단일 파일 분산 가능한 웹 서버 인 Redbean을 기반으로하는 빠르고 최소한의 웹 프레임 워크입니다.
개발 및 배포에 필요한 모든 것은 외부 종속성이없는 단일 파일로 제공되며 Windows, Linux 또는 MacOS에서 Redbean이 실행되는 포장 후. 다음은 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 () Redbean이 포장 된 후 ./redbean.com을 사용하여 시작할 수 있습니다. ./redbean.com 사용하여 "Hello, World"를 반환하는 서버를 시작하여 http : // localhost : 8080/hello/world로 전송 된 http (들) 요청으로 돌아갑니다.
Redbean은 독특하고 강력한 특성을 가진 단일 파일 분산 가능한 크로스 플랫폼 웹 서버입니다. LUA 기반 웹 프레임 워크 (Lapis, Lor, Sailor, Pegasus 등)가 있지만 그중 어느 누구도 실험 프레임 워크 ANPAN이 있지만 Redbean과 통합되지 않습니다.
Fullmoon은 가장 단순하고 가장 효율적인 방식으로 확장하고 확대하여 Redbean이 제공하는 모든 기능을 보여주는 관점에서 작성된 가볍고 최소한의 웹 프레임 워크입니다. 빠르게 실행되며 배터리가 포함되어 있습니다 (경로, 템플릿, JSON 생성 등).
Fullmoon은 LUA 철학을 따르며 필요에 따라 결합하고 기초로 사용할 수있는 최소한의 도구 세트를 제공합니다.
fork , socket , 공유 메모리 등다음 명령을 실행하여 Redbean 사본을 다운로드하십시오 (Windows 에서이 명령을 실행하면 두 번째 명령을 건너 뜁니다) :
curl -o redbean.com https://redbean.dev/redbean-2.2.com
chmod +x redbean.com최신 버전 번호는 다음 요청으로 검색 할 수 있습니다.
curl https://redbean.dev/latest.txt또 다른 옵션은 소스 빌드에 대한 지침에 따라 소스에서 Redbean을 구축하는 것입니다.
fullmoon.lua .lua/ directory로 복사하십시오.init.lua 라는 파일에 저장하십시오 (예 : 설명에 표시된 LUA 코드). 또 다른 옵션은 응용 프로그램 코드를 별도의 파일 (예 : .lua/myapp.lua )에 배치하고 require "myapp" .init.lua 에 추가를 추가하는 것입니다. 이것은 포함 된 모든 예제가 제시되는 방법입니다.
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) 하나의 경로를 구성하고 (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 문자열 인 경우 요청 경로를 비교하기 위해 경로 표현식으로 사용됩니다. 테이블 인 경우 요소는 경로로 사용되는 문자열이며 해시 값은 경로가 확인되는 조건입니다. 두 번째 매개 변수가 함수 인 경우 모든 조건이 충족되면 실행됩니다. 문자열 인 경우 경로 표현식으로 사용되며 요청은 지정된 경로에서 전송되는 것처럼 처리됩니다 (내부 리디렉션으로 작용). 조건이 충족되지 않으면 다음 경로가 확인됩니다. 경로 표현식에는 여러 매개 변수와 선택적 부분이있을 수 있습니다. 작업 핸들러는 요청 및 경로 매개 변수 및 헤더, 쿠키 및 세션에 대한 액세스를 제공하는 요청 테이블을 수락합니다.
setTemplate(name, template[, parameters]) : 지정된 이름 또는 디렉토리에서 템플릿 세트의 템플릿을 등록합니다. template 이 문자열 인 경우 템플릿 핸들러로 컴파일됩니다. 함수 인 경우 템플릿 렌더링이 요청 될 때 저장 및 호출됩니다. 테이블 인 경우 첫 번째 요소는 템플릿 또는 함수이며 나머지는 옵션으로 사용됩니다. 예를 들어, ContentType 옵션 중 하나로 지정하면 생성 된 컨텐츠의 Content-Type 헤더를 설정합니다. 여러 템플릿 ( 500 , json 등)이 기본적으로 제공되며 덮어 쓸 수 있습니다. parameters 이름/값 쌍 (템플릿의 변수로 참조)으로 저장된 템플릿 매개 변수가있는 테이블입니다.
serveResponse(status[, headers][, body]) : 제공된 status , headers 및 body 값을 사용하여 HTTP 응답을 보냅니다. headers HTTP 헤더 이름/값 쌍으로 채워진 선택 테이블입니다. 제공된 경우이 헤더 세트는 동일한 요청을 처리하는 동안 이전에 설정된 다른 모든 헤더를 제거합니다 . 헤더 이름은 대주 적 으로 인식적이 지만 대시가있는 헤더 이름에 대한 별칭을 제공합니다. {ContentType = "foo"} {["Content-Type"] = "foo"} 에 대한 대체 형식입니다. body 는 선택적 문자열입니다.
serveContent(name, parameters) : 제공된 매개 변수를 사용하여 템플릿을 렌더링합니다. name 은 템플릿의 이름 ( setTemplate call에 의해 설정된대로)의 이름이며 parameters 템플릿 매개 변수가 이름/값 쌍 (템플릿의 변수로 참조)이있는 테이블입니다.
run([options]) : 구성된 경로를 사용하여 서버를 실행합니다. 기본적으로 서버는 LocalHost 및 Port 8080에서 리스팅됩니다. options 테이블에서 addr 및 port 값을 설정하여 이러한 값을 변경할 수 있습니다.
실행 예제에는 .init.lua 파일에 require 사항을 포함시켜야합니다. 각 예제 코드와 함께 모듈을로드합니다. 따라서 showcase.lua 에 구현 된 showcase 예제. .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`Showcase 예제는 몇 가지 Fullmoon 기능을 보여줍니다.
serveAsset 사용)serveRedirect 사용)다음 파일은 Redbean Executable/Archive에 추가해야합니다.
.init.lua- "쇼케이스"가 필요합니다. .lua/fullmoon.lua .lua/showcase.lua
TechEmpower 예제는 Fullmoon 및 Memory Inmory SQLite 데이터베이스를 사용하여 웹 프레임 워크 벤치 마크의 다양한 테스트 유형을 구현합니다.
이 예제는 몇 가지 Fullmoon/Redbean 기능을 보여줍니다.
다음 파일은 Redbean Executable/Archive에 추가해야합니다.
.init.lua- "TechBench"가 필요합니다. .lua/fullmoon.lua .lua/techbench.lua
HTMX 보드 예제는 HTMX 라이브러리를 사용하여 클라이언트에게 전달되는 HTML 단편을 생성하는 간단한 응용 프로그램을 보여줍니다.
이 예제는 몇 가지 Fullmoon/Redbean 기능을 보여줍니다.
다음 파일은 Redbean Executable/Archive에 추가해야합니다.
.init.lua- "htmxboard"가 필요합니다. .lua/fullmoon.lua .lua/htmxboard.lua 자산/스타일 .CSS tmpl/* - 예제/htmxboard/tmpl 디렉토리의 모든 파일
참고 1 : 모든 데이터가 메모리에 저장 되므로이 예제는 Uniprocess 모드에서 실행됩니다.
참고 2 :이 예제는 외부 리소스에서 HTMX, Hyperscript 및 정렬 가능한 라이브러리를 검색하지만 이러한 라이브러리는 로컬 자산으로 저장 될 수 있으므로 완전히 자급 자족 할 수있는 휴대용 배포 패키지를 제공합니다.
HTMX SSE 예제는 클라이언트에게 스트리밍 할 수있는 서버-잠급 이벤트 (SSE)를 생성하는 방법을 보여줍니다 (HTMX 라이브러리 및 SSE 확장을 사용한 결과를 보여줍니다).
이 예제는 몇 가지 Fullmoon/Redbean 기능을 보여줍니다.
streamContent 사용)다음 파일은 Redbean Executable/Archive에 추가해야합니다.
.init.lua- "htmxsse"가 필요합니다. .lua/fullmoon.lua .lua/htmxsse.lua
각 FullMoon 응용 프로그램은 5 개의 주요 구성 요소와 동일한 기본 흐름을 따릅니다.
요청 라우팅에서 시작하여 각 구성 요소를 살펴 보겠습니다.
Fullmoon은 동일한 프로세스를 사용하여 각 HTTP 요청을 처리합니다.
false 또는 nil 이외의 다른 것이 있으면 응답을 제공합니다 (그리고 프로세스를 계속합니다). 일반적으로 경로 정의는 요청 URL (및 조건 세트)을 동작 핸들러 (일반 LUA 기능)에 바인딩합니다. 모든 조건은 경로 정의와 일치하는 각 URL에 대해 임의 순서로 점검됩니다. 조건이 실패하자마자 경로 처리가 중단되고 다음 경로는 한 가지 예외로 확인됩니다. 모든 조건은 otherwise 값을 설정할 수있어 지정된 상태 코드로 응답을 트리거합니다.
경로가 요청과 일치하지 않으면 기본 404 처리가 트리거되어 사용자 정의 404 템플릿 ( fm.setTemplate("404", "My 404 page...") )를 등록하여 사용자 정의 할 수 있습니다.
각 경로는 정확하게 일치하는 경로를 취하므로 경로 "/hello" 는 요청과 /hello , /hello-world /hell , /hello/world 와 일치하지 않습니다. 아래 경로는 "안녕하세요, 세계!" /hello Path에 지시 된 모든 요청에 대해 다른 모든 요청에 대해 404를 반환합니다.
fm . setRoute ( " /hello " , function ( r ) return " Hello, World! " end ) /hello 그 일부인 경로와 일치하기 위해 선택적 매개 변수와 SPLAT를 사용할 수 있습니다.
고정 경로 외에도 모든 경로에는 매개 변수에 대한 자리 표시자가 포함될 수 있으며, 여기에는 A로 식별됩니다 :
fm . setRoute ( " /hello/:name " ,
function ( r ) return " Hello, " .. ( r . params . name ) end ) 각 매개 변수는 "/hello/:name" matches /hello/alice , /hello/bob , /hello/123 / 제외한 하나 이상의 문자와 일치하며 /hello/bob/and/alice 일치하지 않습니다. 비 일치 전방 슬래시) 또는 /hello/ (일치하는 조각의 길이는 0이기 때문에).
매개 변수 이름에는 영숫자 문자와 _ 포함될 수 있습니다.
r.params.name 사용하여 이전 예제에서 name 매개 변수의 값을 얻는 데 사용될 수 있도록 요청 테이블 및 해당 params 테이블을 사용하여 매개 변수에 액세스 할 수 있습니다.
지정된 경로 조각 또는 매개 변수는 괄호 안에 래핑하여 선택 사항으로 선언 할 수 있습니다.
fm . setRoute ( " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end ) 위의 예에서, /hello 및 /hello/Bob 모두 허용되지만 /hello/ :name .
타의 추종을 불허하는 옵션 매개 변수는 값으로 false 발생하므로 위의 경우 "Hello, World!" /hello request URL에 대해 반환됩니다.
하나 이상의 선택적인 매개 변수를 지정할 수 있고 옵션 파편이 중첩 될 수 있으므로 "/posts(/:pid/comments(/:cid))" 및 "/posts(/:pid)/comments(/:cid)" 유효한 경로 값입니다.
Splat라는 다른 종류의 매개 변수는 * 로 작성되었으며 전방 슬래시 ( / )를 포함하여 0 개 이상의 문자와 일치합니다. splat은 또한 params 테이블에 splat 이름으로 저장됩니다. 예를 들어, "/download/*" 경로는 my/file.zip /download/my/file.zip 값을 가져옵니다. 동일한 경로에서 여러 개의 splat가 필요한 경우, Splats는 다른 매개 변수와 유사한 이름을 할당 할 수 있습니다 : /download/*path/*fname.zip path/*fname.zip ( /download/*path/:fname.zip 사용하여 동일한 결과를 얻을 수 있지만. , 첫 번째 SPLAT는 파일 이름을 제외한 모든 경로 부품을 캡처합니다).
모든 매개 변수 (SPLAT 포함)는 경로의 어느 부분에도 나타날 수 있으며 다른 텍스트로 둘러싸여 있으며 정확히 일치해야합니다. 이것은 "/download/*/:name.:ext" 및 params.name get file , params.ext zip params.splat /download/my/path/file.zip my/path 값을 가져옵니다.
Splat을 사용하는 또 다른 이유는 같은 경로를 가진 여러 경로가 시스템에 등록 될 수 있도록하는 것입니다. 현재 구현은 동일한 이름의 경로를 덮어 쓰고 이름 지정된 SPLAT를 사용하여 고유 한 경로를 생성 할 수 있습니다. 예를 들어,
fm . setRoute ( " /*dosomething1 " , function ( r ) return " something 1 " end )
fm . setRoute ( " /*dosomething2 " , function ( r ) return " something 2 " end )이것은 작업 핸들러에 점검 해야하는 일련의 조건이있는 경우 상황에서 사용할 수 있으며 두 경로를 하나로 결합 할 수 있지만 때로는 더 깨끗한 상태입니다.
매개 변수의 기본값은 길이가 하나 이상의 모든 문자 ( / 제외)입니다. 다른 유효한 문자 세트를 지정하려면 변수 이름의 끝에 추가 할 수 있습니다. 예를 들어, 다음을 사용하여 :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] 숫자가 포함되지 않는 매개 변수와 일치합니다.
세트 만 허용되고 값이 여전히 하나 이상의 문자를 허용하기 때문에 반복 횟수를 변경할 수 없습니다 (따라서 :id[%d]* 제로 또는만큼 숫자를 수락하는 유효한 방법이 아닙니다). 허용 가능한 형식을 설명하는 데 더 많은 유연성이 필요하다면 맞춤 유효성 검사기를 사용하여 일치하는 논리를 확장 할 수 있습니다.
각 동작 핸들러에게 전달되는 request 테이블의 params 테이블을 사용하여 경로 매개 변수와 동일한 방식으로 쿼리 및 양식 매개 변수에 액세스 할 수 있습니다. 매개 변수와 쿼리/양식 이름 사이에 충돌이있는 경우 매개 변수 이름이 우선합니다 .
문자열 값 대신 테이블을 반환 할 수있는 특별한 경우가 하나 있습니다. 쿼리/양식 매개 변수 이름이 [] 로 끝나면 모든 일치하는 결과 (하나 이상)가 테이블로 반환됩니다. 예를 들어, 쿼리 문자열의 경우 a[]=10&a[]&a[]=12&a[]= params["a[]"] 는 {10, false, 12, ""} 입니다.
이 매개 변수 이름을 작성하려면 여러 괄호가 필요할 수 있으므로 params.a 두 가지 양식이 동일한 테이블을 반환하는 params["a[]"] 의 바로 가기로 사용할 수 있습니다.
멀티 파트 매개 변수는 요청시 처리되며 params 테이블을 사용하여 나머지 매개 변수와 동일한 방식으로 액세스 할 수 있습니다. 예를 들어, 이름이 simple 하고 more 매개 변수는 multipart/form-data 컨텐츠 유형이있는 메시지에서 params.simple 및 params.more 사용하여 검색 할 수 있습니다.
멀티 파트 컨텐츠 중 일부는 해당 헤더 내의 추가 헤더 및 매개 변수를 포함 할 수 있으므로 params 테이블의 multipart 필드로 액세스 할 수 있습니다.
fm . setRoute ({ " /hello " , simple = " value " }, function ( r )
return " Show " .. r . params . simple .. " " .. r . params . multipart . more . data )
end ) multipart 테이블에는 멀티 파트 메시지의 모든 부분이 포함되어 있으므로 ( ipairs 사용하여 반복 할 수 있음) 매개 변수 이름 ( params.multipart.more )을 사용하여 액세스 할 수 있습니다. 각 요소는 또한 다음 필드를 포함하는 테이블입니다.
nil 않다면.nil 않다면. 이 멀티 파트 프로세싱은 임의의 멀티 파트 하위 유형을 소비하고 재귀적인 멀티 파트 메시지를 처리합니다. 또한 start 매개 변수와 일치하는 Content-ID 값이있는 부분을 첫 번째 위치에 삽입합니다.
단일 경로를 보여주는 이전의 모든 예에도 불구하고 실제 응용 프로그램의 경우는 거의 없습니다. 여러 경로가 있으면 항상 등록 된 순서대로 평가 됩니다.
하나의 setRoute 호출은 동일한 조건 세트가 있고 동일한 작업 핸들러를 공유 할 때 여러 경로를 설정할 수 있습니다.
fm . setRoute ({ " /route1 " , " /route2 " }, handler )이것은 각 경로를 개별적으로 설정하는 두 통화와 같습니다.
fm . setRoute ( " /route1 " , handler )
fm . setRoute ( " /route2 " , handler )경로가 설정된 순서대로 경로를 평가한다는 점을 감안할 때, 더 선택적인 경로를 먼저 설정해야합니다. 그렇지 않으면 평가할 기회가 없습니다.
fm . setRoute ( " /user/bob " , handlerBob )
fm . setRoute ( " /user/:name " , handlerName ) "/user/:name" 액션 핸들러가 비 false 결과를 반환하는 한, 경로가 반대 순서로 설정된 경우 /user/bob 확인하지 않을 수 있습니다.
앞에서 설명한대로, 경로가 일치하지 않으면 404 상태 코드가있는 응답이 반환됩니다. 이것이 바람직 하지 않은 경우가있을 수 있습니다. 예를 들어, 응용 프로그램에 LUA 스크립트가 포함되어있는 경우 명시 적으로 등록되지 않은 요청을 경로로 처리합니다. 이 경우 기본 레드 비트 처리를 구현하는 캐치-모든 경로가 추가 될 수 있습니다 (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두 개의 경로가 같은 이름을 사용하는 경우 이름은 마지막으로 등록 된 이름과 관련이 있지만 두 경로는 여전히 존재합니다.
경로 이름은 URL 생성에만 사용되는 외부/정적 경로와 함께 사용할 수도 있습니다.
경로가 경로 생성에만 사용되는 경우 경로 처리기가 필요하지도 않습니다.
fm . setRoute ({ " https://youtu.be/:videoid " , routeName = " youtube " })
fm . makePath ( " youtube " , { videoid = " abc " }) -- > https://youtu.be/abc경로 일치 프로세스 중에 액션 핸들러가없는 경로가 건너 뜁니다.
내부 경로는 한 세트의 URL 세트를 다른 URL 세트로 리디렉션 할 수 있습니다. 대상 URL은 정적 리소스 또는 .lua 스크립트를 가리킬 수 있습니다. 예를 들어, 한 위치에 대한 요청을 다른 위치로 리디렉션 해야하는 경우 다음 구성은 대상 리소스가 존재하는 한 /blog/ url 아래 /new-blog/ URL의 리소스에 대한 요청을 다시 리디렉션합니다.
fm . setRoute ( " /blog/* " , " /new-blog/* " ) 이 경로는 /blog/post1 에 대한 요청을 받아들이고 /new-blog/post1 자산이 존재하는 한 /new-blog/post1 의 응답으로 제공됩니다. 자산이 존재하지 않으면 다음 경로가 확인됩니다. 마찬가지로 fm.setRoute("/static/*", "/*") 사용하면 /static/help.txt 에 대한 요청이 resource /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"버전을 제공하는 "Nice"URL을 해결합니다. 이는 3xx 상태 코드를 반환하여 클라이언트 측 리디렉션을 트리거하지 않고 내부적으로 재 경매를 처리합니다. 또한 첫 번째 규칙에 의해 처리되지 않으므로 "원래 /blog/mylink.html URL을 제공하기 위해 두 번째 규칙이 필요합니다 /new-blog/mylink.html.html , 존재하지 않을 가능성이 없으므로 경로가 건너 뜁니다. 다음 경로가 확인됩니다. 경로 분리기의 취급이 필요한 경우 : *path 사용할 수 있습니다 :file , * 경로 분리기를 허용합니다.
애플리케이션이 요청 속성의 특정 값 (예 : 메소드)에 따라 다른 함수를 실행 해야하는 경우이 라이브러리는 두 가지 주요 옵션을 제공합니다. (1) 속성 값을 확인하십시오 값 값을 확인하십시오 (예 : request.method == "GET" 사용). request.method == "GET" check) 및 (2) 지정된 속성 값을 사용하여 요청 만 액션 핸들러에 도달하도록 요청을 걸러내는 조건을 추가합니다. 이 섹션에서는 두 번째 옵션에 대해 자세히 설명합니다.
기본적으로 등록 된 각 경로는 모든 HTTP 메소드 (Get, Put, Post 등)에 응답하지만 특정 HTTP 방법에만 응답하도록 각 경로를 구성 할 수 있습니다.
fm . setRoute ( fm . GET " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end ) 이 경우 구문 fm.GET"/hello(/:name)" GET 요청 만 허용하도록 경로를 구성합니다. 이 구문은 경로와 추가 필터링 조건이있는 테이블을 전달하는 것과 같습니다.
fm . setRoute ({ " /hello(/:name) " , method = " GET " },
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )둘 이상의 메소드를 지정 해야하는 경우 하나의 문자열 값 대신 메소드 목록이있는 테이블을 전달할 수 있습니다.
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 상태 코드가 일치하지 않으면 (한 가지 예외와 함께) 반환됩니다.
method 외에도 host , clientAddr , serverAddr , scheme , 요청 헤더 및 매개 변수를 사용하여 다른 조건을 적용 할 수 있습니다. 예를 들어, name = "Bob" 조건 중 하나로 지정하면 조치 핸들러를 호출 할 수있는 name 매개 변수의 값이 "bob"이되도록합니다.
Content-Type 헤더의 값이 멀티 ContentType = "multipart/form-data" multipart/form-data 인 경우, 헤더 이름을 키로 사용하여 모든 요청 헤더를 확인할 수 있습니다. 헤더 값에는 다른 요소 ( Content-Type 값의 일부로 경계 또는 숯)가 포함될 수 있으며 실제 미디어 유형 만 비교됩니다.
헤더의 이름, 매개 변수 및 속성의 이름이 겹칠 수 있으므로 다음 순서로 확인됩니다.
ContentType 와 같은 여러 단어로 구성된 요청 헤더method , port , host 등) 및 Host 헤더도 먼저 확인되므로 (한 단어에도 불구하고) 헤더 Host 기반으로 Host 필터를 참조하면서 속성 host 기반으로 host 필터를 참조하십시오.
문자열 값이 조건부 경로에서 사용할 수있는 유일한 값은 아닙니다. 둘 이상의 값이 허용되는 경우 테이블을 전달하면 허용 가능한 값 목록을 제공 할 수 있습니다. 예를 들어, Bob 과 Alice 허용되는 값이라면 name = {Bob = true, Alice = true} 는 이것을 조건으로 표현합니다.
테이블에 전달 된 두 개의 특수 값은 Regex 또는 패턴 검증을 적용 할 수 있습니다.
regex : 정규 표현식이있는 문자열을 수락합니다. 예를 들어, name = {regex = "^(Bob|Alice)$"} 이 섹션의 앞부분에서 나와있는 해시 점검과 동일한 결과를 얻습니다.pattern : LUA 패턴 표현식으로 문자열을 수락합니다. 예를 들어, name = {pattern = "^%u%l+$"} 대문자로 시작하는 값을 허용하고 하나 이상의 소문자 문자가 이어집니다. 이 두 검사는 테이블 존재 점검과 결합 할 수 있습니다. 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 )Validator 함수는 실제로 수표를 적용하도록 요청하는 동안 호출되는 함수를 반환한다는 점을 명심해야합니다. 이전 예에서, 반환 된 함수는 헤더 값을 받아들이고 생성 중에 전달 된 한계와 비교합니다.
경우에 따라 조건을 충족시키지 못하면 다른 경로를 확인하지 않고 클라이언트에게 응답을 되돌릴만한 충분한 이유입니다. 이와 같은 경우, 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 기능의 결과와 비교하는 두 조건을 검증합니다. 조건 중 하나가 일치하지 않으면 otherwise 값으로 지정된 상태 코드는 나머지 응답과 함께 반환됩니다.
otherwise 조건이 ContentLength Check 에만 적용 해야하는 경우, 그렇지 않으면 유효성 검사 기능과 함께 otherwise 값은 ContentLength Check와 관련된 테이블로 이동할 수 있습니다.
fm . setRoute ( fm . POST { " /upload " ,
ContentLength = { isLessThan ( 100000 ), otherwise = 413 }
}, function ( r ) ... handle the upload ... end ) 마지막 두 가지 예의 차이점은이 예에서는 ContentLength 검사 실패 만 413 응답을 트리거하는 반면 (다른 모든 메소드가 다른 경로로 떨어집니다), 이전 method 및 ContentLength 길이 검사 실패는 동일한 413 응답을 트리거한다는 것입니다.
점검 된 값이 nil 인 경우 테이블에 대한 수표가 유효한 것으로 간주되고 경로가 허용됩니다. 예를 들어, params.name 의 값이 name = "Bo" 이면 문자열에 대한 선택적 매개 변수를 확인하는 것이 실패하지만 동일한 점검이 테이블에 대해 동일한 점검 nil 이루어지면 통과합니다 ( name = {Bo=true, Mo=true} Regex/Pattern Checks를 포함하여 name = {Bo=true, Mo=true} ). 이것이 바람직하지 않은 경우 사용자 정의 유효성 검사기 기능은 예상 값을 명시 적으로 확인할 수 있습니다.
다음 예를 고려하십시오.
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 )이 예에서 유효성 검사기는 최소 및 최대 길이에 대해 "이름"및 "비밀번호"라는 두 매개 변수를 확인하고 매개 변수 중 하나가 확인에 실패 할 때 메시지를 반환하도록 구성됩니다.
실패한 점검으로 인해 경로가 건너 뜁니다. 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 함수는 성공 또는 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 : (함수) 하나의 매개 변수가 전달되는 함수를 호출하고 true 또는 nil | false [, error] .oneof : ( value | { table of values to be compared against } ) 매개 변수가 제공된 값 중 하나와 일치하는지 확인합니다.pattern : (문자열) 매개 변수가 LUA 패턴 표현식과 일치하는지 확인합니다.수표 외에도 규칙에는 옵션이 포함될 수 있습니다.
optional : (BOOL) nil 일 때 매개 변수 선택 사항을 만듭니다. 모든 매개 변수는 기본적으로 필요 하므로이 옵션을 사용하면 매개 변수가 제공되지 않을 때 규칙을 건너 뛸 수 있습니다. 매개 변수가 nil이 아닌 경우 모든 규칙이 여전히 적용됩니다.msg : (String) 수표 중 하나가 실패하면 고객 메시지를 추가하여 개별 수표에서 메시지를 덮어 씁니다. 메시지에는 자리 표시 자 ( %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 . serve413It can also be used as the action handler itself:
fm . setRoute ( fm . PUT " /status " , fm . serve402 ) serveContent(name, parameters) renders a template using provided parameters. name is a string that names the template (as set by a setTemplate call) and parameters is a table with template parameters (referenced as variables in the template).
Fullmoon's function makeStorage is a way to connect to, and use a SQLite3 database. makeStorage returns a database management table which contains a rich set of functions to use with the connected database.
The run method executes the configured application. By default the server is launched listening on localhost and port 8080. Both of these values can be changed by passing addr and port options:
fm . run ({ addr = " localhost " , port = 8080 }) The following options are supported; the default values are shown in parentheses and options marked with mult can set multiple values by passing a table:
addr : sets the address to listen on (mult)brand : sets the Server header value ( "redbean/v# fullmoon/v#" )cache : configures Cache-Control and Expires headers (in seconds) for all static assets served. A negative value disables the headers. Zero value means no cache.certificate : sets the TLS certificate value (mult)directory : sets local directory to serve assets from in addition to serving them from the archive within the executable itself (mult)headers : sets default headers added to each response by passing a table with HTTP header name/value pairslogMessages : enables logging of response headerslogBodies : enables logging of request bodies (POST/PUT/etc.)logPath : sets the log file path on the local file systempidPath : sets the pid file path on the local file systemport : sets the port number to listen on (8080)privateKey : sets the TLS private key value (mult)sslTicketLifetime : sets the duration (in seconds) of the ssl ticket (86400)trustedIp : configures IP address to trust (mult). This option accepts two values (IP and CIDR values), so they need to be passed as a table within a table specifying multiple parameters: trustedIp = {{ParseIp("103.31.4.0"), 22}, {ParseIp("104.16.0.0"), 13}}tokenBucket : enables DDOS protection. This option accepts zero to 5 values (passed as a table within a table); an empty table can be passed to use default values: tokenBucket = {{}} Each option can accept a simple value ( port = 80 ), a list of values ( port = {8080, 8081} ) or a list of parameters. Since both the list of values and the list of parameters are passed as tables, the list of values takes precedence, so if a list of parameters needs to be passed to an option (like trustedIp ), it has to be wrapped into a table: trustedIp = {{ParseIp("103.31.4.0"), 22}} . If only one parameter needs to be passed, then both trustedIp = {ParseIp("103.31.4.0")} and trustedIp = ParseIp("103.31.4.0") can work.
The key and certificate string values can be populated using the getAsset method that can access both assets packaged within the webserver archive and those stored in the file system.
There are also default cookie and session options that can be assigned using cookieOptions and sessionOptions tables described below.
cookieOptions sets default options for all cookie values assigned using request.cookie.name = value syntax ( {httponly=true, samesite="Strict"} ). It is still possible to overwrite default values using table assignment: request.cookie.name = {value, secure=false} .
sessionOptions sets default options for the session value assigned using request.session.attribute = value syntax ( {name="fullmoon_session", hash="SHA256", secret=true, format="lua"} ). If the secret value is set to true , then a random key is assigned each time the server is started ; if verbose logging is enabled (by either adding -v option for Redbean or by using fm.setLogLevel(fm.kLogVerbose) call), then a message is logged explaining how to apply the current random value to make it permanent.
Setting this value to false or an empty string applies hashing without a secret key.
The results shown are from runs in the same environment and on the same hardware as the published redbean benchmark (thanks to @jart for executing the tests!). Even though these tests are using pre-1.5 version of redbean and 0.10 version of Fullmoon, the current versions of redbean/Fullmoon are expected to deliver similar performance.
The tests are using exactly the same code that is shown in the introduction with one small change: using {%= name %} instead of {%& name %} in the template, which skips HTML escaping. This code demonstrates routing, parameter handling and template processing.
$ wrk -t 12 -c 120 http://127.0.0.1:8080/user/paul
Running 10s test @ http://127.0.0.1:8080/user/paul
12 threads and 120 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 312.06us 4.39ms 207.16ms 99.85%
Req/Sec 32.48k 6.69k 71.37k 82.25%
3913229 requests in 10.10s, 783.71MB read
Requests/sec: 387477.76
Transfer/sec: 77.60MB
The following test is using the same configuration, but redbean is compiled with MODE=optlinux option:
$ wrk -t 12 -c 120 http://127.0.0.1:8080/user/paul
Running 10s test @ http://127.0.0.1:8080/user/paul
12 threads and 120 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 346.31us 5.13ms 207.31ms 99.81%
Req/Sec 36.18k 6.70k 90.47k 80.92%
4359909 requests in 10.10s, 0.85GB read
Requests/sec: 431684.80
Transfer/sec: 86.45MB
The following two tests demonstrate the latency of the request handling by Fullmoon and by redbean serving a static asset (no concurrency):
$ wrk -t 1 -c 1 http://127.0.0.1:8080/user/paul
Running 10s test @ http://127.0.0.1:8080/user/paul
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 15.75us 7.64us 272.00us 93.32%
Req/Sec 65.54k 589.15 66.58k 74.26%
658897 requests in 10.10s, 131.96MB read
Requests/sec: 65241.45
Transfer/sec: 13.07MB
The following are the results from redbean itself on static compressed assets:
$ wrk -H 'Accept-Encoding: gzip' -t 1 -c 1 htt://10.10.10.124:8080/tool/net/demo/index.html
Running 10s test @ htt://10.10.10.124:8080/tool/net/demo/index.html
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 7.40us 1.95us 252.00us 97.05%
Req/Sec 129.66k 3.20k 135.98k 64.36%
1302424 requests in 10.10s, 1.01GB read
Requests/sec: 128963.75
Transfer/sec: 102.70MB
Berwyn Hoyt included Redbean results in his lua server benchmark results, which shows redbean outperforming a comparable nginx/openresty implementation.
Highly experimental with everything being subject to change.
The core components are more stable and have been rarely updated since v0.3. Usually, the documented interfaces are much more stable than undocumented ones. Those commits that modified some of the interfaces are marked with COMPAT label, so can be easily identified to review for any compatibility issues.
Some of the obsolete methods are still present (with a warning logged when used) to be removed later.
Paul Kulchenko ([email protected])
See LICENSE.