見出し画像

Rubyのgem、deviseをコードリーディングして理解を深める

良質なコードを読むことの大切さをこちらの文章をもとに
前回の投稿で記載させていただきました。

掲題にもあるとおり、gemのdeviseを読み解きたいと思います。
自分のアプリケーション開発でも使用頻度が高く、大枠の機能ができていることや、deviseを導入すれば”signed_in”や"current_user"が使えるようになりますが、それが何故そうなっているのか本質を理解できておらず、コードリーディングすることで、理解を深めたいと思ったからです。

deviseのすべてを理解しようとすると、ボリュームが多いと思ったので、
今回は"current_user"をピックアップし、深掘りしたいと思います。

そもそもdeviseとは?からです。

deviseとはrailsで作ったwebアプリケーションに簡単に認証機能を実装できるgemのことです。

認証機能と聞くと難しく感じますがようは、login,logout機能のことです。
deviseは認証機能を追加するためのgemの中で一番使用されており、コマンドを何度か打つだけで簡単に認証機能を実装することができます!

認証機能を自分で実装しようと思うとどうしても工数がかかったり、安全性の面で不安が出てきてしまいます。

deviseはそんな問題を一挙に解決してくれる優れもののgemです!

SAMURAI ENGINEERより


それでは、順を追って読み解いていきたいと思います。

READMEを読む

deviseは全部で10個のモジュールが存在しており、
これらから、deviseの機能を構成しています。

