Piotr Sarnacki home

RSoC status: routes (a.k.a OMG it's hard)

In my last post I briefly described some of the RSoC changes and plans. One of the things that I left is router. The topic is much harder and I think it deserves separate blog post.

Basically, the problem with engines and routes is that now you can have more than one router. That leads to many issues that need to be solved. As with my last post, things described here are not in Rails core yet, so you will have to wait a bit to actually use it.

For the good start, let’s identify things to consider:

Recognition

At the beginning I would like to explain how does recognition work. Here is a simple example of engine mounted in application:

# APP/config/routes.rb
Foo::Application.routes.draw do
  resources :users
  mount Blog::Engine => "/blog"
end

# ENGINE/config/routes.rb
Blog::Engine.routes.draw do
  resources :posts
end

As you can see engine provides :posts resource and it’s mounted at /blog path. Let’s study simple request to our blog:

GET /blog/posts

Application’s router will match the first part of the path, which is /blog, with /blog mount point and pass the request to rack app mounted there (which in that case is Blog::Engine). To allow engine recognizing path properly it needs to pass /posts as a path. The /blog (which I will call prefix later on) is not a part of mounted engine, but we don’t want to loose that information by simply removing it from PATH. In that case /blog prefix is attached to env["SCRIPT_NAME"] (SCRIPT_NAME is part of rack’s spec). That way, engine will get only /posts part as PATH, which will allow to properly recognize it.

The only one problem with recognition is connected with rack middlewares. Consider such example:

# APP/config/routes.rb
Foo::Application.routes.draw do
  mount Blog::Engine => "/blog"
  match "/blog/omg" => "omg#index"
end

# APP/app/controllers/omg_controller.rb
class OmgController < ApplicationController
  use SomeMiddleware
end

# ENGINE/lib/blog/engine.rb
class Blog::Engine < Rails::Engine
  config.use SomeMiddleware
end

What will happen when you will request /blog/omg path? It will call the Blog::Engine first, as it has higher priority than /blog/omg. Request will pass through the Engine’s middleware stack firing SomeMiddleware and hit the Engine’s router. If /omg is not a valid route for Blog::Engine it will return 404 and Application’s router will try next route, that is /blog/omg pointing to OmgController. Middleware stack for that controller also includes SomeMiddleware so it will be fired again, which is probably not something that we want.

The solution is to mount apps with lower priority than other routes.

Generation

The problem with generation is related to mount point, the place where engine is mounted. Consider such example:

# APP/config/routes.rb
MyRailsApp::Application.routes.draw do
  match "/" => "users#index"

  scope "/:user", :user => "drogus" do
    mount Blog::Engine => "/blog"
  end
end

# ENGINE/lib/blog/engine.rb
class Blog::Engine < Rails::Engine
end

# ENGINE/config/routes.rb
Blog::Engine.routes.draw do
  resources :posts
end

Blog::Engine is mounted at "/:user/blog" (let’s just suppose that blog implements multi user setup) and it provides posts resources. As you can remember from recognition part, /:user/blog part is engine’s prefix. Additionally, default user is set to “drogus”, so when :user is not specified it will be set to drogus. Imagine that you need to generate path to posts. Something that you would normally achieve by calling posts_path method. With more than one router the situation is not so simple. We can’t simply use named url helpers (like posts_path) inapplication’s controllers, so we need some other way to handle that (if you’re wondering why can’t we just use posts_path, it’s explained in named routes section of this post).

First problem connected with that is API. Jeremy Kemper came up with helper that will allow using mounted engine’s url helpers by simply calling some_engine.posts_path. The name of that helper is taken from Engine’s name or from :as option used in mount method. Because :as option is not provided in my example, helper’s name will be blog_engine (based on Blog::Engine). Using that helper we can generate paths for Engine:

blog_engine.posts_path
blog_engine.url_for @post
blog_engine.polymorphic_path @post
blog_engine.url_for :controller => "posts", :action => "index"

We can also generate application’s urls with app helper:

app.root_url

The next problem to solve is behavior of such helpers. At first, it seems that it’s fairly easy, but in reality there is quite a few cases to handle. The thing that is most important while considering url generation is context. We can generate urls in application’s controllers, engine’s controller’s, ActionMailer or regular classes not related to Rails itself. Let’s go through each case.

Generating engine’s url inside application’s controller

# APP/app/controllers/foo_controller.rb
class FooController < ApplicationController
  def index
    blog_engine.posts_path #=> "/drogus/blog/posts"
  end
end

Generating posts path inside one of application controllers (and consequently views and helpers) should generate prefix according to mount point. In that particular situation, default user will be inserted in place of :user, so the url will be "/drogus/blog/posts". You could also do blog_engine.posts_path(:user => "john"), which would generate "/john/blog/posts".

Generating engine’s url inside engine’s controller

# ENGINE/app/controllers/posts_controller.rb
class PostsController < ActionController::Base
  include Blog::Engine.routes.url_helpers

  def index
    posts_path #=> ??
  end
end

This situation is a bit different. The first thing you will probably notice is that I used posts_path instead of blog_engine.posts_path. This is possible because I included url_helpers from engine’s routes. From various reasons it can’t be included automagically for engine’s controllers, but the plan is to make it work without any explicit includes. Why can’t we just use blog_engine.posts_path? In that example it would work, but note that engine can be mounted with different :as option. With mount looking like:

mount Blog::Engine => "/blog", :as => "blog"

you would have to use blog.posts_path instead of blog_engine.posts_path. Basically engine should not need to know how it’s mounted. That said, we don’t have any information about options used to mount it, all that we know is what’s the request that was used to reach the engine.

But what about the generated path? Someone could say that it should also generate prefix, but that would not work as expected. Imagine that someone requested one of your users blog with path "/dhh/blog/posts/1". When you click on link with url generated by posts_path, you should stay in the same scope, so url should depend on you current path. This is achieved by using env["SCRIPT_NAME"] value. In request to "/dhh/blog/posts/1", the script name would be set to "/dhh/blog", as this is the part of path that does not belong to engine. It should be clear now, that example above will generate "/dhh/blog/posts" path for such request.

The next thing that is worth mentioning is _routes method. When you call posts_path directly, it must use routes object. This object is available through _routes method. This method is defined when you include url_helpers, so it points to application’s routes by default. When Blog::Engine.routes.url_helpers are included in PostsController, _routes is changed to use engine’s routes, and because of that we can use posts_path safely.

Generating application’s url inside engine’s controller

# ENGINE/app/controllers/posts_controller.rb
class PostsController < ActionController::Base
  def index
    app.root_path
  end
end

We would like to generate root_path from our application’s router. Obviously it should generate "/" path, without Engine’s prefix. The only exception is situation when app is hosted in a sub path (eg /myapp). This can be done with Phusion Passenger, using RailsBaseURI option. /myapp part would be passed as SCRIPT_NAME in such case. Nothing complicated, we already know how to use SCRIPT_NAME, right? Not really (wouldn’t it be to simple? ;-). The problem is, SCRIPT_NAME is kept as a string. Let’s see how the request to "/myapp/user/blog/posts" looks like.

At first it hits the application. SCRIPT_NAME is set to /myapp by Passenger and the path is /user/blog/posts. Now, application’s router recognizes that this request should get to engine, so Blog::Engine is called. As engine needs only /posts as path, the prefix (/user/blog) will be attached to env["SCRIPT_NAME"] resulting in /myapp/user/blog. As this is one string and we need to get just application’s script name, solution is not obvious. How do we get the original script name? Right now our approach is to use whatever is set in Rails.application.routes.default_url_options[:script_name], so it should be set to “/myapp” in that case.

Generating engine’s url in any other class (including ActionMailer)

In that case, url should be generated with prefix (which would be /user/blog in my example). In ActionMailer we are not inside request, so script_name is not available and with that in mind we need to generate the full path with "/user/blog/" at the beginning.

Solution

Although it may look complicated to handle all those use cases, solution is relatively easy. The first thing to do is to assume that prefix should be always generated. This gets all cases with prefix out of our way. What’s not so obvious, it also makes generating application’s url in engine work. As application’s routes are not mounted, prefix is nil, so we can safely add it to “generate prefix” case. What’s left? Generating engine’s url inside engine. We need to use env["SCRIPT_NAME"] here. How to check if we should attach script name? We need to check if routes used to generate url are the same as routes connected with current request.

Right now, to make it possible, router object is passed via env["action_dispatch.routes"]. When application is called, it sets it to Rails.application.routes and then when engine is called, it sets its own router there. That way, we always now in which controller are we. If _routes method points to the same routes as the env["action_dispatch.routes"] it means that we try to generate engine’s url inside engine and we should use the SCRIPT_NAME.

Named routes

The initial idea was to allow using named routes from 2 routers in one scope, just like that:

# APP/app/controllers/foo_controller.rb
class FooController < ActionController::Base
  # Rails.application.routes_url_helpers are included by default
  include Blog::Engine.routes.url_helpers

  def index
    posts_path #=> "/blog/posts" - path from engine's router
    root_path #=> "/" - path from application's router
  end
end

Although it would be handy if you will have to use paths from mounted engine a lot, it is the cause of many issues:

There are some solutions for all of those problems, but as they’re all a bit hacky and complicated, we decided to leave that topic and not allow to use helpers from 2 routers in one scope. It means that you will have to explicitly say that you want to use router other then the default one (for example by using blog_engine.posts_path helper). It also means that including url_helpers from engine will overwrite current routes, so in case above, you can’t directly use application routes any more. In such case you should use app helper.

I hope that this post is good introduction to current status of router usage in mountable apps. As usual: if you have any ideas, feature requests, critique or any other thoughts that could help bringing mountable apps to life, you’re more than welcome ;-)


If you liked this post consider following me on twitter.
blog comments powered by Disqus
Fork me on GitHub