Rails: The easiest way to differ path parameter from query string parameter? - ruby-on-rails

Rails router gives us an easy way to define optional path parameters, like this:
# config/routes.rb
scope "(:locale)", locale: /ru|de|fr/ do
resources :books
end
Thus we can access /users path and get the default locale, or /ru/books and get the locale in params[:locale].
But with the same setup we can also call the page /books?locale=ru and get the same effect (both path parameters and query string parameters are treated equally and put in the params hash). If locale is processed in a global before_action as Rails i18n guide suggests we can even set locale for the pages that are not supposed to be localized.
So my question is what is the simplest and cleanest way differ path parameters from query string parameters (with the goal to ignore certain query string parameters)?

Answering my own question:
There is a method ActionDispatch::Request#query_parameters. It returns only the parameters set via query string.
There are also method path_parameters and symbolized_path_parameters. It is obvious they return parameters derived from path (including controller and action). They can be called on request inside a controller action. (They are not documented under ActionDispatch::Request, this is why I missed them initially.)
Rails 5 (edit Jan 9, 2017): As of Rails 5 method symbolized_path_parameters was removed. Method path_parameters is now documented.

Related

How to get an organized search URL result with slashes orders (/)?

I want a search section on the "index" from books_controller with some filter options from different authors, categories and other attributes. For example, I can search for a category "romance" and max pages = 200. The problem is that I'm getting this (with pg_search gem)
http://localhost:3000/books?utf8=%E2%9C%93&query%5Btitle%5D=et&button=
but I want this:
http://localhost:3000/books/[category_name]/[author]/[max_pages]/[other_options]
In order that if I want to disable the "max_pages" from the same form, I will get this clean url:
http://localhost:3000/books/[category_name]/[author]/[other_options]
It'll work like a block that I can add and remove.
What is the method I should use to get it?
Obs: this website, for example, has this kind of behavior on the url.
Thank you all.
You can make a route for your desired format and order. Path parameters are included in the params passed to the controller like URL parameters.
get "books/:category_name/:author/:max_pages/:other_options", to: "books#search"
class BooksController < ApplicationController
def search
params[:category_name] # etc.
end
end
If other options is anything including slashes, you can use globbing.
get "books/:category_name/:author/:max_pages/*other"
"/books/history/farias/100/example/other"
params[:other]# "example/other"
So that gets you the basic form, now for the other you showed it could just be another path since the parameter count changed.
get "books/:category_name/:author/*other_options", to: "books#search"
params[:max_pages] # nil
If you have multiple paths with the same number of parameters, you can add constraints to separate them.
get "books/:category_name/:author/:max_pages/*other", constraints: {max_pages: /\d+/}
get "books/:category_name/:author/*other"
The Rails guide has some furth information, from "Segment Contraints" and "Advanced Constraints": http://guides.rubyonrails.org/routing.html#segment-constraints
If the format you have in mind does not reasonably fit into the provided routing, you could also just glob the entire URL and parse it however you wish.
get "books/*search"
search_components = params[:search].split "/"
#...decide what you want each component to mean to build a query
Remember that Rails matches the first possible route, so you need to put your more specific ones (e.g. with :max_pages and a constraint) first else it might fall through (e.g. and match the *other).

Rails routing, not a supported controller name error

I am in the process of learning Rails, I am trying to make a small search functionality, I am setting up the route for this like this:
get 'search?q=:keyword' => 'search?q=#show'
and in the url I am trying to access this using
http://localhost:3000/search?q=test
but this is giving me this error: not a supported controller name.
Youssef
The reason is that you are trying to route with the query string ?= still in the path. Rails is a little smarter than that so the parameters will be passed automatically.
get 'search' => 'search#show'
Will retain the parameters in the redirect without you needing to do anything extra.

Custom Rails 4 Routes containing "/"

