Last active
September 22, 2023 21:39
-
-
Save carlosramireziii/6d0ca6b414d8a6af08371c30ba4dedcd to your computer and use it in GitHub Desktop.
A validator and RSpec matcher for restricting an attachment’s content type using Active Storage
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "rspec/expectations" | |
RSpec::Matchers.define :allow_content_type do |*content_types| | |
match do |record| | |
matcher.matches?(record, content_types) | |
end | |
chain :for do |attr_name| | |
matcher.for(attr_name) | |
end | |
chain :with_message do |message| | |
matcher.with_message(message) | |
end | |
private | |
def matcher | |
@matcher ||= AllowContentTypeMatcher.new | |
end | |
class AllowContentTypeMatcher | |
def for(attr_name) | |
@attr_name = attr_name | |
end | |
def with_message(message) | |
@message = message | |
end | |
def matches?(record, content_types) | |
Array.wrap(content_types).all? do |content_type| | |
record.send(attr_name).attach attachment_for(content_type) | |
record.valid? | |
!record.errors[attr_name].include? message | |
end | |
end | |
private | |
attr_reader :attr_name | |
def attachment_for(content_type) | |
suffix = content_type.to_s.split("/").last | |
{ io: StringIO.new("Hello world!"), filename: "test.#{suffix}", content_type: content_type } | |
end | |
def message | |
@message || I18n.translate("activerecord.errors.messages.content_type") | |
end | |
end | |
end | |
RSpec::Matchers.alias_matcher :allow_content_types, :allow_content_type |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ContentTypeValidator < ActiveModel::EachValidator | |
def validate_each(record, attribute, value) | |
unless value.attached? && value.content_type.in?(content_types) | |
value.purge if record.new_record? # Only purge the offending blob if the record is new | |
record.errors.add(attribute, :content_type, options) | |
end | |
end | |
private | |
def content_types | |
options.fetch(:in) | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
en: | |
activerecord: | |
errors: | |
messages: | |
content_type: is not a valid file format |
I get
FrozenError: can't modify frozen Hash
. Not sure where it's coming from.
@thelucid Yes, I got a similar error with the @meara approach. can't modify frozen attributes
- Were you able to fix it? I added the check back and it worked as well:
def validate_each(record, attribute, value)
return unless value.attached?
return if value.content_type.in?(content_types)
value.purge if record.new_record?
record.errors.add(attribute, :content_type, **options)
end
@carlosramireziii Thank you, it works nicely.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great question! If a persisted record has an existing attachment then we don't want to purge it on a validation error because then we'll lose the existing value. I believe this behavior will be different in Rails 6+ because attachments are no longer persisted immediately upon attachment.
If you call
destroy
on an attachment then it won't automatically delete the associated blob. You'll want to make sure you use thepurge
method for deleting the attachment and blob.This is exactly the directory structure I use as well!
Awesome!