Database Authenticatable:
パスワードをハッシュしてデータベースに保存し、サインイン中にユーザーの信頼性を検証します。
Omniauthable:
OmniAuth(https://github.com/omniauth/omniauth)のサポートを追加しています。
・Confirmable:
確認手順が記載されたメールを送信し、ログイン時にアカウントがすでに確認されているかどうかを確認します。
・Recoverable:
ユーザーパスワードをリセットし、リセット命令を送信します。
・Registerable:
登録プロセスを通じてユーザーのサインアップを処理し、ユーザーがアカウントを編集および破棄できるようにします。
・Rememberable:
保存されたCookieからユーザーを記憶するためのトークンの生成とクリアを管理します。
・Trackable:
サインインカウント、タイムスタンプ、IPアドレスを追跡します。
・Timeoutable:
指定された期間にアクティブになっていないセッションを期限切れにします。
・Validatable:
電子メールとパスワードの検証を提供します。これはオプションであり、カスタマイズできるため、独自の検証を定義できます。
・Lockable:
指定された回数のサインイン試行の失敗後にアカウントをロックします。電子メールまたは指定された期間の後にロックを解除できます。

deviseを導入して主に使用していた機能は、ユーザー登録やログイン根幹になっているであろうRegisterableや、一度サインインすると、毎回ログインせずとも一定時間内であればログインできるようcookie上で記憶できるように処理するRememberable、一定期間のログインが無いとログインを求めるTimeoutableが多そうです。
今回のcurrent_userは、この辺りに関連がある機能な気がします。

また、deviseを導入することで利用可能になるヘルパーメソッドとして、
以下の記載を見つけました。

#ユーザーがサインインしているかどうかを確認
user_signed_in?

#現在サインインしているユーザーの場合に使用可能なヘルパー
current_user

#このスコープのセッションにアクセスが可能
user_session

これに加えて、以下の記載に注目しました。

#例えば、DeviseモデルがUserではなくMemberと呼ばれている場合、
 利用できるヘルパーは以下の通りであることに注意してください。

before_action :authenticate_member!

member_signed_in?

current_member

member_session

いままで、"user_signed_in?"や、"current_user"は
それが構文の1つとして存在していると思っていましたが、
"(モデル名)_signed_in?"や"current_(モデル名)"が
正確な構文ということがわかりました。

ディレクトリ/ファイル構造を読む

deviseは、以下のファイル群で構成されています。

ファイル構造全体を理解しようとすると、自分のなかでも混乱しそうなので、テーマを絞って進めたいと思います。
今回は"current_user"について調べたいので、ヘルパー関連のファイルを探したところ、2つのファイルを見つけました。

・app>helpers>devise_helper.rb
・lib>devise>controllers>helpers.rb
この2つに絞って見ていきます。

・app>helpers>devise_helper.rbを見る

# frozen_string_literal: true

module DeviseHelper
  # Retain this method for backwards compatibility, deprecated in favor of modifying the
  # devise/shared/error_messages partial.
  def devise_error_messages!
    ActiveSupport::Deprecation.warn <<-DEPRECATION.strip_heredoc
      [Devise] `DeviseHelper#devise_error_messages!` is deprecated and will be
      removed in the next major version.

      Devise now uses a partial under "devise/shared/error_messages" to display
      error messages by default, and make them easier to customize. Update your
      views changing calls from:

          <%= devise_error_messages! %>

      to:

          <%= render "devise/shared/error_messages", resource: resource %>

      To start customizing how errors are displayed, you can copy the partial
      from devise to your `app/views` folder. Alternatively, you can run
      `rails g devise:views` which will copy all of them again to your app.
    DEPRECATION

    return "" if resource.errors.empty?

    render "devise/shared/error_messages", resource: resource
  end
end

ここでは、"DeviseHelper"というモジュールの中で、
"devise_error_messages!"メソッドが定義されています。

しかし、英文を読み進めていくと
・現在は"devise/shared/error_messages"で記載されているエラーメッセージをしようしており、このメソッドは廃止予定であること
・互換性のために保守されているメソッドであること
という2点が主に記載されています。

廃止される理由は改めて考えてみたいですが、
devise側でモジュールを設けなくとも、エラーメッセージを
validateとviewのみで完結できてしまうから、とか何かしらの意図があると思うので、ここは別途でもう少し考えたいと思います。

・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

        ActiveSupport.on_load(:action_controller) do
          if respond_to?(:helper_method)
            helper_method "current_#{mapping}", "#{mapping}_signed_in?", "#{mapping}_session"
          end
        end
      end

Deviseモジュール>class_evalクラス?>ヘルパーに関係する
メソッドが並んでいるという構造です。

まずは、class_evalがクラスなのか、
はたまた別のものを指しているのかを調べました。

class_evalとは動的にクラス変数を定義できるもの、つまり
レシーバーであるクラスのインスタンス変数(クラスインスタンス変数)やクラス変数を定義したり、上書きすることができる
レシーバーであるクラスのインスタンスメソッドを定義したり、上書きすることができる
となるので、ここで"current_(モデル名)"の定義を行なっているようです。

REDOMEに書いてあった通り、deviseのヘルパーメソッドは、
"(モデル名)_signed_in?"や"current_(モデル名)"のように、
紐づいているモデル名によって、メソッド名が変わるため、
class_evalのモジュールを使って、動的にヘルパーメソッドを定義していると理解しました。

それでは、class_evalで定義されているメソッドを細かく見てみます。

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

ここでいくつか要素が登場しているので、
分解して整理し、それぞれ調べたいと思います。

・mapping
self.define_helpersメソッドのなかで、定義してあります。

mapping = mapping.name

mappingが別のどこかで定義されているはずなので、
呼び出されている部分を確認し、探します。

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

"Devise::Mapping.new(resource, options)"でインスタンスが生成されたものが代入されています。
なので、この要素を調べるべくMappingクラスを探すと、lib>devise>mapping内でMappingクラスがあります。

  class Mapping #:nodoc:
    attr_reader :singular, :scoped_path, :path, :controllers, :path_names,
                :class_name, :sign_out_via, :format, :used_routes, :used_helpers,
                :failure_app, :router_name

    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   #ここ

Mapping.newで呼び出されたinitializeメソッドを見ます。
第2引数で指定されているoptionsの理解が追いつかなかったのですが、
nameにdeviseに紐づくモデル名が引数として渡ってきた場合、

@singular = (モデル名).to_s.tr(' / ' , ' _' ).singularize.to_sym

となります。
(モデル名)に続く要素は、以下の通りなので、
@singularには、(モデル名)が代入されます。

to_s:文字列を返す
tr:patternで指定した文字を置き換える
singularize:複数形の名詞を単数形に置き換える
to_sym:シンボル値を返す

また、Mappingクラス内にあるalias~は、
既存のメソッドに対して、別名が付けられるメソッドのため、
singularにnameという名前を付け直していることがわかりました。

つまり、(モデル名)がUserだった場合、mapping.nameには
userという値が入っていることになります。


class_eval内で定義されていたメソッドに戻ります。

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

先ほど記載した通り、mappingにはuserが代入されているため、
current_userメソッドが定義されているかたちとなります。

メソッド内で、定義されている"@current_#{mapping}"に自己代入されている値を最後に紐解いていきます。

warden.authenticateのwardenですが、調べてみるとgemの一種でした。

deviseファイル内を調べると、かなり多くの"warden"という記述が見つかりました。そもそもdeviseはwardenをもとにして作成されているようです。

早速wardenとは、、を調べてみました。

WardenはRackベースのミドルウェアで、RubyのWebアプリケーションで認証の仕組みを提供するために設計されました。Rack Machineryにフィットする共通のメカニズムで、認証のための強力なオプションを提供します。

wardencommunity

このまま、、wardenについて書いてしまうと、
本題から外れてしまう、かつ文量が長くなってしまうため、
一旦、deviseのコードリーディングとしてはここまでにしたいと思います。

はじめてコードリーディングを行いましたが、
それぞれの関数の繋がりや、新たな気付きがあったため
とても有意義だったと思います!
是非、初学者の方は一度お試しください!

長くなりましたが、ここまで読んでいただきありがとうございました!

この記事が気に入ったらサポートをしてみませんか?