こんにちは!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ガイドを参考にしたのかと思われます。
気になるポイントを解説していきます。
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"
なるほど。人間が読みやすいようによしなに加工して文字列を返すメソッドのようでした。
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行ほどですが、カスタムバリデーションを作る時のいい見本になるのではないかと思いました。
勉強になりました。