0

I have used friendly_id and globalize gem. So I can generate routes as;

/search/france/weekly/toyota-95

Here is my routes;

namespace :search do
  resources :car_countries, path: '', only: [] do
    resources :rental_types, path: '', only: [] do
      resources :car_types, path: '', only: [:show] do
      end
    end
  end  
end

But the thing is now I would like to also get city either;

/search/nice/weekly/toyota-95

or

/search/france/nice/weekly/toyota-95

The problem is I want to have both with city name and without city name (only country). They should go to same controller which is at the end car_types.

So if I add car_cities to routes, I get error when there is no city but only country.

namespace :search do
 resources :car_countries, path: '', only: [] do
    resources :car_cities, path: '', only: [] do
       resources :rental_types, path: '', only: [] do
         resources :car_types, path: '', only: [:show] do
         end
       end
     end  
  end  

  resources :car_countries, path: '', only: [] do
    resources :rental_types, path: '', only: [] do
      resources :car_types, path: '', only: [:show] do
      end
    end
  end  
end

How can I do that?

Shalafister's
  • 761
  • 1
  • 7
  • 34
  • 1
    Take a look into [Route Globbing and Wildcard Segments](http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments). – Gerry Jul 07 '17 at 15:43

1 Answers1

3

As Gerry says, take a look at route globbing http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments. I'd suggest to send everything to a single controller action and either do stuff there or delegate it to a search model/service object (depends on your taste).

Example:

# in config/routes.rb
get 'search/*q' => 'searches#show'

# in app/controllers/searches_controller.rb
class SearchesController < ApplicationController
  def search
    # This should work for your simple use case but it will become pretty confusing if you add more filters.

    search_params = params[:search].split('/')
    if search_params.count == 4
      country, city, rental_type, car_type = search_params
    else
      country, rental_type, car_type = search_params
    end

    # Do whatever with these variables, e.g. Car.for_country(country)...
  end
end

A more stable solution would be to make use of the fact that rental types are probably a closed set (daily, weekly, ...) and use segment constraints for this part in the routes:

# config/routes.rb
scope to: 'searches#show', constraints: { rental_type: /(daily|weekly|monthly)/ } do
  get '/search/:country/:rental_type/:car_type'
  get '/search/:country/:city/:rental_type/:car_type'
end

This should differentiate the two URLs based on the fact that :city can never match the rental type constraint.

Yet another option would be to use a full blown constraints object (http://guides.rubyonrails.org/routing.html#advanced-constraints):

# config/routes.rb
class SearchConstraint
  def initialize
    # assuming that all your objects have the friendly_id on the name field
    @country_names = %w[austria germany]
    @rental_type_names = %w[daily weekly monthly]
    @car_type_names = %w[toyota-prius vw-golf]
    @city_names = %w[innsbruck munich berlin]
  end

  def matches?(request)
    checks = []

    # only check for parts if they're actually there
    checks << @country_names.include?(request.parameters[:country]) if request.parameters[:country].present?
    checks << @rental_type_names.include?(request.parameters[:rental_type]) if request.parameters[:rental_type].present?
    checks << @car_type_names.include?(request.parameters[:car_type]) if request.parameters[:car_type].present?
    checks << @city_names.include?(request.parameters[:city]) if request.parameters[:city].present?

    checks.all? # or, if you want it more explicit: checks.all? { |result| result == true }
  end
end

scope to: 'searches#show', constraints: SearchConstraint.new do
  get '/search/:country/:rental_type/:car_type'
  get '/search/:country/:city/:rental_type/:car_type'
end

Note that this last approach is probably the cleanest and least hacky approach (and it's quite easy to test) but it also comes at the cost if involving the database in every request to these particular URLs and the URLs fail hard if there's an issue with the database connection.

Hope that helps.

Clemens Kofler
  • 1,878
  • 7
  • 11
  • Thank you for your help! I think I will go with the 'stable solution'. One question tho. Why should I use constraints? – Shalafister's Jul 07 '17 at 17:09
  • and when I say rake routes, I can not see the path of "scope to: 'searches#show', constraints: { rental_type: /(daily|weekly|monthly)/ } do .." – Shalafister's Jul 07 '17 at 17:18
  • You need to use constraints to make sure that the Rails parameter parser can differentiate between the different parameters. It simply prevents issues where Rails interprets "germany" as a city, "berlin" as a car type etc. and becomes useful if you start adding other combinations (e.g. if requirements change and you need to add another field, like `get '/search/:country/:city/:location/:rental_type'` where location is "airport", "train-station" or whatever). It may not be necessary right now but it'll prevent issues down the line. – Clemens Kofler Jul 07 '17 at 17:26
  • As for the question with `rake routes`: Both routes are unnamed, so they probably show up but they don't have a name – so the left part in the output isn't shown. – Clemens Kofler Jul 07 '17 at 17:28
  • Exactly. Left part is not shown. But then how can I access? I tried search_path as it is show action but did not work. – Shalafister's Jul 07 '17 at 17:32
  • Do you think adding .., as: 'search_country' is logical? – Shalafister's Jul 07 '17 at 17:38
  • 1
    Multiple options: 1) Write your own URL helper for it (`def search_path(params = {})`) and manually write the URL. 2) Append `as: :search` and `as: :search_with_city` to the routes. 3) Manually write the URL directly into the templates. I suggest using option 1. – Clemens Kofler Jul 07 '17 at 17:39