Specifying Entire Path As Optional Rails 3.0.0 - ruby-on-rails

I want to create a Rails 3 route with entirely optional parameters. The example broken route is:
match '(/name/:name)(/height/:height)(/weight/:weight)' => 'people#index'
Which results in 'rake:routes' yielding:
/(/name/:name)(/height/:height)(/weight/:weight)
And thus adding an initial slash to all links:
...
The route works if I specify it as:
match '/people(/name/:name)(/height/:height)(/weight/:weight)' => 'people#index'
But I want to have this as the root URL (as with the first example, which does not work). Thanks.

I don't know if this can be done with Rails' routing engine, but you can add a custom middleware to your stack and it should work just fine.
Put this in lib/custom_router.rb
class CustomRouter
def initialize(app)
#app = app
end
def call(env)
if env['REQUEST_PATH'].match(/^\/(name|height|weight)/)
%w(REQUEST_PATH PATH_INFO REQUEST_URI).each{|var| env[var] = "/people#{env[var]}" }
end
#app.call(env)
end
end
and add
config.middleware.use "CustomRouter"
to your config/application.rb.
You can then set the route like
match '/people(/name/:name)(/height/:height)(/weight/:weight)' => 'people#index'
and it will work.

Does it work if you use a separate root mapping?
root :to => 'people#index'
match '(/name/:name)(/height/:height)(/weight/:weight)' => 'people#index'
It does seem like a pretty major oversight in the new routing system.
There could be a way to hack it through a rack middleware (maybe overriding Rack::URLMap), but that's a little out of my league.

Related

Rails routes, constraints and variable scope

I have the following wildcard routes & constraints setup ...
get '*path' => 'profiles#show', constraints: SlugConstraint.new
get '*path' => 'blogs#show', constraints: SlugConstraint.new
and
class SlugConstraint
def initialize
#slugs = Slug.all.map(&:name)
end
def matches?(request)
request.url =~ /\/(.+)/
#slugs.include?($1)
end
end
... a variation based on the issue I described here:
Rails wildcard route with database lookup & multiple controllers
My issue now is that if the first call to SlugConstraint.new returns false (so that the 2nd routes.rb SlugConstraint.new now gets called) I don't want to have to redo the call to:
Slug.all.map(&:name)
How do I properly save (or scope) the #slugs data from the first constraint call that failed, so that I can access it if needed in the next constraint call?
Thanks.
Routing
You're not going to be able to use 2 routing patterns for the same path
When you send a request to Rails (or any other MVC application), Rails will take the path you've sent & consequently try to assign the right route (controller#action) for it.
This happens sequentially - IE Rails will look through your routes from top -> bottom until it finds one which corresponds. As you have two routes to match the same path, you're not going to be able to use the set up you have
--
App-Wide Slugs
What you're looking for is something called app-wide slugs - which essentially means you're able to manage a single slug path, and have a system in the back-end to accommodate it.
You're on the brink of being able to achieve this, and whilst I don't have any code to help, I do have an idea, which I found here:
#config/routes.rb
get '*path' => MyRouter.new, constraints: SlugConstraint.new
#lib/my_router.rb
class MyRouter
def call(env)
# Matched from routes, you can access all matched parameters
view_name= env['action_dispatch.request.path_parameters'][:view_name]
# Compute these the way you like, possibly using view_name
controller= 'post'
my_action= 'show'
controller_class= (controller + '_controller').camelize.constantize
controller_class.action(my_action.to_sym).call(env)
end
end
This will allow you to pick up the slugged paths, whilst routing to the correct controller. This is TOTALLY untested & just a stab in the dark - if you want to go over it with me, comment & we can have a look

Routing Error with Post/Put requests (Passenger Headers)

