[Rails]改行が2文字でカウントされてバリデーションが正しく動作しない件（カスタムバリデータ（validate_eachメソッド）で解決）

タイトルの通りです。

# 結論
- `ActiveModel::EachValidator`の派生クラスとして`○○Validator`を作る
- その中で改行の文字数カウントを修正してからバリデーション判定

早速ですが具体例を見てみましょう。

# 現象

とても簡略化したお問い合わせフォームを用意しました。
文字数制限は50文字とします。
(ちなみにその投稿画面には、javascriptで文字数をカウントして表示しているとします。)

```ruby:view/contacts/new.html.erb
<%= form_with url: contacts_path, method: :post, local: true do |f| %>
  <%= f.text_area :message %>
  <%= f.submit "送信する" %>
<% end %>
```

必須かつ50文字を超えたらエラーメッセージが表示されます。

```ruby:model/contact.rb
class Contact < ApplicationRecord
  validate :message, presence: true, length: { maximum: 50, message: '%{count}文字以内で入力してください' }
  # 〜（中略）〜
end
```

この状態でユーザーが以下のように送信したらどうなるでしょうか。

```text
1234567890↵       （↵ = 改行）
1234567890↵
1234567890↵
1234567890↵
123456
```

46文字 + 改行４つ = HTMLの50文字
要件を満たしていますが…

`エラーメッセージ`
`「50文字以内で入力してください」`

ユーザー「なんでや！」

# 追求

paramsで中を見てみる

```terminal
[1] pry(#<ContactsController>)> params
=> <ActionController::Parameters {"utf8"=>"✓",
   "message"=>"1234567890\r\n1234567890\r\n1234567890\r\n1234567890\r\n123456",
   〜（中略）〜 }>
```

改行が`\r\n`となっている！？

# 原因

改行が`\r\n`と2文字で表現されていることによるものでした。

なので今回は改行を1文字としてカウントさせて改めて文字数制限に引っかかるかどうかを、
**カスタムバリデータのvalidate_eachメソッド**で作成します。

#実装

```ruby:models/contact.rb
class Contact < ApplicationRecord
  # 変更前
  validate :message, presence: true, length: { maximum: 50, message: '%{count}文字以内で入力してください' }
  # 変更後
  validate :message, presence: true, correct_line_break: true
end
```

`length`パラメーターの代わりに`corrent_line_break`というカスタムバリデータを付与しています。
　

```ruby:models/validators/correct_line_break_validator.rb
class CorrectLineBreakValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    # 改行以外の文字数 （「\r\n」以外の文字数をcountメソッドで数える）
    text_length = value&.count("^\r\n") || 0
    # 改行の回数 （「\r\n」という塊の数だけscanメソッドで配列にし、その長さをlengthメソッドで求めている）
    break_length = value&.scan("\r\n")&.length || 0
    # 文字数 + 改行の回数
    correct_text_length = text_length + break_length
    # 50超ならエラーメッセージ追加
    record.errors.add(attribute, "50文字以内で入力してください") if correct_text_length > 50
  end
end
```

ロジックはコメントを見ていただければわかるかと思います。

`undefined method 'count' for nil:NilClass`対策でつけている
`&.`についてはこちらを参考にしてください。
[Ruby の &. と #try の違い]
(http://secret-garden.hatenablog.com/entry/2016/09/02/000000)

### 使い方

- `app/models`配下にファイルを作成する。
- `ActiveModel::EachValidator`を継承する。
- `def validate_each(record, attribute, value)`を定義する
- 引数の意味は
  - `record`は検証対象のオブジェクト名（Contact）
  - `attribute`検証対象のフィールド名（:message）
  - `value`検証対象の値（"1234567890\r\n......123456"）

### 注意点

命名ルールとして、

- **モデルで指定するカスタムバリデータ名**（`correct_line_break`: true）
- 「models/validators/」配下に作成する**○○_validator.rbのファイル名**（models/validators/`correct_line_break`_validator.rb）
- **クラス名**（class `CorrectLineBreak`Validator）

は同じである必要があります。
上２つはスネークケース、クラス名はパスカルケースですね。

これで意図したバリデーション検証が可能となりました。

# 発展

50文字という静的な値の設定は、他の箇所でも使いたい場合に適していません。
動的に文字数を変更するため、パラメータを渡す方法もあります。

```ruby:models/contact.rb
class Contact < ApplicationRecord
  # 変更前
  validate :message, presence: true, correct_line_break: true
  # 変更後
  validate :message, presence: true, correct_line_break: { maximum: 100 }
end
```

```ruby:correct_line_break_validator.rb
class CorrectLineBreakValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    text_length = value&.count("^\r\n") || 0
    break_length = value&.scan("\r\n")&.length || 0
    correct_text_length = text_length + break_length
    record.errors.add(attribute, "#{options[:maximum]}文字以内で入力してください") if correct_text_length > options[:maximum]
  end
end
```

パラメータは`options[:maximum]`で取得が可能です。

# 参考資料
[Ruby on Rails 5アプリケーションプログラミング](https://www.amazon.co.jp/Ruby-Rails-5%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E5%B1%B1%E7%94%B0-%E7%A5%A5%E5%AF%9B/dp/4774188832)
[【Rails】カスタムバリデータの使い方](https://www.task-notes.com/entry/20160918/1474210236)
[Railsの文字数のバリデーション問題を解決する 改行文字が2文字と換算されてしまう場合の対処方法](https://kyoto-diary.com/rails-length-validation/)