Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Controlling routes loading order from Engines

So in Rails3 Engines come with their own models/controllers/views and of course routes. Now the question is: How do you ensure that Engine routes will be loaded before (or after) application routes and all other Engines that are present?

Here's an example of my Rails app routes:

match '*(path)', :to => 'foo_controller#bar_action'

And my Engine:

match '/news', :to => 'bar_controller#foo_action'

So by default Engines routes will be loaded after the application ones. This means that Engine routes are inaccessible due to that catch-all route in my app. How can force Engine routes to be loaded first (or last)?

like image 280
Grocery Avatar asked Aug 02 '11 16:08

Grocery


1 Answers

What you're trying to do is kinda difficult. As has been mentioned engine routes are loaded after the app routes and overriding this behaviour may be problematic. I can think of several things that you can try.

Use An Initializer After Routing Paths Initializer

There is an initializer in engine.rb inside the rails source, one way to accomplish what you're after is to try to hook into the functionality that it deals with. The initializer looks like this by default:

initializer :add_routing_paths do |app|
  paths.config.routes.to_a.each do |route|
    app.routes_reloader.paths.unshift(route) if File.exists?(route)
  end
end

Essentially, this should take the paths to all the routes files that Rails knows about and try and add them to the routes reloader (the thing that reloades your routes file automagically for you if it is changed). You can define another initializer to be executed right after this one, you will then inspect the paths stored in the routes reloader, pull out the path that belongs to your engine, remove it from the paths array and insert it back, but at the end of the paths array. So, in your config/application.rb:

class Application < Rails::Application
  initializer :munge_routing_paths, :after => :add_routing_paths do |app|
    engine_routes_path = app.routes_reloader.paths.select{|path| path =~ /<regex that matches path to my engine>/}.first
    app.routes_reloader.paths.delete(engine_routes_path)
    app.routes_reloader.paths << engine_routes_path
  end
end

This may or may not work, either way I don't really recommend it, it's not particularly elegant (i.e. ugly hack playing with the guts of rails).

Use Rails 3.1

This may not be an option, but if it is, I'd probably go with this one. In Rails 3.1 you can have 2 different types of Engines, full and mountable (here is an SO question talking about some of the differences). But in essence you would alter your Engine to be a mountable engine, the routes in a mountable engine are namespaced and you can explicitly include them in the routes file of your main app e.g.:

Rails.application.routes.draw do
  mount MyEngine::Engine => "/news"
end

You can also scope your mounted engine routes and do all sorts of other fancy routy things (more info here). Long story short, if you can go to 3.1 then this is the approach to use.

Dynamically Insert The Routes From Your Engine Into Your Main App

One of the most well known Rails engines around at the moment is Devise. Now, devise is an engine that will potentially add quite a number of routes to your app, but if you'll look at the devise source you'll see that it doesn't actually have a config/routes.rb file at all! This is because devise dynamically adds its routing goodness to your main app's routes.rb file.

When you run the model generator that comes with devise, one of the things the generator will do is add a line such as devise_for :model at the top of your routes.rb file, right after the Rails.application.routes.draw do line. So your route.rb looks similar to this after you execute a generator to create a User model:

Rails.application.routes.draw do
  devise_for :users
  ...
end

Now, devise_for is a magical method that comes as part of devise (in lib/devise/rails/routes.rb), but in essence it will create a bunch of regular routes that we all know based on the model that you generated.

The thing we need to know, is how devise insert this line in the apps routes.rb file, then we can write a generator in our engine that will insert any of our routes at the top of the main apps routes.rb file. For this we look at lib/generators/devise/devise_generator.rb. In the add_devise_routes method the last line is route devise_route. Route is a Thor action which inserts the string passed to it into the main app's routes.rb file. So we can write a generator of our own and do something similar e.g.:

class MyCrazyGenerator < Rails::Generators::NamedBase
  ...
  def add_my_crazy_routes
    my_route  = "match '/news', :to => 'bar_controller#foo_action'"
    route my_route
  end
end

Of course you would need to make sure all the generator infrastructure is in place but that's the essence of it. Devise is written by some very smart rails dudes and used by quite a lot of people, emulating what they do is likely a pretty good way to go. Out of the 3 things that I suggested this one would be the way I would handle your issue (given that moving to rails 3.1 is probably not an option).

like image 92
skorks Avatar answered Sep 30 '22 15:09

skorks