WebAPIの定義にservant 、データ永続化ライブラリに persistent 、フロントエンドフレームワークにPureScript製のpuxを使っています。
全てのソースは https://github.com/orehathiya/example-servant-persistent にあります。
servant + persistent を使ったWebAPIの紹介は以下のように既に記事がいくつか存在するのでpux との連携部分について主に紹介します。
- https://qiita.com/jabaraster/items/e8ebbe6d25b535947aba
- https://qiita.com/cyclone_t/items/52ad44cfbb4603e123f3
servantの型定義からPureScriptの型とAPIクエリ関数を生成する
servantは実装するAPIの仕様を型によって定義することができ、自動でHTTPリクエストパラメータやレスポンスのシリアライズ/デシリアライズを行ったり、自動でドキュメントを生成したり、APIにアクセスするクライアント関数を生成したりすることができます。
これによってAPIの実際の実装とドキュメントやクライアント関数との間に乖離が発生することを防ぐことができます。
サンプルでは servant-purescript を使ってservantで定義したHaskellのAPIの型とデータ型からPureScriptのAPIクエリ関数とデータ型を生成しています。
自分で定義したUserとReport型は何も問題なく生成できますが、PersistentのEntity aとKey a型はそのままGenericのインスタンスにできない(GADTs等はできないらしい)ので構造が似たようなMyEntityとMyKeyを定義してそのRepで上書きしています(このあたりもっと綺麗な書き方があれば教えて欲しいです)。
data MyEntity record = Entity
{ key :: Key record
, value :: record
} deriving (Generic)
newtype MyKey a =
Key Int
deriving (Generic)
instance Generic (Entity a) where
type Rep (Entity a) = Rep (MyEntity a)
from = from
to = to
instance Generic (Key a) where
type Rep (Key a) = Rep (MyKey a)
from = from
to = to
myTypes :: [SumType 'Haskell]
myTypes =
[ mkSumType (Proxy :: Proxy (Entity A))
, mkSumType (Proxy :: Proxy (Key A))
, mkSumType (Proxy :: Proxy User)
, mkSumType (Proxy :: Proxy Report)
]
生成される型とAPIクエリ関数
生成される型は以下のようになります。
newtype User =
User {
name :: String
, age :: Int
}
derive instance genericUser :: Generic User
derive instance newtypeUser :: Newtype User _
newtype Report =
Report {
imp :: Int
, click :: Int
, ctr :: Int
}
derive instance genericReport :: Generic Report
derive instance newtypeReport :: Newtype Report _
Haskell側の型とほぼおなじですね。Generic型クラスのインスタンスにもなっているのでpurescript-argonaut-generic を使ってJSONのシリアライズ/デシリアライズができます。APIクエリ関数は以下のようになります(多いのでgetUserGetByName関数だけ抜粋)。
getUserGetByName :: forall eff m.
MonadAsk (SPSettings_ SPParams_) m => MonadError AjaxError m => MonadAff ( ajax :: AJAX | eff) m
=> String -> m (Entity User)
getUserGetByName name = do
spOpts_' <- -="" ask="" let="" o="" of="" spopts_="" spsettings_=""> o
let spParams_ = case spOpts_.params of SPParams_ ps_ -> ps_
let baseURL = spParams_.baseURL
let httpMethod = "GET"
let queryString = ""
let reqUrl = baseURL <> "user" <> "/" <> "get"
<> "/" <> encodeURLPiece spOpts_' name <> queryString
let reqHeaders =
[]
let affReq = defaultRequest
{ method = httpMethod
, url = reqUrl
, headers = defaultRequest.headers <> reqHeaders
}
affResp <- -="" affjax="" affreq="" d="" decodejson="case" let="" of="" spopts_.decodejson="" spsettingsdecodejson_=""> d
getResult affReq decodeJson affResp
->->
APIの定義通りnameパラメータを受け取ってEntity User型を返す関数になっています。
PUXから使う
生成されたAPIクエリ関数はimportして以下のように使えます。
foldp (ReceiveUser (Entity user)) (State st) =
noEffects $ State st {
user = Just (Entity user)
, status = "User"
}
foldp (RequestUser) state = runEffectActions state [ReceiveUser <$> getUserGetByName "Alice"]
puxはいわゆるElm ArchitectureのPureScript製のフロントエンドフレームワーク です。foldpはElmのupdate、Reduxのreducerに相当する状態更新のための関数です。このfoldp関数では状態(state)の更新と副作用のあるeffectfulな処理の両方を行うことができます(詳しくは公式ドキュメント参照)。
effect内で自動生成された先ほどのgetUserGetByName関数を実行してReceiveUserイベントを発火し、state内のuserに取得したユーザーデータをセットしています。
まとめ
WebAPIを作成する時のよくある悩みとしてドキュメントやクライアントと実装の乖離が起こる事や、サーバーサイドとクライアントサイドで同じようなモデルを2回定義する必要がある事などがありますが、servantを使うとこれらの悩みを解消することができます。
皆様もぜひservantを使って楽々WebAPI開発を楽しみましょう!
参考サイト
- https://github.com/eskimor/purescript-bridge
- https://github.com/haskell-servant/example-servant-persistent
- https://github.com/alexmingoia/pux-starter-app
0 件のコメント:
コメントを投稿