【Rails】OpenWeatherMap APIで特定の都市の気象データを取得してみた

## はじめに
**※この記事は、『[RUNTEQ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/runteq)』の20日目に参加しております。**

今回、個人開発のアプリを作成するにあたり、初めてAPIを叩いてみました。

「[OpenWeatherMap API](https://openweathermap.org/)」を使用して、ローカル開発環境にてAPIお試し用のアプリを作成し、情報を取得するまでの一連の流れについてまとめました。

そもそもAPIとは何なのか？という部分も曖昧だったので、こちらの記事にまとめております。

https://qiita.com/rk2530/private/48d6100a36484e3b1ee1

## 今回のゴール
フォームに都市名を入力し、検索ボタンを押すと気象データが表示されるようにする。

## 注意点
::: note warn
私はプログラミング学習中で、初学者です。
内容に誤りがある場合がございます。
もし間違いがあればご指摘いただけますと幸いです。
:::

## OpenWeatherMapとは？
[OpenWeatherMap](https://openweathermap.org/)は、世界各地の過去、現在、予測の各種気象データの無料APIを提供するオンラインサービスです。

天気以外にも、温度、湿度、風速など、さまざまな情報を取得できます。

https://openweathermap.org/

## 事前準備
- [OpenWeatherMap](https://openweathermap.org/)にアカウントを作成
- APIキーを取得

アカウントを作成し、マイページの`My API keys`を開くと、キーが表示されます。

## テスト用のRailsアプリケーションを作成
```:ターミナル
rails new apitest
cd apitest
```
## 必要なgemをインストール
`httparty`と`dotenv-rails`という`gem`をインストールします。
```rb:Gemfile
gem 'httparty'
gem 'dotenv-rails', groups: [:development, :test]
```
```:ターミナル
bundle install
```

### httpartyについて
`httparty` は、`Rails`でAPIを叩く際に使用される`gem`です。

https://github.com/jnunemaker/httparty

【特徴】
- `HTTP`リクエストを送信するための簡潔で読みやすい構文を提供している
- `GET`、`POST`、`PUT`、`DELETE`など、さまざまな`HTTP`メソッドをサポートしているため、あらゆる種類のAPIと簡単に連携できる
- `JSON`形式のデータの扱いに優れている
- APIからの応答を簡単に解析し、`Ruby`オブジェクトとして扱うことができるため、応答データの処理が容易になる
- `HTTP`リクエストが失敗した場合のエラーハンドリング機能も備わっているため、エラー状況をより効果的に処理できる

### dotenv-railsについて
`dotenv-rails`は、`Rails`で環境変数を管理することができる`gem`です。

https://github.com/bkeepers/dotenv

【特徴】
- `.env`ファイルを使用して、環境変数を簡単に設定し管理することができるため、開発環境やテスト環境での構成が容易になる
- ローカル開発環境において、データベースの接続情報や外部APIのキーなどの機密情報を、安全に管理するための簡単な方法を提供している
- Railsアプリケーションが起動する際に、自動的に `.env` ファイルを読み込み、環境変数をRailsの環境にマージする
- 環境ごと（例えば `.env.development`、`.env.test`）に異なる `.env` ファイルを使用することができ、環境固有の設定を管理しやすくなる

## config/credentials.yml.encを使用する方法もある
機密情報（今回の場合はAPIキー）を保存する方法として、Rails 5.2以降では`config/credentials.yml.enc`ファイルを使用する方法もあり、どちらを選ぶべきなのかChatGPT先生に聞いてみたところ、

>- セキュリティが最優先の場合、またはチームメンバー間で機密情報を共有する必要がある場合は、config/credentials.yml.enc が適しています。
>- 開発の柔軟性や環境設定の簡略化を優先する場合、または個々の開発者が独自の設定を持つ必要がある場合は、.env ファイルが便利です。

とのことで、今回は`.env`ファイルを使用したのですが、間違いがありましたらご指摘いただけますと幸いです。

## 環境変数を定義
プロジェクトのルートディレクトリに`.env`ファイルを作成し、環境変数を定義します。

```rb:.env
OPENWEATHERMAP_API_KEY = "取得したAPIキーをここに記述"
```

次に`.gitignore`ファイルに `/.env`を追加します。

```rb:.gitignore
/.env
```

`.gitignore`ファイルに記述することで、`.env`ファイルが`Git`によるバージョン管理から除外され、`GitHub`に機密情報が誤って`commit`されることを防ぎます。

## APIを叩いてみる
「APIを叩く」という言い回しは、指定されたAPIのエンドポイント（URL）に対して、特定の`HTTP`メソッド（`GET`、`POST`、`PUT`、`DELETE`など）を使用してリクエストを行うことを意味します。

```:HTTPリクエスト
https://api.openweathermap.org/data/2.5/weather?q=kawasaki,jp&appid={API key}&lang=ja
```
`HTTP`リクエストを送ると、多くのAPIではJSON形式でレスポンスが返ってくるため、必要なデータだけを抽出し、数値を加工して見やすくしていきたいと思います。

```json:JSON形式のAPIレスポンス例
{
 "coord":{
  "lon":139.7172,"lat":35.5206
 },
 "weather":[
  {
   "id":500,
   "main":"Rain",
   "description":"小雨",
   "icon":"10n"
  }
 ],
 "base":"stations",
 "main":{
  "temp":283.51,
  "feels_like":282.43,
  "temp_min":282.08,
  "temp_max":285.38,
  "pressure":1023,
  "humidity":70
 },
 "visibility":10000,
 "wind":{
  "speed":3.09,
  "deg":340
 },
 "rain":{
  "1h":0.13
 },
 "clouds":{
  "all":75
 },
 "dt":1702572846,
 "sys":{
  "type":1,"id":8074,"country":"JP","sunrise":1702590161,"sunset":1702625355
 },
 "timezone":32400,
 "id":1859642,
 "name":"川崎市",
 "cod":200
}
```

## ルーティング
```config/routes.rb
Rails.application.routes.draw do
  get 'weather', to: 'weather#index', as: :weather
  get 'weather/show', to: 'weather#show', as: :show_weather
end
```

## コントローラ
```controllers/weather_controller.rb
# gem httpartyを読み込む
require 'httparty'
# URIモジュールを読み込む（URLのエンコードや解析に使われる）
require 'uri'

class WeatherController < ApplicationController
  before_action :validate_city, only: [:show]

  def index
  end

  def show
    # WeatherServiceのインスタンスを生成し、fetch_weatherメソッドを呼び出して天気情報を取得
    response = WeatherService.new(params[:city]).fetch_weather
    # HTTPレスポンスが成功かどうかをチェック
    if response.success?
      # HTTParty からのレスポンスをパースして、Rubyのハッシュ形式のデータ構造（weather_data）に変換
      weather_data = response.parsed_response
      @weather = {
        # 取得したweather_dataハッシュから特定の気象データを取り出し、適切な形式に変換
        name: weather_data["name"],
        temp_celsius: kelvin_to_celsius(weather_data["main"]["temp"]).round(2),
        feels_like_celsius: kelvin_to_celsius(weather_data["main"]["feels_like"]).round(2),
        temp_min_celsius: kelvin_to_celsius(weather_data["main"]["temp_min"]).round(2),
        temp_max_celsius: kelvin_to_celsius(weather_data["main"]["temp_max"]).round(2),
        humidity: weather_data["main"]["humidity"],
        wind_speed: weather_data["wind"]["speed"],
        description: weather_data["weather"][0]["description"]
      }
    else
      # 成功でない場合、ユーザーをindexアクションにリダイレクトし、エラーメッセージを表示
      redirect_to action: :index, alert: "天気情報の取得に失敗しました。"
    end
  end

  private

  def validate_city
    # 都市名が空であれば、ユーザーをindexアクションにリダイレクトし、警告メッセージを表示
    redirect_to(action: :index, alert: "都市名を入力してください。") if params[:city].blank?
  end

  # ケルビン単位の温度を摂氏単位に変換するkelvin_to_celsiusメソッドを定義
  def kelvin_to_celsius(kelvin)
    kelvin - 273.15
  end
end

class WeatherService
  # HTTPartyモジュールをWeatherServiceクラスにインクルード
  include HTTParty
  # OpenWeatherMap APIのベースURIを設定
  base_uri 'api.openweathermap.org'

  def initialize(city)
    # 環境変数からOpenWeatherMapのAPIキーを取得
    api_key = ENV['OPENWEATHERMAP_API_KEY']
    # APIリクエストのためのオプションを設定
    @options = { query: { q: "#{city},jp", appid: api_key, lang: 'ja' } }
  end

  def fetch_weather
    # HTTPartyによって提供されるgetメソッドをクラスメソッドとして呼び出す
    self.class.get("/data/2.5/weather", @options)
  end
end
```
- `.round(2)`は温度を小数点以下2桁まで表示させるために使用
- 摂氏温度はケルビン温度から273.15を引くことで計算

## ビュー
```erb:views/weather/index.html.erb
<%= form_with url: show_weather_path, method: :get do |form| %>
  <%= form.label :city, "都市名:" %>
  <%= form.text_field :city %>
  <%= form.submit "検索" %>
<% end %>
```

```erb:views/weather/show.html.erb
<h1><%= @weather[:name] %> の天気</h1>

<p><strong>温度:</strong> <%= @weather[:temp_celsius] %> °C</p>
<p><strong>体感温度:</strong> <%= @weather[:feels_like_celsius] %> °C</p>
<p><strong>最低温度:</strong> <%= @weather[:temp_min_celsius] %> °C</p>
<p><strong>最高温度:</strong> <%= @weather[:temp_max_celsius] %> °C</p>
<p><strong>湿度:</strong> <%= @weather[:humidity] %> %</p>
<p><strong>風速:</strong> <%= @weather[:wind_speed] %> m/s</p>
<p><strong>天気:</strong> <%= @weather[:description] %></p>

<div>
  <%= link_to '戻る', weather_path, class: "btn btn-primary" %>
</div>
```

## 完成！
都市名を入力し、検索ボタンを押すと気象データが表示されるようになりました。
無料プランの場合、1分間に60回APIを呼び出せるそうです。

[![Image from Gyazo](https://i.gyazo.com/ffa6ff7427a360d9ce81cce4cab84405.gif)](https://gyazo.com/ffa6ff7427a360d9ce81cce4cab84405)

（母の実家がある青森は寒そうです…）
[![Image from Gyazo](https://i.gyazo.com/0770784ceb39cfed02df3281f3a78f7e.gif)](https://gyazo.com/0770784ceb39cfed02df3281f3a78f7e)

## まとめ
初めてAPIを叩いてみましたが、[OpenWeatherMap API](https://openweathermap.org/)は無料プランでもさまざまな情報を取得でき、とても便利だなと感じました。これから個人開発を始めるため、APIを活用してより良いアプリを作っていけたらいいなと思います。

最後まで読んでくださりありがとうございました！