ElmでJSON APIを叩く最低限

ElmでWebサービス的なものを作るとなった時にAPIサーバとの通信は必須になってくる。
普段JSのaxiosを使っている自分は割と手間取ったのでメモっておく。
基本的には[Elm Guide](https://guide.elm-lang.jp/effects/http.html)の `HTTP` `JSON` 項目の内容をなぞっているだけ。


# 想定読者

- The Elm Architecture(Model,View,Update)の概要をなんとなく理解している
- Cmdの使い方・挙動をなんとなく理解している

# バージョン

- Elm 0.19

# ソース

[Github](https://github.com/dyoshikawa/elm-json-api-example)

`yarn server` でjson-serverを動かした状態で `yarn start` でフロントエンド環境を立ち上げると動作確認できるようになっている。

# デモ
![mjdh6-1033x.gif](https://qiita-image-store.s3.amazonaws.com/0/176237/759c537a-1e36-fd26-42f7-e6fcd7dfd49b.gif)



# Http.get/postとHttp.requestの違い

[HTTPパッケージのドキュメント](https://package.elm-lang.org/packages/elm/http/latest/Http)を見ると `get` `post` 関数と `request` 関数がある。
一見、 `get` `post` の方がシンプルに使えて良さそうに見えるが、リクエストヘッダーの設定ができないっぽい。
実際には `request` を多用することになりそう。

# APIリクエストの流れ

実際にAPIを叩くコードを見ていく。

## リクエスト関数

json-serverで返ってくる値は次のように設定した。

```json
{
  "articles": [
    {
      "id": 1,
      "title": "json-server",
      "author": "typicode"
    },
    {
      "id": 2,
      "title": "faker.js",
      "author": "Marak"
    }
  ],
  "profile": {
    "name": "typicode"
  }
}

```

まずはprofileのリソースを取得する。
次のようにリクエスト関数を定義。

```elm
getProfile : Cmd Msg
getProfile =
    Http.request
        { method = "GET"
        , headers =
            [ Http.header "Authorization" ("Bearer " ++ "TOKEN")
            , Http.header "Accept" "application/json"
            , Http.header "Content-Type" "application/json"
            ]
        , url = "http://localhost:3000/profile"
        , expect = Http.expectJson GotProfile profileDecoder
        , body = Http.emptyBody
        , timeout = Nothing
        , tracker = Nothing
        }
```

`headers` Authorizationは認証が必要という想定で適当な値を設定してある。
`url` 叩くエンドポイントを設定。
`body` 空なので `Http.emptyBody` を渡している。
`expect` `Http.expectJson` はレスポンス結果がJSONであることを期待する。引数としてレスポンスが返ってきた後のJSONデコーダー関数とデコードされた値を受け取るUpdate関数を渡す。
`timeout` タイムアウト。特に設定したくないのでNothingとしている。

`tracker` については何を設定する項目なのかわかっていない。
[ドキュメント](https://package.elm-lang.org/packages/elm/http/latest/Http)によると、

> The tracker lets you cancel and track requests.
> トラッカーを使用すると、リクエストをキャンセルして追跡できます。

やはりちょっとわからないが、ひとまずNothingとしている。

## JSONデコーダー

`/profile` を叩くと

```json
{
  "name": "typicode"
}
```

が返ってくる。
これに対しては次のようにデコーダーを定義する。

```elm
profileDecoder : Decoder String
profileDecoder =
    field "name" string
```

`field` 関数の第一引数にキー名、第二引数にDecoder関数を渡す。
今回は文字列型なので `Json.Decode.string` 関数を渡す。
特にこのあたりは使い方を知っているだけで理解が怪しい点。

## Update関数

作成したリクエスト関数はUpdateの中で使う。
Msgの型は下のように定義。

```elm
type Msg
    = GetProfile
      GotProfile (Result Http.Error String)
```

[Result型](https://package.elm-lang.org/packages/elm/core/latest/Result])は１つ目に失敗時のエラーの型、２つ目に成功時に受け取る値の型を定義する。

```elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GetProfile ->
            ( model
            , getProfile
            )

        GotProfile result ->
            case result of
                Ok name ->
                    ( { model | name = name }, Cmd.none )

                Err _ ->
                    ( model, Cmd.none )
```

ViewからはUpdate関数である `GetProfile` を呼ぶ。
`GetProfile` はリクエスト関数の `getProfile` を呼ぶ。
`getProfile` でAPIへのリクエストが行われ、受け取ったレスポンスをデコードして `GotProfile` へ渡す。
`GotProfile` ではデコードされた値が渡ってくるので、その値をModelに格納する。

## JSONが配列で返ってくる場合

`/articles` を叩くと、

```json
[
  {
    "id": 1,
    "title": "json-server",
    "author": "typicode"
  },
  {
    "id": 2,
    "title": "faker.js",
    "author": "Marak"
  }
]
```

このように配列が返ってくる。
Elm的にはListとして受け入れる必要がある。

ListのデコードはElm Guideでは記載されていないが、[ググって出てきたスタックオーバーフローの回答](https://stackoverflow.com/questions/39320948/decode-json-array-with-objects-in-elm)がかなり参考になった。

考え方としてはまずListでない単一要素のデコーダーを定義する。
続いてそのデコーダーをベースにListにデコーダーを定義する。

```elm
getArticles : Cmd Msg
getArticles =
    Http.request
        { method = "GET"
        , headers =
            [ Http.header "Authorization" ("Bearer " ++ "TOKEN")
            , Http.header "Accept" "application/json"
            , Http.header "Content-Type" "application/json"
            ]
        , url = "http://localhost:3000/articles"
        , expect = Http.expectJson GotArticles articlesDecoder -- ③Listのデコーダーを渡す
        , body = Http.emptyBody
        , timeout = Nothing
        , tracker = Nothing
        }


articleDecoder : Decoder Article  -- ①まず単一のデコーダーを定義
articleDecoder =
    Json.Decode.map3 Article
        (Json.Decode.field "id" Json.Decode.int)
        (Json.Decode.field "title" Json.Decode.string)
        (Json.Decode.field "author" Json.Decode.string)


articlesDecoder : Decoder (List Article) -- ②上を基にListのデコーダーを定義
articlesDecoder =
    Json.Decode.list articleDecoder
```

デコーダーが定義できてしまえば後は同じで、あまり迷わず実装できた。

```elm
type Msg
    = GetProfile
      GotProfile (Result Http.Error String)
      GetArticles
      GotArticles (Result Http.Error (List Article))
```

Update関数の引数もList型で定義しておく。


# 参考
https://guide.elm-lang.org/effects/json.html
https://package.elm-lang.org/packages/elm/http/latest/Http
https://stackoverflow.com/questions/39320948/decode-json-array-with-objects-in-elm