※この記事は古いバージョンのPhoenixで書かれています。
※Phoenix v1.3対応に書き直した記事が以下にアップされています。
(ElmからPhoenix v1.3のRest APIを叩いてみる - Qiita)
前回はElmとPhoenixでWebアプリを構築する基礎を示しました(ElmのバックエンドとしてPhoenixを使う - Qiita)。今回はそれをベースにして、ElmからPhoenixのRest APIを叩いてみるプログラムを作成します。
今回作るElmコードは、前に書いた記事「Elmでマテリアルデザインをためしてみた! - Qiita」のものとほぼ同じものです。この時はお手軽なjson-serverを利用しましたが、今回は本格的にPhoenixで実装します。
1.PhoenixでRest APIを実装する
まずプロジェクトを作成します。
mix phoenix.new phoenix_elm_test
cd phoenix_elm_test
次にRest APIを素早く作るためにphoenix.gen.jsonを利用します。phoenix.gen.jsonはcontrollerやviewsなどのRest APIリソースを生成してくれます。これでコードを書くことなくRest APIを実装できてしまうから驚きものです。Phoenixの威力を実感できるところです。
mix phoenix.gen.json Article articles title:string story:text
コマンドを実行すると以下のようなメッセージが表示されます。controllerやview、modelsなどが作成されているのがわかります。
mix phoenix.gen.json Article articles title:string story:text
mix phoenix.gen.json is deprecated. Use phx.gen.json instead.
* creating web/controllers/article_controller.ex
* creating web/views/article_view.ex
* creating test/controllers/article_controller_test.exs
* creating web/views/changeset_view.ex
mix phoenix.gen.model is deprecated. Use phx.gen.schema instead.
* creating web/models/article.ex
* creating test/models/article_test.exs
* creating priv/repo/migrations/20180119043039_create_article.exs
Add the resource to your api scope in web/router.ex:
resources "/articles", ArticleController, except: [:new, :edit]
Remember to update your repository by running migrations:
$ mix ecto.migrate
次にメッセージのガイドに従ってweb/router.exに以下の行を追加します。
resources "/articles", ArticleController, except: [:new, :edit]
次にmix ecto.migrateのコマンドを促していますので、mix ecto.create実行後に実行します。migrationとは、migrationファイルというテーブル定義書から、tableを作成したりfieldやindexを追加したり、ロールバックさせたりする機能です。
mix ecto.create
mix ecto.migrate
migrationの結果は、以下のようにファイルができることです。
ls -l priv/repo/migrations
合計 4
-rw-r--r-- 1 root root 215 1月 19 13:30 20180119043039_create_article.exs
最終的にサーバーを起動し、アクセスしてみましょう
mix phx.server
以下のcurlコマンドでデータを入力します
curl "http://www.mypress.jp:4000/api/articles" -X POST -d "article[title]=12月29日 晴れ" -d "article[story]=今日は晴れでした。"
curl "http://www.mypress.jp:4000/api/articles" -X POST -d "article[title]=12月30日 曇り" -d "article[story]=今日は大掃除をしました。エアコンの掃除は大変でした。"
curl "http://www.mypress.jp:4000/api/articles" -X POST -d "article[title]=12月31日 晴れのち雨" -d "article[story]=今日は大晦日です。夜更かしします。"
article[title]やarticle[story]のarticleはphoenix.gen.jsonの第一引数で指定したschema名です。これは自動生成されたweb/controllers/article_controller.exで、以下のようにcreateの引数がarticleで受けているのに対応します。curlコマンドでarticles[title]などとするとエラーになります。
def create(conn, %{"article" => article_params}) do
以下のcurlコマンドで入力データを取得してみます。
curl -X GET "http://www.mypress.jp:4000/api/articles"
以上でArtcle Schemaに関する設定が終わりました。
Articleと同じようにしてTag Schamaの設定を行っていきます。
-----------------メモ
※ Schema名のTagはcontroll名等にも使われる。Articleとは別のものにする
※プロジェクト作成が失敗したらDBをdropする
mix ecto.drop
※curlによるデータのdelete
curl http://www.mypress.jp:4000/api/articles/1 -X DELETE
-----------------
mix phoenix.gen.json Tag tags name:string
mix phoenix.gen.json is deprecated. Use phx.gen.json instead.
* creating web/controllers/tag_controller.ex
* creating web/views/tag_view.ex
* creating test/controllers/tag_controller_test.exs
mix phoenix.gen.model is deprecated. Use phx.gen.schema instead.
* creating web/models/tag.ex
* creating test/models/tag_test.exs
* creating priv/repo/migrations/20180119065727_create_tag.exs
Add the resource to your api scope in web/router.ex:
resources "/tags", TagController, except: [:new, :edit]
Remember to update your repository by running migrations:
$ mix ecto.migrate
web/router.exに以下の行を追加します。
resources "/tags", TagController, except: [:new, :edit]
mix ecto.migrate
ls -l priv/repo/migrations
合計 8
-rw-r--r-- 1 root root 215 1月 19 15:52 20180119065217_create_article.exs
-rw-r--r-- 1 root root 182 1月 19 15:57 20180119065727_create_tag.exs
mix phx.server
curl "http://www.mypress.jp:4000/api/tags" -X POST -d "tag[name]=猫"
curl "http://www.mypress.jp:4000/api/tags" -X POST -d "tag[name]=犬"
curl "http://www.mypress.jp:4000/api/tags" -X POST -d "tag[name]=クジラ"
curl "http://www.mypress.jp:4000/api/tags" -X POST -d "tag[name]=ヤギ"
curl "http://www.mypress.jp:4000/api/tags" -X POST -d "tag[name]=タカ"
curl -X GET "http://www.mypress.jp:4000/api/tags"
-----------------
■失敗 tags[name]はダメ。Tagを使わないとダメ
curl "http://www.mypress.jp:4000/api/tags" -X POST -d "tags[name]=ワシ"
■成功
curl "http://www.mypress.jp:4000/api/tags" -X POST -d "tag[name]=ワシ"
-----------------
2.Elmコードの設定
ここでは「ElmのバックエンドとしてPhoenixを使う - Qiita」記事に従って設定を行います。前回と異なるのはインストールするelm-packageの種類と、Main.elmの内容です。以下にそれを示します。
まずはパッケージのインストールです。
mkdir web/elm
cd web/elm
elm-package install elm-lang/html
elm-package install elm-lang/http
elm-package install elm-lang/navigation
elm-package install evancz/url-parser
elm-package install debois/elm-mdl
Main.elmは「Elmでマテリアルデザインをためしてみた! - Qiita」のものとほぼ同じものですが、Rest APIの返してくるJSONが少し異なるので、その分Decoderの指定も変えてあります。それ以外は全く同じで、非同期のHTTP通信でデータを取得し、マテリアルデザインでページを描画するコードになっています。そのまま動けば自分としては、Elm+Phoenixの実験は大満足です。
import Html exposing (..)
import Html.Attributes exposing (href,style)
import Html.Events exposing (onClick)
import Http
import Navigation
import UrlParser as Url exposing ((</>), (<?>), s, int, stringParam, top)
import Json.Decode as Decode
import Task exposing (Task)
-- ***MDL-1
import Material
import Material.Scheme
import Material.Button as Button
import Material.Options as Options exposing (css)
import Material.Card as Card
import Material.Color as Color
import Material.Elevation as Elevation
import Material.List as Lists
main =
Navigation.program MsgUrlChange
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
init : Navigation.Location -> ( Model, Cmd Msg )
init location = -- ***MDL-2: Boilerplate: Always use this initial Mdl model store.
( Model Nothing (Url.parsePath route location) [] [] Material.model
, Cmd.none
)
-- MODEL
type Tag
= Tag String
type alias Article =
{ story : String
, title : String
}
type alias Model =
{ loading : Maybe Route
, page : Maybe Route
, tags : List Tag
, articles : List Article
, mdl : Material.Model -- ***MDL-3 : Boilerplate: model store for any and all Mdl components you use.
}
-- URL PARSING
type Route
= RouteHome
| RouteTags
| RouteArticles
| RouteArticlePost Int
| RouteTagsArticles
| RouteMain
route : Url.Parser (Route -> a) a
route =
Url.oneOf
[ Url.map RouteHome top
, Url.map RouteTags (Url.s "tags")
, Url.map RouteArticles (Url.s "articles")
, Url.map RouteArticlePost (Url.s "articles" </> int)
, Url.map RouteTagsArticles (Url.s "tags-articles")
, Url.map RouteMain (Url.s "Main.elm")
]
-- UPDATE
type Msg
= MsgNewUrl String
| MsgUrlChange Navigation.Location
| MsgNewTags (Result Http.Error (List Tag))
| MsgNewArticles (Result Http.Error (List Article))
| MsgNewTagsArticles (Result Http.Error (List Tag, List Article) )
| MsgNewArticlePost (Result Http.Error Article)
| Mdl (Material.Msg Msg) -- ***MDL-4 : Boilerplate: Msg clause for internal Mdl messages.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
MsgNewUrl url ->
( model
,Navigation.newUrl url
)
MsgUrlChange location ->
let
_ = Debug.log "location=" location
newpage = Url.parsePath route location
in
case newpage of
Nothing ->
( {model | page=newpage}, Cmd.none)
Just RouteHome ->
( {model | page=newpage}, Cmd.none)
Just RouteTags ->
( { model | loading = newpage }, getTags )
Just RouteArticles ->
( { model | loading = newpage }, getArticles )
Just (RouteArticlePost n) ->
( { model | loading = newpage }, getArticlePost n )
Just RouteTagsArticles ->
( { model | loading = newpage }, getTagsArticles )
Just RouteMain ->
( {model | page=newpage}, Cmd.none)
MsgNewTags (Ok newtags) ->
( { model | page=model.loading, tags=newtags, articles=[] }, Cmd.none)
MsgNewTags (Err _) ->
(model, Cmd.none)
MsgNewArticles (Ok newarticles) ->
( { model | page=model.loading, tags=[], articles=newarticles } , Cmd.none)
MsgNewArticles (Err _) ->
(model, Cmd.none)
MsgNewTagsArticles (Ok ( t, a ) ) ->
( { model | page=model.loading, tags=t, articles=a } , Cmd.none)
MsgNewTagsArticles (Err _) ->
(model, Cmd.none)
MsgNewArticlePost (Ok newarticle) ->
( { model | page=model.loading, tags=[], articles=[newarticle] } , Cmd.none)
MsgNewArticlePost (Err _) ->
(model, Cmd.none)
-- ***MDL-5 : Boilerplate: Mdl action handler.
Mdl msg_ ->
Material.update Mdl msg_ model
-- HTTP
url_tags =
"http://www.mypress.jp:4000/api/tags"
url_articles =
"http://www.mypress.jp:4000/api/articles"
requestTags : Http.Request (List Tag)
requestTags =
-- Http.get url_tags ( Decode.field "animals" ( Decode.list ( Decode.map Tag Decode.string ) ) )
Http.get url_tags ( Decode.field "data" ( Decode.list ( Decode.map Tag (Decode.field "name" Decode.string) ) ) )
requestArticles : Http.Request (List Article)
requestArticles =
-- Http.get url_articles ( Decode.list article )
Http.get url_articles ( Decode.field "data" ( Decode.list article ) )
article : Decode.Decoder Article
article =
Decode.map2 toArticle (Decode.field "title" Decode.string) (Decode.field "story" Decode.string)
toArticle : String -> String -> Article
toArticle t s =
{ story=s, title=t }
getTags : Cmd Msg
getTags =
Http.send MsgNewTags requestTags
getArticles : Cmd Msg
getArticles =
Http.send MsgNewArticles requestArticles
getTagsArticles : Cmd Msg
getTagsArticles =
Task.attempt MsgNewTagsArticles ( Task.map2 toPair ( Http.toTask (requestTags) ) ( Http.toTask (requestArticles) ) )
toPair : List Tag -> List Article -> (List Tag, List Article)
toPair t a =
( t, a )
getArticlePost : Int -> Cmd Msg
getArticlePost n =
let
url_post = url_articles ++ "/" ++ toString n
in
Http.send MsgNewArticlePost ( Http.get url_post ( Decode.field "data" article ) )
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
type alias Mdl = -- ***MDL-6
Material.Model
-- VIEW
view : Model -> Html Msg
view model =
div []
[ h1 [] [ text "Links" ]
, ul [] (List.map (viewLink model) [ "/", "/tags/", "/articles/","/tags-articles/","/articles/1/", "/articles/2/", "/articles/3/" ])
, div [] [ viewRoute model ]
]
|> Material.Scheme.top -- ***MDL-7
viewLink : Model -> String -> Html Msg -- ***MDL-8
viewLink model url =
-- li [] [ button [ onClick (MsgNewUrl url) ] [ text url ] ]
li [ style [ ("display", "inline-block") ] ] [
Button.render Mdl
[ 0 ]
model.mdl
[ Button.raised
, Options.onClick (MsgNewUrl url)
, css "margin" "0 6px"
]
[ text url ]
]
viewRoute : Model -> Html msg
viewRoute model =
let
_ = Debug.log "maybeRoute=" model.page
in
case model.page of
Nothing ->
h2 [] [ text "404 Page Not Found!"]
Just route ->
viewPage route model
viewPage : Route -> Model -> Html msg
viewPage route model =
case route of
RouteHome ->
div []
[ h2 [] [text "Welcomw to My Page!"]
, p [] [ text "これはテストページのトップです" ]
]
RouteTags ->
div []
[ h2 [] [text "タグ一覧"]
, p [] [ text "これはタグの一覧ページです" ]
, Lists.ul [] (List.map viewTags model.tags)
]
RouteArticles ->
div []
[ h2 [] [text "ブログ一覧"]
, p [] [ text "これはブログの一覧ページです" ]
, ul [] (List.map viewArticles model.articles)
]
RouteTagsArticles ->
div []
[ h2 [] [text "タグ&ブログ一覧"]
, p [] [ text "これはタグ&ブログの一覧ページです" ]
, Lists.ul [] (List.map viewTags model.tags)
, ul [] (List.map viewArticles model.articles)
]
RouteArticlePost id ->
div []
[ h2 [] [text "ブログ記事表示"]
, p [] [ text ("これはブログの記事("++ toString id ++")を表示します") ]
, ul [] (List.map viewArticles model.articles)
]
RouteMain ->
div []
[ h2 [] [text "初期画面"]
,p [] [ text "これはプログラムがロードされた初期画面です。" ]
]
viewTags (Tag t) =
Lists.li []
[ Lists.content []
[ Lists.icon "inbox" []
, text t
]
]
white : Options.Property c m
white =
Color.text Color.white
viewArticles a =
div [ style [("padding","10px"), ("margin","10px")] ]
[
Options.div
[ Elevation.e6
, css "width" "800px"
, Options.center
]
[
Card.view
[ css "width" "800px"
, Color.background (Color.color Color.Pink Color.S500)
]
[ Card.title [] [ Card.head [ white ] [ text a.title ] ]
, Card.text [ white ] [ text <| a.story ]
]
]
]
3.画面キャプチャ
最後に画面キャプチャーを示します。
■トップページです
http://www.mypress.jp:4000/
■/TAGS/ボタンをクリック
http://www.mypress.jp:4000/tags/
■/ARTICLES/ボタンをクリック
http://www.mypress.jp:4000/articles/
■/TAGS-ARTICLES/ボタンをクリック
http://www.mypress.jp:4000/tags-articles/
■/ARTICLES/2/ボタンをクリック
http://www.mypress.jp:4000/articles/2/
以上ですが、思い通りの結果が得られて、めでたしめでたしです。大満足です。