RESTfulなAPIを提供する場合、クライアントの都合にあわせて、フォーマットを変えてレスポンスを返したいケースがあります。サーバー上で管理しているDBから、表現だけをHTML, JSON, XML, CSVなどに変更して返すイメージです。例えば、人物情報(名前、年齢、性別など)の一覧を返す際には以下のようなデータが返されることになります。
- HTML
<table border> <tr> <th>name</th> <th>sex</th> <th>age</th> </tr> <tr> <td>Taro Yamada</td> <td>Male</td> <td>18</td> </tr> <tr> <td>Hanako Yamada</td> <td>Female</td> <td>25</td> </tr> <tr> <td>Ichiro Suzuki</td> <td>Male</td> <td>43</td> </tr> </table>
[ {"name":"Taro Yamada", "sex":"Male", "age":18}, {"name":"Hanako Yamada", "sex":"Female", "age":25}, {"name":"Ichiro Suzuki", "sex":"Male", "age":43} ]
Taro Yamada,Male,18 Hanako Yamada,Female,25 Ichiro Suzuki,Male,43
通常はフォーマット毎に別のURLを割り当てますが、Yesodでは1つのURLで複数フォーマットのレスポンスを返す実装が簡単にできます。
このエントリでは、http://localhost:3000/に対応する一つのハンドラの中でTypedContentを利用して複数のフォーマットを扱う方法を紹介します。クライアントが送信するリクエストに記載されるAcceptヘッダによって、サーバーの挙動を変えます。
準備:
まず、素のYesodのscaffolding siteを起動できる環境を作ります。stackが利用できる環境を前提にしています。- yesod-simpleを指定して空の"typedcontent"プロジェクトを生成
- 生成したプロジェクトディレクトリに移動し、依存ツールをビルド
- 生成したtypedcontentプロジェクトをビルド
- develサーバーを起動
% stack new typedcontent yesod-simpleyesod-simpleは最も単純なyesodテンプレートでDB関連ライブラリへの依存がありません。
% cd typedcontent % stack build yesod-bin cabal-install
% stack build
% stack exec -- yesod develブラウザからhttp://localhost:3000/を開いてdevelサーバーにアクセスできることを確認してください。
基礎1:Home.hsを修正してHTMLを返すシンプルなハンドラを作ってみる
- Handler/Home.hsに人物情報データ定義を追加 yesod-simpleテンプレートではDBを利用しないプロジェクトが生成されます。プロジェクト内のHandlerディレクトリにHome.hsとCommon.hsが存在しているので、Home.hsをエディタで開いて、以下のコードを追加してください。
- Handler/Home.hsのgetHomeRの定義を更新 既存のgetHomeRの実装を削除し、toTableHtmlで置き換えて下さい。
- 動作確認 ブラウザでhttp://localhost:3000/にアクセスし、以下のようなテーブルが表示されていればOKです。
-- サーバー上で管理するデータ定義。 data Sex = Male | Female deriving (Show) data Person = Person { name :: Text , age :: Int , sex :: Sex } deriving (Show) -- サーバー上のサンプルデータ。3人分の情報を保持。 samplePersonList :: [Person] samplePersonList = [ (Person "Taro Yamada" 18 Male) , (Person "Hanako Yamada" 25 Female) , (Person "Ichiro Suzuki" 41 Male) ]
-- HTML tableフォーマットでレスポンスを生成。 toTableHtml :: Handler Html toTableHtml = withUrlRenderer [hamlet| <table border> <tr> <th>name <th>age <th>sex $forall person <- samplePersonList <tr> <td>#{name person} <td>#{age person} <td>#{show $ sex person} |] getHomeR :: Handler Html getHomeR = toTableHtml
curlコマンドを実行すると実際にサーバーから返されているHTMLフォーマットデータを確認することができます。
% curl http://localhost:3000/
基礎2:クライアント要求に応じてHTMLとplain textのどちらかを返す
いよいよ、クライアントからの要求に応じて異なるフォーマットを返すよう、getHomeRを改変します。ここでは"text/html"が要求されている場合にはHTMLを、"text/plain"が要求されている場合にはshow関数の実行結果を返すようにします。- getHomeRの型をHandler HtmlからHandler TypedContentに変更し、"text/html"と"text/plain"に対応する getHomeRの実装をselectRep/provideRepを用いて以下のように変更します。
- 動作確認 curlコマンドで-HでAcceptヘッダを指定することで、HTMLとplain textの2種類の結果が得られることが確認できます。
- provideRepをprovideRepTypeに置き換えてみる "text/plain"をprovideRepTypeを用いて実装すると以下のようになります。
getHomeR :: Handler TypedContent
getHomeR = selectRep $ do
provideRep $ toTableHtml
provideRep $ return $ repPlain $ show samplePersonList
selectRepはdoブロックの中でprovidRepによって提供される複数のフォーマットから、クライアントの要求に適合するものを選択します。provideRepの引数で渡されているHtml, RepPlainはHasContentTypeのインスタンスであり、hasContentTypeが実装されています。この関数によりmime typeが比較され適切なContentが選択されます。マッチするものがない場合にはクライアントには406 Not Acceptableが返されます。
% curl -H "Accept: text/plain" http://localhost:3000/ [Person {name = "Taro Yamada", age = 18, sex = Male},Person {name = "Hanako Yamada", age = 25, sex = Female},Person {name = "Ichiro Suzuki", age = 41, sex = Male}] % curl -H "Accept: text/html" http://localhost:3000/ <table border><tr><th>name</th> <th>age</th> <th>sex</th> ...Yesodにはquery string parameterからAcceptヘッダを自動生成する便利機能が実装されています。 この仕組みを利用することで、Acceptヘッダを入力できないブラウザ上でも(URL入力だけで)動作を確認できます。
% curl http://localhost:3000/?_accept=text/plain [Person {name = "Taro Yamada", age = 18, sex = Male},Person {name = "Hanako Yamada", age = 25, sex = Female},Person {name = "Ichiro Suzuki", age = 41, sex = Male}]
getHomeR :: Handler TypedContent getHomeR = selectRep $ do provideRep $ toTableHtml -- provideRep $ return $ repPlain $ show samplePersonList provideRepType "text/plain" (return $ show samplePersonList)provideRepTypeを利用することで、HasContentTypeのインスタンスを持っていないフォーマットを返すことができます。
応用:JSON, CSVフォーマットをサポートする
ここまでの手順を応用して、CSV及びJSONフォーマットを返すようにgetHomeRを拡張します。- provideRepTypeを利用してCSVフォーマットを返す実装を追加
- 動作確認
- ついでにJSONもサポート
- 動作確認
-- CSVフォーマットのレスポンスを生成する。 class ToCSV a where toCsv :: a -> Text instance ToCSV Person where toCsv p = (name p) ++ ("," :: Text) ++ (pack $ show $ age p) ++ ("," :: Text) ++ (pack $ show $ sex p) ++ ("\n" :: Text) instance (ToCSV a) => ToCSV [a] where toCsv [] = "" toCsv (x:xs) = (toCsv x) ++ (toCsv xs) getHomeR :: Handler TypedContent getHomeR = selectRep $ do provideRep $ toTableHtml provideRep $ return $ repPlain $ show samplePersonList provideRepType "text/csv" (return $ toCsv samplePersonList) -- 追加!
% curl -H "Accept: text/csv" http://localhost:3000/ Taro Yamada,18,Male Hanako Yamada,25,Female Ichiro Suzuki,41,Male
{-# LANGUAGE DeriveGeneric #-}
...
-- toJSONを自動導出。DeriveGeneric言語拡張が必要。 instance ToJSON Sex instance ToJSON Person getHomeR :: Handler TypedContent getHomeR = selectRep $ do provideRep $ toTableHtml provideRep $ return $ repPlain $ show samplePersonList provideRepType "text/csv" (return $ toCsv samplePersonList) provideJson $ samplePersonList -- 追加!
% curl -H "Accept: application/json" http://localhost:3000/ [{"age":18,"name":"Taro Yamada","sex":"Male"},{"age":25,"name":"Hanako Yamada","sex":"Female"},{"age":41,"name":"Ichiro Suzuki","sex":"Male"}]
まとめ:
TypedContentを利用して一つのURLからHTML, PlainText, CSV, JSONフォーマットのデータを返す方法を説明しました。ハンドラを実装する上で、selectRep/provideRep/provideRepTypeをどう使えばよいか、YesodフレームワークがContentTypeの判定にHasContentTypeを用いている、といったキモになる情報をまとめています。このエントリで利用したコードは以下のgithubリポジトリにコミットしてあるので、参考にしてください。
https://github.com/kurokawh/work_haskell/tree/master/yesodweb/typedcontent
参考:
- Yesod Web Framework Book- Version 1.4
- Hackage
- 関連エントリ
- [haskell][yesod] YesodにおけるRESTfulなJSON API実装チュートリアル
- [haskell][yesod] stackのnewコマンドで指定できるyesod関連templateの説明
- その他
0 件のコメント:
コメントを投稿