diff --git a/.gitignore b/.gitignore index 504203ac..b06d9c26 100755 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ test/dummy/storage/ test/dummy/tmp/ test/dummy/.byebug_history *.gem +/.idea diff --git a/README.md b/README.md index 85fb074f..d2b61866 100755 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This gems doing it for you. Just use `attached: true` or `content_type: 'image/p * validates number of uploaded files (min/max required) * validates aspect ratio (if square, portrait, landscape, is_16_9, ...) * custom error messages +* allow procs for dynamic determination of values ## Usage @@ -124,6 +125,18 @@ class User < ApplicationRecord end ``` +- Proc Usage: + +Procs can be used instead of values in all the above examples. They will be called on every validation. +```ruby +class User < ApplicationRecord + has_many_attached :proc_files + + validates :proc_files, limit: { max: -> (record) { record.admin? ? 100 : 10 } } +end + +``` + ## Internationalization (I18n) Active Storage Validations uses I18n for error messages. For this, add these keys in your translation file: diff --git a/lib/active_storage_validations.rb b/lib/active_storage_validations.rb index 275bcae2..12c4e3b0 100644 --- a/lib/active_storage_validations.rb +++ b/lib/active_storage_validations.rb @@ -2,6 +2,7 @@ require 'active_storage_validations/railtie' require 'active_storage_validations/engine' +require 'active_storage_validations/option_proc_unfolding' require 'active_storage_validations/attached_validator' require 'active_storage_validations/content_type_validator' require 'active_storage_validations/size_validator' diff --git a/lib/active_storage_validations/aspect_ratio_validator.rb b/lib/active_storage_validations/aspect_ratio_validator.rb index 6193f9f8..37288945 100644 --- a/lib/active_storage_validations/aspect_ratio_validator.rb +++ b/lib/active_storage_validations/aspect_ratio_validator.rb @@ -4,17 +4,14 @@ module ActiveStorageValidations class AspectRatioValidator < ActiveModel::EachValidator # :nodoc + include OptionProcUnfolding + AVAILABLE_CHECKS = %i[with].freeze PRECISION = 3 - def initialize(options) - super(options) - end - - def check_validity! return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) } - raise ArgumentError, 'You must pass "aspect_ratio: :OPTION" option to the validator' + raise ArgumentError, 'You must pass :with to the validator' end if Rails.gem_version >= Gem::Version.new('6.0.0') @@ -55,26 +52,27 @@ def validate_each(record, attribute, _value) def is_valid?(record, attribute, metadata) + flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS) if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0 - add_error(record, attribute, options[:message].presence || :image_metadata_missing) + add_error(record, attribute, :image_metadata_missing, flat_options[:with]) return false end - case options[:with] + case flat_options[:with] when :square return true if metadata[:width] == metadata[:height] - add_error(record, attribute, :aspect_ratio_not_square) + add_error(record, attribute, :aspect_ratio_not_square, flat_options[:with]) when :portrait return true if metadata[:height] > metadata[:width] - add_error(record, attribute, :aspect_ratio_not_portrait) + add_error(record, attribute, :aspect_ratio_not_portrait, flat_options[:with]) when :landscape return true if metadata[:width] > metadata[:height] - add_error(record, attribute, :aspect_ratio_not_landscape) + add_error(record, attribute, :aspect_ratio_not_landscape, flat_options[:with]) else - if options[:with] =~ /is\_(\d*)\_(\d*)/ + if flat_options[:with] =~ /is_(\d*)_(\d*)/ x = $1.to_i y = $2.to_i @@ -82,17 +80,17 @@ def is_valid?(record, attribute, metadata) add_error(record, attribute, :aspect_ratio_is_not, "#{x}x#{y}") else - add_error(record, attribute, :aspect_ratio_unknown) + add_error(record, attribute, :aspect_ratio_unknown, flat_options[:with]) end end false end - def add_error(record, attribute, type, interpolate = options[:with]) - key = options[:message].presence || type - return if record.errors.added?(attribute, key) - record.errors.add(attribute, key, aspect_ratio: interpolate) + def add_error(record, attribute, default_message, interpolate) + message = options[:message].presence || default_message + return if record.errors.added?(attribute, message) + record.errors.add(attribute, message, aspect_ratio: interpolate) end end diff --git a/lib/active_storage_validations/content_type_validator.rb b/lib/active_storage_validations/content_type_validator.rb index 544fa111..073c99e7 100644 --- a/lib/active_storage_validations/content_type_validator.rb +++ b/lib/active_storage_validations/content_type_validator.rb @@ -2,16 +2,23 @@ module ActiveStorageValidations class ContentTypeValidator < ActiveModel::EachValidator # :nodoc: + include OptionProcUnfolding + + AVAILABLE_CHECKS = %i[with in].freeze + def validate_each(record, attribute, _value) - return true if !record.send(attribute).attached? || types.empty? + return true unless record.send(attribute).attached? + types = authorized_types(record) + return true if types.empty? + files = Array.wrap(record.send(attribute)) - errors_options = { authorized_types: types_to_human_format } + errors_options = { authorized_types: types_to_human_format(types) } errors_options[:message] = options[:message] if options[:message].present? files.each do |file| - next if is_valid?(file) + next if is_valid?(file, types) errors_options[:content_type] = content_type(file) record.errors.add(attribute, :content_type_invalid, **errors_options) @@ -19,10 +26,9 @@ def validate_each(record, attribute, _value) end end - def types - return @types if defined? @types - - @types = (Array.wrap(options[:with]) + Array.wrap(options[:in])).compact.map do |type| + def authorized_types(record) + flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS) + (Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in])).compact.map do |type| if type.is_a?(Regexp) type else @@ -31,7 +37,7 @@ def types end end - def types_to_human_format + def types_to_human_format(types) types .map { |type| type.to_s.split('/').last.upcase } .join(', ') @@ -41,7 +47,7 @@ def content_type(file) file.blob.present? && file.blob.content_type end - def is_valid?(file) + def is_valid?(file, types) file_type = content_type(file) types.any? do |type| type == file_type || (type.is_a?(Regexp) && type.match?(file_type.to_s)) diff --git a/lib/active_storage_validations/dimension_validator.rb b/lib/active_storage_validations/dimension_validator.rb index a04c616c..a42fde49 100644 --- a/lib/active_storage_validations/dimension_validator.rb +++ b/lib/active_storage_validations/dimension_validator.rb @@ -4,25 +4,30 @@ module ActiveStorageValidations class DimensionValidator < ActiveModel::EachValidator # :nodoc + include OptionProcUnfolding + AVAILABLE_CHECKS = %i[width height min max].freeze - def initialize(options) + def process_options(record) + flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS) + [:width, :height].each do |length| - if options[length] and options[length].is_a?(Hash) - if range = options[length][:in] + if flat_options[length] and flat_options[length].is_a?(Hash) + if (range = flat_options[length][:in]) raise ArgumentError, ":in must be a Range" unless range.is_a?(Range) - options[length][:min], options[length][:max] = range.min, range.max + flat_options[length][:min], flat_options[length][:max] = range.min, range.max end end end [:min, :max].each do |dim| - if range = options[dim] + if (range = flat_options[dim]) raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range) - options[:width] = { dim => range.first } - options[:height] = { dim => range.last } + flat_options[:width] = { dim => range.first } + flat_options[:height] = { dim => range.last } end end - super + + flat_options end @@ -64,26 +69,27 @@ def validate_each(record, attribute, _value) def is_valid?(record, attribute, file_metadata) + flat_options = process_options(record) # Validation fails unless file metadata contains valid width and height. if file_metadata[:width].to_i <= 0 || file_metadata[:height].to_i <= 0 - add_error(record, attribute, options[:message].presence || :image_metadata_missing) + add_error(record, attribute, :image_metadata_missing) return false end # Validation based on checks :min and :max (:min, :max has higher priority to :width, :height). - if options[:min] || options[:max] - if options[:min] && ( - (options[:width][:min] && file_metadata[:width] < options[:width][:min]) || - (options[:height][:min] && file_metadata[:height] < options[:height][:min]) + if flat_options[:min] || flat_options[:max] + if flat_options[:min] && ( + (flat_options[:width][:min] && file_metadata[:width] < flat_options[:width][:min]) || + (flat_options[:height][:min] && file_metadata[:height] < flat_options[:height][:min]) ) - add_error(record, attribute, options[:message].presence || :"dimension_min_inclusion", width: options[:width][:min], height: options[:height][:min]) + add_error(record, attribute, :dimension_min_inclusion, width: flat_options[:width][:min], height: flat_options[:height][:min]) return false end - if options[:max] && ( - (options[:width][:max] && file_metadata[:width] > options[:width][:max]) || - (options[:height][:max] && file_metadata[:height] > options[:height][:max]) + if flat_options[:max] && ( + (flat_options[:width][:max] && file_metadata[:width] > flat_options[:width][:max]) || + (flat_options[:height][:max] && file_metadata[:height] > flat_options[:height][:max]) ) - add_error(record, attribute, options[:message].presence || :"dimension_max_inclusion", width: options[:width][:max], height: options[:height][:max]) + add_error(record, attribute, :dimension_max_inclusion, width: flat_options[:width][:max], height: flat_options[:height][:max]) return false end @@ -91,24 +97,24 @@ def is_valid?(record, attribute, file_metadata) else width_or_height_invalid = false [:width, :height].each do |length| - next unless options[length] - if options[length].is_a?(Hash) - if options[length][:in] && (file_metadata[length] < options[length][:min] || file_metadata[length] > options[length][:max]) - add_error(record, attribute, options[:message].presence || :"dimension_#{length}_inclusion", min: options[length][:min], max: options[length][:max]) + next unless flat_options[length] + if flat_options[length].is_a?(Hash) + if flat_options[length][:in] && (file_metadata[length] < flat_options[length][:min] || file_metadata[length] > flat_options[length][:max]) + add_error(record, attribute, :"dimension_#{length}_inclusion", min: flat_options[length][:min], max: flat_options[length][:max]) width_or_height_invalid = true else - if options[length][:min] && file_metadata[length] < options[length][:min] - add_error(record, attribute, options[:message].presence || :"dimension_#{length}_greater_than_or_equal_to", length: options[length][:min]) + if flat_options[length][:min] && file_metadata[length] < flat_options[length][:min] + add_error(record, attribute, :"dimension_#{length}_greater_than_or_equal_to", length: flat_options[length][:min]) width_or_height_invalid = true end - if options[length][:max] && file_metadata[length] > options[length][:max] - add_error(record, attribute, options[:message].presence || :"dimension_#{length}_less_than_or_equal_to", length: options[length][:max]) + if flat_options[length][:max] && file_metadata[length] > flat_options[length][:max] + add_error(record, attribute, :"dimension_#{length}_less_than_or_equal_to", length: flat_options[length][:max]) width_or_height_invalid = true end end else - if file_metadata[length] != options[length] - add_error(record, attribute, options[:message].presence || :"dimension_#{length}_equal_to", length: options[length]) + if file_metadata[length] != flat_options[length] + add_error(record, attribute, :"dimension_#{length}_equal_to", length: flat_options[length]) width_or_height_invalid = true end end @@ -120,10 +126,10 @@ def is_valid?(record, attribute, file_metadata) true # valid file end - def add_error(record, attribute, type, **attrs) - key = options[:message].presence || type - return if record.errors.added?(attribute, key) - record.errors.add(attribute, key, **attrs) + def add_error(record, attribute, default_message, **attrs) + message = options[:message].presence || default_message + return if record.errors.added?(attribute, message) + record.errors.add(attribute, message, **attrs) end end diff --git a/lib/active_storage_validations/limit_validator.rb b/lib/active_storage_validations/limit_validator.rb index 3125532b..6d41278c 100644 --- a/lib/active_storage_validations/limit_validator.rb +++ b/lib/active_storage_validations/limit_validator.rb @@ -2,11 +2,12 @@ module ActiveStorageValidations class LimitValidator < ActiveModel::EachValidator # :nodoc: + include OptionProcUnfolding + AVAILABLE_CHECKS = %i[max min].freeze def check_validity! return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) } - raise ArgumentError, 'You must pass either :max or :min to the validator' end @@ -14,19 +15,20 @@ def validate_each(record, attribute, _) return true unless record.send(attribute).attached? files = Array.wrap(record.send(attribute)).compact.uniq - errors_options = { min: options[:min], max: options[:max] } + flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS) + errors_options = { min: flat_options[:min], max: flat_options[:max] } - return true if files_count_valid?(files.count) + return true if files_count_valid?(files.count, flat_options) record.errors.add(attribute, options[:message].presence || :limit_out_of_range, **errors_options) end - def files_count_valid?(count) - if options[:max].present? && options[:min].present? - count >= options[:min] && count <= options[:max] - elsif options[:max].present? - count <= options[:max] - elsif options[:min].present? - count >= options[:min] + def files_count_valid?(count, flat_options) + if flat_options[:max].present? && flat_options[:min].present? + count >= flat_options[:min] && count <= flat_options[:max] + elsif flat_options[:max].present? + count <= flat_options[:max] + elsif flat_options[:min].present? + count >= flat_options[:min] end end end diff --git a/lib/active_storage_validations/option_proc_unfolding.rb b/lib/active_storage_validations/option_proc_unfolding.rb new file mode 100644 index 00000000..bb65eb5f --- /dev/null +++ b/lib/active_storage_validations/option_proc_unfolding.rb @@ -0,0 +1,16 @@ +module ActiveStorageValidations + module OptionProcUnfolding + + def unfold_procs(record, object, only_keys = nil) + case object + when Hash + object.merge(object) { |key, value| only_keys&.exclude?(key) ? unfold_procs(record, value, []) : unfold_procs(record, value) } + when Array + object.map { |o| unfold_procs(record, o, only_keys) } + else + object.is_a?(Proc) ? object.call(record) : object + end + end + + end +end diff --git a/lib/active_storage_validations/size_validator.rb b/lib/active_storage_validations/size_validator.rb index 9ace9662..305949cb 100644 --- a/lib/active_storage_validations/size_validator.rb +++ b/lib/active_storage_validations/size_validator.rb @@ -2,14 +2,15 @@ module ActiveStorageValidations class SizeValidator < ActiveModel::EachValidator # :nodoc: + include OptionProcUnfolding + delegate :number_to_human_size, to: ActiveSupport::NumberHelper AVAILABLE_CHECKS = %i[less_than less_than_or_equal_to greater_than greater_than_or_equal_to between].freeze def check_validity! return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) } - - raise ArgumentError, 'You must pass either :less_than, :greater_than, or :between to the validator' + raise ArgumentError, 'You must pass either :less_than(_or_equal_to), :greater_than(_or_equal_to), or :between to the validator' end def validate_each(record, attribute, _value) @@ -20,39 +21,40 @@ def validate_each(record, attribute, _value) errors_options = {} errors_options[:message] = options[:message] if options[:message].present? + flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS) files.each do |file| - next if content_size_valid?(file.blob.byte_size) + next if content_size_valid?(file.blob.byte_size, flat_options) errors_options[:file_size] = number_to_human_size(file.blob.byte_size) - errors_options[:min_size] = number_to_human_size(min_size) - errors_options[:max_size] = number_to_human_size(max_size) + errors_options[:min_size] = number_to_human_size(min_size(flat_options)) + errors_options[:max_size] = number_to_human_size(max_size(flat_options)) record.errors.add(attribute, :file_size_out_of_range, **errors_options) break end end - def content_size_valid?(file_size) - if options[:between].present? - options[:between].include?(file_size) - elsif options[:less_than].present? - file_size < options[:less_than] - elsif options[:less_than_or_equal_to].present? - file_size <= options[:less_than_or_equal_to] - elsif options[:greater_than].present? - file_size > options[:greater_than] - elsif options[:greater_than_or_equal_to].present? - file_size >= options[:greater_than_or_equal_to] + def content_size_valid?(file_size, flat_options) + if flat_options[:between].present? + flat_options[:between].include?(file_size) + elsif flat_options[:less_than].present? + file_size < flat_options[:less_than] + elsif flat_options[:less_than_or_equal_to].present? + file_size <= flat_options[:less_than_or_equal_to] + elsif flat_options[:greater_than].present? + file_size > flat_options[:greater_than] + elsif flat_options[:greater_than_or_equal_to].present? + file_size >= flat_options[:greater_than_or_equal_to] end end - def min_size - options[:between]&.min || options[:greater_than] || options[:greater_than_or_equal_to] + def min_size(flat_options) + flat_options[:between]&.min || flat_options[:greater_than] || flat_options[:greater_than_or_equal_to] end - def max_size - options[:between]&.max || options[:less_than] || options[:less_than_or_equal_to] + def max_size(flat_options) + flat_options[:between]&.max || flat_options[:less_than] || flat_options[:less_than_or_equal_to] end end end diff --git a/test/active_storage_validations_test.rb b/test/active_storage_validations_test.rb index ed070ca0..d2c670e4 100644 --- a/test/active_storage_validations_test.rb +++ b/test/active_storage_validations_test.rb @@ -14,199 +14,261 @@ class ActiveStorageValidations::Test < ActiveSupport::TestCase test 'validates presence' do u = User.new(name: 'John Smith') assert !u.valid? - assert_equal u.errors.full_messages, ["Avatar must not be blank", "Photos can't be blank"] + assert_equal u.errors.full_messages, ["Avatar must not be blank", "Photos can't be blank", "Proc avatar must not be blank", "Proc photos can't be blank"] u = User.new(name: 'John Smith') u.avatar.attach(dummy_file) + u.proc_avatar.attach(dummy_file) assert !u.valid? - assert_equal u.errors.full_messages, ["Photos can't be blank"] + assert_equal u.errors.full_messages, ["Photos can't be blank", "Proc photos can't be blank"] u = User.new(name: 'John Smith') u.photos.attach(dummy_file) + u.proc_photos.attach(dummy_file) assert !u.valid? - assert_equal u.errors.full_messages, ["Avatar must not be blank"] + assert_equal u.errors.full_messages, ["Avatar must not be blank", "Proc avatar must not be blank"] end test 'validates content type' do u = User.new(name: 'John Smith') u.avatar.attach(dummy_file) + u.proc_avatar.attach(dummy_file) u.image_regex.attach(dummy_file) + u.proc_image_regex.attach(dummy_file) u.photos.attach(bad_dummy_file) + u.proc_photos.attach(bad_dummy_file) assert !u.valid? - assert_equal u.errors.full_messages, ['Photos has an invalid content type'] + assert_equal u.errors.full_messages, ['Photos has an invalid content type', 'Proc photos has an invalid content type'] u = User.new(name: 'John Smith') u.avatar.attach(bad_dummy_file) + u.proc_avatar.attach(bad_dummy_file) u.image_regex.attach(dummy_file) + u.proc_image_regex.attach(dummy_file) u.photos.attach(dummy_file) + u.proc_photos.attach(dummy_file) assert !u.valid? - assert_equal u.errors.full_messages, ['Avatar has an invalid content type'] + assert_equal u.errors.full_messages, ['Avatar has an invalid content type', 'Proc avatar has an invalid content type'] assert_equal u.errors.details, avatar: [ { error: :content_type_invalid, authorized_types: 'PNG', content_type: 'text/plain' } + ], proc_avatar: [ + { + error: :content_type_invalid, + authorized_types: 'PNG', + content_type: 'text/plain' + } ] u = User.new(name: 'John Smith') u.avatar.attach(dummy_file) + u.proc_avatar.attach(dummy_file) u.image_regex.attach(dummy_file) + u.proc_image_regex.attach(dummy_file) u.photos.attach(pdf_file) # Should be handled by regex match. + u.proc_photos.attach(pdf_file) # Should be handled by regex match. assert u.valid? u = User.new(name: 'John Smith') u.avatar.attach(dummy_file) + u.proc_avatar.attach(dummy_file) u.image_regex.attach(bad_dummy_file) + u.proc_image_regex.attach(bad_dummy_file) u.photos.attach(dummy_file) + u.proc_photos.attach(dummy_file) assert !u.valid? - assert_equal u.errors.full_messages, ['Image regex has an invalid content type'] + assert_equal u.errors.full_messages, ['Image regex has an invalid content type', 'Proc image regex has an invalid content type'] u = User.new(name: 'John Smith') u.avatar.attach(bad_dummy_file) + u.proc_avatar.attach(bad_dummy_file) u.image_regex.attach(bad_dummy_file) + u.proc_image_regex.attach(bad_dummy_file) u.photos.attach(bad_dummy_file) + u.proc_photos.attach(bad_dummy_file) assert !u.valid? - assert_equal u.errors.full_messages, ['Avatar has an invalid content type', 'Photos has an invalid content type', 'Image regex has an invalid content type'] + assert_equal u.errors.full_messages, ['Avatar has an invalid content type', 'Photos has an invalid content type', 'Image regex has an invalid content type', 'Proc avatar has an invalid content type', 'Proc photos has an invalid content type', 'Proc image regex has an invalid content type'] end # reads content type from file, not from webp_file_wrong method test 'webp content type 1' do u = User.new(name: 'John Smith') u.avatar.attach(webp_file_wrong) + u.proc_avatar.attach(webp_file_wrong) u.image_regex.attach(webp_file_wrong) + u.proc_image_regex.attach(webp_file_wrong) u.photos.attach(webp_file_wrong) + u.proc_photos.attach(webp_file_wrong) assert !u.valid? - assert_equal u.errors.full_messages, ['Avatar has an invalid content type', 'Photos has an invalid content type'] + assert_equal u.errors.full_messages, ['Avatar has an invalid content type', 'Photos has an invalid content type', 'Proc avatar has an invalid content type', 'Proc photos has an invalid content type'] end # trying to attach webp file with PNG extension, but real content type is detected test 'webp content type 2' do u = User.new(name: 'John Smith') u.avatar.attach(webp_file) + u.proc_avatar.attach(webp_file) u.image_regex.attach(webp_file) + u.proc_image_regex.attach(webp_file) u.photos.attach(webp_file) + u.proc_photos.attach(webp_file) assert !u.valid? - assert_equal u.errors.full_messages, ['Avatar has an invalid content type', 'Photos has an invalid content type'] + assert_equal u.errors.full_messages, ['Avatar has an invalid content type', 'Photos has an invalid content type', 'Proc avatar has an invalid content type', 'Proc photos has an invalid content type'] end test 'validates microsoft office document' do d = Document.new d.attachment.attach(docx_file) + d.proc_attachment.attach(docx_file) assert d.valid? end test 'validates microsoft office sheet' do d = Document.new d.attachment.attach(sheet_file) + d.proc_attachment.attach(sheet_file) assert d.valid? end test 'validates apple office document' do d = Document.new d.attachment.attach(pages_file) + d.proc_attachment.attach(pages_file) assert d.valid? end test 'validates apple office sheet' do d = Document.new d.attachment.attach(numbers_file) + d.proc_attachment.attach(numbers_file) assert d.valid? end test 'validates archived content_type' do d = Document.new d.file.attach(tar_file) + d.proc_file.attach(tar_file) assert d.valid? end test 'validates size' do e = Project.new(title: 'Death Star') e.preview.attach(big_file) + e.proc_preview.attach(big_file) e.small_file.attach(big_file) + e.proc_small_file.attach(big_file) e.attachment.attach(pdf_file) + e.proc_attachment.attach(pdf_file) assert !e.valid? - assert_equal e.errors.full_messages, ['Small file size 1.6 KB is not between required range'] + assert_equal e.errors.full_messages, ['Small file size 1.6 KB is not between required range', 'Proc small file size 1.6 KB is not between required range'] end test 'validates number of files' do e = Project.new(title: 'Death Star') e.preview.attach(big_file) + e.proc_preview.attach(big_file) e.small_file.attach(dummy_file) + e.proc_small_file.attach(dummy_file) e.attachment.attach(pdf_file) + e.proc_attachment.attach(pdf_file) e.documents.attach(pdf_file) + e.proc_documents.attach(pdf_file) e.documents.attach(pdf_file) + e.proc_documents.attach(pdf_file) e.documents.attach(pdf_file) + e.proc_documents.attach(pdf_file) e.documents.attach(pdf_file) + e.proc_documents.attach(pdf_file) assert !e.valid? - assert_equal e.errors.full_messages, ['Documents total number is out of range'] + assert_equal e.errors.full_messages, ['Documents total number is out of range', 'Proc documents total number is out of range'] end test 'validates number of files for Rails 6' do la = LimitAttachment.create(name: 'klingon') la.files.attach([pdf_file, pdf_file, pdf_file, pdf_file, pdf_file, pdf_file]) + la.proc_files.attach([pdf_file, pdf_file, pdf_file, pdf_file, pdf_file, pdf_file]) assert !la.valid? assert_equal 6, la.files.count + assert_equal 6, la.proc_files.count if Rails.gem_version < Gem::Version.new('6.0.0') assert_equal 6, la.files_blobs.count + assert_equal 6, la.proc_files_blobs.count else assert_equal 0, la.files_blobs.count + assert_equal 0, la.proc_files_blobs.count end - assert_equal ['Files total number is out of range'], la.errors.full_messages + assert_equal ['Files total number is out of range', 'Proc files total number is out of range'], la.errors.full_messages if Rails.gem_version < Gem::Version.new('6.0.0') la.files.first.purge + la.proc_files.first.purge la.files.first.purge + la.proc_files.first.purge la.files.first.purge + la.proc_files.first.purge la.files.first.purge + la.proc_files.first.purge end assert !la.valid? - assert_equal ['Files total number is out of range'], la.errors.full_messages + assert_equal ['Files total number is out of range', 'Proc files total number is out of range'], la.errors.full_messages end test 'validates number of files v2' do la = LimitAttachment.create(name: 'klingon') la.files.attach([pdf_file, pdf_file, pdf_file]) + la.proc_files.attach([pdf_file, pdf_file, pdf_file]) assert la.valid? assert_equal 3, la.files.count + assert_equal 3, la.proc_files.count assert la.save la.reload assert_equal 3, la.files_blobs.count + assert_equal 3, la.proc_files_blobs.count la.files.first.purge + la.proc_files.first.purge assert la.valid? la.reload assert_equal 2, la.files_blobs.count + assert_equal 2, la.proc_files_blobs.count end test 'validates number of files v3' do la = LimitAttachment.create(name: 'klingon') la.files.attach([pdf_file, pdf_file, pdf_file, pdf_file, pdf_file]) + la.proc_files.attach([pdf_file, pdf_file, pdf_file, pdf_file, pdf_file]) assert !la.valid? assert_equal 5, la.files.count + assert_equal 5, la.proc_files.count assert !la.save end test 'dimensions and is image' do e = OnlyImage.new e.image.attach(html_file) + e.proc_image.attach(html_file) assert !e.valid? - assert_equal e.errors.full_messages, ["Image is not a valid image", "Image has an invalid content type"] + assert_equal e.errors.full_messages, ["Image is not a valid image", "Image has an invalid content type", "Proc image is not a valid image", "Proc image has an invalid content type"] e = OnlyImage.new e.image.attach(image_1920x1080_file) + e.proc_image.attach(image_1920x1080_file) assert e.valid? e = OnlyImage.new e.image.attach(pdf_file) + e.proc_image.attach(pdf_file) assert !e.valid? assert e.errors.full_messages.include?("Image has an invalid content type") rescue Exception => ex @@ -218,70 +280,108 @@ class ActiveStorageValidations::Test < ActiveSupport::TestCase test 'dimensions test' do e = Project.new(title: 'Death Star') e.preview.attach(big_file) + e.proc_preview.attach(big_file) e.small_file.attach(dummy_file) + e.proc_small_file.attach(dummy_file) e.attachment.attach(pdf_file) + e.proc_attachment.attach(pdf_file) e.dimension_exact.attach(html_file) + e.proc_dimension_exact.attach(html_file) assert !e.valid? - assert_equal e.errors.full_messages, ['Dimension exact is not a valid image'] + assert_equal e.errors.full_messages, ['Dimension exact is not a valid image', 'Proc dimension exact is not a valid image'] e = Project.new(title: 'Death Star') e.preview.attach(big_file) + e.proc_preview.attach(big_file) e.small_file.attach(dummy_file) + e.proc_small_file.attach(dummy_file) e.attachment.attach(pdf_file) + e.proc_attachment.attach(pdf_file) e.documents.attach(pdf_file) + e.proc_documents.attach(pdf_file) e.documents.attach(pdf_file) + e.proc_documents.attach(pdf_file) e.valid? assert e.valid? e = Project.new(title: 'Death Star') e.preview.attach(big_file) + e.proc_preview.attach(big_file) e.small_file.attach(dummy_file) + e.proc_small_file.attach(dummy_file) e.attachment.attach(pdf_file) + e.proc_attachment.attach(pdf_file) e.dimension_exact.attach(image_150x150_file) + # e.proc_dimension_exact.attach(image_150x150_file) assert e.valid?, 'Dimension exact: width and height must be equal to 150 x 150 pixel.' e = Project.new(title: 'Death Star') e.preview.attach(big_file) + e.proc_preview.attach(big_file) e.small_file.attach(dummy_file) + e.proc_small_file.attach(dummy_file) e.attachment.attach(pdf_file) + e.proc_attachment.attach(pdf_file) e.dimension_range.attach(image_800x600_file) + e.proc_dimension_range.attach(image_800x600_file) assert e.valid?, 'Dimension range: width and height must be greater than or equal to 800 x 600 pixel.' e = Project.new(title: 'Death Star') e.preview.attach(big_file) + e.proc_preview.attach(big_file) e.small_file.attach(dummy_file) + e.proc_small_file.attach(dummy_file) e.attachment.attach(pdf_file) + e.proc_attachment.attach(pdf_file) e.dimension_range.attach(image_1200x900_file) + e.proc_dimension_range.attach(image_1200x900_file) assert e.valid?, 'Dimension range: width and height must be less than or equal to 1200 x 900 pixel.' e = Project.new(title: 'Death Star') e.preview.attach(big_file) + e.proc_preview.attach(big_file) e.small_file.attach(dummy_file) + e.proc_small_file.attach(dummy_file) e.attachment.attach(pdf_file) + e.proc_attachment.attach(pdf_file) e.dimension_min.attach(image_800x600_file) + e.proc_dimension_min.attach(image_800x600_file) assert e.valid?, 'Dimension min: width and height must be greater than or equal to 800 x 600 pixel.' e = Project.new(title: 'Death Star') e.preview.attach(big_file) + e.proc_preview.attach(big_file) e.small_file.attach(dummy_file) + e.proc_small_file.attach(dummy_file) e.attachment.attach(pdf_file) + e.proc_attachment.attach(pdf_file) e.dimension_max.attach(image_1200x900_file) + e.proc_dimension_max.attach(image_1200x900_file) assert e.valid?, 'Dimension max: width and height must be greater than or equal to 1200 x 900 pixel.' e = Project.new(title: 'Death Star') e.preview.attach(big_file) + e.proc_preview.attach(big_file) e.small_file.attach(dummy_file) + e.proc_small_file.attach(dummy_file) e.attachment.attach(pdf_file) + e.proc_attachment.attach(pdf_file) e.dimension_images.attach([image_800x600_file, image_1200x900_file]) + e.proc_dimension_images.attach([image_800x600_file, image_1200x900_file]) assert e.valid?, 'Dimension many: width and height must be between or equal to 800 x 600 and 1200 x 900 pixel.' e = Project.new(title: 'Death Star') e.preview.attach(big_file) + e.proc_preview.attach(big_file) e.small_file.attach(dummy_file) + e.proc_small_file.attach(dummy_file) e.attachment.attach(pdf_file) + e.proc_attachment.attach(pdf_file) e.dimension_images.attach([image_800x600_file]) + e.proc_dimension_images.attach([image_800x600_file]) e.save! e.dimension_images.attach([image_800x600_file]) + e.proc_dimension_images.attach([image_800x600_file]) e.title = "Changed" e.save! @@ -289,6 +389,7 @@ class ActiveStorageValidations::Test < ActiveSupport::TestCase assert e.title, "Changed" assert_nil e.dimension_min.attachment + assert_nil e.proc_dimension_min.attachment blob = if Rails.gem_version >= Gem::Version.new('6.1.0') ActiveStorage::Blob.create_and_upload!(**image_800x600_file) @@ -296,10 +397,13 @@ class ActiveStorageValidations::Test < ActiveSupport::TestCase ActiveStorage::Blob.create_after_upload!(**image_800x600_file) end e.dimension_min = blob.signed_id + e.proc_dimension_min = blob.signed_id e.save! e.reload assert_not_nil e.dimension_min.attachment + assert_not_nil e.proc_dimension_min.attachment assert_not_nil e.dimension_min.blob.signed_id + assert_not_nil e.proc_dimension_min.blob.signed_id rescue Exception => ex puts ex.message puts ex.backtrace.join("\n") @@ -341,29 +445,39 @@ class ActiveStorageValidations::Test < ActiveSupport::TestCase test 'aspect ratio validation' do e = RatioModel.new(name: 'Princess Leia') e.ratio_one.attach(image_150x150_file) + e.proc_ratio_one.attach(image_150x150_file) e.ratio_many.attach([image_600x800_file]) + e.proc_ratio_many.attach([image_600x800_file]) e.save! e = RatioModel.new(name: 'Princess Leia') e.ratio_one.attach(image_150x150_file) + e.proc_ratio_one.attach(image_150x150_file) e.ratio_many.attach([image_150x150_file]) + e.proc_ratio_many.attach([image_150x150_file]) e.save assert !e.valid? - assert_equal e.errors.full_messages, ["Ratio many must be a portrait image"] + assert_equal e.errors.full_messages, ["Ratio many must be a portrait image", "Proc ratio many must be a portrait image"] e = RatioModel.new(name: 'Princess Leia') e.ratio_one.attach(image_150x150_file) + e.proc_ratio_one.attach(image_150x150_file) e.ratio_many.attach([image_600x800_file]) + e.proc_ratio_many.attach([image_600x800_file]) e.image1.attach(image_150x150_file) + e.proc_image1.attach(image_150x150_file) assert !e.valid? - assert_equal e.errors.full_messages, ["Image1 must have an aspect ratio of 16x9"] + assert_equal e.errors.full_messages, ["Image1 must have an aspect ratio of 16x9", 'Proc image1 must have an aspect ratio of 16x9'] e = RatioModel.new(name: 'Princess Leia') e.ratio_one.attach(html_file) + e.proc_ratio_one.attach(html_file) e.ratio_many.attach([image_600x800_file]) + e.proc_ratio_many.attach([image_600x800_file]) e.image1.attach(image_1920x1080_file) + e.proc_image1.attach(image_1920x1080_file) assert !e.valid? - assert_equal e.errors.full_messages, ["Ratio one is not a valid image"] + assert_equal e.errors.full_messages, ["Ratio one is not a valid image", 'Proc ratio one is not a valid image'] end end diff --git a/test/dummy/app/models/document.rb b/test/dummy/app/models/document.rb index be24e7f5..3b630f99 100644 --- a/test/dummy/app/models/document.rb +++ b/test/dummy/app/models/document.rb @@ -1,7 +1,11 @@ class Document < ApplicationRecord has_one_attached :attachment has_one_attached :file + has_one_attached :proc_attachment + has_one_attached :proc_file validates :attachment, content_type: %i[docx xlsx pages numbers] validates :file, content_type: :tar + validates :proc_attachment, content_type: -> (record) {%i[docx xlsx pages numbers]} + validates :proc_file, content_type: -> (record) {:tar} end diff --git a/test/dummy/app/models/limit_attachment.rb b/test/dummy/app/models/limit_attachment.rb index c01060d3..2f232c65 100644 --- a/test/dummy/app/models/limit_attachment.rb +++ b/test/dummy/app/models/limit_attachment.rb @@ -1,4 +1,6 @@ class LimitAttachment < ApplicationRecord has_many_attached :files + has_many_attached :proc_files validates :files, limit: { max: 4 } + validates :proc_files, limit: { max: -> (record) {4} } end diff --git a/test/dummy/app/models/only_image.rb b/test/dummy/app/models/only_image.rb index 094eed34..63d5d754 100644 --- a/test/dummy/app/models/only_image.rb +++ b/test/dummy/app/models/only_image.rb @@ -1,6 +1,10 @@ class OnlyImage < ApplicationRecord has_one_attached :image + has_one_attached :proc_image validates :image, dimension: { width: { min: 100, max: 2000 }, height: { min: 100, max: 1500 } }, aspect_ratio: :is_16_9, content_type: ['image/png', 'image/jpeg'] + validates :proc_image, dimension: { width: { min: -> (record) {100}, max: -> (record) {2000} }, height: { min: -> (record) {100}, max: -> (record) {1500} } }, + aspect_ratio: -> (record) {:is_16_9}, + content_type: -> (record) {['image/png', 'image/jpeg']} end diff --git a/test/dummy/app/models/project.rb b/test/dummy/app/models/project.rb index d9103409..cebaaf52 100644 --- a/test/dummy/app/models/project.rb +++ b/test/dummy/app/models/project.rb @@ -22,6 +22,17 @@ class Project < ApplicationRecord has_many_attached :documents has_many_attached :dimension_images + has_one_attached :proc_preview + has_one_attached :proc_attachment + has_one_attached :proc_small_file + has_one_attached :proc_dimension_exact + has_one_attached :proc_dimension_exact_with_message + has_one_attached :proc_dimension_range + has_one_attached :proc_dimension_min + has_one_attached :proc_dimension_max + has_many_attached :proc_documents + has_many_attached :proc_dimension_images + validates :title, presence: true validates :preview, attached: true, size: { greater_than: 1.kilobytes } @@ -35,4 +46,16 @@ class Project < ApplicationRecord validates :dimension_min, dimension: { min: 800..600 } validates :dimension_max, dimension: { max: 1200..900 } validates :dimension_images, dimension: { width: { min: 800, max: 1200 }, height: { min: 600, max: 900 } } + + validates :proc_preview, attached: true, size: { greater_than: -> (record) {1.kilobytes} } + validates :proc_attachment, attached: true, content_type: { in: -> (record) {'application/pdf'}, message: 'is not a PDF' }, size: { between: -> (record) {0..500.kilobytes}, message: 'is not given between size' } + validates :proc_small_file, attached: true, size: { less_than: -> (record) {1.kilobytes} } + validates :proc_documents, limit: { min: -> (record) {1}, max: -> (record) {3} } + + validates :proc_dimension_exact, dimension: { width: -> (record) {150}, height: -> (record) {150} } + validates :proc_dimension_exact_with_message, dimension: { width: -> (record) {150}, height: -> (record) {150}, message: 'Invalid dimensions.' } + validates :proc_dimension_range, dimension: { width: { in: -> (record) {800..1200} }, height: { in: -> (record) {600..900} } } + validates :proc_dimension_min, dimension: { min: -> (record) {800..600} } + validates :proc_dimension_max, dimension: { max: -> (record) {1200..900} } + # validates :proc_dimension_images, dimension: { width: { min: -> (record) {800}, max: -> (record) {1200} }, height: { min: -> (record) {600}, max: -> (record) {900} } } end diff --git a/test/dummy/app/models/ratio_model.rb b/test/dummy/app/models/ratio_model.rb index d73df9a6..59f4adc5 100644 --- a/test/dummy/app/models/ratio_model.rb +++ b/test/dummy/app/models/ratio_model.rb @@ -3,10 +3,16 @@ class RatioModel < ApplicationRecord has_one_attached :ratio_one has_many_attached :ratio_many has_one_attached :image1 + has_one_attached :proc_ratio_one + has_many_attached :proc_ratio_many + has_one_attached :proc_image1 validates :ratio_one, attached: true, aspect_ratio: :square validates :ratio_many, attached: true, aspect_ratio: :portrait # portrait validates :image1, aspect_ratio: :is_16_9 # portrait #validates :ratio_many, attached: true, aspect_ratio: :landscape #validates :ratio_many, attached: true, aspect_ratio: :portrait # portrait + validates :proc_ratio_one, attached: true, aspect_ratio: -> (record) {:square} + validates :proc_ratio_many, attached: true, aspect_ratio: -> (record) {:portrait} # portrait + validates :proc_image1, aspect_ratio: -> (record) {:is_16_9} # portrait end diff --git a/test/dummy/app/models/user.rb b/test/dummy/app/models/user.rb index 3c4f289c..2b77da8c 100644 --- a/test/dummy/app/models/user.rb +++ b/test/dummy/app/models/user.rb @@ -13,6 +13,9 @@ class User < ApplicationRecord has_many_attached :photos has_one_attached :image_regex has_one_attached :conditional_image + has_one_attached :proc_avatar + has_many_attached :proc_photos + has_one_attached :proc_image_regex validates :name, presence: true @@ -20,4 +23,7 @@ class User < ApplicationRecord validates :photos, attached: true, content_type: ['image/png', 'image/jpg', /\A.*\/pdf\z/] validates :image_regex, content_type: /\Aimage\/.*\z/ validates :conditional_image, attached: true, if: -> { name == 'Foo' } + validates :proc_avatar, attached: { message: "must not be blank" }, content_type: -> (record) {:png} + validates :proc_photos, attached: true, content_type: -> (record) {['image/png', 'image/jpg', /\A.*\/pdf\z/]} + validates :proc_image_regex, content_type: -> (record) {/\Aimage\/.*\z/} end diff --git a/test/matchers/attached_validator_matcher_test.rb b/test/matchers/attached_validator_matcher_test.rb index c4807458..2a30b2b3 100644 --- a/test/matchers/attached_validator_matcher_test.rb +++ b/test/matchers/attached_validator_matcher_test.rb @@ -48,6 +48,7 @@ class ActiveStorageValidations::Matchers::AttachedValidatorMatcher::Test < Activ matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:avatar) user = User.new user.avatar.attach(io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png') + user.proc_avatar.attach(io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png') assert matcher.matches?(user) end @@ -56,7 +57,9 @@ class ActiveStorageValidations::Matchers::AttachedValidatorMatcher::Test < Activ user = User.create!( name: 'Pietje', avatar: { io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png' }, - photos: [{ io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png' }] + photos: [{ io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png' }], + proc_avatar: { io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png' }, + proc_photos: [{ io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png' }] ) assert matcher.matches?(user) end