HomeRamblings  ⁄  ProgrammingRuby Language

Dynamic Routing in Rails Revisited

Published: April 15, 2015 (over 2 years ago)
Updated: over 2 years ago

Ever since my original post Creating Dynamic Routes at runtime in Rails 4, I have been digging into Rails routing more and more. I’ve gotten a lot of feedback on the post itself as well as comments in the IRC #rubyonrails channel with an over abundance of passionate pleas not to do routing this way in Rails.

But it seems a lot of people desire do to it this way and I have even been inspired to put a gem together to do it. A few days ago, I introduced a refactored version at 0.1.1 and then two days later, a radically refactored version at 0.2.2. Let’s talk about this gem and why it’s upsetting some Rails purists.

This is a long post and describes a bit of my journey from a good solution to a slightly better to ultimately what is probably my best use of advanced Ruby techniques to-date. There were a lot of lessons learned along the way so I’m pulling them together in this post. Hopefully it shows you a behind-the-scenes glimpse in an enlightening way as to how I advanced my art of developing in Ruby as I chase mastery of the language. To that end, I’ll cover the biggest objection to using this gem, then how the gem was built and finally, I’ll touch briefly on putting the gem to use in your projects.

Objection!

First, let’s get the ugly out on the table. What is “This Way” anyhow? This gem solves a routing issue in a decidedly non-Railsy way. The Rails way is to declare routes at the resource level or action by action and to follow the RESTful way as much as it makes sense to do so. In short, the fewest number of route declarations you need to get your job done.

For example, consider the following:

1
2
3
4
Cc::Application.routes.draw do
  resources :posts, controller: :ramblings, only: [:create, :new, :edit, :update]
  patch 'revert_to_revision/:id', controller: :ramblings, action: :revert, as: :revision
end

This gives us the following set of routes:

    posts POST   /posts(.:format)                  ramblings#create
 new_post GET    /posts/new(.:format)              ramblings#new
edit_post GET    /posts/:id/edit(.:format)         ramblings#edit
     post PATCH  /posts/:id(.:format)              ramblings#update
          PUT    /posts/:id(.:format)              ramblings#update
 revision PATCH  /revert_to_revision/:id(.:format) ramblings#revert

That’s all that’s needed to handle create, insert, update, delete, and showing of posts and in the case of the revert_to_revision, an action that can revert an edited post back to another revision.

