田舎で並行処理の夢を見る

試していることなど...需要がないかもしれないけど細々とアウトプットしてます

Elixirのtrotを使って爆速APIを立ち上げる

おなじみgit探検隊

Elixirに限ったことではないが、定期的にgitでトレンドのレポジトリはチェックするようにしている
そうすると大体、何が流行っていて何に注目が集まっているかが何となく分かる
最近は中国語のREAD.MEが多くて翻訳ないと詰む

さておき、また今回も良さげなElixirのレポジトリを発見した

Trot
公式ドキュメント(hex)

An Elixir web micro-framework

説明の通り、ウェブのマイクロフレームワーク
開発自体は3年程前からされているようでラストコミットは2018年の7月っぽい

ちょうどElixirでAPIをさくっと作りたかったのでありがたい

なぜPhoenixで作らないの?バカなの?

信じてもらえるか分からないが実はPhoenixでjsonAPIを作ったことはある
普通に楽々作れるし、ドキュメントもある

じゃあ、なんでtrotを使うんだって話ですけど
単に作業量が圧倒的に少ないからに限る

cloud functionやらpythonのresponderやらトレンドはミニマムに高速で作ることだと思っている
Phoenixは1つエンドポイントを作るだけとかマイクロサービスを作る際にはオーバースペック感がある
そんな機能は別に使わへんがな... あとは学習コストの問題も

