Rails Active Record Validation Contexts With Inheritance

If you’ve used validates in a Rails Active Record model, you know they work great – at least until the first bit of complexity arises. At that point, they can quickly morph in to a ball of conditional spaghetti convoluting the initial reason the validation was added in the first place.

I recently had an experience using has_secure_password where I wanted control the length of the user-supplied password. Adding a length validation to the password accessor invalidated existing records, so I was in a bit of a bind. In the end, I sub-classed the Active Record model to create a unique model made specifically for that context. This allowed me to inherit the core functionality from the model and sprinkle on existing validations for specific use cases. This was a new tactic for me and I’m still not sure how I feel about it. I like the fact that it removed complexity from the User model. This, in hopes, will keep the minimize the likelihood of it becoming a God object.

The Problem

Using has_secure_password is a relatively easy way to add authentication to a Rails app. In order to disguise the plain text passwords, an accessor for the plain password is added that encrypts it before saving.

The only true Active Record validation has_secure_password adds is a confirmation of the password — and only when the password is present. This allows us to create a user object without supplying a password, or maybe saving straight to the password_digest field, which is used to store the encrypted password in the database.

I wanted to enforce a minimum password length, because what good is a 1 character password (or 0 for that matter) ?!?!

The first thing I did was add this to the User model:

validates :password, length: { minimum: 8 }

This works for new users, but not for those with a password_digest already. Attempting to updated an existing user produces the following error:

ActiveRecord::RecordInvalid: Validation failed: Password is too short (minimum is 8 characters)

The next step was to conditionalize only on create:

validates :password, length: { minimum: 8 }, on: :create

Except, that wasn’t right either because I’d definitely want to allow users to update their password, in which case, the length validation wouldn’t be enforced.

I found another post suggesting to allow nil using:

validates :password, length: { minimum: 8 }, allow_nil: true

But, again, that felt weird and doesn’t read particularly well when you’re looking through the source trying to understand what condition would generate a nil password.

Other solutions included mixing conditionals and checking model dirty state and some combination of all of the above.

I’m guessing some combination of the above would’ve worked, but something didn’t feel quite right. A quick glance over any of those solutions left me wanting something cleaner and more approachable. Because it’s a complex and tremendously important part of the app, I wanted to feel comfortable with the solution.

The Solution

I recently read Growing Rails Application in Practice. The most interesting takeaway for me was the idea of sub-classing an Active Record object to exactly the problem described above.

Consider this…we have our User model with has_secure_password:

class User < ActiveRecord::Base
  has_secure_password
end

As we saw above, the variety of validation contexts made the standard ActiveModel validation awkward. What if we sub-class User and add the validation contexts for a specific use case? In our case, minimum length:

class User::AsSignUp < User
  validates :password, length: { minimum: 8 }
end

In this case, we’re create a separate model, for the purpose of signing up, and perhaps other user-related attribute management (profile, password reset, etc.).

Now, instead of passing the User model to the view from the controller, we pass an instantiated version of the new context-specific model class:

def create
  @user = User::AsSignUp.find(current_user.id)
  …
end

Lastly, because the sub-class name is inferred within the form, we have to do one more thing to make the params are accessible on the create action using params[:user]. We’ll change the form from:

<%= form_for @user, url: user_confirm_path(@user.invitation_token) do |f| %>

to:

<%= form_for @user, as: :user, url: user_confirm_path(@user.invitation_token) do |f| %>

Because the remainder of the app operates fine without any need for the password validation, the User can be used where necessary and without worry of it becoming invalid because the password accessor isn’t present.

Summary

While sub-classing models in Rails is generally frowned upon, this use case is one of the few that felt reasonable. It feels relatively low cost and stays in isolation. I’d love to hear how you might have solved this problem. I looked and explored a handful of solutions. While others worked, none seems as expressive as the one above.

A form object using ActiveModel or similar could’ve been an alternative option. I didn’t explore it for this particular use case, mostly because I wanted to give this one a shot. However, I have no doubt it would’ve at least worked equally as well.

I should also point out that I’m familiar with the built-in validation contexts in ActiveModel. And for whatever reason, I’ve not used them before. I’ll probably give it a shot on another occasion for comparison.

What are your thoughts on this technique?

Comments

Build a Ruby Gem Course - FREE!

Join 1,300+ other Rubyists and take your Ruby gem skills to the next level!

Whether you're an expert Rubyist, or just starting out, this FREE 10-day email course will guide you through the process of creating your own Ruby gems from start to finish.

Enter your email below to get started today: