Search code examples
ruby-on-railsroutesactivemodelruby-on-rails-4

Changing respond_to url for an ActiveModel object


Summary:

How do I customize the path that respond_to generates for an ActiveModel object?

Update: I'm looking for a hook, method override, or configuration change to accomplish this, not a workaround. (A workaround is easy but not elegant.)

Context & Example:

Here is an example to illustrate. I have a model, Contract, which has a lot of fields:

class Contract < ActiveRecord::Base
  # cumbersome, too much for a UI form
end

To make the UI code easier to work with, I have a simpler class, SimpleContract:

class SimpleContract
  include ActiveModel::Model

  # ...

  def contract_attributes
    # convert SimpleContract attributes to Contract attributes
  end

  def save
    Contract.new(contract_attributes).save
  end
end

This works well, but I have a problem in my controller...

class ContractsController < ApplicationController
  # ...

  def create
    @contract = SimpleContract.new(contract_params)
    flash[:notice] = "Created Contract." if @contract.save
    respond_with(@contract)
  end

  # ...
end

The problem is that respond_with points to simple_contract_url, but I want it to point to contract_url instead. What is the best way to do that? (Please note that I'm using ActiveModel.)

(Note: I'm using Rails 4 Beta, but that isn't central to my problem. I think a good answer for Rails 3 will work as well.)

Sidebar: if this approach to wrapping a model in a lightweight ActiveModel class seem unwise to you, please let me know in the comments. Personally, I like it because it keeps my original model simple. The 'wrapper' model handles some UI particulars, which are intentionally simplified and give reasonable defaults.


Solution

  • First, here is an answer that works:

    class SimpleContract
      include ActiveModel::Model
    
      def self.model_name
        ActiveModel::Name.new(self, nil, "Contract")
      end
    end
    

    I adapted this answer from kinopyo's answer to Change input name of model.

    Now, for the why. The call stack of respond_to is somewhat involved.

    # Start with `respond_with` in `ActionController`. Here is part of it:
    
    def respond_with(*resources, &block)
      # ...
      (options.delete(:responder) || self.class.responder).call(self, resources, options)
    end
    
    # That takes us to `call` in `ActionController:Responder`:
    
    def self.call(*args)
      new(*args).respond
    end
    
    # Now, to `respond` (still in `ActionController:Responder`):
    
    def respond
      method = "to_#{format}"
      respond_to?(method) ? send(method) : to_format
    end
    
    # Then to `to_html` (still in `ActionController:Responder`):
    
    def to_html
      default_render
    rescue ActionView::MissingTemplate => e
      navigation_behavior(e)
    end
    
    # Then to `default_render`:
    
    def default_render
      if @default_response
        @default_response.call(options)
      else
        controller.default_render(options)
      end
    end
    

    And that is as far as I've gotten for the time being. I have not actually found where the URL gets constructed. I know that it happens based on model_name, but I have not yet found the line of code where it happens.