Search code examples
ruby-on-railsrails-routing

How to conditionally include route parameter in Rails?


I'm trying to allow for one resource, Site, to conditionally have a parent resource, Organization.

Here's what I currently have:

resources :site do
  resources :posts do
    resources :comments
  end
end

this results in paths like

/sites/1
/sites/1/edit
/sites/1/posts
/sites/1/posts/123/comments
# etc. etc.

I want the ability to have paths like

/parent_id/sites/1
/parent_id/sites/1/edit
/parent_id/sites/1/posts
/parent_id/sites/1/posts/123/comments

but only if the Site belongs to an Organization.

I also don't want to have to change every single path helper already in use across my site (there are literally hundreds of places).

Is this possible?

Here's what I've tried:

scope "(:organization_id)/", defaults: { organization_id: nil } do
  resources :site
end

# ...

# in application_controller.rb
def default_url_options(options = {})
  options.merge(organization_id: @site.organization_id) if @site&.organization_id
end

but that didn't work. organization_id wasn't getting set.

# in my views
link_to "My Site", site_path(site)
# results in /sites/1 instead of /321/sites/1

I also tried setting the organization_id in a route constraint, and that didn't work as well.


Solution

  • I ended up writing a monkey patch overriding the dynamic method generated for all of my relevant paths. I used a custom route option, infer_organization_from_site that I look for when dynamically generating routes. If the option is set, I add site.organization as the first argument to the helper call.

    # in config/routes.rb
    
    scope "(:organization_id)", infer_organization_from_site: true do
      resources :sites do
        resources :posts
        # etc.
      end
    end 
    
    # in an initializer in config/initializers/
    
    module ActionDispatch
      module Routing
        # :stopdoc:
        class RouteSet
          class NamedRouteCollection
    
            private
    
            # Overridden actionpack-4.2.11/lib/action_dispatch/routing/route_set.rb
            # Before this patch, we couldn't add the organization in the URL when
            # we used eg. site_path(site) without changing it to site_path(organization, site).
            # This patch allows us to keep site_path(site), and along with the proper
            # optional parameter in the routes, allows us to not include the organization
            # for sites that don't have one.
            def define_url_helper(mod, route, name, opts, route_key, url_strategy)
              helper = UrlHelper.create(route, opts, route_key, url_strategy)
              mod.module_eval do
                define_method(name) do |*args|
                  options = nil
                  options = args.pop if args.last.is_a? Hash
                  if opts[:infer_organization_from_site]
                    args.prepend args.first.try(:organization)
                  end
                  helper.call self, args, options
                end
              end
            end
          end
        end
      end
    end