【Rails】Fromオブジェクトパターンを使用したActiveStorageの拡張子バリデーションを設定する方法

大阪でRailsを中心に学習している薬剤師エンジニア（初学者）こと、ヨマ([@yoma_2003](https://twitter.com/yoma_2003))です！  
Fromオブジェクトパターンを使用したActiveStorageの拡張子バリデーションを設定する方法をまとめます。

※おことわり※  
断定口調でまとめますが、初学者であるため間違い等あればご指摘頂けると嬉しいです。

# はじめに

### やりたいこと

以下の様にPostモデルとCommentモデルに同時にデータを登録するため、PostCommentモデルというFormオブジェクトを作成した時、
**PostCommentモデル（Formオブジェクト）において、Postモデルで保存する画像データに拡張子のバリデーションを設定したい。**


![formobj.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711042/32c4781c-d08f-8436-269a-7b69bb817193.png)

```ruby:post_comment.rb
class PostComment
  include ActiveModel::Model
  attr_accessor :post_name, :image, :comment_name, :content

  def save
    post = Post.create(name: post_name, image: image)
    Comment.create(name: comment_name, content: content, post_id: post.id)
  end
end
```

```ruby:post.rb
class Post < ApplicationRecord
  has_one_attached :image
  has_many :comments
end
```
```ruby:comment.rb
class Comment < ApplicationRecord
  belongs_to :post
end
```


# 結論

PostCommentモデル（Formオブジェクト）に以下の様にバリデーションを自作する。

```ruby:post_comment.rb
validate :post_image_validation

def post_image_validation
  if !self.image.content_type.in?(%('image/jpeg image/png'))
    self.errors.add(:image, 'は JPEG 形式または PNG 形式のみ選択してください')
  end
end
```

# 解説

## 1. バリデーションを自作する方法

ActiveStorageにはバリデーション機能が存在せず、以下の様に自作します。
```ruby:モデル内
validate　:post_image_validation

def post_image_validation
  #バリデーション処理を定義
end
```

## 2. モデルから作成されるインスタンスの画像データオブジェクトの違いについて
 モデルから作成されるインスタンスの画像データには`content_type`カラムというファイル形式を格納する場所があり、ここに指定のファイル形式が含まれているかどうかを検証しますが、「ActiveStorageとアソシエーションを組む通常のモデルのインスタンス」と「Formオブジェクトのインスタンス」では作成される画像データの**オブジェクト形式が異なり、格納されている場所も異なります。**


### 投稿データをPostモデルのみに保存する場合

![model.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711042/b793910c-a4f1-2408-1432-e048640e841c.png)

※PostモデルはActiveStorageとアソシエーションを組む、通常のモデル

**作成された画像データ**
→`ActiveStorageクラスのオブジェクト`として**Postモデルインスタンスのimageカラム内のblobカラム**に格納されます。
```
pry(#<PostsController>)> @post.image.blob

=> #<ActiveStorage::Blob:0x00007fa8ab1b5458
   id: nil,
   key: nil,
   filename: "test.png",
   content_type: "image/png",
   metadata: {"identified"=>true},
   byte_size: 5000,
   checksum: "・・・",
   created_at: nil>
```


Postモデルにバリデーションを設定すればよいので、以下の様にバリデーションを設定します。

```ruby:post.rb
  validate :post_image_validation

  def post_image_validation
    if !self.image.blob.content_type.in?(%('image/jpeg image/png'))
      self.errors.add(:image, 'は JPEG 形式または PNG 形式のみ選択してください')
    end
  end
```

### Formオブジェクトを経由してPostモデルに保存する場合　（今回の場合）
![formobj.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711042/32c4781c-d08f-8436-269a-7b69bb817193.png)

※PostCommentモデルはFormオブジェクト

**作成された画像データ**
→`ActionDispatchクラスのオブジェクト`として**PostCommentモデルインスタンスのimageカラム**に格納されます。

```
pry(#<PostsController>)> @post_comment.image

=> #<ActionDispatch::Http::UploadedFile:0x00007fa8ab1ed290
   @content_type="image/png",
   @headers="Content-Disposition: form-data; name=\"post_comment[image]\"; filename=\"test.png\"\r\nContent-Type: image/png\r\n",
   @original_filename="test.png",
   @tempfile=#<File:/var/folders/mf/000000000000/T/RackMultipart20220816-6841-0000.png>>
```

※ActiveStorageとのアソシエーションを定義していないPostCommentモデルでは、アップロードされた画像データはActionDispatchオブジェクトとしてインスタンス内にデータが格納されます。

PostCommentモデルにバリデーションを設定すればよいので、以下の様にバリデーションを設定します。

```ruby:post_comment.rb
validate :post_image_validation

def post_image_validation
  if !self.image.content_type.in?(%('image/jpeg image/png'))
    self.errors.add(:image, 'は JPEG 形式または PNG 形式のみ選択してください')
  end
end
```

# おわりに

今回、ActiveStorageのバリデーションに関する記事はいくつか見つけられましたが、Formオブジェクトでのバリデーションに関する記事はあまりみつからなかったためまとめてみました。
正直、タイトルはギリギリ語弊がありそうな感じがします、、、

# 参考

https://qiita.com/xusaku_/items/36a61e35b6cd863bbf9d

https://api.rubyonrails.org/v6.0.5/classes/ActionDispatch/Http/UploadedFile.html

https://api.rubyonrails.org/v7.0/classes/ActiveStorage/Blob.html