Rails Tutorial の拡張機能:RSSフィード機能を作ってみた

Rails Tutorialの第14章にある、拡張機能を作る件の続きです。

今回はRSSフィード機能を作ります。

##要件の確認

チュートリアルでは、

>ユーザーごとのマイクロポストをRSSフィードする機能を実装してください。次にステータスフィードをRSSフィードする機能も実装し、余裕があればフィードに認証スキームも追加してアクセスを制限してみてください。

と要件が書いてあります。

RSSは利用したことはありますが、どういう仕組みなのかはあまりよく分かっていません。
作った人がいるかネットで調べてみます。

feed.rss.builderという標準で搭載されている Builder 機能があるようです。

次にフィードに認証スキームですが、ログオンしていないとフォローできないと言う意味と捉えました。
実例があるのかネットで調べます。

Twitterが認証しているかネットで調べます。Twitter自体はRSSを配信しておらず、別のサイトからRSSを取得しているようです。facebookも昔はオフィシャルの機能としてRSSを提供していたが、打ち切ったとのことした。


>参考：認証ありのサイトでRSSを取得するには
>http://ユーザ名:パスワード@RSSフィードのURL

でできるとかなり古いネットの記事で見つけましたが、URLに記載するというのはパスワード漏洩のリスクが高そうです。

調べた結果から要件をまとめました。
１．自分自身のポストを、RSSフィードで出力する機能
２．RSSフィードを登録するアイコンとリンクをホームに追加
３．ステータスフィード、つまりフォローしている人も含むポストを、RSSフィードで出力する機能
４．RSSに認証をかけるのは難しそうなので今回はやらない

## 仕様を設計

要件から具体的な機能に落とし込みます。

1.自分自身のポストを、RSSフィードで出力する機能
urlのxxx/rssにアクセスすると、RSSファイルが返される。

２．RSSフィードを登録するアイコンとリンクをホームに追加
ホームにリンクを追加する。

３．ステータスフィード、つまりフォローしている人も含むポストを、RSSフィードで出力する機能
urlのxxx/rssにアクセスすると、RSSファイルが返される。

##自分自身のポストをRSS出力：コントローラーの追加

トピックブランチを作ります。

```
ubuntu:~/environment/sample_app (master) $ git checkout -b rss
```
http://miner.hatenablog.com/entry/2017/07/20/142532　
を参考にします。


ルーティングを追加します。

```ruby:config/routes.rb
  get :rss, to: 'rss#index', defaults: { format: :rss }
```
コントローラーを生成します。

```
ubuntu:~/environment/sample_app (rss) $ rails generate controller Rsss
      create  app/controllers/rsss_controller.rb
      create    app/views/rsss
      create    test/controllers/rsss_controller_test.rb
      create    app/helpers/rsss_helper.rb
      create      app/assets/javascripts/rsss.coffee
      create      app/assets/stylesheets/rsss.scss
```

コントローラーに、userのshowを参考にして、追記します。


```ruby:app/controllers/rsss_controller.rb
class RsssController < ApplicationController

  layout false
  def index
    @user = User.find(params[:id])
    @microposts = @user.microposts.limit(10) 
    
    respond_to do |format|
      format.rss
      format.atom
    end
  end
end
```

##ビューを作成
ビューを作成します。

app/views/rsss/index.rss.builderファイルを作成します。

```app/views/rsss/index.rss.builder
cache 'feed_cache_key', expires_in: 30.minutes do
  xml.instruct! :xml, :version => "1.0"
  xml.rss :version => "2.0" do
    xml.channel do
      xml.title "Rails Tutorial sample site"
      xml.description "Rails Tutorial sample site"
      xml.link root_url

      @microposts.each do |b|
        xml.item do
          xml.title b.title
          xml.description strip_tags(b.rich_text_body.to_s.gsub(/\r\n|\r|\n|\s|\t/, "")).truncate(120)
          xml.image image_url(url_for(b.image))
          xml.pubDate b.time.to_s(:rfc822)
          xml.link blog_url(b)
          xml.guid blog_url(b)
        end
      end
    end
  end
end
```

https://pgmg-rails.com/blogs/24　
を参考にします。

cloud9の構文チェックの色がつかなくなりました。拡張子なのか原因は分かりませんでした。

```ruby:app/views/rsss/index.rss.builder
         xml.title b.content
          xml.description b.content
          xml.image image_url(url_for(b.picture)) if b.picture?
          xml.pubDate b.updated_at.to_s(:rfc822)
```  

##RSSが出力されるか確認

RSSが出力されるかを確認してみます。

rails serverで、/rssにアクセスします。
https://XXXXXXXXXXXXXXX.amazonaws.com/rss

エラーが出力されました。

ActionController::RoutingError (uninitialized constant RssController):

routesを確認します。

```
buntu:~/environment/sample_app (rss) $ rails routes
                 Prefix Verb   URI Pattern                             Controller#Action
                   root GET    /                                       static_pages#home
...
                    rss GET    /rss(.:format)                          rss#index {:format=>:rss}
```

