【Rails】入力された値を元にGETリクエストを送ってデータを取得し、DBに保存する

## すること

今回は[GET statuses/oembed — Twitter Developers](https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-oembed)を用いてツイッターカードを表示します。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322882/9e230f72-496a-0487-e967-4d3dba1030ef.png">
埋め込みHTMLを取得するためには認証は必要ないので、単純なリクエストでデータを取得することが可能です。

流れは以下の通りです。

1. 入力されたURLからIDを取得
2. リクエストを送信してデータを取得
3. URL, jsonをデータベースに保存
4. 保存したデータを表示

### 開発環境

Ruby 2.6.5
Rails 5.2.3
MySQL 8.0.19


## コントローラ

別段珍しいことはしていません。
redirect_to, renderは適当です。

```rb:tweets_controller.rb
class TweetsController < ApplicationController
  def create
    if @tweet.create(tweet_params)
      redirect_to tweet_path(@tweet)
    else
      render :new
    end
  end

  def update
    @tweet = Tweet.find(params[:id])
    if @tweet.update(tweet_params)
      redirect_to tweet_path(@tweet)
    else
      render :edit
    end
  end

  private

  def tweet_params
    params.require(:tweet).permit(:url)
  end
end
```

## モデル

事前に、jsonを入れる予定の`t_json`カラムをマイグレーションで作成しておきます。
（追記）jsonを1カラムを入れるのは、変更やデータの取得がしづらいためアンチパターンと言われています。使う際は注意してください。

```rb:tweet.rb
# == Schema Information
#
# Table name: tweets
#
#  id         :bigint           not null, primary key
#  url        :string(255)      not null
#  t_json     :json             not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class Tweet < ApplicationRecord
  validates :url, presence: true, length: { maximum: 200 }
end
```

このままでは、入力したurlしか保存されません。ということで、urlからt_jsonを取得し保存するまでの流れを順に追っていきます。

### 1. 入力されたURLからIDを取得

URLは、例えば`https://twitter.com/Interior/status/507185938620219395`という形になりますが、リクエストに必要なのはIDの`507185938620219395`の部分だけです。
ということで、入力されたURLからIDとなる文字列を抜き出す`tweet_id`というインスタンスメソッドを作成します。

```rb
  def tweet_id
    url.split('/').last
  end
```

ちなみに、`tweet.tweet_id`というような書き方で、モデルの外部からもIDを取得できます。


### 2. リクエストを送信してデータを取得

TwitterAPIにGETリクエストを送ります。
curlコマンドでは以下のようなコマンドになります。

```terminal:ExampleRequest
$ curl 'https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2FInterior%2Fstatus%2F507185938620219395'
```

これをRubyで実装します。2つの方法を挙げます。

#### open_uriを用いる方法
 
```rb
  def tweet_json
    url = "https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2FInterior%2Fstatus%2F#{tweet_id}"
    responce = OpenURI.open_uri(url)  # リクエストを送り、返り値をresponceに代入
    ActiveSupport::JSON.decode(responce.read)
  end
```

#### Net::HTTPを用いる方法

```rb
  def tweet_json
    url = "https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2FInterior%2Fstatus%2F#{tweet_id}"
    uri = URI.parse(url)
    responce = Net::HTTP.get(uri)  # リクエストを送り、返り値をresponceに代入
    JSON.parse(responce)
  end
```

[Ruby の HTTP リクエストを送る方法の性能比較](https://qiita.com/cuzic/items/694646c42162de4da0c8)によると、Net::HTTPの方が速いようです。

これで以下のようなjson形式のデータを返すメソッドを作成できました。

```json
{
  "url": "https://twitter.com/Interior/status/507185938620219395",
  "author_name": "US Dept of Interior",
  "author_url": "https://twitter.com/Interior",
  "html": "<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Happy 50th anniversary to the Wilderness Act! Here&#39;s a great wilderness photo from <a href="https://twitter.com/YosemiteNPS">@YosemiteNPS</a>. <a href="https://twitter.com/hashtag/Wilderness50?src=hash">#Wilderness50</a> <a href="http://t.co/HMhbyTg18X">pic.twitter.com/HMhbyTg18X</a></p>&mdash; US Dept of Interior (@Interior) <a href="https://twitter.com/Interior/status/507185938620219395">September 3, 2014</a></blockquote>n<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>",
  "width": 550,
  "height": null,
  "type": "rich",
  "cache_age": "3153600000",
  "provider_name": "Twitter",
  "provider_url": "https://twitter.com",
  "version": "1.0"
}
```


### 3. 取得したjsonを`tweet.t_json`に格納して保存

tweetを保存する前に、`before_save`を用いて`t_json`に値を入れることにします。
`set_json`メソッドの役割は、**URLが変更されていた場合、`t_json`に値を入れる**というものです。
外部APIを叩きすぎるのはよくないので、URLが変わっていない場合は特に変更しません（今回の例では他に変更しうるカラムがないので別に必要ないのですが）。

```tweet.rb
class Tweet < ApplicationRecord
  before_save :set_json # 追加

  def set_json
    self.t_json = tweet_json if will_save_change_to_url?
  end

  def tweet_json
    # TwitterAPIから取得したJSONデータを返す
  end
end
```

### 最終的なモデル

```tweet.rb
class Tweet < ApplicationRecord
  before_save :set_json
  validates :url, presence: true, length: { maximum: 200 }

  def tweet_id
    url.split('/').last
  end

  private  # 以下のメソッドは外部から呼び出さないのでprivateにする

  def set_json
    self.t_json = tweet_json if will_save_change_to_url?
  end

  def tweet_json
    url = "https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2FInterior%2Fstatus%2F#{tweet_id}"
    uri = URI.parse(url)
    responce = Net::HTTP.get(uri)
    JSON.parse(responce)
  end
```


## ビュー

Twitterカードを表示するにはjsonの`"html"`をそのままHTMLとして出力すればいいので、ビューファイルで以下のように記述します。

```erb:view.html.erb
<%= @tweet.t_json['html'].html_safe %>
```

## ツイート情報を取得する際のオプション

```rb
"https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2FInterior%2Fstatus%2F#{tweet_id}&omit_script=true"
```

このように`&`でつなげます。（パラメータの種類については[公式ページ](https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-oembed)参照）
`omit_script=true`にすると、`<script>`部分を除いたHTMLが返されるので、1ページに複数のツイートを表示する場合はおすすめです。

```erb:view.html.erb
<% @tweets.each do |tweet| %>
  <%= tweet.t_json['html'].html_safe %>
<% end %>

<%= javascript_include_tag '//platform.twitter.com/widgets.js' %>
```


## リンク

- [RubyでJSON形式の結果が返ってくるURLをParseする](https://qiita.com/awakia/items/bd8c1385115df27c15fa)

例外処理など細かく行う場合

- [Ruby on RailsでAPIを利用して、簡単なアプリケーションを作成する](https://programming-beginner-zeroichi.jp/articles/140)
- [railsふかぼり01：railsでWebAPIを叩く](http://jumtech.hatenablog.jp/entry/2016/08/20/140720)