How to programmatically get a Rails Routes constraint? - ruby-on-rails

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.

Related

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 route constraints with query string or numbers only

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.

Routes regex constraint not working as expected with subgroup in Rails 4

I have the following constraint:
get '/:name' => "products#show", :as => :product, :constraints => {name: /\w+(-\w+)*/}
The following URL:
/aa--aa
Will return a No route matches [GET] "/aa--aa"
But if I do /\w+(-\w+)*/.match('aa--aa') I will get a MatchData object. So, how does Rails handles Regex constraints? Why is this not being consistent with .match?
Rails will embed your constraint as this:
/\A#{tests[key]}\Z/ === parts[key]
See this: formatter
That's why this won't pass.
The ^\w+(-\w+)*$ regex cannot match aa--aa, because dashes need to be separated by words.
I don't know Ruby but I suppose ^ and $ are implicit in your constraints, and I guess you're getting a result for the aa substring because the anchors are no longer implied (I may be totally wrong here).
I suggest the following pattern:
\w+(?:-+\w+)*
I just added a + quantifier to the dash, and made the group non-capturing.

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