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:
- My application needs to operate on a list of numbers, provided by a user.
- the user can provide an ad-hoc list (for simple short entries).
- the user can provide a text file if that’s easier for them.
- 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:
- Receiving the input from the form
- mapping the input to your application’s domain
- 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