DeviseのコードリーティングでRailsを学ぶ

普段RailsでWebアプリの開発をしていますが、使わない機能や内部の処理など知らないことは多いです。そこでDeviseで使う機能の実装をみて知見を増やそうかと思います。なぜ、Deviseかというとボリュームが多いGemが良かったのと、認証周りの詳しい動きが知りたかったからです。  

## 概要

今回は`authenticate_user!`に焦点を当ててコードの中身を見ていこうと思います。`authenticate_user`は認証済みのユーザーか確認するメソッドでDeviseで最もよく見るメソッドの１つかと思います。  
  
またコードを読むDeviseのバージョンは`v4.6.2`にしています。  

## define_helpers

`authenticate_user!`の定義場所を探そうとしたら、まずは`define_helpers`というメソッドに出会います。`define_helpers`では認証で利用するメソッドの定義をしています。　　

>
https://github.com/plataformatec/devise/blob/2e5b5fcd705b06c518ab0156b96badb91c4cb6ea/lib/devise/controllers/helpers.rb#L112

```lib/devise/controllers/helpers.rb
def self.define_helpers(mapping) #:nodoc:
  mapping = mapping.name

  class_eval <<-METHODS, __FILE__, __LINE__ + 1
    def authenticate_#{mapping}!(opts={})
      opts[:scope] = :#{mapping}
      warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
    end

    def #{mapping}_signed_in?
      !!current_#{mapping}
    end

    def current_#{mapping}
      @current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
    end

    def #{mapping}_session
      current_#{mapping} && warden.session(:#{mapping})
    end
  METHODS
```

### class_eval

`class_eval <<-METHODS, __FILE__, __LINE__ + 1`でヒアドキュメントとなっているところが定義部です。`authenticate_user!`以外にも`current_user`などもここで定義しているようです。class_eval`はメタプログラミングで使われるメソッドでこれを使用すると引数に渡した文字列をメソッド定義のコードとして読んでくれます。  

Devise利用では多くは認証のモデル名をUserにしますが、任意のモデル名にすることができます。その場合は`authenticate_***`と動的に定義しないので`class_eval`を使ってメソッドを実装しています。 
  
`class_eval`を試しに使ってみた例です。    

```devise_memo.rb

class DeviseMemo
  def self.define_methods(mapping)
    class_eval <<-METHODS, __FILE__, __LINE__ + 1
      def authenticate_#{mapping}!
        p "called authenticate_#{mapping}!"
      end

      def #{mapping}_signed_in?
        p "called #{mapping}_signed_in?"
      end
    METHODS
  end
end

DeviseMemo.define_methods("user")
devise_memo = DeviseMemo.new
devise_memo.authenticate_user!
devise_memo.user_signed_in?
```

```console
$ ruby devise_memo.rb
"called authenticate_user!"
"called user_signed_in?"
```

### warden

`authenticate_user!`の中身は短い、というか結局は[warden](https://github.com/wardencommunity/warden)というGemを使って認証確認しているようですね。 
ここを見ないといけなくなりました。これは次の機会にしておきます。

```ruby
def authenticate_#{mapping}!(opts={})
  opts[:scope] = :#{mapping}
  warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end
```

`current_user`や`user_session`もwardenを使っているので、Deviseの根幹を握っているようです。

## add_mapping

`define_helpers`の呼び出し場所を確認してみると`lib/devise.rb`の中にある`add_mapping`で呼ばれています。
> https://github.com/plataformatec/devise/blob/2e5b5fcd705b06c518ab0156b96badb91c4cb6ea/lib/devise.rb#L349

```lib/devise.rb
def self.add_mapping(resource, options)
  mapping = Devise::Mapping.new(resource, options)
  @@mappings[mapping.name] = mapping
  @@default_scope ||= mapping.name
  @@helpers.each { |h| h.define_helpers(mapping) }
  mapping
end
```

ちなみに`add_mapping`は`devise_for`の中で呼ばれます。`devise_for`も次の機会で詳しく中身を見ていきたいです。  

```lib/devise/rails/routes.rb
def devise_for(*resources)
  @devise_finalized = false
  raise_no_secret_key unless Devise.secret_key
  options = resources.extract_options!

  (省略)

  resources.each do |resource|
    mapping = Devise.add_mapping(resource, options)
```

`extract_options!`は可変長の引数を分解できるメソッドです。  
下記が簡単な例です。`extract_options!`はActiveSupportのメソッドなのでRailsコンソールで試すのが楽です。    

```ruby
def devise_for_test(*resources)
  p resources
  options = resources.extract_options!
  p resources
  p options
end

devise_for_test :users, controllers: [ sessions: 'users/sessions' ]
```

```shell
# 出力

[:users, {:controllers=>[{:sessions=>"users/sessions"}]}]
[:users]
{:controllers=>[{:sessions=>"users/sessions"}]}
```

`devise_for`でモデル名とオプションに分けられて`Devise.add_mapping(resource, options)`と呼び出されます。
そして`add_mapping`内で`mapping = Devise::Mapping.new(resource, options)`とマッピングを作成しています。  

`mapping`は`define_helpers`で`mapping = mapping.name`と`name`だけが使われていましたが、`Devise::Mapping`の中を覗いてみると、  

```lib/devise/controllers/helpers.rb
class Mapping #:nodoc:
  
  alias :name :singular
  
  def initialize(name, options) #:nodoc:
    @scoped_path = options[:as] ? "#{options[:as]}/#{name}" : name.to_s
    @singular = (options[:singular] || @scoped_path.tr('/', '_').singularize).to_sym
```

となっています。 
`name`に注目してコードを繋げてみると、認証モデルがUserである場合は`@singular = :users.to_s.tr('/', '_').singularize.to_sym`とみれます。`singularize`は複数形を単数形に変換するメソッドで、最終的に`@singular = :user`となり`singular`のエイリアスが`name`となっているので`mapping.name`で`:user`が取得できます。 
すると`define_methods`の引数に`:user`が渡され`authenticate_user!`が出来上がるという流れになっています。

## 最後に

`authenticate_user!`をみても普段使わないようなメソッドや手法がありなかなか有意義でした。これはDeviseのほんの一部なのでまだまだみるべきところはたくさんあります。今後もDeviseのコードリーディングで勉強していこうと思いますが、今日の学びからwardenの方を先に読んでいくことになりそうです。

## 参考記事

- [What does class_eval <<-“end_eval”, __FILE__, __LINE__ mean in Ruby?](https://stackoverflow.com/questions/2496102/what-does-class-eval-end-eval-file-line-mean-in-ruby)
- [class_evalの第１引数に文字列を渡したときの挙動についてメモ](https://qiita.com/shinofumijp@github/items/d3b47e9af80108712112)
- [Railsで引数を可変長で受ける](https://qiita.com/shibukk/items/218fc04189bec9a4904d)