public
Description: response for lets you decorate your actions respond_to blocks
Homepage: http://blog.ardes.com/response_for
Clone URL: git://github.com/ianwhite/response_for.git
Click here to lend your support to: response_for and make a donation at www.pledgie.com !
response_for / lib / ardes / response_for.rb
100644 181 lines (165 sloc) 7.32 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
module Ardes #:nodoc:
  # included into ActionController::Base
  module ResponseFor
    def self.included(base)
      base.class_eval do
        extend ClassMethods
        alias_method_chain :default_render, :response_for
        alias_method_chain :template_exists?, :response_for
        alias_method_chain :respond_to, :response_for
      end
    end
    
    module ClassMethods
      # response_for allows you to specify a default response for your actions that don't specify a
      # respond_to block.
      #
      # Using response_for, you may keep the response logic out of the action, so that it can be overriden easily
      # without having to rewrite the entire action. This is very useful when subclassing controllers.
      #
      # == Usage
      #
      # response_for :action1 [, :action2], [,:types => [:mime, :type, :list]] [ do |format| ... end] # or
      #
      # === Example
      #
      # class FooController < ApplicationController
      # def index
      # @foos = Foo.find(:all)
      # end
      #
      # def show
      # @foo = Foo.find(params[:id])
      # end
      # end
      #
      # # this controller needs to respond_to fbml on index, and
      # # js, html and xml (templates) on index and show
      # class SpecialFooController < FooController
      # response_for :index do |format|
      # format.fbml { render :inline => turn_into_facebook(@foos) }
      # end
      #
      # response_for :index, :show, :types => [:html, :xml, :js]
      # end
      #
      # === when response_for kicks in
      #
      # response_for only kicks in if the action (or any filters) have not already redirected or rendered.
      #
      # This means that if you foresee wanting to override your action's responses, you should write them without
      # a respond_to block, but with a response_for block (the latter can be overridden by subsequent response_fors, the
      # former cannot)
      #
      # === Other examples
      #
      # response_for :index, :types => [:fbml] # index will respond to fbml and try to render, say, index.fbml.builder
      #
      # response_for :update do |format| # this example is for a resources_controller controller
      # if !(resource.new_record? || resource.changed?) # => resource.saved?
      # format.js { render(:update) {|page| page.replace dom_id(resource), :partial => resource }}
      # else
      # format.js { render(:update) {|page| page.visual_effect :shake, dom_id(resource) }}
      # end
      # end
      #
      # === Notes
      #
      # * If the before_filters or action renders or redirects, then response_for will not be invoked.
      # * you can stack up multiple response_for calls, the most recent has precedence
      # * the specifed block is executed within the controller instance, so you can use controller
      # instance methods and instance variables (i.e. you can make it look just like a regular
      # respond_to block).
      # * you can add a response_for an action that has no action method defined. This is just like
      # defining a template for an action that has no action method defined.
      # * you can combine the :types option with a block, the block has precedence if you specify the
      # same mime type in both.
      def response_for(*actions, &block)
        (options = actions.extract_options!).assert_valid_keys(:types)
        
        types_block = if options[:types]
          proc {|responder| Array(options[:types]).each {|type| responder.send type}}
        end
        
        # store responses against action names
        actions.collect(&:to_s).each do |action|
          action_responses[action] ||= []
          action_responses[action].unshift types_block if types_block
          action_responses[action].unshift block if block
        end
      end
    
      # remove any response for the specified actions. If no arguments are given,
      # then all repsonses for all actions are removed
      def remove_response_for(*actions)
        if actions.empty?
          instance_variable_set('@action_responses', nil)
        else
          actions.each {|action| action_responses.delete(action.to_s)}
        end
      end
      
      # return action_responses Hash. On initialize, set and return hash whose values are copies of superclass action_responses, if any
      def action_responses
        instance_variable_get('@action_responses') || instance_variable_set('@action_responses', copy_of_each_of_superclass_action_responses)
      end
      
      # takes any responses from the argument (a controller, or responses module) and adds them to this controller's responses
      def include_responses_from(responses_container)
        responses_container.action_responses.each do |action, responses|
          action_responses[action] ||= []
          action_responses[action].unshift(*responses)
        end
      end
      
    private
      def copy_of_each_of_superclass_action_responses
        (superclass.action_responses rescue {}).inject({}){|m,(k,v)| m.merge(k => v.dup)}
      end
    end
    
  protected
    # does a response exist for the current action?
    def response_exists?
      self.class.action_responses.keys.include?(action_name.to_s)
    end
    
    # we extend template_exists? to return true if a template OR a response exists corresponding to the current action.
    # This is so that a default render will be triggered when no action, but a repsonse does exist.
    def template_exists_with_response_for?
      response_exists? || template_exists_without_response_for?
    end
 
    # if there are responses for the current action, then respond_to them
    #
    # we rescue the case where there were no responses, so that the default_render
    # action will be performed
    def respond_to_action_responses
      if !@respond_to_performed && (responses = self.class.action_responses[action_name]) && responses.any?
        respond_to do |responder|
          responses.each {|response| instance_exec(responder, &response) }
        end rescue Responder::NoResponsesError
      end
    end
    
    # this method is invoked if we've got to the end of an action without
    # performing, which is when we respond_to any repsonses defined
    def default_render_with_response_for
      respond_to_action_responses
      default_render_without_response_for unless performed?
    end
    
    def respond_to_with_response_for(*args, &block)
      @respond_to_performed = true
      respond_to_without_response_for(*args, &block)
    end
    
    # included into ActionController::MimeResponds::Responder
    module Responder
      class NoResponsesError < RuntimeError; end
      
      def self.included(responder)
        responder.class_eval do
          # we make the responder raise an error if there are no responses
          def respond_with_response_for
            raise NoResponseError if @responses.empty?
            respond_without_response_for
          end
          alias_method_chain :respond, :response_for
        end
      end
    end
    
    module VERSION #:nodoc:
      MAJOR = 0
      MINOR = 2
      TINY = 1
 
      STRING = [MAJOR, MINOR, TINY].join('.')
    end
  end
end