aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/metal/renderers.rb
blob: 26752571f8518e3772fb18bc7e6d026da2f715b4 (plain) (blame)
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
# frozen_string_literal: true

require "set"

module ActionController
  # See <tt>Renderers.add</tt>
  def self.add_renderer(key, &block)
    Renderers.add(key, &block)
  end

  # See <tt>Renderers.remove</tt>
  def self.remove_renderer(key)
    Renderers.remove(key)
  end

  # See <tt>Responder#api_behavior</tt>
  class MissingRenderer < LoadError
    def initialize(format)
      super "No renderer defined for format: #{format}"
    end
  end

  module Renderers
    extend ActiveSupport::Concern

    # A Set containing renderer names that correspond to available renderer procs.
    # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
    RENDERERS = Set.new

    included do
      class_attribute :_renderers, default: Set.new.freeze
    end

    # Used in <tt>ActionController::Base</tt>
    # and <tt>ActionController::API</tt> to include all
    # renderers by default.
    module All
      extend ActiveSupport::Concern
      include Renderers

      included do
        self._renderers = RENDERERS
      end
    end

    # Adds a new renderer to call within controller actions.
    # A renderer is invoked by passing its name as an option to
    # <tt>AbstractController::Rendering#render</tt>. To create a renderer
    # pass it a name and a block. The block takes two arguments, the first
    # is the value paired with its key and the second is the remaining
    # hash of options passed to +render+.
    #
    # Create a csv renderer:
    #
    #   ActionController::Renderers.add :csv do |obj, options|
    #     filename = options[:filename] || 'data'
    #     str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
    #     send_data str, type: Mime[:csv],
    #       disposition: "attachment; filename=#{filename}.csv"
    #   end
    #
    # Note that we used Mime[:csv] for the csv mime type as it comes with Rails.
    # For a custom renderer, you'll need to register a mime type with
    # <tt>Mime::Type.register</tt>.
    #
    # To use the csv renderer in a controller action:
    #
    #   def show
    #     @csvable = Csvable.find(params[:id])
    #     respond_to do |format|
    #       format.html
    #       format.csv { render csv: @csvable, filename: @csvable.name }
    #     end
    #   end
    def self.add(key, &block)
      define_method(_render_with_renderer_method_name(key), &block)
      RENDERERS << key.to_sym
    end

    # This method is the opposite of add method.
    #
    # To remove a csv renderer:
    #
    #   ActionController::Renderers.remove(:csv)
    def self.remove(key)
      RENDERERS.delete(key.to_sym)
      method_name = _render_with_renderer_method_name(key)
      remove_method(method_name) if method_defined?(method_name)
    end

    def self._render_with_renderer_method_name(key)
      "_render_with_renderer_#{key}"
    end

    module ClassMethods
      # Adds, by name, a renderer or renderers to the +_renderers+ available
      # to call within controller actions.
      #
      # It is useful when rendering from an <tt>ActionController::Metal</tt> controller or
      # otherwise to add an available renderer proc to a specific controller.
      #
      # Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt>
      # include <tt>ActionController::Renderers::All</tt>, making all renderers
      # available in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>.
      #
      # Since <tt>ActionController::Metal</tt> controllers cannot render, the controller
      # must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>,
      # and <tt>ActionController::Renderers</tt>, and have at least one renderer.
      #
      # Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers,
      # you may specify which renderers to include by passing the renderer name or names to
      # +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer
      # (+_render_with_renderer_json+) might look like:
      #
      #   class MetalRenderingController < ActionController::Metal
      #     include AbstractController::Rendering
      #     include ActionController::Rendering
      #     include ActionController::Renderers
      #
      #     use_renderers :json
      #
      #     def show
      #       render json: record
      #     end
      #   end
      #
      # You must specify a +use_renderer+, else the +controller.renderer+ and
      # +controller._renderers+ will be <tt>nil</tt>, and the action will fail.
      def use_renderers(*args)
        renderers = _renderers + args
        self._renderers = renderers.freeze
      end
      alias use_renderer use_renderers
    end

    # Called by +render+ in <tt>AbstractController::Rendering</tt>
    # which sets the return value as the +response_body+.
    #
    # If no renderer is found, +super+ returns control to
    # <tt>ActionView::Rendering.render_to_body</tt>, if present.
    def render_to_body(options)
      _render_to_body_with_renderer(options) || super
    end

    def _render_to_body_with_renderer(options)
      _renderers.each do |name|
        if options.key?(name)
          _process_options(options)
          method_name = Renderers._render_with_renderer_method_name(name)
          return send(method_name, options.delete(name), options)
        end
      end
      nil
    end

    add :json do |json, options|
      json = json.to_json(options) unless json.kind_of?(String)

      if options[:callback].present?
        if content_type.nil? || content_type == Mime[:json]
          self.content_type = Mime[:js]
        end

        "/**/#{options[:callback]}(#{json})"
      else
        self.content_type ||= Mime[:json]
        json
      end
    end

    add :js do |js, options|
      self.content_type ||= Mime[:js]
      js.respond_to?(:to_js) ? js.to_js(options) : js
    end

    add :xml do |xml, options|
      self.content_type ||= Mime[:xml]
      xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
    end
  end
end