読者です 読者をやめる 読者になる 読者になる

くりふぃの日記

プログラミングとかのメモ書きとかに使えたらいいなぁ。

servantで簡単WebAPI

Haskell servant

最近はもっぱらHaskellの勉強中で、最近ようやく簡単なモナド変換子の取り扱いにも慣れてきたところで、 WebAPIの実装練習をしてみようかなと使ってみたservantが非常に使い勝手がよかったので紹介記事を書きました。

servantとは

servantは型レベルでWebAPIを書けるライブラリである。 WebAPIの定義を型レベルで宣言することで、APIサーバーのルーティングなどを型安全に書くことができる。

servantでAPIサーバーを実装するにあたってwaiを使用することになる。

servantの使い方

servantではまずAPIの型を定義する必要がある。この型は型演算子を用いて定義でき、一種のDSLのようになっている。

その型を用いて関数を定義することで簡単にAPIサーバーを実装することが可能となっている。

例えば/usersのようなURLのAPIを定義し、実装をすると以下のようなコードとなる。

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}

module Main where

import Data.Aeson as A
import Data.Aeson.TH as ATH
import Network.Wai as W
import Network.Wai.Handler.Warp as Warp
import Servant as S

data User = User { userId:: Int, userFirstName :: String, userLastName :: String } deriving Show
$(ATH.deriveJSON A.defaultOptions ''User)

-- /users
type API = "users" :> S.Get '[S.JSON] [User]

main :: IO ()
main = Warp.run 8080 app

app :: W.Application
app = S.serve api server

api :: S.Proxy API
api = S.Proxy

server :: S.Server API
server = return users

users :: [User]
users = [User 1 "firstname1" "lastname1", User 2 "fistname2" "lastname2"]

これだけでビルドして実行すればAPIサーバーとしてリクエストを受け取りUserのリストをJSONでレスポンスしてくれる。 型演算子:>がURLのスラッシュを表していると考えるとわかりやすい。

Get '[JSON] UserについてはGETでUserをJSONで返却するという意味になっている。 JSON処理にAesonを使用していてAesonのToJSONのインスタンスである型の値を渡すことでservant側で勝手に変換してくれる。

このため、処理は単純な関数として実装でき、最終的に必要な値を返却すればよいので非常にシンプルである。

API一つの処理を関数はExceptT ServantErr IO aという型である必要がある。

例外処理を書かなければ単にIO aの関数を書いてliftIOで持ち上げるだけとなるのでHaskellで副作用を取り扱えるのであれば難しい要素は一切ない。

※副作用もなければ例示したコードのように値をreturnでモナドに包んでやるだけである。

エラー処理に関してもExceptTでServantErrを使用する必要があるというだけであるため、Haskellで例外の取り扱いが分かっているのでのあれば特に複雑な構造というわけでもないはずである。

これだけ簡単に型安全なAPIサーバー実装が行えるのだからservantはとても強力なライブラリであるといえよう。

複数のAPI

しかし、APIサーバーは当然1つのURLではなく複数のURLを持つものである。また、URLの一部をパラメータとして利用するケースも多いにある。

この定義に追加してURLにIDを含んだ形で受け取りその値を利用して一つのUserの値を取得して返却するAPIを定義するにはどのようにすべきか。

答えは以下のようにtypeの定義を変えた上でserverに処理を追加してあげればよい。

-- 変更・追加部分のみ

-- GET /users
-- GET /users/:id
type API = "users" :> S.Get '[S.JSON] [User]
    :<|>   "users" :> S.Capture "id" Int :> S.Get '[S.JSON] User

-- (略)

server :: S.Server API
server = return users
    :<|> return user

-- (略)

-- 単純化するためCaptureで取得した値を捨てている
user :: Int -> User
user _ = User 1 "firstname1" "lastname1"

:<|>演算子によって複数のAPIを持つ型を定義できる。処理についても型と同じように:<|>演算子を使って繋げてあげるだけよい。

そしてCapture "id" Intという記述でその部分がパラメータとして利用できるようになる。 Captureの第二引数として型を定義しているため、それを受け取り処理する関数の型も決まるため型安全に処理を記述することができる。

