Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to DRY up Rails 3 controllers by overriding methods like respond_with?

I'm trying to create a JSONP API for my Rails 3 application. Right now in my controllers, I have a lot of actions which follow this pattern:

# This is from my users_controller.rb, as an example

def index
  @users = User.all
  respond_with(@users, :callback => params[:callback])
end

While this works as is, I would like to DRY it up by not having to repeat the :callback => params[:callback] in every action's call to respond_with. How can I do this?

Update: One thing I've realized that is ugly about my above code is that the :callback => params[:callback] option will be passed for any response format, not just JSON. The following code is probably more correct:

def index
  @users = User.all
  respond_with(@users) do |format|
    format.json { render :json => @users, :callback => params[:callback]}
  end
end

There are a couple ways I've considered to address this problem, but I can't figure out how to make them work:

  • Override render (perhaps in the application controller) so that it accepts a :jsonp option that automatically includes the :callback => params[:callback] parameter. This way I could change the above code to the following, which is somewhat shorter:
def index
  @users = User.all
  respond_with(@users) do |format|
    format.json { render :jsonp => @users}
  end
end
  • Create a responder that overrides to_json in order to solve my problem. That way I could leave out the block and just call respond_with(@users, :responder => 'MyResponder') to solve the issue. Or perhaps I could include this code in an application responder using plataformatec's responders gem so that respond_with(@users) by itself would be sufficient.
like image 716
evanrmurphy Avatar asked Dec 17 '10 03:12

evanrmurphy


4 Answers

Note that technically, it is incorrect to render JSON with a callback parameter, since you get a JavaScript response (a function call to the JSON-P callback) rather than a JSON result. So if you have

render :json => my_object, :callback => params[:callback]

and a request for /users?callback=func comes in, Rails would answer

func({…})

with content type application/json, which is incorrect, since the above response is clearly not JSON but JavaScript.

The solution I use is

def respond_with_json(item)
  respond_with do |format|
    format.json { render :json => item }
    format.js   { render :json => item, :callback => params[:callback] }
  end
end

which responds correctly with or without callback. Applying this to the aforementioned solution, we get:

def custom_respond_with(*resources, &block)
  options = resources.extract_options!

  if params[:callback]
    old_block = block
    block = lambda do |format|
      old_block.call(format) if block_given?
      format.js { render :json => resources[0], :callback => params[:callback] }
    end
  end

  respond_with(*(resources << options), &block)
end

Also note the correction to resources[0], otherwise you end up wrapping resources in an extra array as a result of the splat operator.

like image 72
Ruben Verborgh Avatar answered Oct 26 '22 23:10

Ruben Verborgh


THere's a gem that can do this to: rack-jsonp-middleware.

The setup instructions are pretty scant on the site, but I did create a little Rails project that uses it - which you can take a look at the commits and see what I did to get the middleware up and running.

https://github.com/rwilcox/rack_jsonp_example

like image 27
RyanWilcox Avatar answered Oct 27 '22 00:10

RyanWilcox


This is bit 'low-tech' compared to the reponder solution, but what about just creating a private method in your appliation_controller.rb to handle this. The params variable will be available to it and you could pass the @users object to it.

#application_controller.rb
private
  def jsonp(my_object)
    render :json => my_object, :callback => params[:callback]
  end

#controller
def index
  @users = User.all
  respond_with(@users) do |format|
    format.json { jsonp(@users)}
  end
end
like image 28
johnmcaliley Avatar answered Oct 26 '22 23:10

johnmcaliley


Thanks to samuelkadolph for helping me in the #rubyonrails IRC channel today. He provided a solution in this gist, copied below for convenience:

def custom_respond_with(*resources, &block)
  options = resources.extract_options!

  if options[:callback]
    old_block = block
    block = lambda do |format|
      old_block.call(format) if block_given?
      format.json { render :json => [] }
    end
  end

  respond_with(*(resources << options), &block)
end

I haven't tried this in my application yet, but I can see that it should work. He also confirmed that I could similarly override the respond_with method itself simply by changing the name of this method and changing the last line of the definition to super(*(resources << options), &block).

I think this will work for me. However, I'm still interested in knowing how to write a custom responder to do the job. (It would be a more elegant solution, IMHO.)

Update: I tried this in my application and it works with some minor changes. Here is the version I'm using now in the private section of my ApplicationController, designed to automatically provide the :callback => params[:callback] option to JSON requests:

def custom_respond_with(*resources, &block)
  options = resources.extract_options!

  if params[:callback]
    old_block = block
    block = lambda do |format|
      old_block.call(format) if block_given?
      format.json { render :json => resources, :callback => params[:callback] }
    end
  end

  respond_with(*(resources << options), &block)
end

Note that I had to change if options[:callback] to if params[:callback] in order to get it working.

like image 34
evanrmurphy Avatar answered Oct 27 '22 01:10

evanrmurphy