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

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

Elixirでクソ簡単に公開APIをcallする

公開APIについて

以前「清流elixir」の勉強会でこの公開API、通称ジブリAPI(勝手に命名)を
Enumとパイプ演算子を使って遊ぶ予定だったんですけど
当日になってcurlの戻り値が%{}(マップ形式)となっていないことに気づく(そりゃそう

知見としてもかなり強力なのでElixirからAPIを叩けるようにしないとなーと
やってみたらクソ簡単でした

せっかくなので戻り値から特定のデータを抽出するところまで触れてみます

プロジェクトの準備

mixとmixを使ったプロジェクトの立ち上げについてはこの記事で詳しく触れているので省略します
いつものようにmix newコマンドを叩く

mix new call_api
cd call_api

今回の使用するライブラリは以下の2つ

  • httpoison(Httpクライアント)
  • poison(Jsonを解析するやつ)

いつものように./call_api/mix.exsのdpsに上2つを記述します
./call_api(project_name)/mix.exs

defp deps do
    [
      {:httpoison, "~> 1.4"},
      {:poison, "~> 3.1"}
    ]
end

からの

mix deps.get

でライブラリをダウンロードする
準備はこれで完了

使用するエンドポイント

ドキュメントにあるこのエンドポイントをGETでcallします

https://ghibliapi.herokuapp.com/films

こんな感じでリスト内にJson形式のデータが複数返ってくる

[
  {
    "id": "2baf70d1-42bb-4437-b551-e5fed5a87abe",
    "title": "Castle in the Sky",
    "description": "The orphan Sheeta inherited a mysterious crystal that links her to the mythical sky-kingdom of Laputa. With the help of resourceful Pazu and a 
    rollicking band of sky pirates, she makes her way to the ruins of the once-great civilization. Sheeta and Pazu must outwit the evil Muska, who plans to use 
    Laputa's science to make himself ruler of the world.",
    "director": "Hayao Miyazaki",
    "producer": "Isao Takahata",
    "release_date": "1986",
    "rt_score": "95"
  },
  {
    "id": "12cfb892-aac0-4c5b-94af-521852e46d6a",
    "title": "Grave of the Fireflies",
    "description": "In the latter part of World War II, a boy and his sister, orphaned when their mother is killed in the firebombing of Tokyo, are left to survive on their 
    own in what remains of civilian life in Japan. The plot follows this boy and his sister as they do their best to survive in the Japanese countryside, battling hunger, 
    prejudice, and pride in their own quiet, personal battle.",
    "director": "Isao Takahata",
    "producer": "Toru Hara",
    "release_date": "1988",
    "rt_score": "97"
  }
  :
  :
]

さっそくコードを書いていく

今回の目的を達成するために必要な流れはこんな感じ
1. APIをcallして戻り値を得る(Httpoison)
2. 戻り値を解析する(Poison)
3. 解析した結果から特定のデータのみを抽出する(パターンマッチ)

ここまで考えたらあとはコードに落としていくのみ
とりあえず戻り値からtitleを抽出してみる
./call_api/lib/call_api.ex

def fetch_ghibli_films() do
    #Httpoison使ってAPIをcall
    {status, res} = HTTPoison.get("https://ghibliapi.herokuapp.com/films")
    case status do
      :ok ->
        #戻り値のbody(json)を解析
        Poison.Parser.parse!(res.body)
          |> Enum.map(&(&1["title"]))
      :error -> :error
    end
end

前回のAmazonレビューのスクレイピングと異なり
今回はHttpoison.get!()を使わずにget()を使ってます
get()の戻り値には {:ok, return} もしくは {:error reason}という値が返ってくるので
APIのcallの成否をstatusのアトム値をcase文使って分岐させています

この時点でresにはこんなような値が入っている

%HTTPoison.Response{
  body: "[\n  {\n    ...,
  headers: [
    {"Server", "Cowboy"},
    :
  ],
  request: %HTTPoison.Request{
    body: "",
    headers: [],
    method: :get,
    options: [],
    params: %{},
    url: "https://ghibliapi.herokuapp.com/films"
  },
  request_url: "https://ghibliapi.herokuapp.com/films",
  status_code: 200
}

bodyの値(json)をPoison使って解析します
これでようやくElixirで扱える形になった

Poison.Parser.parse!(res.body)

[
  %{
    "description" => "The orphan...",
    "director" => "Hayao Miyazaki",
    "id" => "2baf70d1-42bb-4437-b551-e5fed5a87abe",
    "locations" => ["https://ghibliapi.herokuapp.com/locations/"],
    "people" => ["https://ghibliapi.herokuapp.com/people/"],
    "producer" => "Isao Takahata",
    "release_date" => "1986",
    "rt_score" => "95",
    "species" => ["https://ghibliapi.herokuapp.com/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2"],
    "title" => "Castle in the Sky",
    "url" => "https://ghibliapi.herokuapp.com/films/2baf70d1-42bb-4437-b551-e5fed5a87abe",
    "vehicles" => ["https://ghibliapi.herokuapp.com/vehicles/"]
  },
  :
  :
]

マップのパターンマッチについて

あとは上記のリストinマップからtitleの値をどうやってパターンマッチさせるかですね
思いつくのはこれぐらい

info = %{"title" => "Castle in the Sky"}

#超シンプルなやつ
info["title"] #Castle in the Sky

#これだとエラー
info[title] #値がねーよと怒られる。そりゃ当然

#エラーにはならないが戻り値が空
info[:title]
info["new_title"]
#"title"というkeyがあるか
%{"title" => title} = info #title -> Castle in the Sky

#atom形式でないからこの書き方はできない
%{"title": title} = info 

#存在しないkeyを指定するとerror
%{"new_title": title} = info #error
#Mapモジュールを使用する
Map.get(info, "title") #Castle in the Sky

#存在しないkeyをしてもerrorにはならず空
Map.get(info, "new_title")

とりあえず一番シンプルな方法を今回は選択した
あとはリスト内のマップに対して操作を適用したいのでいつも通りにEnum.map()を使えば良い

Enum.map(&(&1["title"]))

お待たせしました
ではfetch_ghibli_films() を呼び出してみる

iex(8)> CallApi.fetch_ghibli_films()
["Castle in the Sky", "Grave of the Fireflies", "My Neighbor Totoro",
 "Kiki's Delivery Service", "Only Yesterday", "Porco Rosso", "Pom Poko",
 "Whisper of the Heart", "Princess Mononoke", "My Neighbors the Yamadas",
 "Spirited Away", "The Cat Returns", "Howl's Moving Castle",
 "Tales from Earthsea", "Ponyo", "Arrietty", "From Up on Poppy Hill",
 "The Wind Rises", "The Tale of the Princess Kaguya", "When Marnie Was There"]

お、良い感じに取れてますね
無事にAPIをcallすることが出来た上に、簡単な解析まで完了しました

実はもっと頑張ると3行で書くことが出来ます

def fetch_ghibli_films() do
    HTTPoison.get!("https://ghibliapi.herokuapp.com/films").body
      |> Poison.Parser.parse!()
      |> Enum.map(&(&1["title"]))
end

おまけのコーナー

directorが"Hayao Miyazaki"である作品のtitleを抽出してみる

def fetch_ghibli_films() do
    HTTPoison.get!("https://ghibliapi.herokuapp.com/films").body
      |> Poison.Parser.parse!()
      |> Enum.filter(&(&1["director"] == "Hayao Miyazaki"))
      |> Enum.map(&(&1["title"]))
end

#result
#["Castle in the Sky", "My Neighbor Totoro", "Kiki's Delivery Service",
# "Porco Rosso", "Princess Mononoke", "Spirited Away", "Howl's Moving Castle",
# "Ponyo", "The Wind Rises"]

この辺りの操作はやっぱりElixirは強力ですね