4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Servant】(5) JSON - Aeson

Last updated at Posted at 2020-02-22

【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 を表しています。

API
type UserAPI1 = "users" :> Get '[JSON] [User]

Servantではデフォルトの Content-Type として、以下のものがあらかじめ定義されています。

Default-Content-Type
data JSON
data PlainText
data FormUrlEncoded
data OctetStream

HTTPのリクエストとレスポンスでJSONをやり取りするときは、エンコードとデコードが必要になります。HaskellでJSONを扱うときはAesonが使われますが、encodeとdecodeの関数も提供されています。
Data.Aeson - hackage

Aeson
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のクラス型は以下のように定義されます。

MimeRender-MimeUnrender-Class
-- 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のインスタンスとなります。

MimeRender-MimeUnrender-JSON-Instance
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が用いられます。さらにAesonAttoparsecを利用しています。

attoparsec: Fast combinator parsing for bytestrings and text
aeson: Fast JSON parsing and encoding

Attoparsecでは、任意のParserを与えることで、入力文字列をパースするための関数を提供しています。

Attoparsec
parseOnly :: Parser a -> Text -> Either String a
endOfInput :: forall t. Chunk t => Parser t ()

2つの関数を組み合わせることによって、全入力を消費するパーサが実現できます。以下のようにして使われます。

parseOnly
parseOnly (myParser <* endOfInput)

ここで2つの関数はApplicativeによって結合されています。

Applicative
liftA2 f x y = f <$> x <*> y
u <* v = liftA2 const u v

AesonではJSON文字列をパース・デコードするためのParserが提供されています。parseOnlyの引数とすることでJSON文字列をパースします。

Aeson
Data.Aeson.Parser.value :: Parser Value

Value型はFromJSONのインスタンスであり、任意の**JSON AST (abstract syntax tree)**を表現するために使われます。

以下にHTMLを MimeRender のインスタンスとする実装を見ていきます。

3. servant-lucid と servant-blaze

Haskell で HTMLを扱うときは、blaze-htmllucidが使われることが多いです。Servantでは両方サポートされています。

servant-lucid
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 は以下のように定義することができます。

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を扱うプログラムを紹介する予定です。

今回は以上です。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?