views:

177

answers:

2

I have to separate models: nested sections and articles, section has_many articles. Both have path attribute like aaa/bbb/ccc, for example:

movies # section
movies/popular # section
movies/popular/matrix # article
movies/popular/matrix-reloaded # article
...
movies/ratings # article
about # article
...

In routes I have:

map.path '*path', :controller => 'path', :action => 'show'

How to create show action like

def show
  if section = Section.find_by_path!(params[:path])
    # run SectionsController, :show
  elsif article = Article.find_by_path!(params[:path])
    # run ArticlesController, :show
  else
    raise ActiveRecord::RecordNotFound.new(:)
  end
end
+1  A: 

Rather than instantiating the other controllers I would just render a different template from PathController's show action depending on if the path matches a section or an article. i.e.

def show
  if @section = Section.find_by_path!(params[:path])
    render :template => 'section/show'
  elsif @article = Article.find_by_path!(params[:path])
    render :template => 'article/show'
  else
    # raise exception
  end
end

The reason being that, whilst you could create instances of one controller within another, it wouldn't work the way you'd want. i.e. the second controller wouldn't have access to your params, session etc and then the calling controller wouldn't have access to instance variables and render requests made in the second controller.

mikej
but I'll have to duplicate all filters from those controllers and all show actions
tig
What filters do you have? Maybe they can be moved to a common superclass or a mixin because presumably you want them to apply to PathController as well as SectionController and ArticleController. Do you ever show a section or acticle through SectionController and ArticleController or are all show requests now routed through PathController?
mikej
for now - access restrictions (is article or section publicly visible), but project is in development so something can appear in near future
tig
+2  A: 

You should use Rack middleware to intercept the request and then rewrite the url for your proper Rails application. This way, your routes files remains very simple.

map.resources :section
map.resources :articles

In the middleware you look up the entity associated with the path and remap the url to the simple internal url, allowing Rails routing to dispatch to the correct controller and invoking the filter chain normally.

Update

Here's a simple walkthrough of adding this kind of functionality using a Rails Metal component and the code you provided. I suggest you look at simplifying how path segments are looked up since you're duplicating a lot of database-work with the current code.

$ script/generate metal path_rewriter
      create  app/metal
      create  app/metal/path_rewriter.rb

path_rewriter.rb

# Allow the metal piece to run in isolation
require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails)

class PathRewriter
  def self.call(env)
    path = env["PATH_INFO"]
    new_path = path

    if article = Article.find_by_path(path)
      new_path = "/articles/#{article.id}"

    elsif section = Section.find_by_path(path)
      new_path = "/sections/#{section.id}"

    end

    env["REQUEST_PATH"] = 
    env["REQUEST_URI"]  = 
    env["PATH_INFO"]    = new_path

    [404, {"Content-Type" => "text/html"}, [ ]]
  end
end

For a good intro to using Metal and Rack in general, check out Ryan Bates' Railscast episode on Metal, and episode on Rack.

Duncan Beevers
I should intercept before routing module?Or is there a way to create route with proc/lambda?
tig
Yes, intercept before the routing module. Then the routes in your "Rails" application stay very normal but a middleware app rewrites incoming urls based on the path reported by the browser.
Duncan Beevers