This One Rails Validation Trick That Makes Your Code More Awesome
13 Mar 2022Rails is filled with small cool features that make life better once you know about them. One that we all know about but might not use that often is validation contexts.
If you are in a situation where an object lives through some kind of workflow or several users are building up an object over time, this might be the tool to help.
A follow-on effect is that validation contexts push you to find good names for states and actions in your application. I’ve always found that good names clarify concepts and make the code cleaner.
What are validation contexts?
The documentation has a good explanation, but here’s a quick recap:
Calls like #save
and #valid
let you pass in a symbol as a validation context.
post.valid?(:publish)
If we have validations set up like this in a class:
class Post
validates :title, presence: true, on: [ :publish ]
end
The system will only check for the presence of the title when you pass in the :publish
context.
By default, Rails will provide the context :create
or :update
when creating or updating an object. (But you can’t pass in a context to #create
or #update
!)
You can pass the :on
value to most validation DSL commands.
How they can make your code more awesome
Validation contexts drive us to create clearer code with good names and let us show different users only the errors that are meaningful to them.
Many entities evolve through a lifecycle on this website. JobPost
is an example. Multiple people will work on a JobPost
like a submitter (i.e., customer) and the publisher (me).
This approach lets us, for example, save work in progress. Or it enables the customer to get through the flow with less friction because they don’t need to fill in all fields.
The code looks something like this:
class JobPost
with_options on: [ :submit ] do
# A minimal set of attributes
end
with_options on: [ :publish ]
# More attributes with stricter requirements
end
def submittable?
valid?(:submit)
end
def publishable?
valid?(:publish)
end
def save_as_submitter
save(context: :submit)
end
def save_as_publisher
save(context: :publish)
end
end
The small but magical thing, at least for me, is wrapping the call to #valid?
in its predicate method. (I’m a little crazy about predicates.) My experience is that this makes for terse, understandable code. It’s subtle but impactful, I think.
And once you do that to valid, it seems to make sense to do it for save, too. So, #save_as_publisher
reveals itself.
That makes your code more readable, and it also lets you communicate to different errors to the user depending on, err, context. (I’ve also found that this makes code in controllers more intention revealing!)
Another example
I worked on a project where I had to ingest noisy legacy data. Lots of this data failed the validation rules for the new app, but I couldn’t discard it. The solution here was validation contexts.
class Contact
with_options on: [ :ingest ] do
validates :state, format: /regex/
# More validations ...
end
with_options on: [ :create, :update ] do
validates :state, included: %w[ MA NH RI ]
# More validations ...
end
def ingestable?
valid?(:ingest)
end
def save_as_ingester
save(context: :ingest)
end
end
This approach let me get most of the legacy data into the new system and let humans resolve problem cases over time. (Or it let them discard it with more confidence and transparency.)
Once I got into the habit of using them, I started finding that I keep finding more places to use them.
In conclusion
Using validation contexts may seem like a slight change in the code, but I’ve found that it helps me think more clearly about objects. It also prompts me to find names for things, which always seems to clean up code (and feels like a triumph!)
And I think it’s neat! Let me know if you’ve used this feature and how it worked out for you!