Skip to content

Instantly share code, notes, and snippets.

@soma-git-practice
Last active December 13, 2024 12:24
Show Gist options
  • Save soma-git-practice/3b676062ef0b0319b60da325771b86bd to your computer and use it in GitHub Desktop.
Save soma-git-practice/3b676062ef0b0319b60da325771b86bd to your computer and use it in GitHub Desktop.
Devise::Models::Authenticatable#downcase_keys

lib/devise/models/authenticatable.rb:198

downcase_keysメソッドを読む。

# test/models/authenticatable_test.rb:51
# 追記

class AuthenticatableTest < ActiveSupport::TestCase
  ...
  test 'downcase_keysを試す' do
    user = User.create!(email: 'EXAMPLE@example.com', password: '1234567')
    assert_equal user.email, 'example@example.com'
  end
end
$ bin/test test/models/authenticatable_test.rb:51
# app/config/boot.rb
require 'bundler/setup'
# lib/devise.rb
module Devise
  mattr_accessor :case_insensitive_keys
  @@case_insensitive_keys = [:email]
end

require 'devise/models' # 538
# lib/devise/models.rb
module Devise
  module Models
    ...
  end
end
# config/initializers/devise.rb ( lib/generators/templates/devise.rb )
Devise.setup do |config|
  require 'devise/orm/active_record' # 39
  
  config.case_insensitive_keys = %i(email gmail) # 61 (動作確認用に置換した)
end
# lib/devise/orm/active_record.rb
ActiveSupport.on_load(:active_record) do
  extend Devise::Models
end
class User < ApplicationRecord < ActiveRecord::Base
  devise :database_authenticatable
end
# lib/devise/models.rb:79
module Devise
  module Models
    def devise(*modules)
      ...
        include Devise::Models::Authenticatable
      ...
    end
  end
end
# lib/devise/models/authenticatable.rb:67
module Devise
  module Models
    module Authenticatable
      extend ActiveSupport::Concern
      ...
    
      included do
        ...
        before_validation :downcase_keys
        ...
      end
    end
  end
end
# lib/devise/models/authenticatable.rb:198
module Devise
  module Models
    module Authenticatable
      def downcase_keys
        self.class.case_insensitive_keys.each { |k| apply_to_attribute_or_variable(k, :downcase) }
      end
    end
  end
end
# lib/devise/models/authenticatable.rb:198

user.class.case_insensitive_keys.each { |k| apply_to_attribute_or_variable(k, :downcase) }

User.case_insensitive_keys.each { |k| apply_to_attribute_or_variable(k, :downcase) }
# lib/devise/models/authenticatable.rb:221
module Devise
  module Models
    module Authenticatable
      extend ActiveSupport::Concern
      ...
      
      module ClassMethods
        Devise::Models.config(self, ... , :case_insensitive_keys, ...)
      end
    end
  end
