# frozen_string_literal: true
require "active_support/core_ext/hash/keys"
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:
# * :partial - See ActionView::PartialRenderer for details.
# * :file - Renders an explicit template file. Add :locals to pass in, if so desired.
# It shouldn’t be used directly with unsanitized user input due to lack of validation.
# * :inline - Renders a ERB template string.
# * :plain - Renders provided text and sets the content type as text/plain.
# * :html - Renders the provided HTML safe string, otherwise
# performs HTML escape on the string first. Sets the content type as text/html.
# * :json - Renders the provided hash or object in JSON. You don't
# need to call .to_json on the object you want to render.
# * :body - Renders provided text and sets content type of text/plain.
#
# If no options hash is passed or if :update 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