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
|
# 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
|