end
# lib/devise/models.rb:31
module Devise
  module Models
    def self.config(mod, *accessors) #:nodoc:
      # 関係ない class << mod; attr_accessor :available_configs; end
      # 関係ない mod.available_configs = accessors

      accessors.each do |accessor|
        mod.class_eval <<-METHOD, __FILE__, __LINE__ + 1
          def #{accessor}
            if defined?(@#{accessor})
              @#{accessor}
            elsif superclass.respond_to?(:#{accessor})
              superclass.#{accessor}
            else
              Devise.#{accessor}
            end
          end

          def #{accessor}=(value)
            @#{accessor} = value
          end
        METHOD
      end
    end
  end
end
# ( lib/devise/models/authenticatable.rb:198 )
module Devise
  module Models
    module Authenticatable
      def downcase_keys
        [:email, :gmail].each { |k| apply_to_attribute_or_variable(k, :downcase) }
      end
    end
  end
end
# lib/devise/models/authenticatable.rb:206
module Devise
  module Models
    module Authenticatable
      def apply_to_attribute_or_variable(attr, method)
        if self[attr]
          self[attr] = self[attr].try(method)

        # Use respond_to? here to avoid a regression where globally
        # configured strip_whitespace_keys or case_insensitive_keys were
        # attempting to strip or downcase when a model didn't have the
        # globally configured key.
        # 追記 これは仮想の属性用の分岐
        elsif respond_to?(attr) && respond_to?("#{attr}=")
          new_value = send(attr).try(method)
          send("#{attr}=", new_value)
        end
      end
    end
  end
end

lib/devise/models/authenticatable.rb:221

DeviseのReadMe読んだところこの箇所を深ぼったことになった。(そのつもりは最初無かった。)

https://github.com/heartcombo/devise?tab=readme-ov-file#configuring-models

分かったこと

Devise::Models::Authenticatable::ClassMethods内でDevise::Models.configを呼び出すと以下が起こる

  • Devise::Models::Authenticatable::ClassMethodsにクラスメソッド(#available_configs)が追加される。

  • Devise::Models::Authenticatable::ClassMethodsのインスタンス変数(@available_configs)にDevise::Models.configの第二引数以降が代入される。

  • Devise::Models::Authenticatable::ClassMethods#available_configsはDevise::Models.devise中で使われる。

  • Devise::Models.configの第二引数以降各々を元にしたクラスメソッドが、モデルクラスに追加される。

    • モデルクラスでself.case_insensitive_keys = [:hoge] と書かない場合、モデルクラス.case_insensitive_keysを呼び出すと、config/initializers/devise.rb > lib/devise.rb ので設定している値を取得する。
    • モデルクラスでself.case_insensitive_keys = [:hoge] と書いた場合、モデルクラス.case_insensitive_keysを呼び出すと、[:hoge]を取得する。
    • スーパークラスがある時、スーパークラス.case_insensitive_keysの結果を取得する。

たぶん

Devise全体ではなくモデルごとに設定加えたい場合、 これはDevise風の書き方ではない。

class User
  devise :database_authenticatable
  self.case_insensitive_keys = [:hoge]
end

これがDevise風の書き方。

class User
  devise :database_authenticatable, case_insensitive_keys: [:email, :email_confirmation]
end

動作確認で使ったコード

# frozen_string_literal: true

source "https://rubygems.org"

gem 'irb'
gem 'minitest', '~> 5.12', '>= 5.12.2'
gem 'activesupport', '7.0.8'
require 'irb'
require 'minitest/autorun'
require 'active_support/concern'
require 'active_support/core_ext/module/attribute_accessors'

# ** 目標 **
# 
#  Deviseを読んでいる時にClassMethodsの中でクラスメソッドを呼んでいるコードに出会った。
#  何をしているのか知りたいから読む。
#  https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/lib/devise/models/authenticatable.rb#L220-L223

# https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/lib/devise.rb#L89-L91
module Devise
  mattr_accessor :request_keys
  @@request_keys = [:devise_value]

  mattr_accessor :case_insensitive_keys
  @@case_insensitive_keys = [:devise_value]

  mattr_accessor :hoge
  @@case_insensitive_keys = [:hoge]
end

# https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/lib/devise/models.rb#L15-L52
module Devise
  module Models
    def self.config(mod, *accessors)
      def mod.available_configs
        @available_configs
      end
      def mod.available_configs=(val)
        @available_configs = val
      end
      mod.available_configs = accessors
  
      accessors.each do |accessor|
        mod.class_eval <<-METHOD, __FILE__, __LINE__ + 1
          def #{accessor}
            if defined?(@#{accessor})
              @#{accessor}
            elsif superclass.respond_to?(:#{accessor})
              superclass.#{accessor}
            else
              Devise.#{accessor}
            end
          end
  
          def #{accessor}=(value)
            @#{accessor} = value
          end
        METHOD
      end
    end
  end
end

# https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/lib/devise/models/authenticatable.rb#L220-L223
module Devise
  module Models
    module Authenticatable
      extend ActiveSupport::Concern

      module ClassMethods
        Devise::Models.config(self, :request_keys, :case_insensitive_keys, :hoge)
      end
    end
  end
end

# https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/lib/devise/models.rb#L88
class User
  include Devise::Models::Authenticatable
  self.case_insensitive_keys = [:gmail]
end

class Qser < User
  self.hoge = [:im_happy]
end

describe 'Authenticatable::ClassMethods' do
  it 'は、パブリックメソッドに available_configs を持つ' do
    _(Devise::Models::Authenticatable::ClassMethods.public_methods.include?(:available_configs)).must_equal true
  end

  it 'は、パブリックメソッドに available_configs= を持つ' do
    _(Devise::Models::Authenticatable::ClassMethods.public_methods.include?(:available_configs=)).must_equal true
  end

  it 'は、インスタンス変数 @available_configs に値を持っている' do
    _(Devise::Models::Authenticatable::ClassMethods.instance_variable_get(:@available_configs)).must_equal %i(request_keys case_insensitive_keys hoge)
  end


  it 'は、インスタンスメソッドに case_insensitive_keys を持つ' do
    _(Devise::Models::Authenticatable::ClassMethods.instance_methods.include?(:case_insensitive_keys)).must_equal true
  end

  it 'は、インスタンスメソッドに case_insensitive_keys= を持つ' do
    _(Devise::Models::Authenticatable::ClassMethods.instance_methods.include?(:case_insensitive_keys=)).must_equal true
  end
end

describe 'User' do
  it "は、Authenticatable::ClassMethodsモジュールがextendされる" do
    _(User.singleton_class.include?(Devise::Models::Authenticatable::ClassMethods)).must_equal true
  end


  it 'は、パブリックメソッドに case_insensitive_keys を持つ' do
    _(User.public_methods.include?(:case_insensitive_keys)).must_equal true
  end

  it 'は、パブリックメソッドに case_insensitive_keys= を持つ' do
    _(User.public_methods.include?(:case_insensitive_keys=)).must_equal true
  end

  describe '#request_keys' do
    it 'モデルクラスで設定しない場合Devise共通の値を取得することになる' do
      _(User.request_keys).must_equal Devise.request_keys
    end
  end

  describe '#case_insensitive_keys' do
    it 'モデルクラスで設定する場合モデルクラス自体のインスタンス変数の値を取得することになる' do
      _(User.case_insensitive_keys).must_equal [:gmail]
    end
  end
end

describe Qser do
  describe '#case_insensitive_keys' do
    it 'モデルクラスで設定しない場合スーパークラス自体のインスタンス変数の値を取得することになる' do
      _(Qser.case_insensitive_keys).must_equal User.case_insensitive_keys
    end

    it 'モデルクラスで設定する場合モデルクラス自体のインスタンス変数の値を取得することになる' do
      _(Qser.hoge).must_equal [:im_happy]
    end
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment