diff options
Diffstat (limited to 'actionpack/lib/action_controller/renderer.rb')
-rw-r--r-- | actionpack/lib/action_controller/renderer.rb | 117 |
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 |