devise_token_authのresource_classが何をしているのか調査する

rails

本記事では、devise_token_authで使用されるresource_classについて解説をしていきます。

devise_token_auth(devise)はrailsを使う上で必須スキルなので、ソースコードまで追えるようになっていると、deviseの一部を変えないといけない時とかに融通を効かせられる様になります。

一つ一つ処理を追っていくことでresource_classを徹底的に追っていこうと思います。

※手っ取り早く答えが知りたい方は全部飛ばして、まとめを見てもらった方がいいと思います!

resource_classの中身を知るためにソースコードを追う

# (1)resource_class 44行目
def resource_class(m = nil)
  if m
    mapping = Devise.mappings[m]
  else
    mapping = Devise.mappings[resource_name] || Devise.mappings.values.first
  end

  mapping.to
end

まずmappingについてですが、右辺がDevise.mappingsとなっていることからdeviseのソースコードから取ってきています。

memo

devise_token_authのapplication_controller.rbではclass ApplicationController < DeviseControllerとあります。これはDeviseのコントローラを継承していて、要はdeviseのライブラリを使用することができる様になっています。

# 2行目
module DeviseTokenAuth
  class ApplicationController < DeviseController # deviseのコントローラを親クラスとして継承している
    include DeviseTokenAuth::Concerns::SetUserByToken

ライブラリを使用している場合、lib(library)ディレクトリ配下にimportしているファイルがあることが多いです。まずはそちらを見てみると、Devise.mappingsは、lib配下のdevise.rbにありました。

# (1-1)mappingsの取得 
module Devise
  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
...

mappingsを定義しているところを見つけました。@@mappingsで定義されていますね。

memo

@@はクラス変数と呼ばれるもので、クラス内で一意の変数です。

@との違いは、@はインスタンス内でしか使用できませんが、@@は子クラスなどの外からの参照が可能です。また今回は、Deviseモジュール内に下記の記載があるので、

  mattr_reader :mappings
  @@mappings = {}

(1)にてDevise.mappingsで@@mappingsの値を取ってくることができます。

@@mappingsはDevise.add_mappingの実行により定義されます。なので、次はadd_mappingが実行される場所を探します。add_mappingメソッドのソースコードはroutes.rbの中に存在しています。

# (1-1-1) add_mappingメソッドの実施 
module ActionDispatch::Routing # 28行目
  class Mapper # 35行目
    def devise_for(*resources) # 226行目
      resources.each do |resource| # 242行目
          mapping = Devise.add_mapping(resource, options)

ここで、devise_for(*resources)でadd_mappingメソッドが実行されています。それではdevise_for(*resources)はどこで定義しているのかというと…devise_token_authのルーティングになります。

#(1-1-1-1) devise_for(*resource)の定義
module ActionDispatch::Routing # 3行目
  class Mapper
    def mount_devise_token_auth_for(resource, opts)
     route = opts[:as] || resource.pluralize.underscore.gsub('/', '_') #20行目
        devise_for route.to_sym,  # 35行目
             class_name: resource,
             module: :devise,
             path: opts[:at].to_s,
             controllers: controllers,
             skip: opts[:skip] + [:omniauth_callbacks]

ソースコードを追う作業が続いて大変ですが、もう少しです。devise_forメソッドは、mount_devise_token_auth_forメソッドの実行で呼び出されますが、mount_devise_token_auth_forメソッドはアプリのroutes.rbに記載があります。

# (1-1-1-1-1)
Rails.application.routes.draw do
  mount_devise_token_auth_for 'User', at: 'auth'
end

devise_token_authをインストールしたとき、mount_devise_token_auth_forでUserを指定しています。Userにdevise_token_authのルーティングをマウントしており、Userコントローラーにdevise機能を持たせています。

memo

ちなみに、(1-1-1-1-1)は下記のように書き換えることができます

# (1-1-1-1-1) コード修正
Rails.application.routes.draw do
  mount_devise_token_auth_for('User', at: 'auth')
end

関数 <半角スペース> <値1> <半角スペース> <値2> …となっている場合、railsでは

関数(値1, 値2, …)という風に引数として定義されます。

アプリのroutes.rbの中のコードは、初期化の時(つまりアプリの起動時)に呼ばれるので、長くなりましたが、ここがresource_classメソッドの中身を定義する上で必要なソースコードの根源となります。ここで今までの流れをおさらいします。

  • resource_class(1)にはmappings(1-1)が必要
  • Devise.add_mappings(1-1-1)によりmappings(1-1)が決まる
  • devise_forメソッド(1-1-1-1)の実行によりDevise.add_mappings(1-1-1)が呼び出される
  • mount_devise_token_auth_forメソッド(1-1-1-1-1)の実行によりdevise_for(1-1-1-1)が呼び出される
  • mount_devise_token_auth_forメソッド(1-1-1-1-1)はアプリのroutes.rbに記載あり。初期化時に呼び出される。

どこから呼び出されているかが分かったところで、resource_classに実際何が入っているのかを調査します!

resource_classには何が入る?

routes.rbでは、mount_devose_token_auth_forメソッドの第一引数に’User’、第二引数にat: ‘auth’を渡しています。

