Rails 4 route constraints with query string or numbers only - ruby-on-rails

I'm trying to implement these two routes with constraints:
get "products/:id", to: "products#show", constraints: { id: /\d/ }
get "products/:name", to: "products#search", constraints: { name: /[a-zA-Z]/ }
The first route should trigger with an URL like localhost:3000/products/3 and the last one should trigger with localhost:3000/products/?name=juice.
Doesn't work, I googled a lot about this problem but I seem to find solutions for the second or third version of Ruby on Rails, and most of them are deprecated now.
Any ideas? Thanks.

You can use the routes as-is if you like the look of the resulting URLs. No need for a query string.
In your controller, you'd have something along these lines:
def show
#product = Product.find(params[:id])
# do stuff
end
def search
# I'm assuming your Product model has a `name` attribute
#product = Product.find_by(name: params[:name])
# do stuff
end
You probably want some error checking on find_by returning nil because :name did not match anything, but that's basically it.
Your routes.rb would have your original route definition:
get "products/:id", to: "products#show", constraints: { id: /[[:digit]]/ }
get "products/:name", to: "products#search", constraints: { name: /[[:alpha:]]/ }
Your application should respond to both:
localhost:3000/products/3
localhost:3000/products/juice

First you have to know what the query string and url.
localhost:3000/products/3 this is url and the three is symbol :id when use question mark localhost:3000/products/3?name=juice, name=juice is query string.
So get "products/:name", to: "products#search", constraints: { name: /[a-zA-Z]/ } you should replace below
get "products/:id?name=:name", to: "products#search", constraints: { id: /\d/,
name: /[a-zA-Z]/ }
example: localhost:3000/products/3?name=xxxxx

I have a feeling the regex matching might be a little different in routes - I think it anchors to the start and end of the parameter every time.
With that in mind, your regexes are matching a single character each. Try id: /\d+/ and name: /.*\D.*/ instead.

It might be easier and more Railsy just to specify a different route:
get "products/:id", to: "products#show"
get "products/search/:name", to: "products#search"
Or, just have your products#index action handle params[:q] as the search string, that's quite common too. Then your URL would be www.example.com/products?q=juice.

Related

How to programmatically get a Rails Routes constraint?

