Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdickey/8eaf646fbd754408c6d9 to your computer and use it in GitHub Desktop.
Save jdickey/8eaf646fbd754408c6d9 to your computer and use it in GitHub Desktop.
First "hasty rewrite" of "naive validation object".
require 'active_support/core_ext/numeric/bytes'
require 'mini_magick'
require 'value_object'
class AvatarValidator
class UploadedFile
def initialize(source)
@data = get_data_from source
end
def size
data.size
end
def to_image
MiniMagick::Image.read data
end
private
attr_reader :data
# `source` is either a filename, a data blob, or (for testing) a (possibly
# mock) instance of UploadedFile. If we pass a data blob to File#exists?,
# it will raise an ArgumentError. This makes it fairly easy to figure out
# which is which.
def get_data_from(source)
return source.send(:data) if source.respond_to?(:data, true)
# Not an instance; keep trying
_ = File.exists? source
# If `source` is *not* a filename, we wouldn't get here
File.open(source) { |f| f.read }
rescue ArgumentError
# source is (presumably) image data
source
end
end # class AvatarValidator::UploadedFile
def initialize(uploaded_file, config = nil)
@uploaded_file = UploadedFile.new uploaded_file
@config = config || AvatarValidator.default_config
end
def validate
return @valid unless @valid.nil?
verify_file_size
validate_image_format
verify_allowed_format
verify_image_dimensions
@valid = true
rescue RuntimeError => e
error_message_for YAML.load(e.message).first
end
def validate_as_json
result = validate
return({ success: true }) if result == true
{ success: false, message: result }
end
def self.default_config
Class.new(ValueObject::Base) do
has_fields :allowed_formats, :max_file_size, :max_height, :max_width
end.new max_file_size: 1.megabyte,
allowed_formats: %w(PNG JPEG GIF),
max_width: 5000,
max_height: 5000
end
private
attr_reader :config, :uploaded_file
def error_message_for(error_data)
matches = {
invalid_file_size: 'Picture size must be no larger than %d bytes',
invalid_image_format: 'Picture must be in a valid image format',
disallowed_format: 'Picture must be an image of type %s',
invalid_height: 'Picture height must not exceed %d pixels',
invalid_width: 'Picture height must not exceed %d pixels'
}
format matches[error_data.keys.first], matches[error_data.values.first]
end
def fail_with(error_key, data_value)
error_item = {}
error_item[error_key] = data_value
fail YAML.dump([error_item])
end
def image
@image ||= uploaded_file.to_image
end
def verify_file_size
return self unless uploaded_file.size > config.max_file_size
fail_with :invalid_file_size, uploaded_file.size
end
def validate_image_format
image.validate!
self
rescue MiniMagick::Error => e
fail_with :invalid_image_format, e.message
end
def verify_allowed_format
return self if config.allowed_formats.include? image.type
message = config.allowed_format.join ' or '
fail_with :disallowed_format, message
end
def verify_image_dimensions
fail_with :invalid_width, config.max_width if image.width > config.max_width
invalid_height = image.height > config.max_height
fail_with :invalid_height, config.max_height if invalid_height
self
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment