bploetz / versionist

A plugin for versioning Rails based RESTful APIs.
MIT License
971 stars 51 forks source link

respond_with error #25

Closed chriskilding closed 12 years ago

chriskilding commented 12 years ago

My controllers are set up to use respond_with so they can handle multiple formats easily.

However I am seeing that, on any actions that need to return a location, particularly POSTs and PUTs, I get an HTTP 500 back.

Under the hood, this is the exception:

Completed 500 Internal Server Error in 454ms

NoMethodError (undefined method `item_url' for #<V1::ItemsController:0xblarg>):
  app/controllers/v1/items_controller.rb:36:in `create'

The item is created successfully, but the respond_with call blows up with this unchecked exception which produces the 500.

Running rake routes says

v1_items POST   /v1/items(.:format) 
new_v1_item GET    /v1/items/new(.:format)
edit_v1_item GET    /v1/items/:id/edit(.:format)
v1_item GET    /v1/items/:id(.:format)
PUT    /v1/items/:id(.:format)
DELETE /v1/items/:id(.:format)

so I appear to have a v1_item_url but not an item_url.

Is this something I've done wrong, or a bug in the way versionist does route namespacing / scoping?

bploetz commented 12 years ago

Can you please include all relevant code (the controller in question, any presenters used, etc) and the full stack trace?

chriskilding commented 12 years ago

Ok, here's the code. He Who Pays The Bills insisted upon a certain level of obfuscation, but hopefully what you need is all there.

Controller:

module V1
  class ItemsController < BaseController
    def create
      item = Item.new(params[:item])

      item.save!

      respond_with item
    end
  end
end

Next controller up:

module V1
  class BaseController < ApplicationController
    before_filter :authenticate_user!
    load_and_authorize_resource    
  end
end

Model:

class Item < ActiveRecord::Base
  # the usual, nothing out of the ordinary here
end

routes.rb:

MyApp::Application.routes.draw do
  api_version(module: "V1", path: "/v1") do
    resources :items

    ... other unrelated resources...
  end

  root :to => 'welcome#index'
  match '*path', to: 'welcome#index'
end

Stack trace:

Started POST "/v1/items" for 127.0.0.1 at 2012-07-23 14:30:55 +0100
  Processing by V1::ItemsController#create as JSON
  Parameters: {"authenticity_token"=>"some token", "item"=>{various correct params}
  # bunch of SQL selects and inserts happen - the record creation does work, you can see it in the DB - and then...
Completed 500 Internal Server Error in 609ms

NoMethodError (undefined method `item_url' for #<V1::ItemsController:0x007fd686928d80>):
  app/controllers/v1/items_controller.rb:36:in `create'

Rendered /Users/chris/.rvm/gems/ruby-1.9.3-p194@myapp/gems/actionpack-3.1.6/lib/action_dispatch/middleware/templates/rescues/_trace.erb (3.0ms)
Rendered /Users/chris/.rvm/gems/ruby-1.9.3-p194@myapp/gems/actionpack-3.1.6/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb (1.0ms)
Rendered /Users/chris/.rvm/gems/ruby-1.9.3-p194@myapp/gems/actionpack-3.1.6/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb within rescues/layout (7.2ms)

No presenters were used.

bploetz commented 12 years ago

This is enough to go on, thanks.

So it looks like your controller is namespaced V1, but your model is not, and that's the disconnect. This is exactly what Presenters are meant for. In most cases, your domain model spans multiple versions of your API, so you don't want to namespace the model classes, but instead you want to create namespaced Presenters which wrap your single domain model. This allows you to add/remove/change fields in the serialized representation of your models in each version of your API, without having to change the model itself.

For example:

# app/presenters/v1/item_presenter.rb
class V1::ItemPresenter < V1::BasePresenter

  attr_accessor :item

  def initialize(item)
    @item = item
  end

  def as_json(options={})
     # ...
  end

  def to_xml(options={}, &block)
    # ...
  end
end

Then your controller would change to look like the following:

module V1
  class ItemsController < BaseController
    def create
      item = Item.new(params[:item])

      item.save!
      status = 200
      response_body = V1::ItemPresenter.new(item)

     respond_to do |format|
      format.xml {render :xml => response_body.to_xml, :status => status}
      format.json {render :json => response_body.as_json, :status => status}
    end
  end
end

I haven't actually tried using Presenters with respond_with, so your mileage may vary, but respond_to as used above definitely works.

chriskilding commented 12 years ago

Aha.... so that's what a presenter does.

I'll have a go building a presenter with respond_with and let you know how it goes. Cheers :)

chriskilding commented 12 years ago

Crap, on the create action I get

Completed 500 Internal Server Error in 610ms

NoMethodError (undefined method `model_name' for V1::ItemPresenter:Class):
  app/controllers/v1/items_controller.rb:36:in `create'

And the offending line in the create action is

respond_with V1::ItemPresenter.new(item)

I guess the presenter has to be beefed up with a few more methods that respond_with needs.

bploetz commented 12 years ago

Closing this issue as I don't think there's a bug here, but keep us posted if you get this working with respond_with.

NickClark commented 11 years ago

This issue here is that respond_with expects the method 'model_name' to be found on the presenter. It would be nice if this gem provided a presenter class to inherit from that would automatically forward this for us.