【Rails】errors.addって何？

モデルでは特に考えずに`errors.add(:base, '名前の文字数オーバー')`とかしておけば良いかーみたいな風潮ありますよね:church:
これ

```.rb
> user = User.new
> user.errors
=> #<ActiveModel::Errors:0x00007fc0ed8b5a50 @base=#<User id: nil, name: nil, created_at: nil, updated_at: nil>, @messages={}, @details={}>
> user.errors.add(:base, '名前の文字数オーバー')
> user.errors.full_messages
=> ["名前の文字数オーバー"] 
```

そもそも`.add`ってなんなのさと、すぐraiseされるならわかるけど、何個も追加(add)できるの？？？😴

## errors.addの使い方一覧
結論から書きます。

```.rb
# シンプルな構文(どんなエラーなのか)
> user.errors.add(:base, '名前の文字数オーバー')
> user.errors.full_messages
 => ["名前の文字数オーバー"]

# 一般的な構文(どのカラムの、どんなエラーなのか)
> user.errors.add(:name, '文字数オーバー')
> user.errors.full_messages
=> ["Name 文字数オーバー"] 

# I18nを利用した構文
# （対象がないと translation missing）
## activerecord.errors.models.user.attributes.name.over_char_limit
## activerecord.errors.models.user.over_char_limit
## activerecord.errors.messages.over_char_limit
## errors.attributes.name.over_char_limit
## errors.messages.over_char_limit
> user.errors.add(:name, :over_char_limit)
> user.errors.full_messages
=> ["Name 文字数オーバー"] 

# すぐraiseしたい構文
> user.errors.add(:name, :over_char_limit, strict: true)
ActiveModel::StrictValidationFailed (Name 文字数オーバー)
# StrictValidationFailed < StandardError です

# 何の為にあるのか不明
> user.errors.add(:name, :over, message: '文字数オーバー')
> user.errors.full_messages
=> ["Name 文字数オーバー"] 
# .details のため？
> user.errors.details
=> {:name=>[{:error=>:over}]}
````

すぐraiseできるじゃん！
良い機会なので、ここから`errors`と`validations`の関係性を考えます。


ここから下は読まなくて良いです。

## railsを調べてみた。

`rails 5-1-stable`ブランチを見てます！

### errorsのmethodはどこから？
すぐ見つけた
[active_model/validations.rb](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/validations.rb)

```activemodel/lib/active_model/validations.rb
# Errors の引数でモデルのオブジェクトを渡してる
def errors
  @errors ||= Errors.new(self)
end
```

モデルは`ActiveRecord::Base`を継承しているので、辿っていくと以下のようにincludeされてる

```.rb
# activerecord/lib/active_record/base.rb
include Validations

# activerecord/lib/active_record/validations.rb
include ActiveModel::Validations
```

### .addのmethodはどうなってるの？
`normalize_message` → `I18n`で使えるように頑張ってパースしてた
raiseとかはここで制御してるんだね
[active_model/errors.rb](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/errors.rb)

```activemodel/lib/active_model/errors.rb
def add(attribute, message = :invalid, options = {})
  message = message.call if message.respond_to?(:call)
  detail  = normalize_detail(message, options)
  message = normalize_message(attribute, message, options)
  if exception = options[:strict]
    exception = ActiveModel::StrictValidationFailed if exception == true
    raise exception, full_message(attribute, message)
  end

  details[attribute.to_sym]  << detail
  messages[attribute.to_sym] << message
end
```

### .addしたけど、いつraiseされるの？
railsでは`.create!`も`.update!`も結果的に`.save!`が呼ばれます。
`perform_validations`の`valid?`でfalseになるとraiseされます。

```activerecord/lib/active_record/validations.rb
def save!(options = {})
  perform_validations(options) ? super : raise_validation_error
end

private

def raise_validation_error
  raise(RecordInvalid.new(self))
end

def perform_validations(options = {})
  options[:validate] == false || valid?(options[:context])
end
```

### RecordInvalidが呼び出されたら
errorsの配列のメッセージはここでjoinされてるんだね

```activerecord/lib/active_record/validations.rb
class RecordInvalid < ActiveRecordError
  def initialize(record = nil)
    if record
      @record = record
      errors = @record.errors.full_messages.join(", ")
      message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid")
    else
      message = "Record invalid"
    end
    super(message)
  end
end
```

以下の`ja.yml`が必要なので忘れないように！

```config/locales/models/ja.yml
ja:
  activerecord:
    errors:
      messages:
        record_invalid: "%{errors}"
```

### valid?では何をしてるの？
1. `errors.clear` : valid?以前に追加されたerrorsを削除
2. `run_validations!` : モデルに紐づいてる:validateを1つずつ実行してる
3. `errors.empty?` : errorsの配列にあればraise!

つまり、コンソール上で`errors.add`をしても何も起きないんです🙄(clearされる)

```activerecord/lib/active_record/validations.rb
def valid?(context = nil)
  context ||= default_validation_context
  output = super(context)
  errors.empty? && output
end
```

```activemodel/lib/active_model/validations.rb
def valid?(context = nil)
  current_context, self.validation_context = validation_context, context
  errors.clear
  run_validations!
ensure
  self.validation_context = current_context
end
```

`run_validations!`はちょっと難しかった
[active_support/callbacks.rb](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/callbacks.rb) の`run_callbacks`が呼ばれるんだけど、これが結構難しい
（結果的に、:validateを1つずつ実行してるだけなんだけどね）

解説は以下のqiitaにありました
[Railsのvalidationの実行過程を調べる](https://qiita.com/wakasa51/items/b3d65eac5cdad7787083)

#### どんな:validateが実行するのか知りたいなら

```.rb
# モデルに紐づいてる:validateの一覧
> user.__callbacks[:validate]
# 自作validatorの一覧
> user._validators
```

#### 自作validatorを作りたいなら
これは弊社でもおなじみのvalidatorの作り方です！
[オリジナルのバリデーションクラス：validates_with](https://morizyun.github.io/ruby/active-record-validation.html#%E3%82%AA%E3%83%AA%E3%82%B8%E3%83%8A%E3%83%AB%E3%81%AE%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%AF%E3%83%A9%E3%82%B9%EF%BC%9Avalidates-with)


## 所感
初めてrailsのソースコード読んでみたけど、結構読みやすかった！
でも`proc`とか`yield`とか意味は知ってるけど、普段使わない関数が急に出てくると困惑するし、急にわからなくなるなあ。

これを機に俺もrailsのソースコードを読み込んでみよう！と10分思ったが、めんどくさいし、タピオカ飲みたいので辞めた🧸