aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/renderer.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller/renderer.rb')
-rw-r--r--actionpack/lib/action_controller/renderer.rb117
1 files changed, 117 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb
new file mode 100644
index 0000000000..49c5b782f0
--- /dev/null
+++ b/actionpack/lib/action_controller/renderer.rb
@@ -0,0 +1,117 @@
+# 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.
+ 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