railsのカスタムバリデーション機能を使ってNGワード（不適切な内容）を防いでみる

NGワード機能を追加しようと考えていたところ、
あまり参考にできる記事が見つからなかったので、記事を書いてみました。

Qiitaで記事を書くのは初めてですので、拙い面もありますがご了承ください。

- やりたいこと
    - 不適切な言葉（下品な言葉など）を防ぎたい
    - htmlタグやurl,連続した文字("あああああ"など)を防ぎたい

- やったこと
    - 1. modelにカスタムバリデーションを定義する（非推奨）
    - 2. カスタムバリデータクラスを作る（推奨）

## テーブル構成
| テーブル名 | カラム名 |
|:-:|:-:|
|User   |name   |
|Micropost   | content  | 
|Comment   | content  | 

## 1,2共通の操作
### ブラックリストの作成（好きなように定義してください。）
```Yaml:config/blacklist.yml
---
- 不適切な単語1
- 不適切な単語2
- 不適切な単語3
```

#### 不適切な単語は.gitignoreに入れてgitに載せないようにしておきましょう。

```text:.gitignore
#以下を追加
/config/blacklist.yml
```



## 1. modelにカスタムバリデーションを定義する（非推奨）
まず、CommentのcontentとMicropostのcontentにNGワード機能を付けたいと思い、以下のようにカスタムバリデーションを定義してみました。

```Ruby:app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  def content_cannot_contain_blacklist_words
#blacklist.ymlから不適切な言葉を読み取る
    blacklist = YAML.load_file('./config/blacklist.yml')
#contentが空でなく、contentの中にblacklistの用語が含まれているかを追加
    if content.present? && blacklist.any?{ |word| content.include?(word) }
      errors.add(:contain_blacklist_words, ": 不適切な言葉は使用できません")
# エラー時「contain_blacklist_words　: 不適切な言葉は使用できません」と表示される。
    end
  end

  def content_cannot_contain_invalid_regex
# same_character_regex: 連続する五文字以上の語（"あああああ"）などを防ぐ
# url_regex: https(http)から始まるurlを防ぐ
# html_regex: htmlタグを防ぐ

# 正規表現のエスケープのため %r!正規表現!で囲む
    invalid_regex = { same_character_regex: %r!(.)\1{4,}!,
                      url_regex: %r!https?://[\w/:%#\$&\?\(\)~\.=\+\-]+!,
                      html_regex: %r!<(".*?"|'.*?'|[^'"])*?>!}
# invalid_regexをkey,valueとして取り出しvalue.match?(content)で正規表現と一致しているかを調べる。
    if content.present? && invalid_regex.any?{|key,value| value.match?(content)}
      errors.add(:contain_invalid_regex, ":　使うことのできない文字列が含まれています")
    end
  end

end
```

###Micropost.rbにvalidateを追加(validationsと間違えないように)

```Ruby:app/models/micropost.rb
class Micropost < ApplicationRecord
# 先ほどapplicationで定義した関数を追加
  validate :content_cannot_contain_blacklist_words
  validate :content_cannot_contain_invalid_regex
end
```
###Comment.rbにもvalidateを追加する

```Ruby:app/models/comment.rb
class Comment < ApplicationRecord
# 先ほどapplicationで定義した関数を追加
  validate :content_cannot_contain_blacklist_words
  validate :content_cannot_contain_invalid_regex
end
```


- このやりかたでの問題点
    - contentにしか適用できていない<br>（カラム名が変わると適用できない。例えばnameにvalidationを適応したい場合、content.present?の部分をname.present?などに変えたコードを用意しなければならないので、同じコードを繰り返してしまう。）
    - validate :関数名　を何度も書かなければならない。
    - そもそもどのカラムに適用してるのかがわかりにくい<br>(validates :content, ~ のように書きたい)

## 2. カスタムバリデータクラスを作る（推奨）
1の問題点を解決しようとするとカスタムバリデータクラスを作ればいいことを発見する

https://techracho.bpsinc.jp/hachi8833/2018_02_13/52286

https://railsguides.jp/active_record_validations.html

この通りにやってみる
### Validatorの定義
まずapp/validators/[validator名].rbを作る。
今回はinvalid_words_validator.rbとした。

```Ruby:app/validators/invalid_words_validator.rb
# InvalidWordsValidatorの部分は先ほど付けた名前をキャメルケースにしたものを使う。
class InvalidWordsValidator < ActiveModel::EachValidator

# record : テーブル名のこと(Micripostとか)
# attribute : カラム名（contentとか）
# value : フォームに入力された値（"おはよう","foobar"とか）
  def validate_each(record, attribute, value)
# blacklist.ymlから不適切な言葉を読み取る
    blacklist = YAML.load_file('./config/blacklist.yml')
# valueが空でないか、blacklistに入力された値が含まれているかを判断
    if value.present? && blacklist.any?{ |word| value.include?(word) }
# contain_blacklist_words "内容~"と出力したくなかったので第二引数を''としている。
#（あまりいい方法ではないかもしれません。）
# :contain_blacklist_wordsはja.ymlなどに定義しておくとよい。
      record.errors.add(:contain_blacklist_words,'')
    end

# same_character_regex: 連続する五文字以上の語（"あああああ"）などを防ぐ
# url_regex: https(http)から始まるurlを防ぐ
# html_regex: htmlタグを防ぐ

# 正規表現のエスケープのため %r!正規表現!で囲む

    invalid_regex = { same_character_regex: %r!(.)\1{4,}!,
                      url_regex: %r!https?://[\w/:%#\$&\?\(\)~\.=\+\-]+!,
                      html_regex: %r!<(".*?"|'.*?'|[^'"])*?>!}
# invalid_regexをinvalid_key,invalid_valueとして取り出しinvalid_value.match?(value)で正規表現と一致しているかを調べる。
    if value.present? && invalid_regex.any?{|invalid_key,invalid_value| invalid_value.match?(value)}
      record.errors.add(:contain_invalid_regex, '')
    end

  end

end

```

### 適用したいモデルに以下を定義する。
今回は例としてuser.rbのnameに定義してみる。

```Ruby:app/models/user.rb
# validates :カラム名,Validator定義した名前: true
validates :name,invalid_words: true
```

以上です。お疲れさまでした。

## 参考にしたサイト
　- [Railsガイド Active Record バリデーション](https://railsguides.jp/active_record_validations.html)
　- [Rails tips: カスタムバリデータクラスを作る（翻訳）](https://techracho.bpsinc.jp/hachi8833/2018_02_13/52286)
　- [Railsアプリでイタズラ投稿(NGワード)と格闘してみた話](https://qiita.com/y_h_tomo/items/be10099abf93893fee57)
　- [コメント機能等で特定のワード（悪口など）を制限することは可能ですか？](https://qiita.com/lebronkoukou/questions/ac3f7211335183da2eaf)