开发者

rails 3 response format and versioning using vendor MIME type in the Accept header

Preamble:

I investigated how to version an API and found several ways to do it. I decided to try peter williams' suggestion and created new vendor mime types to specify version and format. I could find no definitive write-up for doing this following "the rails way" so I pieced together info from several places. I was able to get it working, but there is some goofiness in the way the renderers handle Widget array vs Widget 开发者_StackOverflow中文版instance in respond_with.

Basic steps & problem:

I registered mime types and added renderers for version 1 in both xml and json to ApplicationController, the renderers call to_myproj_v1_xml and to_myproj_v1_json methods in the model. respond_with(@widget) works fine but respond_with(@widgets) throws an HTTP/1.1 500 Internal Server Error saying that the "Template is missing".

Workaround:

"Template is missing" means that no render was called and no matching template exists. by accident, I discovered that it is looking for a class method... so I came up with the code below which works but I'm not really happy with it. The goofiness is mostly in and related to xml = obj.to_myproj_v1_xml(obj) and the duplication in the model.

My question is - has anyone done anything similar in a slightly cleaner fashion?

-= updated code =-

config/initializers/mime_types.rb:

Mime::Type.register 'application/vnd.com.mydomain.myproj-v1+xml', :myproj_v1_xml
Mime::Type.register 'application/vnd.com.mydomain.myproj-v1+json', :myproj_v1_json

app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  protect_from_forgery
  before_filter :authenticate

  ActionController.add_renderer :myproj_v1_xml do |obj, options|
    xml = obj.to_myproj_v1_xml
    self.content_type ||= Mime::Type.lookup('application/vnd.com.mydomain.myproj-v1+xml')
    self.response_body = xml
  end

  ActionController.add_renderer :myproj_v1_json do |obj, options|
    json = obj.to_myproj_v1_json
    self.content_type ||= Mime::Type.lookup('application/vnd.com.mydomain.myproj-v1+json')
    self.response_body  = json
  end
end

app/models/widget.rb:

class Widget < ActiveRecord::Base
  belongs_to :user
  V1_FIELDS = [:version, :model, :description, :name, :id]

  def to_myproj_v1_xml
    self.to_xml(:only => V1_FIELDS)
  end

  def to_myproj_v1_json
    self.to_json(:only => V1_FIELDS)
  end

  def as_myproj_v1_json
    self.as_json(:only => V1_FIELDS)
  end
end

app/controllers/widgets_controller.rb:

class WidgetsController < ApplicationController

  respond_to :myproj_v1_xml, :myproj_v1_json

  def index
    @widgets = @user.widgets
    respond_with(@widgets)
  end

  def create
    @widget = @user.widgets.create(params[:widget])
    respond_with(@widget)
  end

  def destroy
    @widget = @user.widgets.find(params[:id])
    respond_with(@widget.destroy)
  end

  def show
    respond_with(@widget = @user.widgets.find(params[:id]))
  end

...

end

config/initializers/monkey_array.rb

class Array

  def to_myproj_v1_json(options = {})
    a = []
    self.each { |obj| a.push obj.as_myproj_v1_json }
    a.to_json()
  end

  def to_myproj_v1_xml(options = {})
    a = []
    self.each { |obj| a.push obj.as_myproj_v1_json } # yes this is json instead of xml.  as_json returns a hash
    a.to_xml()
  end

end

UPDATE:

Found another solution that feels better but still a little weird (I'm still not completely comfortable with monkey patches), probably ok though... basically moved building the response data from the class method to_myproj_v1_json to a monkey patch on Array. This way when there is an Array of Widgets, it calls the instance method as_myproj_v1_json on each Widget and returns the whole Array as desired format.

One note:

  • as_json has nothing to do with json format, just creates a hash. Add custom formatting to as_myproj_v1_json (or an as_json override if you aren't using custom mime types), then to_json will change a hash to a json string.

i have updated the code below to be what is currently used, so the original question may not make sense. if anyone wants the original question and code shown as was and fixed code in a response i can do that instead.


For the answer: see the question :-)

In short, there are different solutions, of which one is in the question above:

  • Monkey-patch Array to implement a method that will give the (old) v1 JSON back


I haven't seen this content type trick used anywhere in a Rails project before so this is new to me. The way I've typically seen it done is to define a route namespace (e.g. /api/v1/) which goes to a controller (say, Api::Version1Controller).

Also, I know you want to do things the "Rails way", and maybe this sounds crotchety coming from a guy who has been with Rails since 1.3, but the whole respond_with / respond_to stuff is rather magic to me. I didn't know that respond_to looks for a to_XXX method when it serializes objects, for instance (maybe I need to read up on that). Having to monkey-patch Array like that seems rather silly. Besides, for an API, formatting the model data is really the view's job, not the model's. I might look into something like rabl in this case. There's a good writeup about it here.

0

上一篇:

下一篇:

精彩评论

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

最新问答

问答排行榜