Haskell

servant-stache を作りました

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.mustacheUserの値を適用して生成されます。

servant-stacheを作ろうと思ったきっかけは

2年前に書いた記事をアップデートした時に、例として利用していたservant-edeの開発が止まってしまっていたことです。そもそもservant-edeで利用されているHaskellのテンプレートライブラリedeの開発が止まってしまっているのでservant-edeの開発が止まってしまうのも仕方がないことなのですが、テンプレートで書いたHTMLを簡単にservantでサーブできるというservant-edeの機能は捨てがたいものだったので、新しくテンプレートライブラリにstacheを採用した同様のライブラリを作ることにしました。stacheを採用した理由はmustacheという標準的なテンプレートであるということと、stackbuildersという信頼できる企業がメンテナンスしているということです(後者の理由によりstacheの軽量版ライブラリであるmicrostacheの採用は見送りました)。

servant-stache の使い方

例えば以下のような index.mustache というテンプレートを templates フォルダの中に作成します。

templates/index.html
<!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