How to validate an optional file upload (none? xor multiple custom predicates)?

I’ve started using dry-validation (v 0.10.4) for the first time today in a Trailblazer v1.1.2/Rails 5 app and I need to replicate some of the functionality provided by the file_validators gem for an optional image file upload.

At first I tried this:

configure do
  option :form
  def logo?(value)
    # return true if value.nil? or value == a valid image upload

But this fails when the image is nil, though I don’t understand why.

I have written the following (simplified/abbreviated) code:

configure do
  option :form

  def file?(value)
     # true if an uploaded file

  def allowable_file_type?(allowed, value)
    # true if allowed includes value

  def max_file_size?(max, value)
    # true if value <= max

  rule(nil_or_valid_image_upload: [:image]) do |image|
    image.none? ^ (image.file? &
                   image.max_file_size?(2.megabytes) &
                     image.allowable_file_type?(['image/jpeg', 'image/jpg', 'image/png', 'image/gif']))

When running tests, it’s clear that the custom predicates on the right side of my xor logic are not being run and validation passes for invalid files.

Ideally, I would like to not have to run max_file_size? if file? returns false etc.

I assume I’m misusing the dry-validation API somehow and it feels like the latter code is too complex to be correct, so perhaps someone more experienced would kindly point the error of my ways?

Thanks in advance!

Looking into this further, I assume the failure in my code is that I’m attempting to validate an object (Rack::Test::UploadedFile during testing, and ActionDispatch::Http::UploadedFile when sending from a browser through Rails in other environments).

Seeing this:

I now see that I need to extract the attributes of the ::UploadedFile objects and and validate those as data.

I’d expect that this would just work:

required(:image).maybe(:file?, max_file_size?: 2.megabytes, allowable_file_type?: ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'])

I tried various ways of validating the *::UploadedFile object, but the only thing I could get to work was to extract the attributes (content_type and tempfile.size) from the object and validate those using built-in predicates (included_in? and int? with lt?).

Doh! This is what I needed…

optional(:image).maybe(:file?, max_file_size?: 2.megabytes, allowable_file_type?: [‘image/jpeg’, ‘image/jpg’, ‘image/png’, ‘image/gif’])