I'm trying to collect all the Rails routes, and dump their named_route, path and constraint (if there is one).
I'm able to get the named_route, path, but I'm trying to find a way to get the routes constraint.
routes = Rails.application.routes.routes.map do |route|
path = route.path.spec.to_s.gsub!("(.:format)", '')
# route.constraints always output {} ??
puts route.constraints
name = route.name
[name, path]
end.to_h
Here is an example of a route..
USER_NAME_PATTERN ||= /#[a-z0-9][-a-z0-9]+/
get ":username/:slug", to: 'calcs#show', as: :user_calc , constraints: {username: USER_NAME_PATTERN }
How to programmatically get a Rails Routes constraint?
Constraints are a bit complicated, because there are multiple kinds of route constraint, which are specified the same way in routes.rb, but get handled internally somewhat differently.
The kind of constraint in your question is a segment constraint, which imposes a format on one or more segments of the path. Segment constraints are stored in the requirements hash at the path level, using keys that correspond to the segment names (segment names are stored in the parts attribute on the Route).
> route = Rails.application.routes.named_routes.get(:user_calc)
=> #<ActionDispatch::Journey::Route:0x00007f6aa05b2cc0>
> route.parts
=> [:username, :slug, :format]
> route.path.requirements
=> {:username=>/#[a-z0-9][-a-z0-9]+/}
There are also request constraints, which constrains a route based on any method on the Request object that returns a string. These get specified similarly to segment constraints:
get ":username/:slug", to: 'calcs#show', as: :user_calc , constraints: {subdomain: 'admin'}
Request constraints are stored in the constraints attribute on the Route object.
> route = Rails.application.routes.named_routes.get(:user_calc)
=> #<ActionDispatch::Journey::Route:0x00007f6aa05b2cc0>
> route.constraints
=> {:subdomain=>"admin"}
You can also specify constraints using a lambda or class that responds to .matches?. These advanced constraints seem to be handled by a different mechanism, but I haven't dug around enough in the code to understand exactly how.

ElegantRails - Multiple Routes to one Controller Action

I'm wondering if there is a more cleaner or elegant way of translating multiple routes to one controller action using Rails.
#routes.rb
get 'suggestions/proxy', to: 'suggestions#index'
get 'suggestions/aimee', to: 'suggestions#index'
get 'suggestions/arty', to: 'suggestions#index'
...
#suggestion_controller.rb
case request.env['PATH_INFO']
when '/suggestions/proxy'
#suggestions = Suggestion.all.where(:suggestion_type => 'proxy')
when '/suggestions/aimee'
#suggestions = Suggestion.all.where(:suggestion_type => 'aimee')
when '/suggestions/arty'
#suggestions = Suggestion.all.where(:suggestion_type => 'arty')
...
else
#suggestions = Suggestion.all
end
I've read this this post, but I kept getting errors when using it.
It's not a big deal if there's not a lot to be done here. I'm building a website on a video game I like playing called Dirty Bomb and there is a total of 19 mercenaries that need to be listed, so that's why I wanted a more cleaner way of doing this.
Thanks.
Absolutely there is. You can use a parameter directly in your route. Even further, you can then use that parameter directly in your query, rather than using a case statement.
#routes.rb
get 'suggestions/:type', to: 'suggestions#index'
# suggestions_controller.rb
def index
#suggestions = Suggestion.where(suggestion_type: params[:type])
end
It's always a better practice to base your controller actions after parameters, rather than doing any interpretation of the path or request objects.
Hope it works!

Rails 4.2 routing: deep and dynamic defaults for params?

I'm looking to reuse controllers while maintaining pretty links for different resources.
Let's say there are articles, users and comments in the system.
Comments controller index action takes a hash parameter named filter_params and does the filtering. So if params[:filter_params][:user_id] has value 1, it will display all comments by user with ID 1. Similarly with articles and article_id.
What I'm looking for is to have the following routes work:
/users/1/comments, to comments#index, params { filter_params: { user_id: 1 } }
/articles/1/comments, to comments#index, params { filter_params: { article_id: 1 } }
/articles/1/comments?filter_params[user_id]=1, to comments#index, params { filter_params: { article_id: 1, user_id: 1 } }
My initial idea was to use defaults option for routes and construct the default filter_params there. But that seems to accept only a static hash instead of a proc where I could access the request. And on top of that, it would not work with 3rd example, since defaults can't get overwritten and subsequently ?filter_params[user_id]=1 would get ignored.
So is there a way to make it work with only the router? Or should I give up and create a before_filter in CommentsController that stuffs params[:user_id] and params[:article_id] into params[:filter_params]?

Rails: Request-based (& Database-based) Routing

I am trying to get rid of some scope-prefixes I am currently using in my app.
At the moment my Routes look like this (simplified example):
scope 'p'
get ':product_slug', as: :product
end
scope 't' do
get ':text_slug', as: :text
end
which for example generates these paths:
/p/car
/t/hello-world
Now I want the paths to work without the prefixed letters (p & t). So I restrict the slugs to the existing database entries (which btw works great):
text_slugs = Text.all.map(&:slug)
get ':text_slug', as: :text, text_slug: Regexp.new( "(#{text_slugs.join('|')})"
product_slugs = Product.all.map(&:slug)
get ':product_slug', as: :product, product_slug: Regexp.new( "(#{product_slugs.join('|')})"
The problem:
This is a multi-tenant app which means that someones text_slug could be another ones product_slug and vice versa. That's why I have to filter the slugs by the current site (by domain).
A solution would look like this:
text_slugs = Site.find_by_domain(request.host).texts.all.map(&:slug)
get ':text_slug', as: :text, text_slug: Regexp.new( "(#{text_slugs.join('|')})"
But request isn't available in routes.rb and I everything I tried won't work.
The direct call to Rack::Request needs the correct env variable which doesn't seem to be present in Application.routes, otherwise this could work:
req = Rack::Request.new(env)
req.host
I really tried alot and am thankful for any hint!
You may be able to use advanced constraints for this: http://guides.rubyonrails.org/routing.html#advanced-constraints.
class SlugConstraint
def initialize(type)
#type = type
end
def matches?(request)
# Find users subdomain and look for matching text_slugs - return true or false
end
end
App::Application.routes.draw do
match :product_slug => "products#index", :constraints => SlugConstraint.new(:product)
match :tag_slug => "tags#index", :constraints => SlugConstraint.new(:tag)
end
BTW - You may run into problems with testing, but that's another issue...

How to have custom urls that map to values in the db?

I want to have urls like this:
www.example.com/topic1/...
www.example.com/topic2/...
www.example.com/topic3/...
And these should be served using the TopicController.
The values topic1, topic2, topic3, .. are coming from the table in the database (topics).
Is this possible?
What will my route look like then? These topics will be added ofcourse, it isn't something that is static in nature.
Try:
match '*a/' => 'topic#show' # assume the action is show
params[:a] will equal topic1 etc.
The closest solution I can think of would be to define a route such as
match "/topic/:name" => "topic#process_topic"
and the corresponding action in the TopicController
def process_topic
#topic = Topic.find_by_name(params[:name])
case #topic.name
when topic1
...
when topic2
...
end
end

Resources