servant-stache は見る人が見ればわかるように、HaskellのWebフレームワークservantのContent-Typeとして、同じくHaskellのMustacheテンプレートライブラリであるstacheでコンパイルしたファイルをHTML
として利用できるようにするためのライブラリです。
要するにservant-stacheを利用すると以下のようなコードが書けるようになります。
type API = Get '[HTML "user"] User
:<|> "style.css" :> Get '[Template CSS "style"] CSSData
上記のコードでは、例えば/
でレンダリングされるHTMLはテンプレートuser.mustache
にUser
の値を適用して生成されます。
servant-stacheを作ろうと思ったきっかけは
2年前に書いた「【型レベルWeb DSL】 Servantの紹介」のサンプルコードが古くなって動かなくなってたので最新版に対応しました〜🙌https://t.co/cpJDWl8dsa
— lotz△ (@lotz84_) 2018年7月28日
2年前に書いた記事をアップデートした時に、例として利用していたservant-edeの開発が止まってしまっていたことです。そもそもservant-edeで利用されているHaskellのテンプレートライブラリede
の開発が止まってしまっているのでservant-edeの開発が止まってしまうのも仕方がないことなのですが、テンプレートで書いたHTMLを簡単にservantでサーブできるというservant-edeの機能は捨てがたいものだったので、新しくテンプレートライブラリにstacheを採用した同様のライブラリを作ることにしました。stacheを採用した理由はmustacheという標準的なテンプレートであるということと、stackbuildersという信頼できる企業がメンテナンスしているということです(後者の理由によりstacheの軽量版ライブラリであるmicrostache
の採用は見送りました)。
servant-stache の使い方
例えば以下のような index.mustache
というテンプレートを templates
フォルダの中に作成します。
<!DOCTYPE html>
<html>
<head><title>User</title></head>
<body>
<ul>
<li><strong>Name:</strong> {{ name }}</li>
<li><strong>Age:</strong> {{ age }}</li>
</ul>
</body>
</html>
このテンプレートを利用したWebページを配信するServantアプリを作ってみましょう。
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
import GHC.Generics
import Data.Aeson
import qualified Network.Wai.Handler.Warp as Warp
import Servant
import Servant.Mustache
data User = User
{ name :: String
, age :: Int
} deriving (Generic, ToJSON)
type API = Get '[HTML "index"] User
api :: Proxy API
api = Proxy
server :: Server API
server = pure (User "lotz" 28)
main :: IO ()
main = do
loadTemplates "./templates"
putStrLn "Listening on port 8080"
Warp.run 8080 $ serve api server
実行すれば以下のように見えるはずです。

ポイントは以下の2つです。
- Servantアプリを実行する前に
loadTemplates
でテンプレートを読み込む - サーバーとしてテンプレートのパラメータをJSONとして生成するような(ToJSONのインスタンスになっている)型の値を返す関数を実装する
これだけ書けばMustacheのテンプレートで作成したHTMLを簡単に配信することができます。
HTML
は
- Content-Type として
text/html;charset=utf-8
を使用する - パラメータの値は全てサニタイズされる
という特徴を持ちます。
逆に上記の処理を行わずに、素のパラメータでレンダリングしたテンプレートを任意のContent-Typeで返したい時はTemplate
型を使用してください
servant-stache の仕組み
loadTemplates
を行った際に以下のように宣言されているグローバルな変数にテンプレートを保存しています。
{-# NOINLINE __template_store #-}
__template_store :: MVar Stache.Template
__template_store = unsafePerformIO newEmptyMVar
そしてコンテンツをレンダリングする時にテンプレートを取り出して使用します。
instance (KnownSymbol file, Accept ct, ToJSON a) => MimeRender (Template ct file) a where
mimeRender _ val =
encodeUtf8 $ Stache.renderMustache template (toJSON val)
where template = tstore { Stache.templateActual = Stache.PName (pack filename) }
filename = symbolVal (Proxy :: Proxy file)
tstore = unsafePerformIO (readMVar __template_store)
HTML
をレンダリングする際も上述した前処理を行った後はTemplate
のレンダリング処理を利用しています。
おわりに
今後もメンテナンスは続けていくつもりなのでGitHubレポジトリにIssueやPRを投げていただければ対応します。個人的に作ろうと思ってるWebベースのプロジェクトがあるので、次はそっちでドッグフーディングしてみたいと思います。
https://github.com/lotz84/servant-stache