3つ以上でも考え方は同じであり、クエリ文字列の取得、POSTのリクエストボディの取得についても同じように定義できる。

詳しくはservantのドキュメントHackageを見て実際にコードを書いてみてもらいたい。

Phoenixの--no-brunchで消えるもの

Elixir Phoenix

経緯

brunch以外のビルドツールを使いたいときにPhoenixとの連携に何が必要なのかが知りたかった。

brunchなしのプロジェクト

Phoenixを--no-brunchオプション付きでプロジェクト作成するとbrunchが取り除かれたプロジェクトが作成される。

APIサーバプログラムを作るときとかは特にビルドツールはいらないと思うので--no-brunchを使えばいい。

ただ、brunch以外のビルドツールを使いたいときに--no-brunchしてしまうとビルドツール導入時にPhoenixとの連携に必要な設定もすべて自分で行う必要がある。

普通にプロジェクトを作成して必要な部分を置き換えていくという形でもよいが、どのみちPhoenixがbrunchと連携している箇所を知る必要がある。

なので、--no-brunchに対して通常作成したときのプロジェクトの差分を見て連携に必要な設定をすべて暴こうと考えた。

あとで分かるがどちらにしてもコストはあまり変わらないのだが……。

diffの結果

.gitignore

これは単純にbrunchで必要とするファイルのうちgit管理下から取り除くための記述があるかないかだけ。わざわざ書くまでもない。

README.md

以下の記述が増える。brunchの導入に必要なこと。

  * Install Node.js dependencies with `npm install`

config/dev.exs

watcherの設定が増えている。開発環境でビルド対象の変更をwatchして再ビルドを行うための記述だろう。brunch以外のビルドツールを使用する場合にそのビルドツールに対応するwatch方法を定義してあげればよさそうだ。

-  watchers: []
+  watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
+                    cd: Path.expand("../", __DIR__)]]

package.json

brunchの導入に必要となる。npmの設定である。

priv/static

このディレクトリは逆に消えている。brunchでビルドしないので最初に必要なJavaScriptCSSなどが含まれていると思われる。brunchありの場合はbrunch側で作成される。

web/static

vendor以外のディレクトリがなくなっている。このディレクトリにあるファイルがbrunchでビルドされている。

まとめ

大した差分はない。実質wachersの記述しかPhoenixとの連携で必要になる要素はない模様。

前にPhoenixにWebpack乗せたときはかなり調べたのだけれどもこれを見る限りでは--no-brunchでプロジェクトを作成してpackage.jsonで必要なライブラリをインストールしてしまえばwatcherの設定するだけで問題がなさそう。

正直、前に導入したときの苦労とはいったいという感じなので、きちんとビルドツールの取扱を理解してから望むべきだったなと実感。

Elixir/Phoenixで適当になにか作る

Elixir Phoenix

なんで?

Elixirの存在は今よりもっと前から知っていたんだけど、その時は特に興味も抱かずに深く調べなかった。

最近になって職場でElixirの名前が出てきたので何なのか興味を持ちだしてRubyっぽい関数パラダイムの言語というところに惹かれてちょっと触ってみようかなと。

何作る?

具体的に決めてはいないがJSON APIで短いテキストをやり取りするアプリを作る。なにせ全く知らない言語でパラダイムも今までほとんど触れたことのない関数型なのでまともなものは作れないから。

環境

今回の環境を以下に列挙する。

プロジェクト作成

今回はPhoenixFrameworkを利用して作っていこうと思う。

Phoenixの新規プロジェクトを作成するコマンドは以下の通り。

mix phoenix.new PROJECT_NAME

PROJECT_NAMEは任意のプロジェクト名を入力する。自分の環境ではposted_strという名前で作ってます。

コマンドを入力するとプロジェクト名のディレクトリ以下にファイルが生成される。その後コマンド

リソース設計

RESTっぽくリソースの定義。とりあえずユーザーと登録テキストの2つがあればいろいろできるかな。

認証系は今は無視するとしてユーザー情報としてユーザー名。それに紐づく登録テキスト。

とりあえずectoを使ってPostgreSQLに保管しておくことにする。

