Skip to content

Commit

Permalink
Merge pull request #221 from igorkasyanchuk/212-add-filename-to-error…
Browse files Browse the repository at this point in the history
…-message

212 add filename to error message
  • Loading branch information
igorkasyanchuk authored Dec 4, 2023
2 parents ffedd70 + 4dd1ae5 commit 3e2b522
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 56 deletions.
52 changes: 40 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,50 +173,78 @@ en:
image_not_processable: "is not a valid image"
```
In some cases, Active Storage Validations provides variables to help you customize messages:
In several cases, Active Storage Validations provides variables to help you customize messages:
### Aspect ratio
The keys starting with `aspect_ratio_` support two variables that you can use:
- `aspect_ratio` containing the expected aspect ratio, especially usefull for custom aspect ratio
- `filename` containing the current file name

For example :

```yml
aspect_ratio_is_not: "must be a %{aspect_ratio} image"
```

### Content type
The `content_type_invalid` key has two variables that you can use:
The `content_type_invalid` key has three variables that you can use:
- `content_type` containing the content type of the sent file
- `authorized_types` containing the list of authorized content types
- `filename` containing the current file name

For example :

```yml
content_type_invalid: "has an invalid content type : %{content_type}, authorized types are %{authorized_types}"
```

### Number of files
The `limit_out_of_range` key supports two variables that you can use:
- `min` containing the minimum number of files
- `max` containing the maximum number of files
### Dimension
The keys starting with `dimension_` support six variables that you can use:
- `min` containing the minimum width or height allowed
- `max` containing the maximum width or height allowed
- `width` containing the minimum or maximum width allowed
- `height` containing the minimum or maximum width allowed
- `lenght` containing the exact width or height allowed
- `filename` containing the current file name

For example :

```yml
limit_out_of_range: "total number is out of range. range: [%{min}, %{max}]"
dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel."
```

### File size
The keys starting with `file_size_not_` support three variables that you can use:
The keys starting with `file_size_not_` support four variables that you can use:
- `file_size` containing the current file size
- `min` containing the minimum file size
- `max` containing the maxmimum file size
- `filename` containing the current file name

For example :

```yml
file_size_not_between: "file size must be between %{min_size} and %{max_size} (current size is %{file_size})"
```

### Aspect ratio
The keys starting with `aspect_ratio_` support one variable that you can use:
- `aspect_ratio` containing the expected aspect ratio, especially usefull for custom aspect ratio
### Number of files
The `limit_out_of_range` key supports two variables that you can use:
- `min` containing the minimum number of files
- `max` containing the maximum number of files

For example :

```yml
aspect_ratio_is_not: "must be a %{aspect_ratio} image"
limit_out_of_range: "total number is out of range. range: [%{min}, %{max}]"
```

### Processable image
The `image_not_processable` key supports one variable that you can use:
- `filename` containing the current file name

For example :

```yml
image_not_processable: "is not a valid image (file: %{filename})"
```

## Installation
Expand Down
8 changes: 4 additions & 4 deletions lib/active_storage_validations/aspect_ratio_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def validate_each(record, attribute, _value)

files.each do |file|
metadata = Metadata.new(file).metadata
next if is_valid?(record, attribute, metadata)
next if is_valid?(record, attribute, file, metadata)
break
end
end
Expand All @@ -46,7 +46,7 @@ def validate_each(record, attribute, _value)
file.analyze; file.reload unless file.analyzed?
metadata = file.metadata

next if is_valid?(record, attribute, metadata)
next if is_valid?(record, attribute, file, metadata)
break
end
end
Expand All @@ -56,9 +56,9 @@ def validate_each(record, attribute, _value)
private


def is_valid?(record, attribute, metadata)
def is_valid?(record, attribute, file, metadata)
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
errors_options = initialize_error_options(options)
errors_options = initialize_error_options(options, file)

if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
errors_options[:aspect_ratio] = flat_options[:with]
Expand Down
16 changes: 14 additions & 2 deletions lib/active_storage_validations/concerns/errorable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ module ActiveStorageValidations
module Errorable
extend ActiveSupport::Concern

def initialize_error_options(options)
def initialize_error_options(options, file = nil)
not_explicitly_written_options = %i(with in)
curated_options = options.except(*not_explicitly_written_options)

active_storage_validations_options = {
validator_type: self.class.to_sym,
custom_message: (options[:message] if options[:message].present?)
custom_message: (options[:message] if options[:message].present?),
filename: get_filename(file)
}.compact

curated_options.merge(active_storage_validations_options)
Expand All @@ -22,5 +23,16 @@ def add_error(record, attribute, error_type, **errors_options)
# to better understand how Rails model errors work
record.errors.add(attribute, type, **errors_options)
end

private

def get_filename(file)
return nil unless file

case file
when ActiveStorage::Attached then file.blob.filename.to_s
when Hash then file[:filename]
end
end
end
end
5 changes: 2 additions & 3 deletions lib/active_storage_validations/content_type_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@ def validate_each(record, attribute, _value)

files = Array.wrap(record.send(attribute))

errors_options = initialize_error_options(options)
errors_options[:authorized_types] = types_to_human_format(types)

files.each do |file|
next if is_valid?(file, types)

errors_options = initialize_error_options(options, file)
errors_options[:authorized_types] = types_to_human_format(types)
errors_options[:content_type] = content_type(file)
add_error(record, attribute, ERROR_TYPES.first, **errors_options)
break
Expand Down
26 changes: 13 additions & 13 deletions lib/active_storage_validations/dimension_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def validate_each(record, attribute, _value)
files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
files.each do |file|
metadata = Metadata.new(file).metadata
next if is_valid?(record, attribute, metadata)
next if is_valid?(record, attribute, file, metadata)
break
end
end
Expand All @@ -78,28 +78,28 @@ def validate_each(record, attribute, _value)
# Analyze file first if not analyzed to get all required metadata.
file.analyze; file.reload unless file.analyzed?
metadata = file.metadata rescue {}
next if is_valid?(record, attribute, metadata)
next if is_valid?(record, attribute, file, metadata)
break
end
end
end


def is_valid?(record, attribute, file_metadata)
def is_valid?(record, attribute, file, metadata)
flat_options = process_options(record)
errors_options = initialize_error_options(options)
errors_options = initialize_error_options(options, file)

# Validation fails unless file metadata contains valid width and height.
if file_metadata[:width].to_i <= 0 || file_metadata[:height].to_i <= 0
if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
add_error(record, attribute, :image_metadata_missing, **errors_options)
return false
end

# Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
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])
(flat_options[:width][:min] && metadata[:width] < flat_options[:width][:min]) ||
(flat_options[:height][:min] && metadata[:height] < flat_options[:height][:min])
)
errors_options[:width] = flat_options[:width][:min]
errors_options[:height] = flat_options[:height][:min]
Expand All @@ -108,8 +108,8 @@ def is_valid?(record, attribute, file_metadata)
return false
end
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])
(flat_options[:width][:max] && metadata[:width] > flat_options[:width][:max]) ||
(flat_options[:height][:max] && metadata[:height] > flat_options[:height][:max])
)
errors_options[:width] = flat_options[:width][:max]
errors_options[:height] = flat_options[:height][:max]
Expand All @@ -125,21 +125,21 @@ def is_valid?(record, attribute, file_metadata)
[:width, :height].each do |length|
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])
if flat_options[length][:in] && (metadata[length] < flat_options[length][:min] || metadata[length] > flat_options[length][:max])
error_type = :"dimension_#{length}_inclusion"
errors_options[:min] = flat_options[length][:min]
errors_options[:max] = flat_options[length][:max]

add_error(record, attribute, error_type, **errors_options)
width_or_height_invalid = true
else
if flat_options[length][:min] && file_metadata[length] < flat_options[length][:min]
if flat_options[length][:min] && metadata[length] < flat_options[length][:min]
error_type = :"dimension_#{length}_greater_than_or_equal_to"
errors_options[:length] = flat_options[length][:min]

add_error(record, attribute, error_type, **errors_options)
width_or_height_invalid = true
elsif flat_options[length][:max] && file_metadata[length] > flat_options[length][:max]
elsif flat_options[length][:max] && metadata[length] > flat_options[length][:max]
error_type = :"dimension_#{length}_less_than_or_equal_to"
errors_options[:length] = flat_options[length][:max]

Expand All @@ -148,7 +148,7 @@ def is_valid?(record, attribute, file_metadata)
end
end
else
if file_metadata[length] != flat_options[length]
if metadata[length] != flat_options[length]
error_type = :"dimension_#{length}_equal_to"
errors_options[:length] = flat_options[length]

Expand Down
12 changes: 8 additions & 4 deletions lib/active_storage_validations/processable_image_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
def validate_each(record, attribute, _value)
return true unless record.send(attribute).attached?

errors_options = initialize_error_options(options)

changes = record.attachment_changes[attribute.to_s]
return true if changes.blank?

files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)

files.each do |file|
add_error(record, attribute, :image_not_processable, **errors_options) unless Metadata.new(file).valid?
if !Metadata.new(file).valid?
errors_options = initialize_error_options(options, file)
add_error(record, attribute, :image_not_processable, **errors_options) unless Metadata.new(file).valid?
end
end
end
else
Expand All @@ -33,7 +34,10 @@ def validate_each(record, attribute, _value)
files = Array.wrap(record.send(attribute))

files.each do |file|
add_error(record, attribute, :image_not_processable, **errors_options) unless Metadata.new(file).valid?
if !Metadata.new(file).valid?
errors_options = initialize_error_options(options, file)
add_error(record, attribute, :image_not_processable, **errors_options) unless Metadata.new(file).valid?
end
end
end
end
Expand Down
8 changes: 3 additions & 5 deletions lib/active_storage_validations/size_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,12 @@ def validate_each(record, attribute, _value)
return true unless record.send(attribute).attached?

files = Array.wrap(record.send(attribute))

errors_options = initialize_error_options(options)

flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)

files.each do |file|
next if content_size_valid?(file.blob.byte_size, flat_options)
next if is_valid?(file.blob.byte_size, flat_options)

errors_options = initialize_error_options(options, file)
errors_options[:file_size] = number_to_human_size(file.blob.byte_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))
Expand All @@ -58,7 +56,7 @@ def validate_each(record, attribute, _value)

private

def content_size_valid?(file_size, flat_options)
def is_valid?(file_size, flat_options)
return false if file_size < 0

if flat_options[:between].present?
Expand Down
13 changes: 4 additions & 9 deletions test/active_storage_validations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,16 @@ class ActiveStorageValidations::Test < ActiveSupport::TestCase
error: :content_type_invalid,
validator_type: :content_type,
authorized_types: 'PNG',
content_type: 'text/plain'
content_type: 'text/plain',
filename: 'bad_dummy_file.png'
}
], proc_avatar: [
{
error: :content_type_invalid,
validator_type: :content_type,
authorized_types: 'PNG',
content_type: 'text/plain'
content_type: 'text/plain',
filename: 'bad_dummy_file.png'
}
]

Expand Down Expand Up @@ -474,10 +476,3 @@ class ActiveStorageValidations::Test < ActiveSupport::TestCase
assert_equal e.errors.full_messages, ["Ratio one is not a valid image", 'Proc ratio one is not a valid image']
end
end

def image_string_io
string_io = StringIO.new().tap {|io| io.binmode }
IO.copy_stream(File.open(Rails.root.join('public', 'image_1920x1080.png')), string_io)
string_io.rewind
{ io: string_io, filename: 'image_1920x1080.png', content_type: 'image/png' }
end
15 changes: 15 additions & 0 deletions test/dummy/app/models/processable_image/validator/check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: processable_image_validator_checks
#
# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
#

class ProcessableImage::Validator::Check < ApplicationRecord
has_one_attached :has_to_be_processable
validates :has_to_be_processable, processable_image: true
end
5 changes: 1 addition & 4 deletions test/dummy/app/models/size/validator/check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Size::Validator::Check < ApplicationRecord
validates :greater_than, size: { greater_than: 7.kilobytes }
validates :greater_than_or_equal_to, size: { greater_than_or_equal_to: 7.kilobytes }
validates :between, size: { between: 2.kilobytes..7.kilobytes }

has_one_attached :less_than_proc
has_one_attached :less_than_or_equal_to_proc
has_one_attached :greater_than_proc
Expand All @@ -31,7 +31,4 @@ class Size::Validator::Check < ApplicationRecord
validates :greater_than_proc, size: { greater_than: -> (record) { 7.kilobytes } }
validates :greater_than_or_equal_to_proc, size: { greater_than_or_equal_to: -> (record) { 7.kilobytes } }
validates :between_proc, size: { between: -> { 2.kilobytes..7.kilobytes } }

has_one_attached :with_message
validates :with_message, size: { less_than_or_equal_to: 5.megabytes, message: 'Custom message' }
end
12 changes: 12 additions & 0 deletions test/support/files.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,18 @@ def tar_file_with_image_content_type
}
end

def image_string_io
string_io = StringIO.new().tap {|io| io.binmode }
IO.copy_stream(File.open(Rails.root.join('public', 'image_1920x1080.png')), string_io)
string_io.rewind

{
io: string_io,
filename: 'image_1920x1080.png',
content_type: 'image/png'
}
end

def file_1ko
{
io: File.open(Rails.root.join('public', 'file_1ko')),
Expand Down
Loading

0 comments on commit 3e2b522

Please sign in to comment.