複数形とファイル名が合っていないのが問題かとあたりをつけました。
routesのstatic_pages#homeにはファイル名がstatic_pages_controller.erbが対応しています。
なのでrss#indexにはファイル名がrss_controller.erbが対応するのではと考えました。
だとするとroutesがrsss#indexであるべきなので、修正します。


```ruby:config/routes.rb
  get :rss, to: 'rsss#index', defaults: { format: :rss }
```

違うエラーが出力されました。

```
ActiveRecord::RecordNotFound (Couldn't find User with 'id'=):
app/controllers/rsss_controller.rb:5:in `index'
```

先のエラーは解消されコントローラーに進みました。
ログインしていないので、userのidがないためです。

試しにログインしてからアクセスしてみましたが、同じエラーでした。

原因として、パラメーターからidがなぜ取れないのか、
そもそもRSSを取得するときにはログインしていないので、user#showを参考にします。

```
GET    /users/:id(.:format)  users#show
```

になっています。urlでxxx/users/1 にアクセスするとidに1が渡されてきます。
routesに/:idを追加すればよいのではと考え、修正します。

ネットでroutesの書き方を調べます。
https://www.sejuku.net/blog/13078


```ruby:config/routes.rb
  get 'rss/:id', to: 'rsss#index', defaults: { format: :rss }
```

```
ubuntu:~/environment/sample_app (rss) $ rails routes
                 Prefix Verb   URI Pattern                             Controller#Action
                   root GET    /                                       static_pages#home

                        GET    /rss/:id(.:format)                      rsss#index {:format=>:rss}