I have a method that generates a random url ending and a get path that looks like
/path/var1/var2
The only problem is that some of those generated values for var2 have a "/" in them. So if var 2 is "h4rd/erw" rails reads it as
/path/var1/h4rd/erw
or
/path/var1/var2/var3
rails thinks that this is another parameter of the route and i keep get the error
No route matches.
I have thought of setting up the generated value for var2 to not include "/"s or possibly putting a wildcard in the route if that's possible like /path/var1/*. What would be the best solution for this?
You can use route globbing:
get '/path/var1/*var2', to: 'controller#action'
Let's say the request went to /path/var1/h4rd/erw. Then in the controller action, the value of params[:var2] would be h4rd/erw.
I would make sure that the values are always escaped.
string = "my/string"
=> "my/string"
CGI.escape(string)
=> "my%2Fstring"
So your url will be like
/path/var1/h4rd%2Ferw
and it will go to the right controller with the right variables set.
Rails will automatically unescape the values of parameters in the controller "params" variable, so by the time you're dealing with the value in the controller the slash will be back in the string.
The only remaining question is when to do the escaping. If you pass the value as an argument to a path helper then rails should escape it automatically for you, like so:
link_to "Here", my_route_path(:foo => "bar")

Maintain whitelist of Rails routes for Devise

In a Ruby Rails project, I have an array of strings matching the controller/action syntax used by the Rails routing protocols. These are my public routes, requiring no authentication. I want to compare the list to the current controller#action in order to enforce login authentication.
The problem: I cannot figure out exactly how Rails parses a routing string and determines the appropriate controller#action. I need to replicate this functionality for the comparison, but Rails core code is quite soupy, and I haven't been able to pinpoint the logic.
To put it in terms of pseudo-code, here's a sample of my whitelist array in YAML syntax, coming out of a config file...
public_routes: [
'public',
'auth/sessions#new',
'auth/sessions#create',
'admin#login'
]
Then in my ApplicationController...
before_filter :check_authentication!
...
def check_authentication!
Settings.public_routes.each do |this_route|
# parse string this_route into a namespace::controller#action
return if [current route matches parsed route]
end
# enforce authentication procedures here
end
I already have logic to allow all actions on the PublicController (line 1 of public_routes array). Matching the other three is where I am getting tripped up.
P.S. The login enforcement is happening globally within ApplicationController in order to DRY out my controllers, and to centralize my whitelist of publicly permissible routes. I could do it inside each controller, but that's not the goal.
you can get back the current namespace, controller and action value from the params, like params[:controller], so inside your comparison loop, just make up the url. Or you can get back the request url with request.fullpath method.

How can I make rails route helpers always use to_param to generate the path, even when I just pass in an ActiveRecord ID?

So, I'm implementing a pretty/SEO-friendly URL scheme for my rails app. I have a model called Artist, and I would like the Rails artist_path helper to always generate the friendly version of the path.
In my routes.rb file, I have the following line:
get 'artists/:id(/:slug)', :to => 'artists#show', :as => 'artist'
If the slug is left out, or is incorrect (it's calculated by the artist name), the controller 301 redirects to the correct URL. However, for SEO reasons, I want to ensure that all links internal to my site have the correct URL to start with.
The Artist model has the two following (very simple) functions to allow this to work:
def slug
name.parameterize
end
def to_param
"#{id}/#{slug}"
end
If I call artist_path with an artist object, this works as intended:
> app.artist_path(Artist.find 1234)
=> "/artists/1234/artist-name"
However, when I use call it with just the ID, it does not seem to use to_param at all:
> app.artist_path(id: 1234)
=> "/artists/1234"
tl;dr: How can I force Rails to always instantiate the object and call to_param on it when artist_path is called, even when only the ID is specified?
As far as I'm aware, the reason why what you're asking won't work is because when you pass in values to the built-in/automatic URL helpers (like an ID, per your example), those values just get used to "fill in the blanks" in the route URL.
If you pass an array, the values from the array will get used in order until the URL is filled in. If you pass a hash, those properties will get replaced into the URL. An object, like your model, will use it's to_param method to fill in the values... etc.
I understand your concern regarding "having knowledge of the limitations of that model," however, this behavior is standard in Rails and I don't believe it would really throw anyone. When you pass in an ID, as you do in your example, you're not telling the URL helper to "lookup a record via the model using this ID," you're simply telling it to "replace ':id' in the URL string with the ID you're providing." I'm fairly certain the built-in URL helpers wouldn't even know how to lookup the record - what model to use, etc. - other than maybe inferring from the route/controller name.
My best suggestion is to always use the instantiated model/record. If you were hoping the URL Helper would look that up for you, then there's no extra overhead as far as querying the database goes. If there's some additional reason you want to avoid instantiating the record yourself, I'd be glad to hear it and possibly provide other suggestions.

Resources