lib/action_controller/renderer.rb



# frozen_string_literal: true

module ActionController
  # ActionController::Renderer allows you to render arbitrary templates
  # without requirement of being in controller actions.
  #
  # You get a concrete renderer class by invoking ActionController::Base#renderer.
  # For example:
  #
  #   ApplicationController.renderer
  #
  # It allows you to call method #render directly.
  #
  #   ApplicationController.renderer.render template: '...'
  #
  # You can use this shortcut in a controller, instead of the previous example:
  #
  #   ApplicationController.render template: '...'
  #
  # #render allows you to use the same options that you can use when rendering in a controller.
  # For example:
  #
  #   FooController.render :action, locals: { ... }, assigns: { ... }
  #
  # The template will be rendered in a Rack environment which is accessible through
  # ActionController::Renderer#env. You can set it up in two ways:
  #
  # *  by changing renderer defaults, like
  #
  #       ApplicationController.renderer.defaults # => hash with default Rack environment
  #
  # *  by initializing an instance of renderer by passing it a custom environment.
  #
  #       ApplicationController.renderer.new(method: 'post', https: true)
  #
  class Renderer
    attr_reader :defaults, :controller

    DEFAULTS = {
      http_host: "example.org",
      https: false,
      method: "get",
      script_name: "",
      input: ""
    }.freeze

    # Create a new renderer instance for a specific controller class.
    def self.for(controller, env = {}, defaults = DEFAULTS.dup)
      new(controller, env, defaults)
    end

    # Create a new renderer for the same controller but with a new env.
    def new(env = {})
      self.class.new controller, env, defaults
    end

    # Create a new renderer for the same controller but with new defaults.
    def with_defaults(defaults)
      self.class.new controller, @env, self.defaults.merge(defaults)
    end

    # Accepts a custom Rack environment to render templates in.
    # It will be merged with the default Rack environment defined by
    # +ActionController::Renderer::DEFAULTS+.
    def initialize(controller, env, defaults)
      @controller = controller
      @defaults = defaults
      @env = normalize_keys defaults.merge(env)
    end

    # Render templates with any options from ActionController::Base#render_to_string.
    #
    # The primary options are:
    # * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt> for details.
    # * <tt>:file</tt> - Renders an explicit template file. Add <tt>:locals</tt> to pass in, if so desired.
    #   It shouldn’t be used directly with unsanitized user input due to lack of validation.
    # * <tt>:inline</tt> - Renders an ERB template string.
    # * <tt>:plain</tt> - Renders provided text and sets the content type as <tt>text/plain</tt>.
    # * <tt>:html</tt> - Renders the provided HTML safe string, otherwise
    #   performs HTML escape on the string first. Sets the content type as <tt>text/html</tt>.
    # * <tt>:json</tt> - Renders the provided hash or object in JSON. You don't
    #   need to call <tt>.to_json</tt> on the object you want to render.
    # * <tt>:body</tt> - Renders provided text and sets content type of <tt>text/plain</tt>.
    #
    # If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, the default is
    # to render a partial and use the second parameter as the locals hash.
    def render(*args)
      raise "missing controller" unless controller

      request = ActionDispatch::Request.new @env
      request.routes = controller._routes

      instance = controller.new
      instance.set_request! request
      instance.set_response! controller.make_response!(request)
      instance.render_to_string(*args)
    end

    private
      def normalize_keys(env)
        new_env = {}
        env.each_pair { |k, v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
        new_env["rack.url_scheme"] = new_env["HTTPS"] == "on" ? "https" : "http"
        new_env
      end

      RACK_KEY_TRANSLATION = {
        http_host:   "HTTP_HOST",
        https:       "HTTPS",
        method:      "REQUEST_METHOD",
        script_name: "SCRIPT_NAME",
        input:       "rack.input"
      }

      IDENTITY = ->(_) { _ }

      RACK_VALUE_TRANSLATION = {
        https: ->(v) { v ? "on" : "off" },
        method: ->(v) { -v.upcase },
      }

      def rack_key_for(key)
        RACK_KEY_TRANSLATION.fetch(key, key.to_s)
      end

      def rack_value_for(key, value)
        RACK_VALUE_TRANSLATION.fetch(key, IDENTITY).call value
      end
  end
end