aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller')
-rw-r--r--actionpack/lib/action_controller/base.rb29
-rw-r--r--actionpack/lib/action_controller/metal/live.rb74
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb7
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb42
-rw-r--r--actionpack/lib/action_controller/metal/responder.rb6
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb38
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb9
-rw-r--r--actionpack/lib/action_controller/railtie.rb1
8 files changed, 170 insertions, 36 deletions
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 67d261db77..a00eafc9ed 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -2,6 +2,21 @@ require "action_controller/log_subscriber"
require "action_controller/metal/params_wrapper"
module ActionController
+ # The <tt>metal</tt> anonymous class was introduced to solve issue with including modules in <tt>ActionController::Base</tt>.
+ # Modules needes to be included in particluar order. First wee need to have <tt>AbstractController::Rendering</tt> included,
+ # next we should include actuall implementation which would be for example <tt>ActionView::Rendering</tt> and after that
+ # <tt>ActionController::Rendering</tt>. This order must be preserved and as we want to have middle module included dynamicaly
+ # <tt>metal</tt> class was introduced. It has <tt>AbstractController::Rendering</tt> included and is parent class of
+ # <tt>ActionController::Base</tt> which includes <tt>ActionController::Rendering</tt>. If we include <tt>ActionView::Rendering</tt>
+ # beetween them to perserve the required order, we can simply do this by:
+ #
+ # ActionController::Base.superclass.send(:include, ActionView::Rendering)
+ #
+ metal = Class.new(Metal) do
+ include AbstractController::Rendering
+ include ActionController::BasicRendering
+ end
+
# Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed
# on request and then either it renders a template or redirects to another action. An action is defined as a public method
# on the controller, which will automatically be made accessible to the web-server through \Rails Routes.
@@ -160,7 +175,7 @@ module ActionController
# render action: "overthere" # won't be called if monkeys is nil
# end
#
- class Base < Metal
+ class Base < metal
abstract!
# We document the request and response methods here because albeit they are
@@ -200,7 +215,6 @@ module ActionController
end
MODULES = [
- AbstractController::Layouts,
AbstractController::Translation,
AbstractController::AssetPaths,
@@ -247,11 +261,12 @@ module ActionController
include mod
end
- # Define some internal variables that should not be propagated to the view.
- self.protected_instance_variables = [
- :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request,
- :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout
- ]
+ def self.default_protected_instance_vars
+ super.concat [
+ :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request,
+ :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout
+ ]
+ end
ActiveSupport.run_load_hooks(:action_controller, self)
end
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index 8092fd639f..0dd788645b 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -1,5 +1,6 @@
require 'action_dispatch/http/response'
require 'delegate'
+require 'active_support/json'
module ActionController
# Mix this module in to your controller, and all actions in that controller
@@ -32,6 +33,79 @@ module ActionController
# the main thread. Make sure your actions are thread safe, and this shouldn't
# be a problem (don't share state across threads, etc).
module Live
+ # This class provides the ability to write an SSE (Server Sent Event)
+ # to an IO stream. The class is initialized with a stream and can be used
+ # to either write a JSON string or an object which can be converted to JSON.
+ #
+ # Writing an object will convert it into standard SSE format with whatever
+ # options you have configured. You may choose to set the following options:
+ #
+ # 1) Event. If specified, an event with this name will be dispatched on
+ # the browser.
+ # 2) Retry. The reconnection time in milliseconds used when attempting
+ # to send the event.
+ # 3) Id. If the connection dies while sending an SSE to the browser, then
+ # the server will receive a +Last-Event-ID+ header with value equal to +id+.
+ #
+ # After setting an option in the constructor of the SSE object, all future
+ # SSEs sent accross the stream will use those options unless overridden.
+ #
+ # Example Usage:
+ #
+ # class MyController < ActionController::Base
+ # include ActionController::Live
+ #
+ # def index
+ # response.headers['Content-Type'] = 'text/event-stream'
+ # sse = SSE.new(response.stream, retry: 300, event: "event-name")
+ # sse.write({ name: 'John'})
+ # sse.write({ name: 'John'}, id: 10)
+ # sse.write({ name: 'John'}, id: 10, event: "other-event")
+ # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
+ # ensure
+ # sse.close
+ # end
+ # end
+ #
+ # Note: SSEs are not currently supported by IE. However, they are supported
+ # by Chrome, Firefox, Opera, and Safari.
+ class SSE
+
+ WHITELISTED_OPTIONS = %w( retry event id )
+
+ def initialize(stream, options = {})
+ @stream = stream
+ @options = options
+ end
+
+ def close
+ @stream.close
+ end
+
+ def write(object, options = {})
+ case object
+ when String
+ perform_write(object, options)
+ else
+ perform_write(ActiveSupport::JSON.encode(object), options)
+ end
+ end
+
+ private
+
+ def perform_write(json, options)
+ current_options = @options.merge(options).stringify_keys
+
+ WHITELISTED_OPTIONS.each do |option_name|
+ if (option_value = current_options[option_name])
+ @stream.write "#{option_name}: #{option_value}\n"
+ end
+ end
+
+ @stream.write "data: #{json}\n\n"
+ end
+ end
+
class Buffer < ActionDispatch::Response::Buffer #:nodoc:
def initialize(response)
@error_callback = nil
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index 5272dc6cdb..abed6e53cc 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -6,6 +6,13 @@ module ActionController
Renderers.add(key, &block)
end
+ class MissingRenderer < LoadError
+ def initialize(format)
+ @format = format
+ super("No renderer defined for format: #{@format}")
+ end
+ end
+
module Renderers
extend ActiveSupport::Concern
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index bea6b88f91..21224b9c3b 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -1,8 +1,38 @@
module ActionController
- module Rendering
+ # Basic rendering implements the most minimal rendering layer.
+ # It only supports rendering :text and :nothing. Passing any other option will
+ # result in `UnsupportedOperationError` exception. For more functionality like
+ # different formats, layouts etc. you should use `ActionView` gem.
+ module BasicRendering
extend ActiveSupport::Concern
- include AbstractController::Rendering
+ # Render text or nothing (empty string) to response_body
+ # :api: public
+ def render(*args, &block)
+ super(*args, &block)
+ opts = args.first
+ if opts.has_key?(:text) && opts[:text].present?
+ self.response_body = opts[:text]
+ elsif opts.has_key?(:nothing) && opts[:nothing]
+ self.response_body = " "
+ else
+ raise UnsupportedOperationError
+ end
+ end
+
+ def rendered_format
+ Mime::TEXT
+ end
+
+ class UnsupportedOperationError < StandardError
+ def initialize
+ super "Unsupported render operation. BasicRendering supports only :text and :nothing options. For more, you need to include ActionView."
+ end
+ end
+ end
+
+ module Rendering
+ extend ActiveSupport::Concern
# Before processing, set the request formats in current controller formats.
def process_action(*) #:nodoc:
@@ -12,17 +42,17 @@ module ActionController
# Check for double render errors and set the content_type after rendering.
def render(*args) #:nodoc:
- raise ::AbstractController::DoubleRenderError if response_body
+ raise ::AbstractController::DoubleRenderError if self.response_body
super
- self.content_type ||= Mime[lookup_context.rendered_format].to_s
- response_body
+ self.content_type ||= rendered_format.to_s
+ self.response_body
end
# Overwrite render_to_string because body can now be set to a rack body.
def render_to_string(*)
if self.response_body = super
string = ""
- response_body.each { |r| string << r }
+ self.response_body.each { |r| string << r }
string
end
ensure
diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb
index fd5b661209..66ff34a794 100644
--- a/actionpack/lib/action_controller/metal/responder.rb
+++ b/actionpack/lib/action_controller/metal/responder.rb
@@ -202,6 +202,7 @@ module ActionController #:nodoc:
# This is the common behavior for formats associated with APIs, such as :xml and :json.
def api_behavior(error)
raise error unless resourceful?
+ raise MissingRenderer.new(format) unless has_renderer?
if get?
display resource
@@ -269,6 +270,11 @@ module ActionController #:nodoc:
resource.respond_to?(:errors) && !resource.errors.empty?
end
+ # Check whether the neceessary Renderer is available
+ def has_renderer?
+ Renderers::RENDERERS.include?(format)
+ end
+
# By default, render the <code>:edit</code> action for HTML requests with errors, unless
# the verb was POST.
#
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
index 73e9b5660d..62d5931b45 100644
--- a/actionpack/lib/action_controller/metal/streaming.rb
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -193,31 +193,29 @@ module ActionController #:nodoc:
module Streaming
extend ActiveSupport::Concern
- include AbstractController::Rendering
-
protected
- # Set proper cache control and transfer encoding when streaming
- def _process_options(options) #:nodoc:
- super
- if options[:stream]
- if env["HTTP_VERSION"] == "HTTP/1.0"
- options.delete(:stream)
- else
- headers["Cache-Control"] ||= "no-cache"
- headers["Transfer-Encoding"] = "chunked"
- headers.delete("Content-Length")
+ # Set proper cache control and transfer encoding when streaming
+ def _process_options(options) #:nodoc:
+ super
+ if options[:stream]
+ if env["HTTP_VERSION"] == "HTTP/1.0"
+ options.delete(:stream)
+ else
+ headers["Cache-Control"] ||= "no-cache"
+ headers["Transfer-Encoding"] = "chunked"
+ headers.delete("Content-Length")
+ end
end
end
- end
- # Call render_body if we are streaming instead of usual +render+.
- def _render_template(options) #:nodoc:
- if options.delete(:stream)
- Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
- else
- super
+ # Call render_body if we are streaming instead of usual +render+.
+ def _render_template(options) #:nodoc:
+ if options.delete(:stream)
+ Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
+ else
+ super
+ end
end
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index b279ef81a9..ae600b1ebe 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -298,7 +298,7 @@ module ActionController
# params.slice(:d) # => {}
def slice(*keys)
self.class.new(super).tap do |new_instance|
- new_instance.instance_variable_set :@permitted, @permitted
+ new_instance.permitted = @permitted
end
end
@@ -312,10 +312,15 @@ module ActionController
# copy_params.permitted? # => true
def dup
super.tap do |duplicate|
- duplicate.instance_variable_set :@permitted, @permitted
+ duplicate.permitted = @permitted
end
end
+ protected
+ def permitted=(new_permitted)
+ @permitted = new_permitted
+ end
+
private
def convert_hashes_to_parameters(key, value)
if value.is_a?(Parameters) || !value.is_a?(Hash)
diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb
index 5379547c57..0833e65d23 100644
--- a/actionpack/lib/action_controller/railtie.rb
+++ b/actionpack/lib/action_controller/railtie.rb
@@ -1,7 +1,6 @@
require "rails"
require "action_controller"
require "action_dispatch/railtie"
-require "action_view/railtie"
require "abstract_controller/railties/routes_helpers"
require "action_controller/railties/helpers"