Peter Nixey
Peter Nixey

Reputation: 16575

Is it possible to dynamically remove a level of rails resource nesting?

I need routing to work for both a hosted and whitelabel version of a rails app. Both versions are running off the same codebase and on the same server so the routing needs to figure things out but it's not clear to me how to achieve this.

I'm building a job board. Each company signing up can create their own company profile on the site. If they get a paid premium version they can use their own CNAME'd URL and serve the job board off one of their subdomains. All pretty standard stuff.

What routes need to look like on the main site

http://jobsrus.com/companies/company-name
# e.g.
http://jobsrus.com/companies/microsoft

which leads to routes such as

http://jobsrus.com/companies/microsoft/jobs/
http://jobsrus.com/companies/microsoft/newest

What routes need to look like on the whitelabel sites

The company can also whitelabel the job board so that it looks like this:

http://jobs.microsoft.com/jobs
http://jobs.microsoft.com/newest

Clarifying the difference

Just to clarify, the same controller#action will be delivered by both:

http://company-domain/jobs
# and
http://jobsrus.com/companies/company-name/jobs

Ugly routing:

The simplest routing is:

routes.rb

resources :companies do
  ...
  resources :jobs do
    ...
  end
end

which gives:

http://jobsrus.com/companies/microsoft/jobs

# but also

http://jobs.microsoft.com/companies/microsoft/jobs

Whereas we want the latter to be:

http://jobs.microsoft.com/jobs

How to drop this first level of nesting from the route?

My question is very straightforward. How do I drop the companies/company-name nesting level from the route? The only routing necessary for the white label site is:

routes.rb

resources :jobs do
  ...
end

How can I dynamically include or exclude a level of nesting from the routing? I can use the request.host variable to trigger the switch but I don't know how best to either activate or de-activate that layer of nesting.

------ EDIT (and partial solution) -----------------------

Using @m_x's answer I've used constraints to create the routing. In order to better illustrate the problem I've also used a couple of extra routes:

(simplified to just display the :show and :index methods)

def company_resources    
  resources :jobs, only: [:index, :show] do
    resource :applicants, only: [:index, :show] do
      resource :messages, only: [:index, :show]
    end
  end
end

 constraints host: /^(?!jobsrus\.com)/ do
   company_resources
 end

 resources :companies, only: [:index, :show] do
   company_resources
 end

This works well in terms of matching incoming requests, we can see that rake routes produces the matches we're looking for:

        job_applicants_messages GET /jobs/:job_id/applicants/messages(.:format)                       {:host=>/^(?!jobsrus\.com)/, :action=>"show", :controller=>"messages"}
                 job_applicants GET /jobs/:job_id/applicants(.:format)                                {:host=>/^(?!jobsrus\.com)/, :action=>"show", :controller=>"applicants"}
                           jobs GET /jobs(.:format)                                                   {:host=>/^(?!jobsrus\.com)/, :action=>"index", :controller=>"jobs"}
                            job GET /jobs/:id(.:format)                                               {:host=>/^(?!jobsrus\.com)/, :action=>"show", :controller=>"jobs"}
company_job_applicants_messages GET /companies/:company_id/jobs/:job_id/applicants/messages(.:format) {:action=>"show", :controller=>"messages"}
         company_job_applicants GET /companies/:company_id/jobs/:job_id/applicants(.:format)          {:action=>"show", :controller=>"applicants"}
                   company_jobs GET /companies/:company_id/jobs(.:format)                             {:action=>"index", :controller=>"jobs"}
                    company_job GET /companies/:company_id/jobs/:id(.:format)                         {:action=>"show", :controller=>"jobs"}
                      companies GET /companies(.:format)                                              {:action=>"index", :controller=>"companies"}
                        company GET /companies/:id(.:format)                                          {:action=>"show", :controller=>"companies"}

However, there are now no longer any canonical methods for producing the routes. If we want to create a route to a particular company's job index we have to use a different method depending on whether we're on a whitelabel company or on a jobsrus.com company:

# path generator for jobs page on a whitelabel company
jobs_path
# => 'microsoft.com/jobs'

# path generator for jobs page on a company on the main site
company_jobs_path @company
# => 'jobsrus.com/companies/microsoft/jobs'

# what is actually required
company_jobs_path @company
# => 'jobsrus.com/companies/microsoft/jobs' (when on main site)
# => 'microsoft.com/jobs' (when on whitelabel)

I could override the path methods and define some methods that switch depending on the host variable. It would be nice to do this the rails way though. Is this supported?

Upvotes: 2

Views: 442

Answers (1)

m_x
m_x

Reputation: 12564

interesting question. i think it might be possible to do this using request-based constraints.

in an initializer, define a constant :

 YOUR_HOST = 'jobsrus.com'.freeze

then in routes.rb :

 constraints :host => /!#{YOUR_HOST}/ do
   resources :jobs
 end

 resources :companies do
   resources :jobs
 end

the order here is important : if request.host does not match your host name, the first set of routes is available and captures the request before it hits the second set.

but now, you will need to make changes to your controller, so that it can retrieve the company and scope the jobs resources accordingly ( didnt try this, use with caution ):

class JobsController < ApplicationController
  before_filter :resolve_whitelabel

  def resolve_whitelabel
    if request.host != YOUR_HOST
      # not safe as is, just demonstrates the idea
      @client      = Company.find_by_host( request.host ) 
      @scoped_jobs = Job.where( company_id: @client.id ) 
    else
      @scoped_jobs = Job
    end    
  end


  def scoped_jobs
    @scoped_jobs
  end

  def index
    # just an example
    @jobs = scoped_jobs.recent 
  end
end 

you just have to remember always to use the scoped_jobs.

Edit

You can "store" a block in a Proc :

routes = Proc.new do
           resources :jobs
         end

... and then you should be able to convert back this Proc into a block using the & operator :

constraints( :host => /!#{YOUR_HOST}/, &routes )
resources( :companies, &routes )

this needs to be tested, i never used it in this context. Be aware, in particular, that a Proc acts as a closure : it captures its context (variables available in this scope, etc. This is called its 'binding') when it is created (much as a block does). This may lead to unexpected behaviour (though i don't think it will matter in that case, because your Proc's scope is the same as the original block's one).

Upvotes: 1

Related Questions