How to Detect an Integer in Rails 3 Routes? - ruby-on-rails

I would like to do just a little bit of extra logic in rotues.rb, that probably doesn't belong there, but it seems to make the most sense to me.
I have two conflicting routes. To be primitive:
match '/videos/:browseby' => 'videos#browse', :as => "browse_by"
Where :browseby is looking for a string, such as "Tags", to browse videos by tags.
However, (and most probably saw this coming) I also have my basic show resource (again in primitive form):
match '/videos/:id' => 'videos#show', :as => "video"
Where :id is looking for the integer for the video ID.
Is there a way to add a small bit of logic such as...
match '/videos/:id' => 'videos#show', :as => "video", :format(:id) => :integer
(Which is my hypothetical rails syntax, to help show what I'm looking for.)
I know I can munch this in the Controller level, but it makes more sense to me to handle it at the route level.

You could try using :constraints and a regex:
match '/videos/:id' => 'videos#show', :as => "video", :constraints => { :id => /\d/ }
match '/videos/:browseby' => 'videos#browse', :as => "browse_by"
You'll also want to make sure the looser :browseby version comes after the :id version. Note that regex constraints are implicitly anchored at the beginning so that would work as long as your :browseby values didn't start with a number.
If you have tags that do start with numbers then you could use an object for the constraint and then you could include anchors in your regex:
class VideoIdsOnly
def matches?(request)
request.path =~ %r{\A/videos/\d+\z}
end
end
match '/videos/:id' => 'video#show', :as => "video", :constraints => VideoIdsOnly.new
match '/videos/:browseby' => 'videos#browse', :as => "browse_by"

Related

Rails Routing Constraints and UTF-8

I need to handle routes like /:slug. The slugs are constrained elsewhere to match:
\A[\p{Alnum}_.-]{3,}\z
The obvious routes:
get '/:slug' => '...', :constraints => { :slug => /[\p{Alnum}_.-]{3,}/ }
get '/:slug' => '...', :slug => /[\p{Alnum}_.-]{3,}/
work fine with ASCII URLs like /mu-is-too-short and /where.is.pancakes.house but everything falls apart when the URL is /µ-is-too-short. The browser is sending this to the server:
/%C2%B5-is-too-short
and Rails is trying to check the encoded %C2%B5-is-too-short against the :slug constraint and failing because % is not a \p{Alnum}.
I have come up with two kludges:
get '/:slug' => '...', :slug => /.{3,}/
get '/:slug' => '...', :slug => /[^\/]+/, :constraints => lambda { |r| r.path_parameters[:slug].to_s =~ /\A[\p{Alnum}_.-]{3,}\z/ }
The :slug => /[^\/]+/ in the second is needed to keep Rails from thinking that the .b in a.b is a format extension.
Is there a way to make Rails behave sensibly and check the decoded :slug against the constraints or do I have to loosen the constraints or do it by hand?
You can use a lambda for the constraint and this way change the encoding (from http://www.intridea.com/blog/2011/2/21/use-lambdas-for-rails3-route-constraints):
scope :constraints => lambda{|req| !req.session[:user_id].blank? } do
# all my logged in routes
end

Accepting single dynamic, but unused, element in route

I want to accept URLs for my objects that contain an element that is not used, but which would be nice for SEO reasons. For example, I want to accept an url like:
http://localhost:3000/people/USA/123-joe-schmoe
.. where the "/USA" bit has no significance at all, other than signaling to users and search engines that this person resides in the USA.
I've tried setting up my rule using globbing, as well as for example doing something like
match "people/:whatever/:id" => "people#show", :constraints => {:id => /\d+-.*/}, :as => "person"
But this results in an attempt to route to :controller => people, :action => "show", :whatever => #<Person id: 123 ...> which fails.
Is it possible to have the routing ignore the :whatever part and pass in my :id in stead?
You can try a widcard match instead
match "people/*/:id => "doctors#show", :constraints=> {:id=>/\d+=.*/}, :as=>"person"

How to match hash (deep nested) params in Rails3 to make a pretty URL?

If I have this route (in routes.rb):
match 'posts', :to => 'posts#index'
It will show and match the following routes:
# Case 1: non nested hash params
posts_path(:search => 'the', :category => 'old-school')
#=> "/posts?search=the&category=old-school"
# Case 2: nested hash params
posts_path(:filter => {:search => 'the', :category => 'old-school'})
#=> "/posts?filter[search]=the&filter[category]=old-school"
If I want to make the category param part of the main URL, I could do this for the Case 1.
match 'posts(/:category)', :to => 'posts#index'
that will show and match the following routes:
# Case 1: non nested hash params
posts_path(:search => 'the', :category => 'old-school')
#=> "/posts/old-school?search=the"
But how could I do the same if the param is nested (Case 2)?
I would expect the next route definition:
match 'posts(/:filter[category])', :to => 'posts#index'
to work this way:
# Case 2: nested hash params
posts_path(:filter => {:search => 'the', :category => 'old-school'})
#=> "/posts/old-school?filter[search]=the"
But it does not work.
I found this same question in two places with no righ answer:
how-to-specify-nested-parameters-in-the-routes
how-to-accept-hash-parameters-in-routes
The Rails Guides don't specify anthing about this.
Should I assume that this can not be done in rails? really?
you could just make two different routes instead
match 'posts', :to => 'posts#index'
match 'posts/:category', :to => 'posts#index'
The next route will not work as you intended it.
match 'posts(:filter[category])', :to => 'posts#index'
The :filter is just a place holder for either the first argument thats passed into the url helper or the value for the key :filter in a has that is passed in. Any expressions in the route string will not be evaluated.
I guess the answer to your question is that you cannot do this in rails. I would suggest to you though that you do this in another way. It is very helpful in rails to follow the convention and make things easier on yourself.
Looks like you are doing three things here. The base post routes
match 'posts', :to => 'posts#index'
A route that has the category nested in it. Most likely to give the user a better url
match 'posts/:category', :to => 'posts#index'
And a search url which can be the same as the first, or to make your action cleaner, a different one
match 'posts/search', :to => 'posts#search'
There is really no reason I can think of to complicate the routes in the way your are suggesting. A search query url doesn't look nice anyways so why bother handling two urls for searches. Just one will do.
You should definitely take a look at running
rake routes
as this will tell you exactly what you have defined in your routes file. You can also set up routing tests to ensure your custom routes are performing correctly.
Your example does not work (as you indicated)
# Case 2: nested hash params
posts_path(:filter => {:search => 'the', :category => 'old-school'})
#=> "/posts/old-school?filter[search]=the"
But what you should be looking for is this
posts_path(:filter => {:search => 'the', :category => 'old-school'})
#=> "/posts?filter[search]=the&filter[category]=old-school"
This is ok to do it this way.
If you want to keep posts/:category just use this for navigation only, not for search.
Hope that helps

Handle rails route with GPS parameter

I'd like to create a route in my rails app to handle a gps-coordinate parameter. The intention is to find restaurants near the given position.
This is were I started:
match "/restaurants/near/:lat/:lng(/:range)", :to => "restaurants#near", :as => "near", :constraints => {:range => /\d+/}
It seems the router has problems with float parameters, an url like /restaurants/near/53.0123/10.5678 isn't recognized. Do you have a solution or best practice for handling GPS coordinates in rails urls?
Thank you!
The problem is caused because Rails try to use the "dots" for search for the format (.:format)
So, you can add some constraints to fix it, for example:
match "/restaurants/near/:lat/:lng(/:range)", :to => "restaurants#near", :as => "near", :constraints => {:lat => /\-?\d+(.\d+)?/, :lng => /\-?\d+(.\d+)?/ , :range => /\d+/}

Rails REST routing: dots in the resource item ID

I have following in my routes.rb:
resources :users, :except => [:new, :create] do
get 'friends', :as => :friends, :on => :member, :to => "users#friends"
end
and following in my user.rb:
def to_param
self.login
end
And when, for example, user with dots in login (for example 'any.thing') comes from facebook, rails gives routing error (no route found, I suppose that's because it recognises anything after dot as a format or because of route constraints). How can I come over this error?
The following constrain definition permit the dot in id as well as any character except slash.
Supported formats must be explicitly defined (here .html and .json) to not to be taken by id.
resources :foobars,
:constraints => { :id => /[^\/]+(?=\.html\z|\.json\z)|[^\/]+/ }
That constrain definition is worked with Rails 3.1
For earlier Rails versions you may need to backport look-ahead support in regin gem (it is vendored in rack-mount gem)
You could replace periods with another character:
def to_param
login.gsub(/\./,"-") # note: 'self' is not needed here
end
user = User.find_by_login("bart.simpson")
user_path(user) # => "/users/bart-simpson"
EDIT
You're right, this fails to deal with unique logins that map to the same value. Maybe a better way is to use segment constraints in the route:
match 'users/(:id)' => 'users#show',
:constraints => { :id => /[0-9A-Za-z\-\.]+/ }
This should allow "/users/bart-simpson" and /users/bart.simpson" to generate :id => "bart-simpson" and :id => "bart.simpson" respectively. You'd have to alter the regex to add all the acceptable characters for the URL.
Note that this is mentioned in the Rails Routing Guide, section 3.2:
By default dynamic segments don’t accept dots – this is because the
dot is used as a separator for formatted routes. If you need to use a
dot within a dynamic segment add a constraint which overrides this –
for example :id => /[^\/]+/ allows anything except a slash.
To allow the :id segment to contain any character except '/':
match 'users/(:id)' => 'users#show', :constraints => {:id => /[^\/]+/}
It's written elsewhere in one of the answers, but this is IMO the simplest way.

Resources