Mastering Rails Validations: Objectify

In my previous blogpost I showed you how Rails validations might become context dependent and a few ways how to handle such situation. However none of them were perfect because our object had to become context-aware. The alternative solution that I would like to show you now is to extract the validations rules outside, making our validated object lighter.

Not so far from our comfort zone

For start we are gonna use the trick with SimpleDelegator (we use it sometimes in our Fearless Refactoring: Rails Controllers book as an intermediary step).

class UserEditedByAdminValidator < SimpleDelegator include ActiveModel :: Validations validates_length_of :slug , minimum: 1 end

user = User . find ( 1 ) user . attributes = { slug: "summertime-blues" } validator = UserEditedByAdminValidator . new ( user ) if validator . valid? user . save! ( validate: false ) else puts validator . errors . full_messages end

So now you have external validator that you can use in one context and you can easily create another validator that would validate different business rules when used in another context.

The context in your system can be almost everything. Sometimes the difference is just create vs update. Sometimes it is in save as draft vs publish as ready. And sometimes it based on the user role like admin vs moderator.

One step further

But let’s go one step further and drop the nice DSL-alike methods such as validates_length_of that Rails used to bought us and that we all love, to see what’s beneath them.

class UserEditedByAdminValidator < SimpleDelegator include ActiveModel :: Validations validates_with LengthValidator , attributes: [ :slug ], minimum: 1 end

The DSL-methods from ActiveModel::Validations::HelperMethods are just tiny wrappers for a slightly more object oriented validators. And they just convert first argument to Array value of attributes key in a Hash .

Almost there

When you dig deeper you can see that one of validates_with responsibilities is to actually finally create an instance of validation rule.

class UserEditedByAdminValidator < SimpleDelegator include ActiveModel :: Validations validate LengthValidator . new ( attributes: [ :slug ], minimum: 1 ) end

Let’s create an instance of such rule ourselves and give it a name.

Rule as an object

We are going to do it by simply assigning it to a constant. That is one, really global name, I guess :)

SlugMustHaveAtLeastOneCharacter = ActiveModel :: Validations :: LengthValidator . new ( attributes: [ :slug ], minimum: 1 ) class UserEditedByAdminValidator < SimpleDelegator include ActiveModel :: Validations validate SlugMustHaveAtLeastOneCharacter end

Now you can share some of those rules in different validators for different contexts.

Reusable rules, my way

The rules:

SlugMustStartWithU = ActiveModel :: Validations :: FormatValidator . new ( attributes: [ :slug ], with: /\Au/ ) SlugMustHaveAtLeastOneCharacter = ActiveModel :: Validations :: LengthValidator . new ( attributes: [ :slug ], minimum: 1 ) SlugMustHaveAtLeastThreeCharacters = ActiveModel :: Validations :: LengthValidator . new ( attributes: [ :slug ], minimum: 3 )

Validators that are using them:

class UserEditedByAdminValidator < SimpleDelegator include ActiveModel :: Validations validate SlugMustStartWithU validate SlugMustHaveAtLeastOneCharacter end class UserEditedByUserValidator < SimpleDelegator include ActiveModel :: Validations validate SlugMustStartWithU validate SlugMustHaveAtLeastThreeCharacters end

or the highway

I could not find an easy way to register multiple instances of validation rules. So below is a bit hacky (although valid) way to work around the problem.

It gives us a nice ability to group common rules in Array and add or subtract other rules.

Rules definitions:

format_validator = ActiveModel :: Validations :: FormatValidator length_validator = ActiveModel :: Validations :: LengthValidator class SlugMustStartWithU < format_validator def initialize ( * ) super ( attributes: [ :slug ], with: /\Au/ ) end end class SlugMustEndWithZ < format_validator def initialize ( * ) super ( attributes: [ :slug ], with: /z\Z/ ) end end class SlugMustHaveAtLeastOneCharacter < length_validator def initialize ( * ) super ( attributes: [ :slug ], minimum: 1 ) end end class SlugMustHaveAtLeastThreeCharacters < length_validator def initialize ( * ) super ( attributes: [ :slug ], minimum: 5 ) end end

Validators using the rules:

CommonValidations = [ SlugMustStartWithU , SlugMustEndWithZ ] class UserEditedByAdminValidator < SimpleDelegator include ActiveModel :: Validations validates_with * ( CommonValidations + [ SlugMustHaveAtLeastOneCharacter ] ) end class UserEditedByUserValidator < SimpleDelegator include ActiveModel :: Validations validates_with * ( CommonValidations + [ SlugMustHaveAtLeastThreeCharacters ] ) end

Cooperation with rails forms

The previous examples won’t cooperate nicely with Rails features expecting list of errors validations on the validated object, because as I showed in first example, the #errors that are filled are defined on the validator object.

validator = UserEditedByAdminValidator . new ( user ) unless validator . valid? puts validator . errors . full_messages end

But you can easily overwrite the #errors that come from including ActiveModel::Validations , by delegating them to the validated object, which in our case is #user .

class UserEditedByAdminValidator include ActiveModel :: Validations delegate :slug , :errors , to: :user def initialize ( user ) @user = user end validates_with * ( CommonValidations + [ SlugMustHaveAtLeastOneCharacter ] ) private attr_reader :user end

What next?

That was a brief introduction to the more object oriented aspects of rails validations. Subscribe to our newsletter below if you don’t want to miss our next blogpost that are going to be about problems with refactoring in rails, active record aggregates, another part on validations problems and service objects. We have plenty of ideas for our next posts.

You might also want to read some of our other popular blogposts ActiveRecord-related: