How to generate custom response for REST API with Ruby on Rails?
I'm implementing a REST API in Rails 3. We allow for JSON and XML as response formats.
The default respond_with
works fine as long as one wants only the requested resource to be returned, e.g.:
def show
respond_with User.find(params[:id])
end
GET /users/30.xml
<?xml version="1.0" encoding="UTF-8"?>
<user>
<birthday type="date">2010-01-01</birthday>
<company-name>Company</company-name>
<email>email@test.com</email>
<id type="integer">30</id>
</user>
However, I would like to get the following standardized response:
<?xml version="1.0" encoding="UTF-8"?>
<response>
<status>
<success type="boolean">true</success>
</status>
<result>
<user>
<birthday type="date">2010-01-01</birthday>
<company-name>Company</company-name>
<email>email@test.com</email>
<id type="integer">30</id>
</user>
</result>
</response>
How can I achieve this result?
I tried the following, using a custom Response class
class Response
STATUS_CODES = {
:success => 0,
}
extend ActiveModel::Naming
include ActiveModel::Serializers::Xml
include ActiveModel::Serializers::JSON
attr_accessor :status
attr_accessor :result
def initialize(result = nil, status_code = :success)
@status = {
:success => (status_code == :success),
}
@result = result
end
def attributes
@attributes ||= { 'status' => nil, 'result' => nil }
end
end
and redefining the respond_with
method in my ApplicationController
:
def respond_with_with_api_responder(*resources, &block)
respond_with_without_api_responder(Response.new(resources), &block)
end
alias_method_chain :respond_with, :api_responder
However, that does not yield the intended result:
<?xml version="1.0" encoding="UTF-8"?>
<response>
<status>
<success type="boolean">true</success>
</status>
<result type="array"开发者_运维技巧>
<result>
<birthday type="date">2010-01-01</birthday>
<company-name>Company</company-name>
<email>email@test.com</email>
<id type="integer">30</id>
</result>
</result>
</response>
What should be <user>
is now again <result>
. This gets even worse when I return an array as the result, then I get even another <result>
layer. And if I look at the JSON response, it looks almost fine – but notice that there is an array [] too much wrapping the user resource.
GET /users/30.json
{"response":{"result":[{"user":{"birthday":"2010-01-01","company_name":"Company","email":"email@test.com"}}],"status":{"success":true}}}
Any clue what is going on here? How can I get the desired response format? I also tried looking into writing a custom Responder
class, but that boiled down to rewriting the display
method within ActionController:Responder
, giving me the exact same problems:
def display(resource, given_options={})
controller.render given_options.merge!(options).merge!(format => Response.new(resource))
end
I believe that the problem is somehow hidden in the serialization code of ActiveModel
, but I can't seem figure out how I can wrap a resource within a container tag and still achieve that the wrapped resource is being serialized correctly.
Any thoughts or ideas?
Here's what I did in the end:
I got rid of the Response class.
I added to_json and to_xml methods to all models:
[:to_json, :to_xml].each do |method_name| define_method(method_name) do |options = {}| options ||= {} options[:only] ||= # some filtering super(options) end end
I redefined the respond_with method in my ApplicationController:
def api_respond_with(resources, &block) default_respond_with do |format| format.json { render :json => resources, :skip_types => true, :status => :ok } format.xml { render :xml => resources, :skip_types => true, :status => :ok } end end alias_method :default_respond_with, :respond_with alias_method :respond_with, :api_respond_with
I wrote a custom middleware with appropriate methods to add the desired wrapping:
class StandardizedResponseFilter def _call(env) status, headers, response = @app.call(env) if headers['Content-Type'].include? 'application/json' response.body = standardized_json_wrapping(response.body, env) elsif headers['Content-Type'].include? 'application/xml' response.body = standardized_xml_wrapping(response.body, env) end [status, headers, response] end end
If anyone knows a better approach, feel free to leave a comment.
Normally what I'd do in this case is override the ActiveModel#to_xml and ActiveModel#to_json methods. The documentation on #to_xml describes the possible options. You could probably make your Request
object inherit from ActiveModel
, and then override the #to_xml method with a pattern like this:
def to_xml(options = {})
# muck with options such as :only, :except, :methods
options[:methods] ||= []
[:status, :result].each { |m| options[:methods] << m }
super(options)
end
In particular I think you'll find options[:methods] useful because it lets you define arbitrary methods that return attributes and get included in the output.
精彩评论