【Servant】(1) Wai - Qiita
【Servant】(2) Servantチュートリアル - Qiita
【Servant】(3) エンドポイントを増やす - Qiita
【Servant】(4) URLパラメータをハンドラの引数とする - Qiita
【Servant】(5) JSON - Qiita
【Servant】(6) HTML - Qiita
【Servant】(7) Post Data - Qiita
【Servant】(8) Another Monad - Qiita
【Servant】(9) Handlerモナド - Qiita
【Servant】(10) SQLite - Qiita
【Servant】(11) Servant-Client - Qiita
【Servant】(12) Basic 認証 - Qiita
前回まではレスポンスデータはJSONで返していましたが、この辺は定義済みのものとして明示的に示すことはありませんでした。今回はServantのJSON周りの裏側の実装を見ていきたいと思います。基礎的なことの確認で、実際に動作するプログラムはありません。次回にレスポンスデータとしてHTMLを返す実装を見ていく予定です。【Servant】(6) HTML。今回のJSONの実装をなぞることになります
今回も公式ドキュメントで、Servantの基礎を学んでいきます。
Docs » Tutorial » Serving an API - A first example
1. JSON
HTTPヘッダーの復習です。
- Acceptリクエストヘッダーは、クライアントが理解できるコンテンツタイプを MIME タイプで伝えます。以下のContent-Typeレスポンスヘッダーに対応します。
- Content-Type レスポンスヘッダーは、クライアントに返されたコンテンツが実際にはどのような種類のものであるかを伝えます。
以下のAPIの定義に用いられている '[JSON] は JSON の Content-Type を表しています。
type UserAPI1 = "users" :> Get '[JSON] [User]
Servantではデフォルトの Content-Type として、以下のものがあらかじめ定義されています。
data JSON
data PlainText
data FormUrlEncoded
data OctetStream
HTTPのリクエストとレスポンスでJSONをやり取りするときは、エンコードとデコードが必要になります。HaskellでJSONを扱うときはAesonが使われますが、encodeとdecodeの関数も提供されています。
Data.Aeson - hackage
encode :: ToJSON a => a -> ByteString
decode :: FromJSON a => ByteString -> Maybe a
Haskellにおいてはクライアント・サーバ間のHTTP通信では、lazy ByteString でデータがやり取りされます。
- encode - a型の JSON value を lazy ByteString にシリアライズします。レスポンスを送信するときに使われる。
- decode - lazy ByteString から a型の JSON valueへとデ・シリアライズします。リクエストを受け取るときに使われる。
2. MimeRender と MimeUnrender
Servantでは、encode と decode の定義をもう少し拡張して、MimeRender と MimeUnrenderとして実装します。JSONに関して言えば、MimeRenderはencodeそのものですが、MimeUnrender は Maybe a ではなく Either String a を返します。
MimeRender と MimeUnrenderのクラス型は以下のように定義されます。
-- for reference:
class Accept ctype where
contentType :: Proxy ctype -> MediaType
class Accept ctype => MimeRender ctype a where
mimeRender :: Proxy ctype -> a -> ByteString
-- alternatively readable as:
mimeRender :: Proxy ctype -> (a -> ByteString)
class Accept ctype => MimeUnrender ctype a where
mimeUnrender :: Proxy ctype -> ByteString -> Either String a
もちろんJSON は MimeRender と MimeUnrenderのインスタンスとなります。
instance Accept JSON where
contentType _ = "application" // "json"
instance ToJSON a => MimeRender JSON a where
mimeRender _ = encode
eitherDecodeLenient :: FromJSON a => ByteString -> Either String a
eitherDecodeLenient input = do
v :: Value <- parseOnly (Data.Aeson.Parser.value <* endOfInput) (cs input)
parseEither parseJSON v
instance FromJSON a => MimeUnrender JSON a where
mimeUnrender _ = eitherDecodeLenient
eitherDecodeLenient は少し複雑です。深入りはしませんが、 aeson や attoparsec を利用することになります。
2-1. Aeson
HaskellでJSONを扱うためにはAesonが用いられます。さらにAesonはAttoparsecを利用しています。
attoparsec: Fast combinator parsing for bytestrings and text
aeson: Fast JSON parsing and encoding
Attoparsecでは、任意のParserを与えることで、入力文字列をパースするための関数を提供しています。
parseOnly :: Parser a -> Text -> Either String a
endOfInput :: forall t. Chunk t => Parser t ()
2つの関数を組み合わせることによって、全入力を消費するパーサが実現できます。以下のようにして使われます。
parseOnly (myParser <* endOfInput)
ここで2つの関数はApplicativeによって結合されています。
liftA2 f x y = f <$> x <*> y
u <* v = liftA2 const u v
AesonではJSON文字列をパース・デコードするためのParserが提供されています。parseOnlyの引数とすることでJSON文字列をパースします。
Data.Aeson.Parser.value :: Parser Value
Value型はFromJSONのインスタンスであり、任意の**JSON AST (abstract syntax tree)**を表現するために使われます。
以下にHTMLを MimeRender のインスタンスとする実装を見ていきます。
3. servant-lucid と servant-blaze
Haskell で HTMLを扱うときは、blaze-html と lucidが使われることが多いです。Servantでは両方サポートされています。
data HTMLLucid
instance Accept HTMLLucid where
contentType _ = "text" // "html" /: ("charset", "utf-8")
instance ToHtml a => MimeRender HTMLLucid a where
mimeRender _ = renderBS . toHtml
-- let's also provide an instance for lucid's
-- 'Html' wrapper.
instance MimeRender HTMLLucid (Html a) where
mimeRender _ = renderBS
この辺は自分で実装しなくとも、Servant.HTML.Lucidで既に定義されているようです。
servant-blaze は以下のように定義することができます。
-- For this tutorial to compile 'HTMLLucid' and 'HTMLBlaze' have to be
-- distinct. Usually you would stick to one html rendering library and then
-- you can go with one 'HTML' type.
data HTMLBlaze
instance Accept HTMLBlaze where
contentType _ = "text" // "html" /: ("charset", "utf-8")
instance ToMarkup a => MimeRender HTMLBlaze a where
mimeRender _ = renderHtml . Text.Blaze.Html.toHtml
-- while we're at it, just like for lucid we can
-- provide an instance for rendering blaze's 'Html' type
instance MimeRender HTMLBlaze Text.Blaze.Html.Html where
mimeRender _ = renderHtml
今回はservant-lucidを使って実際に動作するプログラムを試みましたが、なにかわからないエラーで断念しました。ただし、次回もう少しプリミティブな形でHTMLを扱うプログラムを紹介する予定です。
今回は以上です。