That’s a clean and “unpolluted” routing space by the purist’s standing.
The (Flowmor Router)[https://github.com/mwlang/flowmor_router], by contrast, creates an explict route for every Post in the database like this:

        ramblings_hello_world GET    /ramblings/hello-world(.:format)         ramblings#show {:id=>1}
           ramblings_why_macs GET    /ramblings/why-macs(.:format)            ramblings#show {:id=>3}
ramblings_a_model_free_wizard GET    /ramblings/a-model-free-wizard(.:format) ramblings#show {:id=>7}
  ramblings_drying_your_views GET    /ramblings/drying-your-views(.:format)   ramblings#show {:id=>8}
  ramblings_geocoded_zipcodes GET    /ramblings/geocoded-zipcodes(.:format)   ramblings#show {:id=>11}
 ramblings_pradipta_s_rolodex GET    /ramblings/pradipta-s-rolodex(.:format)  ramblings#show {:id=>12}
 ...

When the number of routes grows considerably, then site visitors will start to notice a sluggish website. This is a fine argument and one definitely worth considering before you employ such a gem as the Flowmor Router. The theory is sound, but in practice, I am finding that even with 2 or 3,000 route entries, things are very responsive from a routing perspective, so the trade-off to me is negligible against the benefits gained. The Journey router is simply that good!

Refactor, Take #1

I was prompted to refactor the gem when I started refactoring the code for my blog and realized that I had chosen odd names (like derived_field_name instead of title_field) for the various options and also wasn’t happy that I couldn’t just declare everything in one go. I had chosen to inherit from RoutableRecord (which was an abstract model class descended from ActiveRecord::Base) and that choice actually turned out to be a limiting one. What if I needed a base class in my project to which all other models inherited?

Not only that, I needed two different routes for several models because WordPress had them and the search engines knew about ‘em and I wanted to keep my site’s traffic flowing for those indexed pages. I was getting tangled in my own web and wanted it to be a lot easier to declare both routes so I wasn’t making stupid programming mistakes. In a nutshell, I wanted this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Wordpress::Category < ActiveRecord::Base
  acts_as_routable  \
    :category, 
    scope: -> { joins(:posts).uniq },
    controller_action: "ramblings#taxonomy",
    name: :route_name

  acts_as_routable \
    :archives,
    suffix: :category,
    scope: -> { joins(:posts).uniq },
    controller_action: "ramblings#archive",
    name: :route_name
    
  def route_name
    self.slug.blank? ? self.name.strip.underscore.parameterize : self.slug
  end
end

For whatever reason, the slug field wasn’t always populated, so route_name takes care of that by falling back to the name field that’s been “properly” parameterized.

Instead, what I had was this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class WpTermTaxonomy < ActiveRecord::Base
  self.primary_key = "term_taxonomy_id"
  self.table_name = "wp_term_taxonomy"

  def route_base
    cat = case taxonomy
      when "category" then "category"
      when "link_category" then "link"
      when "post_tag" then "tag"
      else taxonomy
    end
  end
  
  def route
    "/#{route_base}/#{slug}"
  end
  
  def archive_route
    "/archives/#{route_base}/#{slug}"
  end

Along with the following route redraw block:

1
2
3
4
5
6
7
8
9
10
class DynamicRouter
  def self.load
    Cc::Application.routes.draw do
      WpTermTaxonomy.categories.reject{|r| r.slug.blank?}.each do |wtt|
        get wtt.route, to: "ramblings#taxonomy", defaults: { id: wtt.term_taxonomy_id }
        get wtt.archive_route, to: "ramblings#archive", defaults: { id: wtt.term_taxonomy_id }
      end
    end
  end
end

I’ll cover how I went from WpTermTaxonomy to Wordpress::Category in another post (that case statement also greatly annoyed me), but for now, notice how I have both route and archive_route combined with the loop that constructs a route for both.

But the worse sin I was committing was that I forced those using the gem to learn the internals of the gem’s implementation in order to further configure routing to their preferences. Basically, they have to know about #route and override the implementation with their own. I documented this in the README, but still, it felt like, well, lumpy Velveeta, to put it politely. It may’ve been object-oriented, but it sure wasn’t at all Rubyish.

Well, in my first refactor, I managed to introduce the acts_as_routable usage pattern in which everything could be declared and configured in that call and one could pass in Procs as options so lazy evaluation was possible, but when I got to part two, doing two different routes on the same record instance, I was effectively stuck. I didn’t know how to proceed, so I released 0.1.1 and chilled for a bit.

Upon reflection, I realized the the trouble was, I implemented the acts_as_routable by mixing in all the methods to the ActiveRecord::Base class and including/extending the current ActiveRecord model when acts_as_routable was invoked. This meant two routes collided whereby the last acts_as_routable masked any previous declarations. To solve this, I knew I had to jettison all those methods from the ActiveRecord::Base model and implement a combined decorator/observer pattern with minimal intrusion. Exactly how to do this really forced me to study the Ruby language’s more advanced features and in the course of experimenting, I came up with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class Foo
  def hello
    puts "gonna say hello"
    "Hello #{world}"
  end
  
  def world
    "World"
  end
  
  attr_reader :say_it
  
  def initialize options = {}
    @say_it = options[:another_way] || method(:hello)
    define_singleton_method(:world, options[:another_world]) if options[:another_world]
  end
  
  def speak
    instance_exec &say_it
  end
end

begin
  foobar = Foo.new
  puts "lazy?"
  puts foobar.speak
rescue Exception => e
  puts "#{e.class.to_s}: #{e.message}"
end

begin
  foobaz = Foo.new(another_way: lambda { "#{hello}, Sir!" })
  puts "lazy?"
  puts foobaz.speak
rescue Exception => e
  puts "#{e.class.to_s}: #{e.message}"
end

begin
  foobaz = Foo.new(another_world: lambda { "Earth" })
  puts "lazy?"
  puts foobaz.speak
rescue Exception => e
  puts "#{e.class.to_s}: #{e.message}"
end

class Bar
  def self.register(name = nil, options = {})
    if name.nil?
      name = "foobaz"
    elsif name.is_a?(Hash) && options == {}
      name, options = "foobar", name
    end
    puts [name, options].inspect
  end
end

Bar.register :something, :foo => 1, :bar => 2
Bar.register \
  foo: 2, 
  bar: -> { puts "yeah" }, 
  baz: 3
Bar.register

Running the above script gives the following output:

>> ruby foo.rb
lazy?
gonna say hello
Hello World
lazy?
gonna say hello
Hello World, Sir!
lazy?
gonna say hello
Hello Earth
[:something, {:foo=>1, :bar=>2}]
["foobar", {:foo=>2, :bar=>#, :baz=>3}]
["foobaz", {}]

Basically, I tried to learn how to do two things: 1) Replace an existing method’s default implementation with one passed in. 2) Detect how options were passed in so I could intelligently decide if the acts_as_routable actor had been explicitly named or not. As it turned out, I didn’t have to go quite so far as replacing method implementations and I’ll show you how I ultimately handled lazy evaluations below. What I will say is that the above experiment was instrumental in my taking my Ruby mastery to another level. This little experiment helped me better understand how lambdas/Procs worked and how that mechanism could be leveraged to solve my current challenge.

Refactor, Take #2

In order to have multiple actors on one model, there had to be virtually zero intrusion on the ActiveRecord::Base model to which we extend with the ability to act_as_routable. I needed a class that could track calls to acts_as_routable and get a handle back to the class from within the ActiveRecord::Base model instance as well as from a global perspective when we redraw all the routes. In other words, I needed a registry of models that were declared as routable. This led to the RouterClasses class which I call “the handler.”

Handling Optional Options

The last final bit I wanted as part of my acts_as_routable method was the ability to declare the actor’s name or leave it off so that it’s implicitly taken from the Model class name. In other words, for the following:

1
2
3
4
class Post < ActiveRecord::Base
   acts_as_routable 
   acts_as_routable :archive
 end

Would lead to two routes per post where the actor for the first is “posts” and the actor for the latter is posts_archive. This bit of wizardry was accomplished like this:

1
2
3
4
5
6
7
8
9
def acts_as_routable(actor = nil, options = {})
  # juggle what was passed in to correct variables
  actor, options = nil, actor if actor.is_a?(Hash) && options == {}
  if actor.nil?
    singular_name = name.underscore.downcase.split("/").last
    actor = singular_name.pluralize 
    options[:controller_action] = options[:controller_action] || "#{singular_name}#show"
  end
end

When the actor was actually the Hash intended for options, options came through as {} while actor was a Hash — That was an easy check and a quick switch at the outset of the method call. At which point, I could infer the actor’s name from the model’s class name.

The RouterClasses handler

When a model is declared as routable, I set a class level attribute (ActiveSupport’s class_attribute) on the RouterClass that was instantiated for the model. Because there could be more than one actor on a model, I gave this attribute a unlikely collisive name. This attribute wasn’t entirely necessary, but it provides a way to hook back into this class’ instance and do more advanced things if necessary. The best use case I have so far is for test driven development and easier unit test cases. While this is a little bit of the tail wagging the dog, I didn’t see a good reason not to do it and it gives other developers flexibility to do something I might not have thought about in implementing the gem. Here’s what that bit looks like:

1
2
3
4
5
6
def acts_as_routable(actor = nil, options = {})
  # Register and assign a distinctive name to the router_class
  router_class = FlowmorRouter::RouterClasses.register(actor, self, options)
  class_attribute router_class.named_instance
  self.send "#{router_class.named_instance}=", router_class
end

To solve the problem of going from the model instance back to the handler, when acts_as_routable is invoked, three new methods are meta-generated like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def acts_as_routable(actor = nil, options = {})
  router_class = FlowmorRouter::RouterClasses.register(actor, self, options)
  
  define_method router_class.route_name_method_name do 
    router_class.route_name self
  end
  
  define_method router_class.url_method_name do 
    router_class.url self
  end
  
  define_method router_class.path_method_name do 
    router_class.path self
  end
end

That bit right there declares the Model#path and Model#url as well as Model#route_name methods so that we can do @post.path, @post.url, and @post.route_name. A couple interesting things are going on here. First, the router_class variable is actually evaluated within the define_method block, so even though it’s short-lived within acts_as_routable, it’s captured and utilized on any calls to these methods at run-time. How neat! That’s the power of Ruby right there.

Secondly, I’m using url_method_name and path_method_name to allow me to have two or more actors on a model. The first time acts_as_routable is called, just #path and #url methods are generated, but the subsequent calls prefix those same methods with the actor name. For example acts_as_routable :archive generates #archive_path and #archive_url Are you digging this, yet?

For further reading, study up on define_method, define_singleton_method, instance_eval, class_eval, exec, and instance_exec. If you can master these, then you’ll be moving swiftly along the journey to truly mastering Ruby.

Defining a Routable Scope

The next problem I wanted to solve was the ability to define a scope on what records were routable and which were omitted from route generation. This led to the following:

1
2
3
def acts_as_routable(actor = nil, options = {})
  scope router_class.scope_name, options[:scope] || lambda {}
end

To pass a scope through, all I needed was acts_as_routable scope: -> { where published: true } (assuming the model had a published field and I only wanted to route published records). In this case, since scope takes a lambda on ActiveRecord::Base already, I just had to give it a name I didn’t think would collide with another developer’s work and then call it when it came time to draw the routes. That was accomplished during route redraws with this little ditty:

1
2
3
4
5
6
7
8
9
10
11
Rails.application.routes.draw do

  FlowmorRouter::RouterClasses.router_classes.each do |router_class|
    router_class.routable.each do |record|
      get router_class.route_path(record),
        to: router_class.controller_action,
        defaults: { id: record.id },
        as: router_class.route_name(record)
    end
  end
end

Notice in the above how I’m calling on those meta-generated methods! RouterClasses’ #routable method simple called the Model’s routable scope that we declared earlier.

Dealing with Rails’ Reload and Lazy auto-load of Models

Lastly, I noticed during development mode that model classes are lazy-loaded and in such situations, they may not auto-load on their routes, whereas the classes that got eager loaded at application initialization time also loaded up their routes. Basically Rails initialization loaded the model files and then later made the call on the routes#redraw and so those models got loaded just fine while the lazy-loaded models did not. This isn’t a problem in Production as all classes are eager loaded when the application starts up. To solve this issue, these lines were added to the acts_as_routable implementation:

1
2
3
4
5
6
7
8
9
10
def acts_as_routable(actor = nil, options = {})
  begin
    unless Rails.configuration.eager_load || Rails.env == "test"
      Rails.application.routes_reloader.reload! 
    end
  rescue SystemStackError
    # NOP -- Supressing Stack Level Too deep error
    # caused by models being loaded lazily during development mode.
  end
end

While I wanted to reload in development mode, I didn’t want to auto-load those routes during test so as to allow more careful and explicit route handling during test scenarios. Occasionally, we get a race condition on route reloads by calling it within the acts_as_routable method. I wasn’t able to determine exactly why this was the case, but simply catching the exception and squashing it resolved the issue. It’s not something that will be seen in production unless eager loading is set to false, so it’s not a real concern for me to address more resolutely at this point.

So, altogether that gives us an acts_as_routable implementation like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def acts_as_routable(actor = nil, options = {})
  # juggle what was passed in to correct variables
  actor, options = nil, actor if actor.is_a?(Hash) && options == {}
  if actor.nil?
    singular_name = name.underscore.downcase.split("/").last
    actor = singular_name.pluralize 
    options[:controller_action] = options[:controller_action] || "#{singular_name}#show"
  end
  
  # Register and assign a distinctive name to the router_class
  router_class = FlowmorRouter::RouterClasses.register(actor, self, options)
  class_attribute router_class.named_instance
  self.send "#{router_class.named_instance}=", router_class

  scope router_class.scope_name, options[:scope] || lambda {}

  define_method router_class.route_name_method_name do 
    router_class.route_name self
  end
  
  define_method router_class.url_method_name do 
    router_class.url self
  end
  
  define_method router_class.path_method_name do 
    router_class.path self
  end
  
  begin
    unless Rails.configuration.eager_load || Rails.env == "test"
      Rails.application.routes_reloader.reload! 
    end
  rescue SystemStackError
    # NOP -- Supressing Stack Level Too deep error
    # caused by models being loaded lazily during development mode.
  end
end

What About Lazy Evaluations?

Next on the block, I had to figure out how to have my cake and eat it too…that is, how could I lazy evaluate some of the options so I could do things like “/:category_name/posts/:post_name” or perhaps “/archives/category/:category_name” or even “/by_category/:category_name/posts/:post_name”

Then it hit me. ActiveRecord callbacks does it swimmingly like this:

1
2
3
4
5
6
7
class Post < ActiveRecord::Base
  before_save :do_some_magic
  
  def do_some_magic
    self.title = "Presto"
  end
end

I didn’t need to get fancy with Procs, I’d just just assume the symbol passed on an option was a method to be called! Bam! Here we go:

1
2
3
4
5
6
7
class Post < ActiveRecord::Base
  acts_as_routable name: :do_some_magic
  
  def do_some_magic
    self.name || self.title.downcase.parameterize
  end
end

The magic that makes the above works is this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class RouterClasses
    
  attr_reader :name_field_attribute, :custom_name

  def initialize actor, model, options
    @name_field_attribute = options[:name_field] || :name
    @title_field_attribute = options[:title_field] || :title
    @custom_name = options[:name]
  end
  
  def to_param value
    return unless value
    value.downcase.gsub(/[^\w\s\d\_\-]/,'').gsub(/\s\s+/,' ').gsub(/[^\w\d]/, delimiter)
  end

  def name_field record
    record.send(name_field_attribute)
  end
  
  def name record
    name = record.send(custom_name) if custom_name
    name ||= name_field(record) || to_param(title_field(record))
    raise UnroutableRecord if name.to_s.strip.blank?
    return name
  end
end

As you can see, if :name is supplied, we just call the method on the record (which we have because we’re iterating over the records during route redraws and passing in via #route_name(record)). We didn’t need to get fancy with passing real procs and/or replacing an existing implementation after all!

Once I realized the power of this pattern, I employed it for a number of other options and then I realized something else just as powerful. I could look at what was passed and do something different when a Proc was passed than when a plain ole symbol was passed. This manifested itself on the :prefix and :suffix properties. For the most part, prefixes and suffixes were just literals along the route path (i.e. :prefix => [:category, :posts] renders “/category/posts/” prefix to the full route). Which was fine until I realized I also needed to substitute an actual category name on some routes like “/ramblings/ruby-programming” (where the category was “ruby-programming”). I didn’t want multiple options to handle it and felt I stretched it a bit with :name and :name_field as it were. Doing it as a Proc would do the trick.

Here’s an example of a prefix defined via a Proc:

1
2
3
4
5
6
7
8
class Post < ActiveRecord::Base
  belongs_to :category
  acts_as_routable :category, prefix: -> { :category_name }
  
  def category_name
    self.category.try(:name) || "General"
  end
end

That gives us a route like “/category/ruby-programming/this-is-the-snazz-bomb” To do this, a very simple check for what was passed in for the options was all that was needed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RouterClasses
  
  attr_reader :prefix

  def initialize actor, model, options
    @prefix = options[:prefix] if options[:prefix]
  end

  def route_prefix record
    return if @prefix.nil?
    Array(@prefix.is_a?(Proc) ? record.send(prefix.call) : @prefix)
  end
  
end

So, when it’s a Proc, we invoke the Proc using #call, which returns the symbol it contained when we declared it, then we send that symbol to the model instance, which, (you hopefully noted above) has the method declared on it.

Conclusion

While the (Flowmor Router gem)[https://github.com/mwlang/flowmor_router] definitely departs from the Rails-way with regards to how routes are declared for records, it is, nevertheless, proving to be a solid approach to the problem of routing unique route names that otherwise have no other sensible pattern to them. One thing I always hated about the catch all matcher encountered in many-a-rails project is that you often ended up in a controller with non-sensical parameters and often stumped as to how you got there. This gem removes that risk altogether.

As to the Rails Purist’s protest, just remember, it’s ok to break the rules when you know the rules and know when it’s appropriate to break them. If you break them blindly, be prepared to deal with the consequences. In this case, the principal consequence is that you’ll have to find a way to sync multiple instances of a running application when model saves results in changes to the routing table. With a single app instance, this is not a problem. Personally, with Nginx/Passenger and an ordinary blog, this also isn’t a problem even if I have multiple processes serving up the app. Those workers get recycled fairly frequently as it is so the blog post is gonna be there one way or another in a matter of minutes in most cases.

Ruby is a powerful language. Remember to take time out on a regular basis to look at other people’s code and study why they wrote something the way they did. I arrived here with my solution by digging deep into ActiveRecord, ActiveSupport, and circling back through core Ruby classes to understand how some of those features were pulled together.

comments powered by Disqus