lib/action_controller/renderer.rb



# frozen_string_literal: true

module ActionController
  # = Action Controller \Renderer
  #
  # ActionController::Renderer allows you to render arbitrary templates without
  # being inside a controller action.
  #
  # You can get a renderer instance by calling +renderer+ on a controller class:
  #
  #   ApplicationController.renderer
  #   PostsController.renderer
  #
  # and render a template by calling the #render method:
  #
  #   ApplicationController.renderer.render template: "posts/show", assigns: { post: Post.first }
  #   PostsController.renderer.render :show, assigns: { post: Post.first }
  #
  # As a shortcut, you can also call +render+ directly on the controller class itself:
  #
  #   ApplicationController.render template: "posts/show", assigns: { post: Post.first }
  #   PostsController.render :show, assigns: { post: Post.first }
  #
  class Renderer
    attr_reader :controller

    DEFAULTS = {
      method: "get",
      input: ""
    }.freeze

    def self.normalize_env(env) # :nodoc:
      new_env = {}

      env.each_pair do |key, value|
        case key
        when :https
          value = value ? "on" : "off"
        when :method
          value = -value.upcase
        end

        key = RACK_KEY_TRANSLATION[key] || key.to_s

        new_env[key] = value
      end

      if new_env["HTTP_HOST"]
        new_env["HTTPS"] ||= "off"
        new_env["SCRIPT_NAME"] ||= ""
      end

      if new_env["HTTPS"]
        new_env["rack.url_scheme"] = new_env["HTTPS"] == "on" ? "https" : "http"
      end

      new_env
    end

    # Creates a new renderer using the given controller class. See ::new.
    def self.for(controller, env = nil, defaults = DEFAULTS)
      new(controller, env, defaults)
    end

    # Creates a new renderer using the same controller, but with a new Rack env.
    #
    #   ApplicationController.renderer.new(method: "post")
    #
    def new(env = nil)
      self.class.new controller, env, @defaults
    end

    # Creates a new renderer using the same controller, but with the given
    # defaults merged on top of the previous defaults.
    def with_defaults(defaults)
      self.class.new controller, @env, @defaults.merge(defaults)
    end

    # Initializes a new Renderer.
    #
    # ==== Parameters
    #
    # * +controller+ - The controller class to instantiate for rendering.
    # * +env+ - The Rack env to use for mocking a request when rendering.
    #   Entries can be typical Rack env keys and values, or they can be any of
    #   the following, which will be converted appropriately:
    #   * +:http_host+ - The HTTP host for the incoming request. Converts to
    #     Rack's +HTTP_HOST+.
    #   * +:https+ - Boolean indicating whether the incoming request uses HTTPS.
    #     Converts to Rack's +HTTPS+.
    #   * +:method+ - The HTTP method for the incoming request, case-insensitive.
    #     Converts to Rack's +REQUEST_METHOD+.
    #   * +:script_name+ - The portion of the incoming request's URL path that
    #     corresponds to the application. Converts to Rack's +SCRIPT_NAME+.
    #   * +:input+ - The input stream. Converts to Rack's +rack.input+.
    # * +defaults+ - Default values for the Rack env. Entries are specified in
    #   the same format as +env+. +env+ will be merged on top of these values.
    #   +defaults+ will be retained when calling #new on a renderer instance.
    #
    # If no +http_host+ is specified, the env HTTP host will be derived from the
    # routes' +default_url_options+. In this case, the +https+ boolean and the
    # +script_name+ will also be derived from +default_url_options+ if they were
    # not specified. Additionally, the +https+ boolean will fall back to
    # +Rails.application.config.force_ssl+ if +default_url_options+ does not
    # specify a +protocol+.
    def initialize(controller, env, defaults)
      @controller = controller
      @defaults = defaults
      if env.blank? && @defaults == DEFAULTS
        @env = DEFAULT_ENV
      else
        @env = normalize_env(@defaults)
        @env.merge!(normalize_env(env)) unless env.blank?
      end
    end

    def defaults
      @defaults = @defaults.dup if @defaults.frozen?
      @defaults
    end

    # Renders a template to a string, just like ActionController::Rendering#render_to_string.
    def render(*args)
      request = ActionDispatch::Request.new(env_for_request)
      request.routes = controller._routes

      instance = controller.new
      instance.set_request! request
      instance.set_response! controller.make_response!(request)
      instance.render_to_string(*args)
    end
    alias_method :render_to_string, :render # :nodoc:

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

      DEFAULT_ENV = normalize_env(DEFAULTS).freeze # :nodoc:

      delegate :normalize_env, to: :class

      def env_for_request
        if @env.key?("HTTP_HOST") || controller._routes.nil?
          @env.dup
        else
          controller._routes.default_env.merge(@env)
        end
      end
  end
end