Authlogic使ってるモデルで認証に関係ないnullableなemailカラム追加したらvalidationエラーが出る件

## 状況説明

ログインは Facebook アカウントの認証のみのつもり。
なので User モデルは実体がなく、ほかのモデルとのリレーションと、後は UserSession の管理だけの空っぽのモデル。

```rb:db/schema.rb
  create_table "users", force: true do |t|
    t.string   "persistence_token", null: false
    t.datetime "created_at",        null: false
    t.datetime "updated_at",        null: false
  end 
```

関連モデルもまだ作っていないので、本当にこれだけ。

```rb:app/models/user.rb
class User < ActiveRecord::Base
  acts_as_authentic
end
```

で、 "任意で構わないけど email を保存したい" という話になったので、 nullable な email カラムを追加。

```rb:db/schema.rb
  create_table "users", force: true do |t|
    t.string   "persistence_token", null: false
    t.string   "email"
    t.datetime "created_at",        null: false
    t.datetime "updated_at",        null: false
  end 
```

そうしたら email が空だとバリデーションエラーになって、保存できなくなった。

```irb
> User.create!
   (0.2ms)  BEGIN
  User Exists (0.3ms)  SELECT  1 AS one FROM "users" WHERE "users"."persistence_token" = '689...' LIMIT 1
   (0.2ms)  ROLLBACK
ActiveRecord::RecordInvalid: Validation failed: Email should look like an email address.
```

え、いやいやいやいや。
そんな validation 作ってないし！
nullable だし！
User モデルだって空っぽだし！

って混乱したんだけれども。
どうも Authlogic を使うための `acts_as_authentic` が怪しい。というか User モデルにはそれしかないし。

という調査内容と、回避方法についての話。
前置き完了。

これの調査した時は、 authlogic 3.4.2 でした。
Rails は 4.2.0 系になってたかな……(うろ覚え)。

## 原因

`authlogic/acts_as_authentic/base.rb` を見ると

```rb:authlogic/acts_as_authentic/base.rb
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Base
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Email
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::LoggedInStatus
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Login
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::MagicColumns
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Password
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PerishableToken
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PersistenceToken
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::RestfulAuthentication
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SessionMaintenance
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SingleAccessToken
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::ValidationsScope
```

なんかすごい勢いで include されてました。
`Authlogic::ActsAsAuthentic::Email` とかあるので、それを見てみる。

```rb:authlogic/acts_as_authentic/email.rb
      module Methods
        def self.included(klass)
          klass.class_eval do
            if validate_email_field && email_field
              validates_length_of email_field, validates_length_of_email_field_options
              validates_format_of email_field, validates_format_of_email_field_options
              validates_uniqueness_of email_field, validates_uniqueness_of_email_field_options
            end
          end
        end
      end
```

おまえたちか！

## 回避方法を探る

`authlogic/acts_as_authentic/base.rb` に `acts_as_authentic` メソッドがいる

```rb:authlogic/acts_as_authentic/base.rb
      module Config
        # This includes a lot of helpful methods for authenticating records which The Authlogic::Session module relies on.
        # To use it just do:
        #
        #   class User < ActiveRecord::Base
        #     acts_as_authentic
        #   end
        #
        # Configuration is easy:
        #
        #   acts_as_authentic do |c|
        #     c.my_configuration_option = my_value
        #   end
        #
        # See the various sub modules for the configuration they provide.
        def acts_as_authentic(unsupported_options = nil, &block)
          # Stop all configuration if the DB is not set up
          return if !db_setup?

          if !unsupported_options.nil?
            raise ArgumentError.new(
              "You are using the old v1.X.X configuration method for Authlogic. Instead of passing a hash of " +
              "configuration options to acts_as_authentic, pass a block: acts_as_authentic { |c| c.my_option = my_value }"
            )
          end

          yield self if block_given?
          acts_as_authentic_modules.each { |mod| include mod }
        end

        # (略)
```

`unsupported_options` ってあるから一瞬喜んだんだけど、これは古いバージョンの書き方で、今はブロックを使えよってことらしい。

`authlogic/acts_as_authentic/email.rb` を見てみると、こっちも Config を持っていて、個別に設定できる項目があるらしい。

```rb:authlogic/acts_as_authentic/email.rb
      # Configuration to modify how Authlogic handles the email field.
      module Config
        # The name of the field that stores email addresses.
        #
        # * <tt>Default:</tt> :email, if it exists
        # * <tt>Accepts:</tt> Symbol
        def email_field(value = nil)
          rw_config(:email_field, value, first_column_to_exist(nil, :email, :email_address))
        end
        alias_method :email_field=, :email_field

        # Toggles validating the email field or not.
        #
        # * <tt>Default:</tt> true
        # * <tt>Accepts:</tt> Boolean
        def validate_email_field(value = nil)
          rw_config(:validate_email_field, value, true)
        end
        alias_method :validate_email_field=, :validate_email_field

        # (略)
```

なるほど。
`email` とか `email_address` というカラムがあると、勝手に email 用のバリデーションしてくれると。
`email_field` に nil を指定しても、デフォルトのカラム名が使われてしまうので、今回のケースにそちらは仕えない。
(例えば `main_address` みたいなカラムに email を保存したいという時に、こちらの設定が使える)

`validate_email_field` はデフォルトが true になってるけれども、これが false になっていれば、今は必要ないバリデーションを動作させずに済みそう。

## 回避！

`acts_as_authentic` にブロックを渡して `validate_email_field` を `false` に指定する。

```rb:app/models/user.rb
 class User < ActiveRecord::Base
-  acts_as_authentic
+  acts_as_authentic do |config|
+    config.validate_email_field = false
+  end
```


これで。

```
pry> User.create!
   (0.1ms)  BEGIN
  User Exists (0.6ms)  SELECT  1 AS one FROM "users" WHERE "users"."persistence_token" = '301...' LIMIT 1
  SQL (0.5ms)  INSERT INTO "users" ("created_at", "persistence_token", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["created_at", "2014-10-03 03:52:22.984187"], ["persistence_token", "301..."], ["updated_at", "2014-10-03 03:52:22.984187"]]
   (0.3ms)  COMMIT
=> #<User:0x007ffbdfd1bb30
 id: 1,
 created_at: Fri, 03 Oct 2014 03:52:22 UTC +00:00,
 updated_at: Fri, 03 Oct 2014 03:52:22 UTC +00:00,
 persistence_token: "3011...",
 email: nil>
```

できた！