开发者

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:

  1. I got rid of the Response class.

  2. 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
    
  3. 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
    
  4. 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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