cowboyとectoでシンプルなRESTサーバを実装してみます。
(素直にPhoenixを使えとの神の声が聞こえてきそうですが、勉学目的で地道に行ってみます)
以下の通り動作するサーバを実装します:
$ curl localhost:3000/hello-school-live/characters
["丈槍 由紀","佐倉 慈","若狭 悠里","恵飛須沢 胡桃","直樹 美紀"]
このサンプルでは最新のcowboy 2系を利用しています(@ColdFreak さん、ご指摘ありがとうございます)。cowboy 2系ではErlang 18以上が必要となります。また、1系のサンプルについてはソースのみですがこちらにあります。
事始め
まずmixでアプリケーションを作成します:
$ mix new --sup hello_school_live
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/hello_school_live.ex
* creating test
* creating test/test_helper.exs
* creating test/hello_school_live_test.exs
Your mix project was created successfully.
You can use mix to compile it, test it, and more:
cd hello_school_live
mix test
Run `mix help` for more commands.
できたディレクトリに移動しておきます:
$ cd hello_school_live
Hello, cowboy!
cowboyは言わずと知れたErlang向けHTTPサーバです、Phoenixも内部ではcowboyを使っています。まず動作確認用に「Hello, School Live!」という文字列を返却するページを作ってみます。
mix.exs
にcowboyへのdependencyを追記します。
mix.exs
def application do
[applications: [:logger, :cowboy], # ← `:cowboy`を追記
mod: {HelloSchoolLive, []}]
end
...省略...
defp deps do
[
{:cowboy, github: "ninenines/cowboy"}, # ← 追記
]
end
lib/hello_school_live.ex
にルーティングの設定とcowboyの起動処理を追記します。
lib/hello_school_live.ex
def start(_type, _args) do
import Supervisor.Spec, warn: false
dispatch = :cowboy_router.compile routes # ← 追記
{:ok, _} = :cowboy.start_http :http, 100, [{:port, 3000}], [{:env, [{:dispatch, dispatch}]}] # ← 追記
children = [
...省略...
end
# ↓の関数を追記
defp routes do
[
{:_, [
{"/hello-school-live", HelloSchoolLive.Handlers.HelloSchoolLive, []}
]
}
]
end
ハンドラを格納するディレクトリを作って:
$ mkdir -p lib/hello_school_live/handlers
lib/hello_school_live/handlers/hello_school_live.ex
を以下内容で作成します:
lib/hello_school_live/handlers/hello_school_live.ex
defmodule HelloSchoolLive.Handlers.HelloSchoolLive do
def init(req, opts) do
req2 = :cowboy_req.reply 200, [{"content-type", "text/plain"}], "Hello, School Live!", req
{:ok, req2, opts}
end
end
dependencyを取得した後、起動してみます、
$ mix deps.get
...省略...
$ iex -S mix
...省略...
ブラウザで http://localhost:3000/hello-school-live にアクセスして「Hello, School Live!」が表示されることを確認します。
Hello, ecto!
ectoは「Elixir向けのデータベースラッパーと統合クエリ言語」と本家では紹介されています、個人的には綺麗にRailsから分離されたActiveRecordのElixir板と理解するのがわかり易いかと。ここではデータベースとしてPostgreSQLを利用します、その他のデータベースを利用する場合、アダプターを適宜置き換えることで動くはず…。
まず動作確認用にiexでcharactersテーブルから一覧取得できるようにしてみます。
mix.exsにectoへのdependencyを追記します:
def application do
[applications: [:logger, :cowboy, :postgrex, :ecto], # ← 追記
mod: {HelloSchoolLive, []}]
end
...省略...
defp deps do
[
{:cowboy, github: "ninenines/cowboy"},
{:postgrex, ">= 0.0.0"}, # ← 追記
{:ecto, "~> 0.14.3"} # ← 追記
]
end
依存関係を解決してコンパイルしておきます:
$ mix do deps.get, compile
...省略...
mix -h
を実行するとecto関連のタスクが追加されているのがわかります
$ mix -h
mix # Run the default task (current: mix run)
...
mix do # Executes the tasks separated by comma
mix ecto.create # Create the storage for the repo
mix ecto.drop # Drop the storage for the repo
mix ecto.gen.migration # Generate a new migration for the repo
mix ecto.gen.repo # Generate a new repository
mix ecto.migrate # Run migrations up on a repo
mix ecto.rollback # Rollback migrations from a repo
mix escript.build # Builds an escript for the project
...
以下コマンドでリポジトリを作成します:
$ mix ecto.gen.repo
* creating lib/hello_school_live
* creating lib/hello_school_live/repo.ex
* updating config/config.exs
Don't forget to add your new repo to your supervision tree
(typically in lib/hello_school_live.ex):
worker(HelloSchoolLive.Repo, [])
上記出力にあるとおり、supervision treeにリポジトリを追加する必要があります。lib/hello_school_live.ex
に以下を追記します:
lib/hello_school_live.ex
def start(_type, _args) do
...省略...
children = [
worker(HelloSchoolLive.Repo, [])
]
...省略...
end
また作成されたconfig/config.exs
のリポジトリ設定を環境に合わせて設定します、以下は私の環境の例です
config/config.exs
config :hello_school_live, HelloSchoolLive.Repo,
adapter: Ecto.Adapters.Postgres,
database: "hello_school_live_repo",
username: "postgres",
password: "postgres",
hostname: "localhost"
以下コマンドでデータベースを作成します:
$ mix ecto.create
Compiled lib/hello_school_live/handlers/hello_school_live.ex
Compiled lib/hello_school_live.ex
Compiled lib/hello_school_live/repo.ex
Generated hello_school_live app
The database for HelloSchoolLive.Repo has been created.
以下コマンドでマイグレーションファイルを作成します:
$ mix ecto.gen.migration characters
* creating priv/repo/migrations
* creating priv/repo/migrations/20150815055420_characters.exs
priv/repo/migrations/<TIMESTAMP>_characters.exs
を以下の通り編集します:
priv/repo/migrations/TIMESTAMP_characters.exs
def change do
create table(:characters) do # ← 追記
add :name, :string
end
end
以下コマンドでマイグレーションを適用します:
$ mix ecto.migrate
14:56:18.082 [info] == Running HelloSchoolLive.Repo.Migrations.Characters.change/0 forward
14:56:18.082 [info] create table characters
14:56:18.087 [info] == Migrated in 0.0s
次にモデルを作成します。まずモデルを格納するディレクトリを作成して、
$ mkdir lib/hello_school_live/models
lib/hello_school_live/models/character.ex
を以下内容で作成します。
lib/hello_school_live/models/character.ex
defmodule HelloSchoolLive.Models.Character do
use Ecto.Model
schema "characters" do
field :name, :string
end
end
iexで動作確認してみます:
$ iex -S mix
iex(1)> character = %HelloSchoolLive.Models.Character{name: "丈槍 由紀"}
iex(2)> HelloSchoolLive.Repo.insert! character
iex(3)> HelloSchoolLive.Repo.insert! %HelloSchoolLive.Models.Character{name: "佐倉 慈"}
iex(4)> HelloSchoolLive.Repo.insert! %HelloSchoolLive.Models.Character{name: "若狭 悠里"}
iex(5)> HelloSchoolLive.Repo.insert! %HelloSchoolLive.Models.Character{name: "恵飛須沢 胡桃"}
iex(6)> HelloSchoolLive.Repo.insert! %HelloSchoolLive.Models.Character{name: "直樹 美紀"}
iex(7)> import Ecto.Query
iex(8)> query = from c in HelloSchoolLive.Models.Character, select: c
iex(9)> HelloSchoolLive.Repo.all(query) |> Enum.each(&(IO.puts &1.name))
15:05:41.468 [debug] SELECT c0."id", c0."name" FROM "characters" AS c0 [] OK query=2.7ms
丈槍 由紀
佐倉 慈
若狭 悠里
恵飛須沢 胡桃
直樹 美紀
:ok
Hello, School Live!
まとめです、冒頭で提示したAPIを実装します。
まずlib/hello_school_live.ex
にルーティング/hello-school-live/characters
を追記します。
lib/hello_school_live.ex
defp routes do
[
{:_, [
{"/hello-school-live", HelloSchoolLive.Handlers.HelloSchoolLive, []},
{"/hello-school-live/characters", HelloSchoolLive.Handlers.Characters, []} # ← 追記
]
}
]
end
このAPIではJSONで応答したいので、応答時にはElxiirのtermをJSONエンコードした文字列を返却する必要があります。ここではElixir向けJSONライブラリのpoisonを利用します。mix.exs
にpoisonのdependencyを追記します
mix.exs
def application do
[applications: [:logger, :cowboy, :postgrex, :ecto, :poison], # ← 追記
mod: {HelloSchoolLive, []}]
end
...省略...
defp deps do
[
{:cowboy, github: "ninenines/cowboy"},
{:postgrex, ">= 0.0.0"},
{:ecto, "~> 0.14.3"},
{:poison, "~> 1.4.0"} # ← 追記
]
end
依存関係を解決しておきます:
$ mix deps.get
次にCharactersハンドラを実装します、lib/hello_school_live/handlers/characters.ex
を以下内容で作成します:
lib/hello_school_live/handlers/characters.ex
defmodule HelloSchoolLive.Handlers.Characters do
import Ecto.Query
def init(req, opts) do
query = from c in HelloSchoolLive.Models.Character, select: c
characters = HelloSchoolLive.Repo.all(query) |> Enum.map(&(&1.name))
req2 = :cowboy_req.reply 200, [{"content-type", "application/json"}],
Poison.Encoder.encode(characters, []), req
{:ok, req2, opts}
end
end
最後に確認です:
$ iex -S mix
別のshellを開いて以下コマンドを実行します
$ curl localhost:3000/hello-school-live/characters
["丈槍 由紀","佐倉 慈","若狭 悠里","恵飛須沢 胡桃","直樹 美紀"]
上記の通り出力されることを確認します。
以上のソースはここにおいてあります。