# (1-1-1-1-1)
Rails.application.routes.draw do
  mount_devise_token_auth_for 'User', at: 'auth'
end

よって、(1-1-1-1)ではresource: ‘User’、opts: {at: ‘auth’}となります。

#(1-1-1-1) devise_for(*resource)の定義
module ActionDispatch::Routing # 3行目
  class Mapper
    def mount_devise_token_auth_for(resource, opts)
     route = opts[:as] || resource.pluralize.underscore.gsub('/', '_') #20行目
        devise_for route.to_sym,  # 35行目
             class_name: resource,
             module: :devise,
             path: opts[:at].to_s,
             controllers: controllers,
             skip: opts[:skip] + [:omniauth_callbacks]

ここで、5行目について、a || bとしています。この構文はaに値があればa、なければbとなります。よって、今回はoptsのキーにasが含まれていないため、resource.pluralize.underscore.gsub(‘/’, ‘_’)が適用されます。↓にresource.pluralize.underscore.gsub(‘/’, ‘_’)の詳細をまとめました。

resource.pluralize.underscore.gsub(‘/’, ‘_’)の取得手順

・resourceはUserです

・pluralizeメソッドは、複数形に変換するため、User→Usersとなります。

・underscoreメソッドは、キャメルケースからスネークケースに変換するメソッドで、Users→usersとなります。

・gsub(‘/’, ‘_’)は、/を_に変換するものですが、ここでは適用されないので結果、usersになります。

以上より、上記の処理は、パス名に変換するものと想定されます。

そして、35行目では、to_symメソッドを使用しているため、usersは:usersに変換され、devise_forの引数として渡されます。

controllersには、controllersが指定されていますが、これは、mount_devise_token_auth_forでcontrollerを指定して上書きする際に適用されます。controllerを記載していない場合は、devise_token_auth上のcontrollerが使用されるようになっています。

ここで、(1-1-1-1)を書き換えます。

devise_for(:users ,class_name: 'User', module: :devise, path: 'auth',
 controllers: controllers, skip:[:omniauth_callbacks])

ここで(1-1-1)へ戻ります。

# (1-1-1) add_mappingメソッドの実施 
module ActionDispatch::Routing # 28行目
  class Mapper # 35行目
    def devise_for(*resources) # 226行目
      resources.each do |resource| # 242行目
          mapping = Devise.add_mapping(resource, options)

Devise.add_mapping関数に入る引数ですが、*resourcesは、可変長引数になります。:users、class_name、module….それぞれに対してDevise_add_mappingメソッドを実行しています。

そして、それぞれに対してDevise::Mapping.newメソッドが実行され、@@mappingsのハッシュにどんどん値が入っていきます。

# (1-1)mappingsの取得 
module Devise
  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メソッド(1-1-2)を確認すると

# (1-1-2) 54行目
def initialize(name, options) #:nodoc:
      @scoped_path = options[:as] ? "#{options[:as]}/#{name}" : name.to_s
      @singular = (options[:singular] || @scoped_path.tr('/', '_').singularize).to_sym

      @class_name = (options[:class_name] || name.to_s.classify).to_s
      @klass = Devise.ref(@class_name)

      @path = (options[:path] || name).to_s
      @path_prefix = options[:path_prefix]

      @sign_out_via = options[:sign_out_via] || Devise.sign_out_via
      @format = options[:format]

      @router_name = options[:router_name]

      default_failure_app(options)
      default_controllers(options)
      default_path_names(options)
      default_used_route(options)
      default_used_helpers(options)
    end

ここで、一旦(1)resource_classのコードに戻ってDevise.mappingsから欲しいプロパティを確認します。

# (1)resource_class 44行目
def resource_class(m = nil)
  if m
    mapping = Devise.mappings[m]
  else
    mapping = Devise.mappings[resource_name] || Devise.mappings.values.first
  end

  mapping.to
end

今回、mには引数が入っていないため、nilとなるのでelseが適用されます。ということで、Devise.mappingsのresource_nameが必要ですが、(1-1-2)にはありませんよね。よって、Devise.mappings.values.firstが使用されます。

ここで、Devise.mappings.values.firstを調べるのが本当はいいかもですが、さすがに疲れてきたし、railsのコンソールが使えるみたいなので値を見てみましょう。

返り値がmapping.toなので、toメソッドも一緒につけて実行します。

[1] pry(main)> Devise.mappings.values.first.to
=> User (call 'User.connection' to establish a connection)

resource_classには、deviseを設定したUserモデルが返りました!

# 102行目
@resource = resource_class.new(sign_up_params)

resource_classは、上記のように、newで新しくインスタンスを作成する処理が書かれていますね。ここのインスタンスは、devise_token_authが紐づくモデルが定義されているのですね!

まとめ

resource_classには、devise_token_authが紐づくモデルが定義されているということがわかりました!おそらくほとんどの人がUserだと思われます!

resource_classは以下の手順で取得できる様になります。

  • railsの初期化の動きで、ルート内のmount_devise_token_auth_forメソッドが呼ばれる
  • devise_forメソッドが呼ばれる
  • add_mappingメソッドが呼ばれ、mappingsが設定される
  • resource_class内のmappingが定義され、mapping.toが返される

コメント

タイトルとURLをコピーしました