views:

168

answers:

6

In my application, I am trying to get my API to mimic GitHub's in how it has the (.:format) in the beginning of the route rather than appending it optionally at the end.

Here is my code that is "working" but can be ignored:

map.namespace :api do |api|
  api.namespace :v1 do |v1|
    v1.resource :company, :path_prefix => "api/v1/:format"
  end
end

I can go to /api/v1/xml/company.json and it Rails will provide json as the params[:format] rather than xml.

When I run rake routes I am getting

/api/v1/:format/company(.:format)

Is there a way to get it to return:

/api/v1/:format/company

Thanks in advance!

A: 

I believe the (.format) is optional (that's what the parenthesis mean), so /api/v1/:format/company(.:format) == /api/v1/:format/company

If you want to change it any more than that, you'll need to hack/monkey patch rails.

BJ Clark
I understand, but provided I put a different extension at the end of my URI, I can override the custom placed :format, there needs to be a format provided, this is why I believe I should put it here.
Garrett
Just straight to the point, I don't want to the user to be able to append a format at all.
Garrett
A: 

I'm just taking a shot here (I'll check and update tomorrow when I can play with the code), but couldn't you avoid using :format and do something like this?

routes:

map.connect ':controller/:return_type/:action/:id'

controller:

@results = MyObject.all

case :return_type
  when 'xml' render :text => @results.to_xml
  when 'json' render :text => @results.to_json
end

If the case is too messy/ugly, you could easily create a helper method that mimics the behavior you're looking for.

jerhinesmith
I would still like to use the standard respond_to, I believe when you start moving away and creating your own you won't get the same functionality.
Garrett
+2  A: 

This is going to require some serious monkeypatching. Also routing is one of the most complex areas of the Rails codebase, both connecting incoming HTTP requests to your code and generating URLS.

Forgive me if I'm being presumptuous, but as far as I can ascertain, your reason for going against Rails convention is to mimic another company. In other words you're willing to disregard the collective wisdom of Rails contributors in favour of following a decision made by a handful of developers.

I think you should ask yourself is your reason for wanting this compelling enough to be commensurate with the effort required?

The harder it is do something other than the Rails way, the more rigorously one should question their decision. The presence of significant hurdles is usually indicative that there is a better way of doing something.

Steve Graham
A: 

Steve Graham says everything that needs to be said, but a question with 200 rep bounty deserves a proper answer. No matter how ill advised. Besides this does seem like it could be useful.

The monkey patch is surprisingly simple. You just need to override ActionController::Resource#map_resource_routes as follows.

def map_resource_routes(map, resource, action, route_path, 
      route_name = nil, method = nil, resource_options = {} )
  if resource.has_action?(action)
    action_options = action_options_for(action, resource, method, resource_options)

    unless route_path.match(/\/:format\//)       # new line of code
      formatted_route_path = "#{route_path}.:format"
    else                                         # new line of code
      formatted_route_path = route_path          # new line of code
    end                                          # new line of code   

    if route_name && @set.named_routes[route_name.to_sym].nil?
      map.named_route(route_name, formatted_route_path, action_options)
    else
      map.connect(formatted_route_path, action_options)
    end
  end
end

Incorporating this patch into your code involves either patching your rails gem or making a plugin, both of which are pretty simple and left as an exercise for the reader.

The code works by skipping the line that adds the option format to all routes if it the path already contains a parameter named format.

Edit: Linked to the Steve Graham answer that this solution refers to. There was only one at the original time of posting.

EmFi
+1  A: 

EmFi is right. I didn't answer the question merely expressed my opinion.

Put the following code into an initializer file within the config initializers directory inside your Rails app. What you name the file is does not matter to the framework as all files in this directory are in the load path. I suggest that you call it actioncontroller_resource_monkeypatch.rb in order to make the intent clear.

ActionController::Resources.module_eval do
  def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} )
    if resource.has_action?(action)
      action_options = action_options_for(action, resource, method, resource_options)

      formatted_route_path = route_path.match(/\/:format\//) ? route_path : "#{route_path}.:format" 

      if route_name && @set.named_routes[route_name.to_sym].nil?
        map.named_route(route_name, formatted_route_path, action_options)
      else
        map.connect(formatted_route_path, action_options)
      end
    end
  end
end

My answer uses the same method as EmFi's i.e. by monkeypatching ActionController::Resources#map_resource_routes. I decided to throw my hat into the ring because it did not offer a full implementation, that was left as an exercise for yourself. I also feel that a ternary assignment of formatted_route_path is much cleaner and more concise than an if-else/unless-else block. One additional line of code instead of five! That's the least I can do for a 200 bounty!

Now run rake routes

new_api_v1_company GET    /api/v1/:format/company/new  {:action=>"new", :controller=>"api/v1/companies"}
edit_api_v1_company GET    /api/v1/:format/company/edit {:action=>"edit", :controller=>"api/v1/companies"}
     api_v1_company GET    /api/v1/:format/company      {:action=>"show", :controller=>"api/v1/companies"}
                    PUT    /api/v1/:format/company      {:action=>"update", :controller=>"api/v1/companies"}
                    DELETE /api/v1/:format/company      {:action=>"destroy", :controller=>"api/v1/companies"}
                    POST   /api/v1/:format/company      {:action=>"create", :controller=>"api/v1/companies"}

TADA!

Steve Graham
A: 

I used this code:

ActionController::Routing::Routes.draw do |map|
  map.namespace(:v1, :path_prefix => "api/v1/:format") do |v1|
    v1.resources :repositories
  end
end

URLs become api/v1/[json/xml/whatever]/<restful url goes here>

I like the idea of namespacing the versions too (like in your question):

class V1::RepositoriesController < V1::ApplicationController

end

Whilst this method allows you to put the format in the URL twice: It is not up to you to ensure that users do not put the format in the URL twice, it is up to your users to ensure that they do not put the format in the URL twice.

Don't spend time solving PEBKACs.

Ryan Bigg
Why the downvote? I believe this to be a legitimate solution.
Ryan Bigg