モデル・コントローラ・ビュー生成

リソースに基づいて骨組みとなるファイルを生成する。Phoenixで定義されているMIXタスクによって行うことができpnoenix.gen.jsonというタスクで骨組みの生成ができる。

他にも以下の様なタスクがあり自動的にファイルを生成してくれる。

  • phoenix.gen.html(HTMLページ用の骨組み生成)
  • phoenix.gen.model(モデルの生成)
  • phoenix.gen.channel(チャンネルの生成)

チャンネルはリアルタイムWEB用に用いるWebsocketなどのインターフェイスのようだ。詳しくはわからない。

早速ユーザー情報と登録テキストの骨組み生成してみる。

mix phoenix.gen.json User users username:string
mix phoenix.gen.json Post posts user_id:integer post:text

骨組み生成はこれで完了。

ルーティング定義

骨組みを生成しただけではルーティングの定義までは行ってくれないようなので、生成時のメッセージを元にルーティングを定義する。

特に難しく考えずメッセージに書かれた内容をコピペする。

これをしないとコンパイルエラーが発生する。

ルーティング確認

現在のルーティングの定義を確認するためのMIXタスクがPhoenixphoenix.routesとして定義されている。

どのようなルーティング定義になっているかを実際に確認してみる。

mix phoenix.routes

出力結果は省略する。以下の情報が確認できるかと思う。

  • ルーティング名(ソースコード上でパスを返却する関数となる)
  • HTTPメソッド
  • パス
  • モジュール名(呼び出されるモジュール名)
  • アクション名(呼び出される関数名)

マイグレーション

骨組みを生成したらモデルの内容を実際にDBに反映させる必要がある。マイグレーションPhoenixで使われているDBライブラリのEctoによって行う。

mix ecto.create
mix ecto.migrate

mix ecto.createでデータベースを作成し、mix ecto.migrateスキーマ定義が更新される。

リレーション定義

ユーザー情報と登録テキストは一対多の関係である。この関係をモデルに定義する。

Railsとだいたい同じ。第二引数にモジュールを指定する必要があるのと、belongs_toでは外部キーを指定して上げる必要が有ることが異なる。

belongs_toはフィールド定義も兼ねているようで、モデル生成で定義された外部キーとなるレコードは定義を消してあげる必要がある。消さないと定義の重複でエラーとなる。

動作確認

これで一通り動作するまでになったと思われるので、ユーザー情報を登録してテキストを投稿するまで確認してみる。

動作確認用のサーバを立ち上げるにはphoenix.serverタスクを実行する。

mix phoenix.server

これでlocalhost:4000にHTTPリクエストを受け付けることができるようになる。

ユーザー情報登録

テスト用のデータは何も用意していないので作成したJSON APIを用いて直接データを登録する。

ユーザー情報登録はUserController#createであり、ルーティングはPOST /usersと定義されている。ここに何らかの手段を用いてJSONデータをPOSTする。

PhoenixではContent-typeをapplication/jsonでデータを送るとマッピングしてControllerのActionとして定義した関数の第二引数にバインドされる。

phoenix.gen.jsonタスクで生成したUserControllerはパターンマッチングによって"user"をキーとした値以外を受け付けないようになっている。そのため以下の様なJSONデータを送信する必要がある。

{"user": {"username": "name"}}

以上のデータを送信することでデータベースにユーザー情報が登録される。

データが作成されたため、GET /usersで登録したデータの一覧が取得できる。

{"data":[{"username":"test","id":1}]}

当然、作成したユーザー情報をIDで指定して取得もできる。

{"data":{"username":"test","id":1}}

登録テキスト投稿

登録テキストの投稿も同様に行える。

{"post": {"user_id": 1, "post": "テキスト"}}

総括

これといったコードは書いていないが簡単なリソースのCRAD処理はPhoenixの機能で簡単に実現できた。ここらへんは完全にRails同様である。

ここから少しずつコードを書いていって何らかのアプリと呼べるようなものを作っていきたいと思う。

その前に自動生成されたソースコードの読み解きを行う必要がありそうなので、次の機会に挙動から分かる範囲で読み解いていこうと思う。