Complex validation

Here’s what I’m trying to achieve:

A form contains a text area and a file field. A user can either put a list of numbers into text area or attach a 1-column CSV. On the backend regardsless of what user has done I just want to make sure that I get an array of numbers.

I’m utterly confused on how to do it.

One approach is that text area and file field have the same name. So the schema would require one key and expect it to be either a string or a file upload. I have no idea how to express the or part or how to validate it’s a file upload.

Another approach is the text area and file field have different names. I have no idea how to make sure only one key is present. And also how to validate the value is a file.

I think, yet another approach might be a custom type that takes in either a string or a file upload and turns it into an array of integers and then make schema validate the key is that type.

Either way, I have no idea how to implement this and if dry-validations and friends are even the right choice for this.

Please advise.

It sounds like you’re describing this:

  1. My application needs to operate on a list of numbers, provided by a user.
  2. the user can provide an ad-hoc list (for simple short entries).
  3. the user can provide a text file if that’s easier for them.
  4. I don’t want the operation code to care about how the user provided the data, just that it receives its list of numbers.

That sounds to me like 3 responsibilities:

  1. Receiving the input from the form
  2. mapping the input to your application’s domain
  3. performing the operation.

You’re asking about the way to do 1 & 2.

If you’re using Rails, a schema for step 1 might look like:


class NumberInputContract < Dry::Validation::Contract
  params do
    optional(:text_input).filled(:string)
    optional(:file_input).value(type?: ActionDispatch::Http::UploadedFile)
  end
end

that makes the presence of both a string param called text_input or an uploaded file param called file_input. You can use rules to require that one and only one of those is provided.

You could also use rules to be more specific, eg:

  rule(:text_input) do
    entries = value.split("\n")
    key.value('must be a list of numbers') unless entries.all? { |e| e.match /^[0-9]+$/ }
  end

What I would do, though, is use a simple schema to determine if you were given text or a file, and then use a second object to turn that into a Result:

Dry::Validation.load_extensions(:monads)
class ParseInputForm
  include Dry::Monads::Do.for(:call)

  class NumberInputContract < Dry::Validation::Contract
    params do
      optional(:text_input).filled(:string)
      optional(:file_input).value(type?: ActionDispatch::Http::UploadedFile)
    end
  end

class << self
 def call(form_params)
    contract = Contract.new

    input = yield contract.(form_params).to_monad

    if input[:file_input]
      yield parse_file_contents(input[:file_input])
    else
      yield parse_text_contents(input[:text_input])
    end
 end  

  private
  def parse_file_contents(file)
    #...
  end

  def parse_text_contents(text)
    #...
  end
end

Now your controller action can be:

  def create
    numbers = ParseInputForm.(params.permit(:file_input, :text_input))
    if numbers.success?
      do_the_thing_with_the_numbers(numbers.value!)
    else
      show_some_error_message
    end
  end
2 Likes