Phoenixで作るGPS Logging System 4 APIの実装

# はじめに
[ひとりLiveView Advent Calendar](https://qiita.com/advent-calendar/2021/liveview_alone) の4日目の記事です

この記事は[Elixir Conf US 2021](https://www.elixirconf.com/)の[発表](https://www.youtube.com/watch?v=vHNqZmLLTj4)したシステムの構築と関連技術の解説を目的とした記事です

今回は以下の４つを実装します

- phx.gen.jsonでAPI作成
- phx.gen.schemaでモデルのみ作成
- 手動でAPI作成
- routerにエンドポイント追加

# phx.gen.json
以下のコマンドでAPIとモデル、クエリーを作成しますが、一日目でphx.gen.liveで作ったので、
--no-schema --no-contextでAPIのみ作成しています

```
mix phx.gen.json Loggers Map maps name:string description:string --no-schema --no-context 
```

```bash
* creating lib/live_logger_web/controllers/map_controller.ex
* creating lib/live_logger_web/views/map_view.ex
* creating test/live_logger_web/controllers/map_controller_test.exs
* creating lib/live_logger_web/views/changeset_view.ex
* creating lib/live_logger_web/controllers/fallback_controller.ex

Add the resource to your :api scope in lib/live_logger_web/router.ex:

    resources "/maps", MapController, except: [:new, :edit]

```

作成されたファイルを見てみましょう

* lib/live_logger_web/controllers/map_controller.ex  
CRUD APIが書かれています
* lib/live_logger_web/views/map_view.ex  
APIのレスポンスを記述します controllerとviewファイルはセットなので忘れないようにしましょう
* lib/live_logger_web/controllers/fallback_controller.ex  
action_fallbackで指定されていて各APIのエラー時にfallback_controllerのエラーにハンドリングされます
* lib/live_logger_web/views/changeset_view.ex  
422 error時のレスポンスが記載されています
* creating test/live_logger_web/controllers/map_controller_test.exs  
テストも自動生成やったぜ

# phx.gen.schema
モデル単体だけを作成したい場合は phx.gen.schemaを実行します
クエリーも作成する場合は phx.gen.contextになります

```
mix phx.gen.schema Loggers.Point points lat:float lng:float device_id:string map_id:references:maps
mix ecto.migrate
```

モデルファイルとマイグレーションファイルのみ作成されます

```bash
* creating lib/live_logger/loggers/point.ex
* creating priv/repo/migrations/20211204133145_create_points.exs

Remember to update your repository by running migrations:

    $ mix ecto.migrate
```

migrateも忘れずに

```
mix ecto.migrate
```

PointのクエリーをLoggersに追加します
aliasで構造体をPointで呼び出せるようにして
list,createを実装します

query関数内で外部の引数を使用する場合はプリミティブな型でピン演算子を付ける必要があります
createはcreate_mapを参考に書きましょう

```lib/live_logger/loggers.ex
defmodule LiveLogger.Loggers do
  ...
  alias LiveLogger.Loggers.Point
  ...
  def list_points(map_id) do
    Point
    |> where([p], p.map_id == ^map_id)
    |> Repo.all
  end
  
  def create_point(attrs \\ %{}) do
    %Point{}
    |> Point.changeset(attrs)
    |> Repo.insert
  end
end
```

リレーションも組んでいきます

```lib/live_logger/loggers/map.ex
defmodule LiveLogger.Loggers.Map do
  use Ecto.Schema
  import Ecto.Changeset

  schema "maps" do
    field :description, :string
    field :name, :string

    belongs_to :user, LiveLogger.Accounts.User
    has_many :points, LiveLogger.Loggers.Point
    timestamps()
  end
  ...
end
```

```lib/live_logger/loggers/point.ex
defmodule LiveLogger.Loggers.Point do
  use Ecto.Schema
  import Ecto.Changeset

  schema "points" do
    field :device_id, :string
    field :lat, :float
    field :lng, :float
    belongs_to :map, LiveLogger.Loggers.Map #field map_idを belongs_toに変更

    timestamps()
  end

  @doc false
  def changeset(point, attrs) do
    point
    |> cast(attrs, [:lat, :lng, :device_id, :map_id]) # map_id追加
    |> validate_required([:lat, :lng, :device_id, :map_id]) # map_id追加
  end
end
```

Map詳細でpointも取得するようにしましょう
preloadで関連先のデータを読み込みます

```lib/live_logger/loggers.ex
defmodule LiveLogger.Loggers do
  ...
  def get_map_with_points!(id) do
    Map
    |> preload(:points)
    |> Repo.get!(id)
  end
  ...
end
```

# 手動でAPI作成
generatorなしで作りますが特に難しくありません
contextとモデルのモジュールをaliasで読み込む
action_fallbackを指定
apiを追加
レスポンスはsend_respのみでJSONは何も返さないためViewファイルはありません

```lib/live_logger_web/controllers/point_controller.ex
defmodule LiveLoggerWeb.PointController do
  use LiveLoggerWeb, :controller

  alias LiveLogger.Loggers
  alias LiveLogger.Loggers.Point

  action_fallback LiveLoggerWeb.FallbackController

  def create(conn, point_params) do
    with {:ok, %Point{}} <- Loggers.create_point(point_params) do
      send_resp(conn, 200, "ok")
    end
  end
end
```

# endpoint追加
routerのコメントアウトされている api scopeをコメントインして
コンソールに出てきてたroutingのコードを追加
pointsはcreateだけなので onlyで指定しています

```lib/live_logger_web/router.ex
defmodule LiveLoggerWeb.Router do
  use LiveLoggerWeb, :router
  ...
  # Other scopes may use custom stacks.
  scope "/api", LiveLoggerWeb do
    pipe_through :api

    resources "/maps", MapController, except: [:new, :edit]
    resources  "/points", PointController, only: [:create]
  end
end
```

# 動作確認
動作確認はPostmanを使用します

get api/maps
![スクリーンショット 2021-12-05 0.57.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/9e48436a-664b-4e19-3a81-57e73c9f1d5d.png)

get api/maps/1

![スクリーンショット 2021-12-05 0.57.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/c680b0aa-12a1-5987-c4db-24dda014e30b.png)

post api/maps
createの引数が %{map: map_params} = params なので map[カラム名]でjson形式になるようにすること

![スクリーンショット 2021-12-05 0.57.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/d027ba01-b9ac-56af-4e80-ca5eec545c73.png)

delete api/maps/2
JSONのレスポンスはなく 204 No Contentが帰ってきています
![スクリーンショット 2021-12-05 0.58.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/28e988e7-1836-9946-d73e-ad402868f48c.png)

create api/points
mapと違って point_paramsとしているのでフラットなformdataで大丈夫です

![スクリーンショット 2021-12-05 0.58.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/61c8224a-5c51-f60b-33b9-66a802c3000a.png)

# 最後に
phx.gen.jsonによってAPIの実装も簡単にできました
json responseからdataが邪魔だなーと思ったら以下のようにすれば消すことができます

```lib/live_logger_web/views/map_view.ex
defmodule LiveLoggerWeb.MapView do
  use LiveLoggerWeb, :view
  alias LiveLoggerWeb.MapView

  def render("index.json", %{maps: maps}) do
    render_many(maps, MapView, "map.json") # dataとMapの囲いを消す
  end
  ...
end
```
本記事は以上になります

次はAPIでの認証を実装します

# Code
https://github.com/thehaigo/live_logger/tree/day4