I've run into a weird problem and after a bunch of research can't get any closer. I've got several forms that upload files via Carrierwave. When I upload the information, part of the route gets cut off (I think).
For example, I have a multi-part form submitting to:
https:/domain/programs/223/add_file as POST
but on submission I get the error
No route matches [POST] "/223/add_file"
even though what's in my address bar is the complete route. And if submit the complete route as a GET request it works fine. When I run rake routes the route shows up just fine.
Here is a subset of my route:
resources :programs do
match "add_file" => "programs#add_file"
If it matters, I'm running Rails 3.2.2 with Passenger on Apache. The problem only happens on this production server, never in development.
Any ideas? I'm stuck on this one as it effects multiple routes and I've tried defining a custom route just for that form with no luck.
Update:
When I remove multi-part => true or the file_field_tag from the form it fixes the problem. It's still an issue but seems to be less about routing than about the form with file uploads.
Create passenger_extension.rb in the lib folder with this code:
Passenger 3
module PhusionPassenger
module Utils
protected
NULL = "\0".freeze
def split_by_null_into_hash(data)
args = data.split(NULL, -1)
args.pop
headers_hash = Hash.new
args.each_slice(2).to_a.each do |pair|
headers_hash[pair.first] = pair.last unless headers_hash.keys.include? pair.first
end
return headers_hash
end
end
end
Passenger 5
module PhusionPassenger
module Utils
# Utility functions that can potentially be accelerated by native_support functions.
module NativeSupportUtils
extend self
NULL = "\0".freeze
class ProcessTimes < Struct.new(:utime, :stime)
end
def split_by_null_into_hash(data)
args = data.split(NULL, -1)
args.pop
headers_hash = Hash.new
args.each_slice(2).to_a.each do |pair|
headers_hash[pair.first] = pair.last unless headers_hash.keys.include? pair.first
end
return headers_hash
end
def process_times
times = Process.times
return ProcessTimes.new((times.utime * 1_000_000).to_i,
(times.stime * 1_000_000).to_i)
end
end
end # module Utils
end # module PhusionPassenger
And then in 'config/application.rb' do:
class Application < Rails::Application
...
config.autoload_paths += %W(#{config.root}/lib)
require 'passenger_extension'
end
And then restart a webserver.
NOTICE: I'm not sure that this doesn't break any other functionality so use it on your own risk and please let me know if you find any harm from this approach.
One issue here is you're not specifying whether the route is defined on the collection or a member. Which one of these is the correct route?
programs/:id/add_file
programs/add_file
You should construct your routes like this:
resources :programs do
post 'add_file', :on => :member
end
or
resources :programs do
member do
post 'add_file'
end
end
The above will take post requests at programs/:id/add_file and send them to ProgramsController.add_file with the params[:id] as the program id.
If you want this on the collection, you could do:
resources :programs do
post 'add_file', :on => :collection
end
or
resources :programs do
collection do
post 'add_file'
end
end
This would take post requests at programs/add_file and send them to ProgramsController.add_file, but no params[:id] would be set.
In general you should always specify whether routes are on the collection or member, and you should specify which verb a route should accept (ie use 'get' or 'post' etc. instead of 'match').
Try the above and see if that solves your problem, if not please let me know and I'll take another look.
I think you may need to add
:via => [:post]
to your route specification. It seems odd that it'd work on development and not on production, but as I understand rails routing, the matcher that you've added is only going to respond to get.
Try changing your match to
match "add_file" => "programs#add_file", :via => [:post]
Also, based on the answer just submitted by Andrew, you're probably better off using the member specifier to be explicit about the fact that the operation is happening on a particular Program with a particular id, and not the collection. It also should save some code in your add_file method which is probably working hard to get the id parameter from the url.

Sinatra on Rails '/' mapping

I might be missing something but how can I map "/" in Rails to execute the Sinatra application? I have:
class Core < Sinatra::Base
get '/' do
"This is root but it is caput."
end
get '/test' do
"This is test and it works"
end
end
So if I do routing like that:
match '/test' => Core
match '/'=>Core
only '/test' fires Sinatra app '/' runs the Rails. Actually I want every route to be handled by Sinatra app.
You just need to remove index.html from the public folder.
You can mount the whole app with mount Core, :at => '/' in order to let Sinatra do all the routing.
Given that Sinatra creates a DSL for defining routes (that are not accessible directly as methods), you'll probably need to add a helper method so you can specifically invoke the route:
i.e.
def launch_sinatra_app
status, headers, body = call env.merge("PATH_INFO" => '/')
[status, headers, body.map(&:upcase)] # proper rack response
end
and then in config/routes.rb for rails:
match :root => 'Core#launch_sinatra_app'

Remove Routes Specified in a Gem?

Is there a way to remove routes specified in a gem in Rails 3? The exception logger gem specifies routes which I don't want. I need to specify constraints on the routes like so:
scope :constraints => {:subdomain => 'secure', :protocol => 'https'} do
collection do
post :query
post :destroy_all
get :feed
end
end
Based on the Rails Engine docs, I thought I could create a monkey patch and add a routes file with no routes specified to the paths["config/routes"].paths Array but the file doesn't get added to ExceptionLogger::Engine.paths["config/routes"].paths
File: config/initializers/exception_logger_hacks.rb
ExceptionLogger::Engine.paths["config/routes"].paths.unshift(File.expand_path(File.join(File.dirname(__FILE__), "exception_logger_routes.rb")))
Am I way off base here? Maybe there is a better way of doing this?
It is possible to prevent Rails from loading the routes of a specific gem, this way none of the gem routes are added, so you will have to add the ones you want manually:
Add an initializer in application.rb like this:
class Application < Rails::Application
...
initializer "myinitializer", :after => "add_routing_paths" do |app|
app.routes_reloader.paths.delete_if{ |path| path.include?("NAME_OF_GEM_GOES_HERE") }
end
Here's one way that's worked for me.
It doesn't "remove" routes but lets you take control of where they match. You probably want every route requested to match something, even if it is a catch all 404 at the bottom.
Your application routes (MyApp/config/routes.rb) will be loaded first (unless you've modified the default load process). And routes matched first will take precedence.
So you could redefine the routes you want to block explicitely, or block them with a catch all route at the bottom of YourApp/config/routes.rb file.
Named routes, unfortunately, seem to follow ruby's "last definition wins" rule. So if the routes are named and your app or the engine uses those names, you need to define the routes both first (so yours match first), and last (so named routes point as you intended, not as the engine defines.)
To redefine the engine's routes after the engine adds them, create a file called something like
# config/named_routes_overrides.rb
Rails.application.routes.draw do
# put your named routes here, which you also included in config/routes.rb
end
# config/application.rb
class Application < Rails::Application
# ...
initializer 'add named route overrides' do |app|
app.routes_reloader.paths << File.expand_path('../named_routes_overrides.rb',__FILE__)
# this seems to cause these extra routes to be loaded last, so they will define named routes last.
end
end
You can test this routing sandwich in the console:
> Rails.application.routes.url_helpers.my_named_route_path
=> # before your fix, this will be the engine's named route, since it was defined last.
> Rails.application.routes.recognize_path("/route/you/want/to/stop/gem/from/controlling")
=> # before your fix, this will route to the controller and method you defined, rather than what the engine defined, because your route comes first.
After your fix, these calls should match each other.
(I posted this originally on the refinery gem google group here: https://groups.google.com/forum/?fromgroups#!topic/refinery-cms/N5F-Insm9co)

Dynamic URL -> Controller mapping for routes in Rails

I would like to be able to map URLs to Controllers dynamically based on information in my database.
I'm looking to do something functionally equivalent to this (assuming a View model):
map.route '/:view_name',
:controller => lambda { View.find_by_name(params[:view_name]).controller }
Others have suggested dynamically rebuilding the routes, but this won't work for me as there may be thousands of Views that map to the same Controller
This question is old, but I found it interesting. A fully working solution can be created in Rails 3 using router's capability to route to a Rack endpoint.
Create the following Rack class:
class MyRouter
def call(env)
# Matched from routes, you can access all matched parameters
view_name= env['action_dispatch.request.path_parameters'][:view_name]
# Compute these the way you like, possibly using view_name
controller= 'post'
my_action= 'show'
controller_class= (controller + '_controller').camelize.constantize
controller_class.action(my_action.to_sym).call(env)
end
end
In Routes
match '/:view_name', :to => MyRouter.new, :via => :get
Hint picked up from http://guides.rubyonrails.org/routing.html#routing-to-rack-applications which says "For the curious, 'posts#index' actually expands out to PostsController.action(:index), which returns a valid Rack application."
A variant tested in Rails 3.2.13.
So I think that you are asking that if you have a Views table and a View model for it where the table looks like
id | name | model
===================
1 | aaa | Post
2 | bbb | Post
3 | ccc | Comment
You want a url of /aaa to point to Post.controller - is this right?
If not then what you suggest seems fine assuming it works.
You could send it to a catch all action and have the action look at the url, run the find_by_name and then call the correct controller from there.
def catch_all
View.find_by_name('aaa').controller.action
end
Update
You can use redirect_to and even send the params. In the example below you I am sending the search parameters
def catch_all
new_controller = View.find_by_name('aaa').controller
redirect_to :controller => new_controller, :action => :index,
:search => params[:search]
end
Here is a nice Rack Routing solution to SEO contributed by zetetic and Steve ross
Testing Rack Routing Using rSpec
It shows you how to write a custom dispatcher (where you can do a db lookup if needed) and with constraints, and testing as well.
As suggested in the question Rails routing to handle multiple domains on single application, I guess you could use Rails Routing - Advanced Constraints to build what you need.
If you have a limited space of controllers (with unlimited views pointing to them), this should work. Just create a constraint for each controller that verifies if the current view matches them.
Assuming you have a space of 2 controllers (PostController and CommentController), you could add the following to your routes.rb:
match "*path" => "post#show", :constraints => PostConstraint.new
match "*path" => "comment#show", :constraints => CommentConstraint.new
Then, create lib/post_constraint.rb:
class PostConstraint
def matches?(request)
'post' == Rails.cache.fetch("/view_controller_map/#{request.params[:view_name]}") { View.find_by_name(request.params[:view_name]).controller }
end
end
Finally, create lib/comment_constraint.rb:
class CommentConstraint
def matches?(request)
'comment' == Rails.cache.fetch("/view_controller_map/#{request.params[:view_name]}") { View.find_by_name(request.params[:view_name]).controller }
end
end
You can do some improvements, like defining a super constraint class that fetches the cache, so you don't have to repeat code and don't risk fetching a wrong cache key name in one of the constraints.

Resources