Rails Form Objects With dry-rb
Building form objects in modern Rails applications is nothing new.
Most Ruby developers are familiar with including Virtus and ActiveModel::Validations to their form objects.
Today I would like to show how to build a form object using dry-types and dry-validation
Let’s build a simple form object for creating postcards. We have 3 models inside the application:
is_state_requiredfields. The second field indicates that to create a proper address, a user needs to provide a state - like in the US.
contentand address fields.
- Form creates new postcard (quite obvious).
- Validates presence of address, city, zip code, content and country.
- Validates format of zip code.
- Validates length of content (if you want something short just tweet it or text it).
- If selected country requires a state, presence of state should be validated.
Let’s start with attributes definition. Our form object needs to inherit from
Dry will generate appropriate getters and the constructor.
Dry::Types.module module and use the types.
Dry-types comes with a wide choice of primitive types along with modifiers.
Things get a bit more complicated when we would like to use our rails models.
We need to register our types in order to create attributes with these types.
Which is done as follows:
TypeName = Dry::Types::Definition.new(::MyRubyClass)
You can also tell dry-types how should the Type be constructed by calling
.constructor with a block.
So our definitions looks like this:
Now we can use
CountryState as types. So finally form’s definition looks like this:
class CreateForm < Dry::Types::Struct
Yeah, we managed to create a simple struct.
If we don’t specify constructor type, a strict constructor will be generated, which means it will throw
ArgumentError if an attribute is missing.
We will handle presence validation using dry-validation so we will use schema or symbolized constructor
more information on constructor types here.
To use schema constructor type we need to call
constructor_type(:schema) inside class body.
To perform validations inside our form object we’ll use dry-validation gem.
It comes with a wide variety of predicates, which are simple to use. Lets start with presence validation:
PostcardSchema = Dry::Validation.Schema do
We create a schema to which we pass model attributes, defined previously, like this:
errors = PostcardSchema.call(to_hash).messages(full: true)
Ok so what’s happened here:
to_h) generates a hash based on attributes
.messages(full: true)returns full error messages
To pass more requirements to validation, like format, length and so on just pass parameters to
Lets take content as an example, not only it should be present but also it should be longer than 20 characters:
The full list of ready to use predicates can be accessed here.
Presence or length validations are delivered by dry-validation.
Unfortunately(?) in real live application those are usually not enough, that’s why dry-validation allows us to write our own predicates.
Let start with a simple one, which will check if the country passed to validation requires state.
PostcardSchema = Dry::Validation.Schema do
As simple as that, just remember to put proper error message to your
errors.yml file, more information about errors file here.
So let’s get to the core and check presence of state only if the country requires it.
We need to tell validation that the state might be present (or might be not).
To do this just put the following line to our schema:
Defining the rule itself goes as follows:
rule(country_requires_state: [:country, :state]) do |country, state|
As simple as that:
- we pass rule name along with the fields that are required inside the rule.
In our case we use country and state.
- Those variables are yielded to the block.
- The rule is translated like this if state is required then check presence of state.
More information about high level rules is available here
Full project, including models and spec is available on my github.
Just to make things simple and readable in a blog post I put everything connected to the object itself into one file.
In real life application this is probably not the best way of organizing your codebase. What can be done:
Typesmodule could be placed as a separate module, probably even a global scope one
PostcardSchemacould be placed outside this form object and used in, for example, update form
- the same goes to constants -
Using dry-types allows you to write type safe components to your application.
This library comes with a wide choice of ready to use types and it’s quite easy to define your own.
I feel like dry-validation approach is more clean than ActiveModel one. You put all your validation logic inside one, clearly bounded place.
Those validations can be easily reused by other forms (like update ones).
The biggest problem with dry-rb, similarly to ROM and Roda, is a lack of documentation which allows fresh users to start easily.
Trust me or not I spent over 2 hours writing the form object. Mostly because the problems with the documentation and lack of blogposts.
So hopefuly this blogpost will save someone’s 2 hours.