ActiveStorageのバリデーション

## はじめに
[Rails5.2から導入された](https://railsguides.jp/5_2_release_notes.html#active-storage)[Active Storage](https://railsguides.jp/active_storage_overview.html)は設定が簡単でとても導入しやすく、シンプルな機能を備えていて、少し使ってみた感じだとよい機能だと思っています。
ただ、実運用で使おうとすると、バリデーションの機能がなくて、惜しい感じです…。

そこで、自分が最低限欲しいと感じた、いくつかのバリデーションを作ったので紹介します。

[サンプルアプリケーションのコードはこちらです。](https://github.com/ken1flan/active_storage_validation_sample)
Rails5.2で書いていますが、ほぼ同じものを6.0に持ってきても動きます。

## 必須
入力フォームで、ファイルの添付を必須強制したいときはごく普通にあると思います。
ただ、普通のpresenceのバリデーションは使えなかったので、別途用意しました。

![localhost_3000_posts(PC(800x600)).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14380/4ee0449e-da6f-a663-9c1f-938ff8da8d0d.png)

### 設置例

`attached_file_presence`で設定できるようにしています。

```
class Post < ApplicationRecord
  has_one_attached :main_image
  has_many_attached :other_images

  validates :main_image, attached_file_presence: true
  validates :other_images, attached_file_presence: true
end
```

### [コード](https://github.com/ken1flan/active_storage_validation_sample/blob/main/app/validators/attached_file_presence_validator.rb)

ファイルが添付されているかどうか検査できる`attached?`を使います。

```ruby:app/validators/attached_file_presence_validator.rb
class AttachedFilePresenceValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors.add(attribute, :blank) unless value.attached?
  end
end
```

## ファイル数

`has_many_attached`を使うと、複数のファイルを添付できますが、個数の制限をしたいときがあると思います。

![localhost_3000_posts(PC(800x600)) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14380/c2195df9-d774-d38e-c419-14a47d436b3c.png)

### 設置例

`attached_file_number`と`maximum`オプションで最大個数を設定できるようにしています。

```
class Post < ApplicationRecord
  has_many_attached :other_images

  validates :other_images, attached_file_number: { maximum: 3 }
end
```

### [コード](https://github.com/ken1flan/active_storage_validation_sample/blob/main/app/validators/attached_file_number_validator.rb)

ファイル数はsizeで取れるので、それを使って検証しています。

```ruby:app/validators/attached_file_number_validator.rb
class AttachedFileNumberValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return true unless value.attached?

    file_number = value.size

    if (limit = options[:maximum]).present? && file_number > limit
      record.errors.add(attribute, :too_many_files, count: limit)
    end
    if (limit = options[:minimum]).present? && file_number < limit
      record.errors.add(attribute, :too_few_files, count: limit)
    end
  end
end
```

エラーメッセージで指定している、`too_many_files`や`too_few_files`は`config/locales`で設定しています。

```yaml:config/locales/ja.yml
ja:
  errors:
    messages:
      too_many_files: は%{count}個以内で入力してください
      too_few_files: は%{count}個以上で入力してください
```

## ファイルサイズ
ユーザに画像を登録できるようにすると、本格的なカメラで撮ったような高精細の画像が添付されてくることがしばしばありますが、リソース上の制約から受け取らないようにしたいときもあると思います。

![localhost_3000_posts(PC(800x600)) (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14380/b491079c-068c-7788-986f-63e0d38d5efe.png)

### 設置例

`attached_file_size`と`maximum`オプションで最大ファイルサイズを設定できるようにしています。

```
class Post < ApplicationRecord
  has_one_attached :main_image
  has_many_attached :other_images

  validates :main_image, attached_file_size: { maximum: 5.megabytes }
  validates :other_images, attached_file_size: { maximum: 5.megabytes }
end
```


### [コード](https://github.com/ken1flan/active_storage_validation_sample/blob/main/app/validators/attached_file_size_validator.rb)

ファイルのサイズを得る`attachement.byte_size`を使っています。
ファイルの単複で微妙に処理が変わるのが嫌で、単数のときは配列に詰めて複数と同じように処理できるようにしています。

```ruby:app/validators/attached_file_size_validator.rb
class AttachedFileSizeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return true unless value.attached?
    return true unless options&.dig(:maximum)

    maximum = options[:maximum]
    attachements = value.is_a?(ActiveStorage::Attached::Many) ? value.attachments : [value.attachment]
    if attachements.any? { |attachment| attachment.byte_size >= maximum }
      record.errors.add(attribute, :less_than, { count: maximum.to_s(:human_size) })
    end
  end
end
```

## ファイルタイプ
例えば画像を登録してもらうつもりのところに、間違ってテキストファイルを登録しないようになど、アップロードするファイルの形式を保存する前にチェックしたいことは多いと思います。

![localhost_3000_posts(PC(800x600)) (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14380/cb09817a-a4a8-37ca-41f6-604c687d5114.png)

### 設置例

`attached_file_type`と`pattern`オプションで最大ファイルサイズを設定できるようにしています。
`pattern`は正規表現で設定します。

```
class Post < ApplicationRecord
  has_one_attached :main_image
  has_many_attached :other_images

  validates :main_image, attached_file_type: { pattern: /^image\// }
  validates :other_images, attached_file_type: { pattern: /^image\// }
end
```

### [コード](https://github.com/ken1flan/active_storage_validation_sample/blob/main/app/validators/attached_file_type_validator.rb)

content_typeを指定のパターンに合致しているかチェックしています。
ファイルの単複で微妙に処理が変わるのが嫌で、単数のときは配列に詰めて複数と同じように処理できるようにしています。

```ruby:
class AttachedFileTypeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return true unless value.attached?
    return true unless options&.dig(:pattern)

    pattern = options[:pattern]
    attachments = value.is_a?(ActiveStorage::Attached::Many) ? value.attachments : [value.attachment]
    if attachments.any? { |attachment| !attachment.content_type.match?(pattern) }
      record.errors.add(attribute, :invalid_file_type)
    end
  end
end
```

エラーメッセージで指定している、`invalid_file_type`は`config/locales`で設定しています。

```yaml:config/locales/ja.yml
ja:
  errors:
    messages:
      invalid_file_type: は不正なファイル形式です
```

## Validatorのテスト
RSpecで書いたものがありますが、全部載せるととても長いので、[こちらを参照してください](https://github.com/ken1flan/active_storage_validation_sample/tree/main/spec/validators)。

ここでのポイントは、アプリケーションで実際に使っているテーブルを使ったモデルを定義して、そこにバリデーションを設置してテストに使うところです。
最初は使いそうなものをスタブやモックを使って作ろうとしていたのですが、ファイルの単複を扱うのでとても記述量が多くなり、わかりにくくなってしまったのでやめました。

## 最後に
バリデータもテストを書いたりすると、結構楽しいですね。これのおかげでActiveStorageと少し仲よくなれた気がします。
書きながらあれこれ探してたら、もっとスマートな[activestorage-validatorというGem](https://github.com/aki77/activestorage-validator)があったので、こちらを使ってもらってもいいかもしれません。

## 参照
- [Active Storage の概要](https://railsguides.jp/active_storage_overview.html)
- [activestorage-validator](https://github.com/aki77/activestorage-validator)