Advanced Route Constraints With Rails
Recently, I was working on a feature sending sms messages with appointment links. The links ended up taking up most of the message body so I wanted to look into some options to shorten the link to conserve space in the body of the message.
Instead of using something like Bitly, I wanted to see if there were any simple options for something that would remain within the app. I did some googling for link shortener gems and tried a couple of the first ones that popped up.
I was able to take my links and create a shortened version that gets sent out in the sms confirmation pretty easily. Everything seemed to be going fine until I noticed some test failures. Some of my static pages were returning 404. After a bit of digging, I noticed that all my static pages, like /about
and /contact
were being intercepted by the shortener.
When it didn’t find a shortened link with an id of about
it was returning a 404 and breaking my static pages.
The easiest solution would have been to just mount the shorteners rails engine under a prefix, something like /srt
.
This is what I ended up with but wanted to attempt to solve the issue using route constraints.
Route constraints are something I’ve heard of, but have never had cause to use. I thought this would be a good chance to take a stab at routing constraints and get some more experience with them.
There are a few different options for route constraints. Some of the advanced options are using a lambda in the route definition and one that’s a little more involved that uses a class for the route constraint.
My first approach was using a lambda within the route definition but eventually moved to using a class.
Rails Guides on Advanced Route Constraints
lambda route constraint example:
get '*path', to: 'restricted_list#index',
constraints: lambda { |request| RestrictedList.retrieve_ips.include?(request.remote_ip) }
class route constrain example:
class ShortenerRouteConstraint
def matches?(request)
# if request.path is not in static routes it should be a short link
static_page_routes.exclude?(request.path)
end
private
def static_page_routes
# dynamically pull all static pages + admin, 404, and errors
static_page_paths = Dir.new("app/views/static").children.map do |f|
"/#{File.basename(f, ".html.erb")}"
end
static_page_paths << "/admin"
static_page_paths << "/404"
static_page_paths << "/500"
end
end
My first pass had me adding the routes of my static pages to an array, and checking the request.path
to see if it was included in the request.
That worked just fine, but after I added a new static page, I got another test fail for 404 errors (this happens when the shortener can’t find an object).
I wanted to see if there was a way to make that check dynamic and keep from updating manually, usually after seeing a spec fail and having to refresh my memory on what’s happening.
I created a route constrain class matching the one above. After having the route constraint class, the next step was to update the routes to use that constraint class.
# config/routes.rb
require 'app/services/ShortenerRouteConstraint.rb'
get "/:id" => "shortener/shortened_urls#show", constraints: ShortenerRouteConstraint.new
Note: I really didn’t know the best place to put that class, so just defaulted to putting it in my app/services
directory and requiring that file at the top of my config/routes.rb
Checking the routes against a dynamic list of view files is absolutely overkill, but wanted to see if I could find a way to set and forget.
After a few minutes skimming through the Ruby docs and found what I needed.
# dynamically pull all static pages
static_page_paths = Dir.new("app/views/static").children.map do |f|
"/#{File.basename(f, ".html.erb")}"
end
This creates a new Dir object for the app/views/static
directory. We access its children, and use map
to return a new array with a format that matches our path for the route constraint.
This will pull the name of any view ending in .html.erb
, including partials, in the app/views/static
directory.
The output looks something like
["/index",
"/contact",
"/success",
"/privacy",
"/pricing",
"/about",
"/terms"]
After checking all the pages in the static directory, I add /admin
, /404
, and /500
to the constraint for the other routes outside of my static pages it was having conflicts with.
Now, whenever I add a new page to my static controller, that route will be added to the whitelist of static pages that should not be checked against shortened routes.
Like I mentioned before, I ended up going a different route (see what I did there?) but thought it was a good exercise in over-engineering a solution for the fun of it.