When developing web applications, we often face the problem of accepting and validating user input, or some data,
coming from external sources. The so-called “Rails way” proposes following way to deal with this problem:
First, restrict form parameters at a controller level, using strong_parameters idiom
Then, validate permitted parameters at a model level, using ActiveModel::Validations
This approach has later evolved into another one, which suggests extracting validations from models into separate classes,
where all validation or even persistence-related actions are performed. This pattern is known as Form Object and usually
involves such solutions as Virtus, Reform,
Dry-Types, makandra/active_type.
Now we will try to refactor both strong_parameters and model validations using
Dry-Validation gem.
Our example application allows some users to register with a mobile phone, and some another group of users can register with an email.
The registration logic also has huge differences.
So, this model contains a bunch of validations:
phone is being validated always
email is validated only if it’s present and mobile_registration attribute is unset
password is validated only on create action, and only if it’s present and mobile_registration attribute is unset
password_confirmation must be equal to password and must be present if password is present
full_name is always validated
For me, this part of model’s code seems way too complicated. The first step in a way to breaking down this complexity is to move validations
away from the model and invoke them only in particular controller actions, where there is no need for condition checking. For example,
if password validation is performed only on create, why not to move it to respective controller’s create action? Also,
obviously we have separate actions that register a mobile user and regular users, so we won’t invoke email and password validations
for users registering with a mobile phone.
Let’s take a look at a controller actions (they are oversimplified purposefully):
Using dry-validation, we can extract all validation related logic into separate classes and also get rid of strong parameters.
What’s the matter with strong parameters? Just remember, how strong_parameters-related methods are growing and become bloated over the time.
So, let’s write a new class for User model validation, that would be used to validate users registering from the web interface:
File with custom errors will look like this:
en:
errors:
unique?: 'Is not unique'
phone: 'Phone should start with plus sign and contain only digits'
This code serves as a replacement for both strong parameters and ActiveModel::Validations. Let’s examine, what it exactly does.
And resulting controller code will be something like this:
I omit the code for the imaginary MobileUserValidator, because it mostly repeats UserValidator, except email,
password and password_confirmation validations. Making the conclusion, dry-validation provides a very convenient way to
replace both ActiveRecord and ActiveModel validations, and is also a much nicer replacement for strong_parameters, if needed.