activestorage-validator gem のコードリーディング

こんにちは!kossyです!




さて、今回はActiveStorageでvalidationを行う際に用いるGem、activestorage-validatorのコードリーディングをしてみようと思います。



バージョン

activestorage-validator 0.1.3


コードを読む

40行程度のコードで簡潔にvalidationを実現していました。

module ActiveRecord
  module Validations
    class BlobValidator < ::ActiveModel::EachValidator
      def validate_each(record, attribute, values) # rubocop:disable Metrics/AbcSize
        return unless values.attached?

        Array(values).each do |value|
          if options[:size_range].present?
            if options[:size_range].min > value.blob.byte_size
              record.errors.add(attribute, :min_size_error, min_size: ActiveSupport::NumberHelper.number_to_human_size(options[:size_range].min))
            elsif options[:size_range].max < value.blob.byte_size
              record.errors.add(attribute, :max_size_error, max_size: ActiveSupport::NumberHelper.number_to_human_size(options[:size_range].max))
            end
          end

          unless valid_content_type?(value.blob)
            record.errors.add(attribute, :content_type)
          end
        end
      end

      private

        def valid_content_type?(blob)
          return true if options[:content_type].nil?

          case options[:content_type]
          when Regexp
            options[:content_type].match?(blob.content_type)
          when Array
            options[:content_type].include?(blob.content_type)
          when Symbol
            blob.public_send("#{options[:content_type]}?")
          else
            options[:content_type] == blob.content_type
          end
        end
    end
  end
end

パブリックメソッドとプライベートメソッドが1つずつの簡素なクラスになっていました。

いわゆるカスタムバリデータの実装で、Railsガイドを参考にしたのかと思われます。

railsguides.jp

気になるポイントを解説していきます。

return unless values.attached?

ActiveStorageを用いて処理をしたデータに生えたメソッドであるattached?を呼び出して、添付ファイルが存在しなければ処理を終了させるようにしています。

Array(values).each do |value|

この部分、なぜArrayクラスにvaluesを渡して処理をしているのかいまいちわかりませんでした。values.eachでもいいと思うのですが、、、

if options[:size_range].present?
  if options[:size_range].min > value.blob.byte_size
    record.errors.add(attribute, :min_size_error, min_size: ActiveSupport::NumberHelper.number_to_human_size(options[:size_range].min))
  elsif options[:size_range].max < value.blob.byte_size
    record.errors.add(attribute, :max_size_error, max_size: ActiveSupport::NumberHelper.number_to_human_size(options[:size_range].max))
  end
end

size_rangeオプションが渡されていれば、データのbytesizeに対してvalidationを実行しています。
オプションで設定した値よりも画像サイズが小さい場合、大きい場合それぞれで、recordのエラーオブジェクトにエラーを追加するコードになっています。

ActiveSupport::NumberHelper.number_to_human_sizeの返り値が気になったので、実行してみました。

$ options ={}

$ options[:size_range] = 1..5.megabytes

$ ActiveSupport::NumberHelper.number_to_human_size(options[:size_range].max)
=> "5MB"

なるほど。人間が読みやすいようによしなに加工して文字列を返すメソッドのようでした。

github.com

unless valid_content_type?(value.blob)
  record.errors.add(attribute, :content_type)
end

valid_content_type?(value.blob)はプライベートメソッドになっていたので、コードを見てみます。

def valid_content_type?(blob)
  return true if options[:content_type].nil?

  case options[:content_type]
  when Regexp
    options[:content_type].match?(blob.content_type)
  when Array
    options[:content_type].include?(blob.content_type)
  when Symbol
    blob.public_send("#{options[:content_type]}?")
  else
    options[:content_type] == blob.content_type
  end
end

options[:content_type]にはimage/pngやapplication/pdfのような文字列が渡ってくる想定で、もし指定がなければ true を返すようにしています。

options[:content_type]に正規表現クラスのインスタンスが渡された場合、Blobが正規表現にマッチするかどうかの結果を返し、
options[:content_type]に配列が渡された場合、Blobが配列で指定されたcontent_typeに含まれるかどうかを返し、
options[:content_type]にシンボルが渡された場合、Blobのcontent_typeがシンボルで渡されたcontent_typeかどうかをpublic_sendメソッドを使って結果を返しています。



もう一度unless文を見てみます。

unless valid_content_type?(value.blob)
  record.errors.add(attribute, :content_type)
end

valid_content_type?がfalseだった場合、recordのエラーオブジェクトにエラー文を追加しています。




40行ほどですが、カスタムバリデーションを作る時のいい見本になるのではないかと思いました。




勉強になりました。



大いに参考にさせていただいたサイト

ActiveStorageのバリデーション - Qiita