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.
 
         加载中,请稍侯......
 加载中,请稍侯......
      
精彩评论