Active Storage Validations
If you are using active_storage
gem and you want to add simple validations for it, like presence or content_type you need to write a custom validation method.
This gems doing it for you. Just use attached: true
or content_type: 'image/png'
validation.
What it can do
- validates if file(s) attached
- validates content type
- validates size of files
- validates dimension of images/videos
- validates number of uploaded files (min/max required)
- validates aspect ratio (if square, portrait, landscape, is_16_9, ...)
- validates if file can be processed by MiniMagick or Vips
- custom error messages
- allow procs for dynamic determination of values
Usage
For example you have a model like this and you want to add validation.
class User < ApplicationRecord
has_one_attached :avatar
has_many_attached :photos
has_one_attached :image
validates :name, presence: true
validates :avatar, attached: true, content_type: 'image/png',
dimension: { width: 200, height: 200 }
validates :photos, attached: true, content_type: ['image/png', 'image/jpeg'],
dimension: { width: { min: 800, max: 2400 },
height: { min: 600, max: 1800 }, message: 'is not given between dimension' }
validates :image, attached: true,
processable_image: true,
content_type: ['image/png', 'image/jpeg'],
aspect_ratio: :landscape
end
or
class Project < ApplicationRecord
has_one_attached :logo
has_one_attached :preview
has_one_attached :attachment
has_many_attached :documents
validates :title, presence: true
validates :logo, attached: true, size: { less_than: 100.megabytes , message: 'is too large' }
validates :preview, attached: true, size: { between: 1.kilobyte..100.megabytes , message: 'is not given between size' }
validates :attachment, attached: true, content_type: { in: 'application/pdf', message: 'is not a PDF' }
validates :documents, limit: { min: 1, max: 3 }
end
More examples
- Content type validation using symbols or regex.
class User < ApplicationRecord
has_one_attached :avatar
has_many_attached :photos
validates :avatar, attached: true, content_type: :png
# or
validates :photos, attached: true, content_type: [:png, :jpg, :jpeg]
# or
validates :avatar, content_type: /\Aimage\/.*\z/
end
Please note that the symbol types must be registered by Marcel::EXTENSIONS
that's used by this gem to infer the full content type.
Example code for adding a new content type to Marcel:
# config/initializers/mime_types.rb
Marcel::MimeType.extend "application/ino", extensions: %w(ino), parents: "text/plain" # Registering arduino INO files
- Dimension validation with
width
,height
andin
.
class User < ApplicationRecord
has_one_attached :avatar
has_many_attached :photos
validates :avatar, dimension: { width: { in: 80..100 }, message: 'is not given between dimension' }
validates :photos, dimension: { height: { in: 600..1800 } }
end
- Dimension validation with
min
andmax
range for width and height:
class User < ApplicationRecord
has_one_attached :avatar
has_many_attached :photos
validates :avatar, dimension: { min: 200..100 }
# Equivalent to:
# validates :avatar, dimension: { width: { min: 200 }, height: { min: 100 } }
validates :photos, dimension: { min: 200..100, max: 400..200 }
# Equivalent to:
# validates :avatar, dimension: { width: { min: 200, max: 400 }, height: { min: 100, max: 200 } }
end
- Aspect ratio validation:
class User < ApplicationRecord
has_one_attached :avatar
has_one_attached :photo
has_many_attached :photos
validates :avatar, aspect_ratio: :square
validates :photo, aspect_ratio: :landscape
# you can also pass dynamic aspect ratio, like :is_4_3, :is_16_9, etc
validates :photos, aspect_ratio: :is_4_3
end
- Proc Usage:
Procs can be used instead of values in all the above examples. They will be called on every validation.
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:
en:
errors:
messages:
content_type_invalid: "has an invalid content type"
file_size_not_less_than: "file size must be less than %{max_size} (current size is %{file_size})"
file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max_size} (current size is %{file_size})"
file_size_not_greater_than: "file size must be greater than %{min_size} (current size is %{file_size})"
file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min_size} (current size is %{file_size})"
file_size_not_between: "file size must be between %{min_size} and %{max_size} (current size is %{file_size})"
limit_out_of_range: "total number is out of range"
image_metadata_missing: "is not a valid image"
dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel."
dimension_max_inclusion: "must be less than or equal to %{width} x %{height} pixel."
dimension_width_inclusion: "width is not included between %{min} and %{max} pixel."
dimension_height_inclusion: "height is not included between %{min} and %{max} pixel."
dimension_width_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixel."
dimension_height_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixel."
dimension_width_less_than_or_equal_to: "width must be less than or equal to %{length} pixel."
dimension_height_less_than_or_equal_to: "height must be less than or equal to %{length} pixel."
dimension_width_equal_to: "width must be equal to %{length} pixel."
dimension_height_equal_to: "height must be equal to %{length} pixel."
aspect_ratio_not_square: "must be a square image"
aspect_ratio_not_portrait: "must be a portrait image"
aspect_ratio_not_landscape: "must be a landscape image"
aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
aspect_ratio_unknown: "has an unknown aspect ratio"
image_not_processable: "is not a valid image"
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 :
aspect_ratio_is_not: "must be a %{aspect_ratio} image"
Content type
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 :
content_type_invalid: "has an invalid content type : %{content_type}, authorized types are %{authorized_types}"
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 -
length
containing the exact width or height allowed -
filename
containing the current file name
For example :
dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel."
File size
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 :
file_size_not_between: "file size must be between %{min_size} and %{max_size} (current size is %{file_size})"
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 :
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 :
image_not_processable: "is not a valid image (file: %{filename})"
Installation
Add this line to your application's Gemfile:
# Rails 5.2 and Rails 6
gem 'active_storage_validations'
# Optional, to use :dimension validator or :aspect_ratio validator
gem 'mini_magick', '>= 4.9.5'
# Or
gem 'ruby-vips', '>= 2.1.0'
And then execute:
$ bundle
Sample
Very simple example of validation with file attached, content type check and custom error message.
Test matchers
Provides RSpec-compatible and Minitest-compatible matchers for testing the validators. Only aspect_ratio
, attached
, content_type
, dimension
and size
validators currently have their matcher developped.
RSpec
In spec_helper.rb
, you'll need to require the matchers:
require 'active_storage_validations/matchers'
And include the module:
RSpec.configure do |config|
config.include ActiveStorageValidations::Matchers
end
Matcher methods available:
describe User do
# aspect_ratio:
# #allowing, #rejecting
it { is_expected.to validate_aspect_ratio_of(:avatar).allowing(:square) }
it { is_expected.to validate_aspect_ratio_of(:avatar).rejecting(:portrait) }
# attached
it { is_expected.to validate_attached_of(:avatar) }
# content_type:
# #allowing, #rejecting
it { is_expected.to validate_content_type_of(:avatar).allowing('image/png', 'image/gif') }
it { is_expected.to validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml') }
# dimension:
# #width, #height, #width_min, #height_min, #width_max, #height_max, #width_between, #height_between
it { is_expected.to validate_dimensions_of(:avatar).width(250) }
it { is_expected.to validate_dimensions_of(:avatar).height(200) }
it { is_expected.to validate_dimensions_of(:avatar).width_min(200) }
it { is_expected.to validate_dimensions_of(:avatar).height_min(100) }
it { is_expected.to validate_dimensions_of(:avatar).width_max(500) }
it { is_expected.to validate_dimensions_of(:avatar).height_max(300) }
it { is_expected.to validate_dimensions_of(:avatar).width_between(200..500) }
it { is_expected.to validate_dimensions_of(:avatar).height_between(100..300) }
# size:
# #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between
it { is_expected.to validate_size_of(:avatar).less_than(50.kilobytes) }
it { is_expected.to validate_size_of(:avatar).less_than_or_equal_to(50.kilobytes) }
it { is_expected.to validate_size_of(:avatar).greater_than(1.kilobyte) }
it { is_expected.to validate_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
it { is_expected.to validate_size_of(:avatar).between(100..500.kilobytes) }
end
(Note that matcher methods are chainable)
All matchers can currently be customized with Rails validation options:
describe User do
# :allow_blank
it { is_expected.to validate_attached_of(:avatar).allow_blank }
# :on
it { is_expected.to validate_attached_of(:avatar).on(:update) }
it { is_expected.to validate_attached_of(:avatar).on(%i[update custom]) }
# :message
it { is_expected.to validate_dimensions_of(:avatar).width(250).with_message('Invalid dimensions.') }
end
Minitest
To use the matchers, make sure you have the shoulda-context gem up and running.
You need to require the matchers:
require 'active_storage_validations/matchers'
And extend the module:
class ActiveSupport::TestCase
extend ActiveStorageValidations::Matchers
end
Then you can use the matchers with the syntax specified in the RSpec section, just use should validate_method
instead of it { is_expected_to validate_method }
as specified in the shoulda-context gem.
Todo
- verify with remote storages (s3, etc)
- verify how it works with direct upload
- add more translations
Tests & Contributing
To run tests in root folder of gem:
-
BUNDLE_GEMFILE=gemfiles/rails_6_1_3_1.gemfile bundle exec rake test
to run for Rails 6.1 -
BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test
to run for Rails 7.0 -
BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test
to run for Rails 7.0 -
BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test
to run for Rails main branch
Snippet to run in console:
BUNDLE_GEMFILE=gemfiles/rails_6_1_3_1.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_6_1_3_1.gemfile bundle exec rake test
BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test
BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test
BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test
Tips:
- To focus a specific test, use the
focus
class method provided by minitest-focus - To focus a specific file, use the TEST option provided by minitest, e.g. to only run size_validator_test.rb file you will execute the following command:
bundle exec rake test TEST=test/validators/size_validator_test.rb
Known issues
- There is an issue in Rails which it possible to get if you have added a validation and generating for example an image preview of attachments. It can be fixed with this:
<% if @user.avatar.attached? && @user.avatar.attachment.blob.present? && @user.avatar.attachment.blob.persisted? %>
<%= image_tag @user.avatar %>
<% end %>
This is a Rails issue, and is fixed in Rails 6.
Contributing
You are welcome to contribute.
Contributors (BIG THANK YOU)
- https://github.com/schweigert
- https://github.com/tleneveu
- https://github.com/reckerswartz
- https://github.com/Uysim
- https://github.com/D-system
- https://github.com/ivanelrey
- https://github.com/phlegx
- https://github.com/rr-dev
- https://github.com/dsmalko
- https://github.com/danderozier
- https://github.com/cseelus
- https://github.com/vkinelev
- https://github.com/reed
- https://github.com/connorshea
- https://github.com/Atul9
- https://github.com/victorbueno
- https://github.com/UICJohn
- https://github.com/giovannibonetti
- https://github.com/dlepage
- https://github.com/StefSchenkelaars
- https://github.com/willnet
- https://github.com/mohanklein
- https://github.com/High5Apps
- https://github.com/mschnitzer
- https://github.com/sinankeskin
- https://github.com/alejandrodevs
- https://github.com/molfar
- https://github.com/connorshea
- https://github.com/yshmarov
- https://github.com/fongfan999
- https://github.com/cooperka
- https://github.com/dolarsrg
- https://github.com/jayshepherd
- https://github.com/ohbarye
- https://github.com/randsina
- https://github.com/vietqhoang
- https://github.com/kemenaran
- https://github.com/jrmhaig
- https://github.com/tagliala
- https://github.com/evedovelli
- https://github.com/JuanVqz
- https://github.com/luiseugenio
- https://github.com/equivalent
- https://github.com/NARKOZ
- https://github.com/stephensolis
- https://github.com/kwent
- https://github.com/Animesh-Ghosh
- https://github.com/gr8bit
- https://github.com/codegeek319
- https://github.com/clwy-cn
- https://github.com/kukicola
- https://github.com/sobrinho
- https://github.com/iainbeeston
- https://github.com/marckohlbrugge
- https://github.com/Mth0158
- https://github.com/technicalpickles
- https://github.com/ricsdeol
- https://github.com/Fonsan
- https://github.com/tagliala
- https://github.com/ocarreterom
- https://github.com/aditya-cherukuri
License
The gem is available as open source under the terms of the MIT License.