```

違うエラーになりました。

```
ActionView::Template::Error (undefined method `blog_url' for #<#<Class:0x000056130c6fb330>:0x000056130c6f9648>
Did you mean?  login_url):
```

例として参考にした記事のそのままだと動かない点だと分かりました。
linkとguidには何が表示されるべきなのか調べます。

```
          xml.link blog_url(b)
          xml.guid blog_url(b)
```

参考にした記事はブログの例であり、各記事のURLを指定していました。
対応するものとして、マイクロポストごとのURLはないので、ユーザーのURLを出すことにします。pathとURLの違いがあいまいだったのでネットで確認しました。
https://qiita.com/higeaaa/items/df8feaa5b6f12e13fb6f

```ruby:app/views/rsss/index.rss.builder
          xml.link user_url(b.user)
          xml.guid user_url(b.user)
```

修正してurlにアクセスしたところ、開くか保存するかのダイアログが表示されました。

![rss.1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639093/162776f1-c30f-75a0-d8a5-96eac4d4fafe.png)


保存したファイルをメモ帳で開いたところ、xmlが出力されていました。


```
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Rails Tutorial sample site</title>
    <description>Rails Tutorial sample site</description>
    <link>https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com/</link>
    <item>
      <title>@Elvis1 reply test</title>
      <description>@Elvis1 reply test</description>
      <pubDate>Wed, 11 Nov 2020 23:27:05 +0000</pubDate>
      <link>https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com/users/1</link>
      <guid>https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com/users/1</guid>
    </item>
    <item>
      <title>Est et placeat voluptates et alias.</title>
      <description>Est et placeat voluptates et alias.</description>
      <pubDate>Wed, 11 Nov 2020 23:27:04 +0000</pubDate>
      <link>https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com/users/1</link>
      <guid>https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com/users/1</guid>
    </item>
..
    <item>
      <title>Sint porro molestiae corporis quidem eligendi.</title>
      <description>Sint porro molestiae corporis quidem eligendi.</description>
      <pubDate>Wed, 11 Nov 2020 23:27:03 +0000</pubDate>
      <link>https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com/users/1</link>
      <guid>https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com/users/1</guid>
    </item>
  </channel>
</rss>
```

このURLをFeedlyに試しに追加してみたのですが、RSSとして認識されませんでした。

```
We were not able to find an RSS feed for https://ｘｘｘｘｘｘｘ.amazonaws.com/rss/1.

Please contact the website and ask them if they offer a valid RSS feed.
```

RSSの問題なのかネットで調べましたがよく分かりませんでした。

https://qiita.com/amymd/items/bb4a9e008af061c89ea9

https://pgmg-rails.com/blogs/24
を参考にコントローラーの書き方を変更してみます。

```ruby:app/controllers/rsss_controller.rb
    respond_to do |format|
      format.html
      format.rss { render :layout => false }
    end
```
ファイルを開くダイアログが表示される動きは変わりませんでした。
ネットで調べましたが分からなかったので、いったんそのままにします。

##ステータスフィードを出力するように変更

今は自分がポストしたものを出力しているので、ステータスフィード、つまりフォローしている人も含むポストを、RSSフィードで出力する機能に変更します。

ステータスフィードを画面に表示しているところを参考にします。
ホーム画面でしたので、static_pagesコントローラーとあたりを付けます。

```ruby:app/controllers/static_pages_controller.rb
@feed_items = current_user.feed.paginate(page: params[:page])
```

を見つけ、feedメソッドで出力できると分かります。コンソールで試してみると、確かにマイクロポストが返ってきました。
コントローラーを変更します。


```ruby:app/controllers/rsss_controller.rb
class RsssController < ApplicationController
  def index
    @user = User.find(params[:id])
    @microposts = @user.feed.limit(10) 
  
    respond_to do |format|
      format.html
      format.rss { render :layout => false }
    end
  end
end
```

rails serverで動かしてみます。ファイルが出力されるので内容を確認します。

```
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Rails Tutorial sample site</title>
    <description>Rails Tutorial sample site</description>
    <link>https://https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com.amazonaws.com/</link>
    <item>
      <title>@Elvis1 reply test</title>
      <description>@Elvis1 reply test</description>
      <pubDate>Wed, 11 Nov 2020 23:27:05 +0000</pubDate>
      <link>https://https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com.amazonaws.com/users/1</link>
      <guid>https://https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com.amazonaws.com/users/1</guid>
    </item>
    <item>
      <title>Est et placeat voluptates et alias.</title>
      <description>Est et placeat voluptates et alias.</description>
      <pubDate>Wed, 11 Nov 2020 23:27:04 +0000</pubDate>
      <link>https://https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com.amazonaws.com/users/6</link>
      <guid>https://https://xxxxxxxxxxxxxxxxxxxxx.amazonaws.com.amazonaws.com/users/6</guid>
    </item>

  </channel>
</rss>
```

ホーム画面と突き合わせてみると、ポストの2つ目は他のユーザーがポストしたもので合っていました。

![rss.2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639093/751b4082-f9bd-e9b2-7f5e-447e62a26bee.png)


RSSでは誰がポストしたマイクロポストなのかが分からないということが分かりました。今回はそのままにして、今後の改善点としておきます。

##テストを作成

動くようになりましたので、テストを書きます。RSSの出力結果をどう扱えばよいのかが分からないのでネットで調べましたが見つかりませんでした。なので画面と同じように書くことにします。

```ruby:test/integration/rss_test.rb
require 'test_helper'

class RssTest < ActionDispatch::IntegrationTest
  
  def setup
    @user = users(:michael)
  end
  
  test "rss show" do
    get rss_path(@user)
    assert_template 'rsss/index'
  end

end
```

エラーになりました。

```
NoMethodError:         NoMethodError: undefined method `rss_path' for #<RssTest:0x000055fc3d47d0f8>

ubuntu:~/environment/sample_app (rss) $ rails routes
                 Prefix Verb   URI Pattern                             Controller#Action
                 new_dm GET    /dms/new(.:format)                      dms#new
                     dm DELETE /dms/:id(.:format)                      dms#destroy
                        GET    /rss/:id(.:format)                      rsss#index {:format=>:rss}

``` 

rss_pathのルーティングには、Prefixのところに名前がなく空白ないので、名前をつけてみます。
https://www.sejuku.net/blog/13078　
を参考にします。


```ruby:config/routes.rb
get 'rss/:id', to: 'rsss#index', as: 'rss', defaults: { format: :rss } 
```

``` 
ubuntu:~/environment/sample_app (rss) $ rails routes
                 Prefix Verb   URI Pattern                             Controller#Action
                     dm DELETE /dms/:id(.:format)                      dms#destroy
                    rss GET    /rss/:id(.:format)                      rsss#index {:format=>:rss}
```

GREENになりました。 

contentが表示されているかをassert_matchで調べるように追加します。テストを実行したところGREENでした。
これで完成とします。

```ruby:test/integration/rss_test.rb
  test "rss show" do
    get rss_path(@user)
    assert_template 'rsss/index'
    @user.feed.limit(10).each do |micropost|
      assert_match micropost.content, response.body
    end
  end
```

いつものようにherokuにアップします。

```
ubuntu:~/environment/sample_app (rss) $ git add -A
ubuntu:~/environment/sample_app (rss) $ git commit -m "Add RSS"       
ubuntu:~/environment/sample_app (rss) $ git checkout master
ubuntu:~/environment/sample_app (master) $ git merge rss
ubuntu:~/environment/sample_app (master) $ git push
ubuntu:~/environment/sample_app (master) $ git push heroku
```

HerokuでRSSのURLにアクセスしてみました。ファイルを開くダイアログが表示される動きは変わらなかったです。

##所要時間

12/21から1/11までの実働13日間の8.0時間です。
ネットで調べるのに時間がかかりました。書いたりテストしたりの時間は、感覚的に半分もかかっていないです。


###参考にした記事のまとめ

http://miner.hatenablog.com/entry/2017/07/20/142532

https://pgmg-rails.com/blogs/24

https://qiita.com/klriutsa/items/6662ef75e804c4323228

https://madogiwa0124.hatenablog.com/entry/2019/02/24/234610

http://maepachi.com/entries/94