From a8559ef08c96e076506d6bfaf5ba1777481d6266 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Wed, 15 Apr 2015 15:30:27 -0300 Subject: [PATCH] Add ActionController API functionality --- actionpack/lib/action_controller.rb | 5 + actionpack/lib/action_controller/api.rb | 157 ++++++++++++++++++ .../action_controller/api/api_rendering.rb | 14 ++ actionpack/test/abstract_unit.rb | 4 + .../controller/api/conditional_get_test.rb | 57 +++++++ .../controller/api/data_streaming_test.rb | 26 +++ .../test/controller/api/force_ssl_test.rb | 20 +++ .../test/controller/api/redirect_to_test.rb | 19 +++ .../test/controller/api/renderers_test.rb | 38 +++++ .../test/controller/api/url_for_test.rb | 20 +++ 10 files changed, 360 insertions(+) create mode 100644 actionpack/lib/action_controller/api.rb create mode 100644 actionpack/lib/action_controller/api/api_rendering.rb create mode 100644 actionpack/test/controller/api/conditional_get_test.rb create mode 100644 actionpack/test/controller/api/data_streaming_test.rb create mode 100644 actionpack/test/controller/api/force_ssl_test.rb create mode 100644 actionpack/test/controller/api/redirect_to_test.rb create mode 100644 actionpack/test/controller/api/renderers_test.rb create mode 100644 actionpack/test/controller/api/url_for_test.rb diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index a1893ce920a3d..952a035a1653d 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -7,6 +7,7 @@ module ActionController extend ActiveSupport::Autoload + autoload :API autoload :Base autoload :Caching autoload :Metal @@ -41,6 +42,10 @@ module ActionController autoload :UrlFor end + autoload_under "api" do + autoload :ApiRendering + end + autoload :TestCase, 'action_controller/test_case' autoload :TemplateAssertions, 'action_controller/test_case' diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb new file mode 100644 index 0000000000000..f9cbbf227ff5f --- /dev/null +++ b/actionpack/lib/action_controller/api.rb @@ -0,0 +1,157 @@ +require 'action_view' +require 'action_controller' +require 'action_controller/log_subscriber' + +module ActionController + # API Controller is a lightweight version of ActionController::Base, + # created for applications that don't require all functionality that a complete + # \Rails controller provides, allowing you to create faster controllers for + # example for API only applications. + # + # An API Controller is different from a normal controller in the sense that + # by default it doesn't include a number of features that are usually required + # by browser access only: layouts and templates rendering, cookies, sessions, + # flash, assets, and so on. This makes the entire controller stack thinner and + # faster, suitable for API applications. It doesn't mean you won't have such + # features if you need them: they're all available for you to include in + # your application, they're just not part of the default API Controller stack. + # + # By default, only the ApplicationController in a \Rails application inherits + # from ActionController::API. All other controllers in turn inherit + # from ApplicationController. + # + # A sample controller could look like this: + # + # class PostsController < ApplicationController + # def index + # @posts = Post.all + # render json: @posts + # end + # end + # + # Request, response and parameters objects all work the exact same way as + # ActionController::Base. + # + # == Renders + # + # The default API Controller stack includes all renderers, which means you + # can use render :json and brothers freely in your controllers. Keep + # in mind that templates are not going to be rendered, so you need to ensure + # your controller is calling either render or redirect in + # all actions. + # + # def show + # @post = Post.find(params[:id]) + # render json: @post + # end + # + # == Redirects + # + # Redirects are used to move from one action to another. You can use the + # redirect method in your controllers in the same way as + # ActionController::Base. For example: + # + # def create + # redirect_to root_url and return if not_authorized? + # # do stuff here + # end + # + # == Adding new behavior + # + # In some scenarios you may want to add back some functionality provided by + # ActionController::Base that is not present by default in + # ActionController::API, for instance MimeResponds. This + # module gives you the respond_to and respond_with methods. + # Adding it is quite simple, you just need to include the module in a specific + # controller or in ApplicationController in case you want it + # available to your entire app: + # + # class ApplicationController < ActionController::API + # include ActionController::MimeResponds + # end + # + # class PostsController < ApplicationController + # respond_to :json, :xml + # + # def index + # @posts = Post.all + # respond_with @posts + # end + # end + # + # Quite straightforward. Make sure to check ActionController::Base + # available modules if you want to include any other functionality that is + # not provided by ActionController::API out of the box. + class API < Metal + abstract! + + module Compatibility + def cache_store; end + def cache_store=(*); end + def assets_dir=(*); end + def javascripts_dir=(*); end + def stylesheets_dir=(*); end + def page_cache_directory=(*); end + def asset_path=(*); end + def asset_host=(*); end + def relative_url_root=(*); end + def perform_caching=(*); end + def helpers_path=(*); end + def allow_forgery_protection=(*); end + def helper_method(*); end + def helper(*); end + end + + extend Compatibility + + # Shortcut helper that returns all the ActionController::API modules except the ones passed in the argument: + # + # class MetalController + # ActionController::API.without_modules(:Redirecting, :DataStreaming).each do |left| + # include left + # end + # end + # + # This gives better control over what you want to exclude and makes it easier + # to create an api controller class, instead of listing the modules required manually. + def self.without_modules(*modules) + modules = modules.map do |m| + m.is_a?(Symbol) ? ActionController.const_get(m) : m + end + + MODULES - modules + end + + MODULES = [ + AbstractController::Rendering, + + UrlFor, + Redirecting, + ApiRendering, + Renderers::All, + ConditionalGet, + RackDelegation, + StrongParameters, + + ForceSSL, + DataStreaming, + + # Before callbacks should also be executed the earliest as possible, so + # also include them at the bottom. + AbstractController::Callbacks, + + # Append rescue at the bottom to wrap as much as possible. + Rescue, + + # Add instrumentations hooks at the bottom, to ensure they instrument + # all the methods properly. + Instrumentation + ] + + MODULES.each do |mod| + include mod + end + + ActiveSupport.run_load_hooks(:action_controller, self) + end +end diff --git a/actionpack/lib/action_controller/api/api_rendering.rb b/actionpack/lib/action_controller/api/api_rendering.rb new file mode 100644 index 0000000000000..3a08d28c393c2 --- /dev/null +++ b/actionpack/lib/action_controller/api/api_rendering.rb @@ -0,0 +1,14 @@ +module ActionController + module ApiRendering + extend ActiveSupport::Concern + + included do + include Rendering + end + + def render_to_body(options = {}) + _process_options(options) + super + end + end +end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index c1be2c9afe23f..3079c571266a5 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -232,6 +232,10 @@ def assert_header(name, value) end module ActionController + class API + extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes) + end + class Base # This stub emulates the Railtie including the URL helpers from a Rails application extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes) diff --git a/actionpack/test/controller/api/conditional_get_test.rb b/actionpack/test/controller/api/conditional_get_test.rb new file mode 100644 index 0000000000000..e729ea9d3357d --- /dev/null +++ b/actionpack/test/controller/api/conditional_get_test.rb @@ -0,0 +1,57 @@ +require 'abstract_unit' +require 'active_support/core_ext/integer/time' +require 'active_support/core_ext/numeric/time' + +class ConditionalGetApiController < ActionController::API + before_action :handle_last_modified_and_etags, :only => :two + + def one + if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123]) + render :text => "Hi!" + end + end + + def two + render :text => "Hi!" + end + + private + + def handle_last_modified_and_etags + fresh_when(:last_modified => Time.now.utc.beginning_of_day, :etag => [ :foo, 123 ]) + end +end + +class ConditionalGetApiTest < ActionController::TestCase + tests ConditionalGetApiController + + def setup + @last_modified = Time.now.utc.beginning_of_day.httpdate + end + + def test_request_with_bang_gets_last_modified + get :two + assert_equal @last_modified, @response.headers['Last-Modified'] + assert_response :success + end + + def test_request_with_bang_obeys_last_modified + @request.if_modified_since = @last_modified + get :two + assert_response :not_modified + end + + def test_last_modified_works_with_less_than_too + @request.if_modified_since = 5.years.ago.httpdate + get :two + assert_response :success + end + + def test_request_not_modified + @request.if_modified_since = @last_modified + get :one + assert_equal 304, @response.status.to_i + assert @response.body.blank? + assert_equal @last_modified, @response.headers['Last-Modified'] + end +end diff --git a/actionpack/test/controller/api/data_streaming_test.rb b/actionpack/test/controller/api/data_streaming_test.rb new file mode 100644 index 0000000000000..0e7d97d1f422f --- /dev/null +++ b/actionpack/test/controller/api/data_streaming_test.rb @@ -0,0 +1,26 @@ +require 'abstract_unit' + +module TestApiFileUtils + def file_path() File.expand_path(__FILE__) end + def file_data() @data ||= File.open(file_path, 'rb') { |f| f.read } end +end + +class DataStreamingApiController < ActionController::API + include TestApiFileUtils + + def one; end + def two + send_data(file_data, {}) + end +end + +class DataStreamingApiTest < ActionController::TestCase + include TestApiFileUtils + tests DataStreamingApiController + + def test_data + response = process('two') + assert_kind_of String, response.body + assert_equal file_data, response.body + end +end diff --git a/actionpack/test/controller/api/force_ssl_test.rb b/actionpack/test/controller/api/force_ssl_test.rb new file mode 100644 index 0000000000000..8578340d8224d --- /dev/null +++ b/actionpack/test/controller/api/force_ssl_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' + +class ForceSSLApiController < ActionController::API + force_ssl + + def one; end + def two + head :ok + end +end + +class ForceSSLApiTest < ActionController::TestCase + tests ForceSSLApiController + + def test_redirects_to_https + get :two + assert_response 301 + assert_equal "https://test.host/force_ssl_api/two", redirect_to_url + end +end diff --git a/actionpack/test/controller/api/redirect_to_test.rb b/actionpack/test/controller/api/redirect_to_test.rb new file mode 100644 index 0000000000000..67832d7dba0de --- /dev/null +++ b/actionpack/test/controller/api/redirect_to_test.rb @@ -0,0 +1,19 @@ +require 'abstract_unit' + +class RedirectToApiController < ActionController::API + def one + redirect_to :action => "two" + end + + def two; end +end + +class RedirectToApiTest < ActionController::TestCase + tests RedirectToApiController + + def test_redirect_to + get :one + assert_response :redirect + assert_equal "http://test.host/redirect_to_api/two", redirect_to_url + end +end diff --git a/actionpack/test/controller/api/renderers_test.rb b/actionpack/test/controller/api/renderers_test.rb new file mode 100644 index 0000000000000..26f53499fde29 --- /dev/null +++ b/actionpack/test/controller/api/renderers_test.rb @@ -0,0 +1,38 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/conversions' + +class Model + def to_json(options = {}) + { :a => 'b' }.to_json(options) + end + + def to_xml(options = {}) + { :a => 'b' }.to_xml(options) + end +end + +class RenderersApiController < ActionController::API + def one + render :json => Model.new + end + + def two + render :xml => Model.new + end +end + +class RenderersApiTest < ActionController::TestCase + tests RenderersApiController + + def test_render_json + get :one + assert_response :success + assert_equal({ :a => 'b' }.to_json, @response.body) + end + + def test_render_xml + get :two + assert_response :success + assert_equal({ :a => 'b' }.to_xml, @response.body) + end +end diff --git a/actionpack/test/controller/api/url_for_test.rb b/actionpack/test/controller/api/url_for_test.rb new file mode 100644 index 0000000000000..0d8691a091cdd --- /dev/null +++ b/actionpack/test/controller/api/url_for_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' + +class UrlForApiController < ActionController::API + def one; end + def two; end +end + +class UrlForApiTest < ActionController::TestCase + tests UrlForApiController + + def setup + super + @request.host = 'www.example.com' + end + + def test_url_for + get :one + assert_equal "http://www.example.com/url_for_api/one", @controller.url_for + end +end