ry6tww
Last Updated: February 25, 2016
·
5.254K
· h3h

Using a regular expression to validate a field in a model and a segment in a route

It’s somewhat common in a Rails application to want to include a custom URL
slug as part of a model. For instance, if you have a User model and you want
those users to have a profile page that’s accessible at /users/john-smith,
then you’ll want to store that "john-smith" value on the model and ensure that
it’s unique. I call this field a “slug”.

Let’s say you want to let users edit their own slug and decide what will go in
their profile URL. In this case, you want to add some restrictions to the
characters allowed in the slug, lest Little Bobby TABLES
pay you an unwelcome visit.

So you want a regular expression format validation on the model attribute
User#slug, but you also want to be able to use the same regular expression for
matching a path segment in your routes file. I accomplish this by using a constant for the
unanchored format regular expression at the top of my model:

class User < ActiveRecord::Base

  SLUG_FORMAT = /([[:lower:]]|[0-9]+-?[[:lower:]])(-[[:lower:]0-9]+|[[:lower:]0-9])*/

  # ...

Then I add an ActiveModel validation on the slug field and I add anchoring
to make the validation of the model field safe from \n and other sneaky
inputs that would evade an unanchored format validator:

validates :slug, uniqueness: {case_sensitive: false},
                 format: {with: Regexp.new('\A' + SLUG_FORMAT.source + '\z')}

Note the use of the RegExp character classes \A and \z for beginning and
end of string, not ^, $ or \Z. You can read the
RegExp docs
to understand the differences, but the gist is that \A and \z signify
exactly the beginning and end of a string, full stop. The others match different
anchor points within a string.

Now, because I stored the unanchored version of the format regular expression in
a constant, I can use that same constant in my routes file:

constraints(slug: User::SLUG_FORMAT, id: /\d+/) do
  get  '/users/:slug' => 'users#show', as: 'user'
end

Now if I ever decide to change the format of my allowed slugs, I only need to
change it in one place: my SLUG_FORMAT constant. There are other fancy things
you can do with slugs, like auto-generating them from a name field and testing
their behavior thoroughly, but I’ll leave those for another time.