だったらさくっと覚えられて素早く使える物がいいよねってことでこうなった(事後
色々とエラーには遭遇してかなり時間は取られたことは内緒

プロジェクトの新規作成

いつも通りです

mix new simple_api

./simple_api/mix.exs に以下を追加
trotのREAD.MEにはtrotだけが記述されているが、僕の環境ではerrorになったので
plug_cowboyも追加している
fakerに関しては後に使うためインストールするようにしているが無くても問題ない

def application do
    [
      extra_applications: [:logger],
      applications: [:trot, :faker]
    ]
  end

defp deps do
    [
      {:trot, github: "hexedpackets/trot"},
      {:plug_cowboy, "~> 1.0"},
      {:faker, "~> 0.12"},
    ]
  end

plug_cowboyをインストールするようにしていなかった時のerror

#mix trot.serverでerrorになってしまう

warning: please add the following dependency to your mix.exs:

    {:plug_cowboy, "~> 1.0"}

次に./sample_api/config/config.exs に以下を追加
公式ドキュメントだと割とひっそり書かれていて気づかない場合があるかもしれないが
少なくともrouterを設定するモジュールを記述しないとnot foundをただ返すだけの
ポンコツサーバーが出来上がる

config :trot, :port, 4000 #どちらでもok(デフォルトが4000番)
config :trot, :router, SimpleApi #使用するモジュールを指定(必須)

これで準備は完了

APIをさくっと作る

今回は特にdbを使っていないので触れるのは「GETとPOST」メゾットのみ
データのやり取りが出来れば十分
公式では

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • OPTIONS

をサポートしている
まずはGETから書いてみる

モジュールの頭に以下を追加

use Trot.Router

このRouterを記述したモジュールが先ほどconfigに記述したモジュール名と等しくなるようにする
./simple_api/lib/simple_api.ex

defmodule SimpleApi do
  use Trot.Router
  get "/easy-get" do
    "what's up!!"
  end
end

たったこれだけ
マクロになっていてサクッと記述できる
第2引数でheader情報にマッチが取れるらしいが今の所使い道は思いつかない
さっそくターミナルからcurlしてみる

❯  curl http://localhost:4000/easy-get
what's up!!%

いいですね。無事に文字列が帰ってきてる
最後に%が付くのはデフォルト? なぜだろう

次にPOST
値の受け取り方に関して公式ドキュメントに一切の記述が無くて不親切だなと思った
色々と調べた結果、Plugの関数を使うことで取得できることが分かったが
かなりここに時間をとられた(1日調べまくった
そもそもconnという謎の変数の存在に気付けへんわ
ここにリクエスト情報が詰まっているっぽい

#bodyの{"user": ___ }を取得する
user = conn.body_params["user"]
#connの中身
%Plug.Conn{
  adapter: {Plug.Adapters.Cowboy.Conn, :...},
  assigns: %{},
  :
  :
  host: "localhost",
  method: "POST",
  owner: #PID<0.369.0>,
  params: %{"user" => "okb"},
  path_info: ["easy-post"],
  path_params: %{},
  port: 4000,
  :
  :
  scheme: :http,
  script_name: [],
  secret_key_base: nil,
  state: :unset,
  status: nil
}

あとは簡単
普通に書くだけ(日本語難しい

post "/easy-post" do
    user = conn.body_params["user"]
    res = "hello, #{user}"
    IO.puts(res)
    res
end

呼び出す

curl -H "Content-Type: application/json" -X POST -d '{"user": "okb"}' http://localhost:4000/easy-post
hello, okb%

無事に値が返ってきた
ついでにサーバーサイドのログにも"hello, okb"が出力されている

基本的な部分に触れたところでもう少し便利なAPIを作ってみる

他のモジュール関数を呼び出す

せっかくなので過去に作成したモジュールを使用する
まずはGETメゾットが叩かれた時にmockデータを返すようなAPIを作る(ずっとほしかった
実装にFakerというライブラリを使っている
詳しくはFaker使ってテキトーなサンプルデータを作るをご覧ください
今回は試しがてらURLから値を受け取る(queryではない)をやってみる

URLに渡した指定数分だけデータを返すようにしている

defmodule CreateMock do
  def user_info(num) do
    1..num |> Enum.map(fn _ -> _create_user_info() end)
  end
  defp _create_user_info do
    %{
      email: Faker.Internet.email(),
      city: Faker.Address.En.city(),
      name: Faker.Name.En.first_name() <> " " <> Faker.Name.En.last_name(),
      phone: Faker.Phone.EnUs.phone(),
      food: Faker.Food.dish(),
      age: trunc(:rand.uniform() * 100) #Erlangモジュールを使って乱数を生成
    }
  end
end

get "/mock/:create_num" do
    #受け取った時点では文字列なので注意
    info = CreateMock.user_info(String.to_integer(create_num))
    %{data: info}
  end

呼び出してみる

 ❯  curl http://localhost:4000/mock/2
{"data":[{"phone":"(988) 898-5220","name":"Hilario Dach","food":"Scotch eggs","email":"wallace.emmerich@spinka.info","city":"East Cierra","age":83},{"phone":"431-588-1937","name":"Faustino Senger","food":"Meatballs with sauce","email":"rey2086@kris.net","city":"South Friedrich","age":0}]}%

やったぜ

次にPOSTで送った2つの文字列が一致しているか、どれだけタイポしているかをチェックする
Elixirで最長共通部分列使ってタイプミスしてるかを調べる

defmodule TypoChecker do
  def main(input_val, ans_val) when input_val == ans_val, do: :good
  def main(input_val, ans_val), do: _main(String.graphemes(input_val), ans_val, 0)
  defp _main([], _, counter), do: counter
  defp _main([head | tail], ans_val, counter) do
    str_lst = String.graphemes(ans_val)
    [_n_head | n_tail] = str_lst
    case type_judge(str_lst, head) do
      :hit -> _main(tail, List.to_string(n_tail), counter+1)
      :empty -> _main(tail, List.to_string(n_tail), counter)
    end
  end

  def type_judge([], _), do: :empty
  def type_judge([head | tail], compare_str) do
    if String.contains?(compare_str, head) do
      :hit
    else
      type_judge(tail, compare_str)
    end
  end
end

post "/typo-check" do
    input_val = conn.body_params["input_val"]
    ans_val = conn.body_params["ans_val"]
    case TypoChecker.main(input_val, ans_val) do
      :good -> "no typo"
      _ -> "is typo"
    end
  end

Elixirのatomをそのままreturnするとerrorになるため
case使って無理やり文字列を返すようにしている
では呼び出す

> curl -H "Content-Type: application/json" -X POST -d '{"input_val": "apple", "ans_val": "apple"}' http://localhost:4000/typo-check
no typo%
> curl -H "Content-Type: application/json" -X POST -d '{"input_val": "apple", "ans_val": "appple"}' http://localhost:4000/typo-check
is typo%

いいですね
使い方さえ分かってしまえばtrotでのAPI作成は爆速ですわ
あとはheorkuとかにデプロイする方法を調査してまた記事にでもします