Skip to content

Commit

Permalink
Add ActionController API functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
spastorino committed Apr 20, 2015
1 parent e539866 commit a8559ef
Show file tree
Hide file tree
Showing 10 changed files with 360 additions and 0 deletions.
5 changes: 5 additions & 0 deletions actionpack/lib/action_controller.rb
Expand Up @@ -7,6 +7,7 @@
module ActionController
extend ActiveSupport::Autoload

autoload :API
autoload :Base
autoload :Caching
autoload :Metal
Expand Down Expand Up @@ -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'

Expand Down
157 changes: 157 additions & 0 deletions 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 <tt>ActionController::Base</tt>,
# 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 <tt>ActionController::API</tt>. 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
# <tt>ActionController::Base</tt>.
#
# == Renders
#
# The default API Controller stack includes all renderers, which means you
# can use <tt>render :json</tt> 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 <tt>render</tt> or <tt>redirect</tt> 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
# <tt>redirect</tt> method in your controllers in the same way as
# <tt>ActionController::Base</tt>. 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
# <tt>ActionController::Base</tt> that is not present by default in
# <tt>ActionController::API</tt>, for instance <tt>MimeResponds</tt>. This
# module gives you the <tt>respond_to</tt> and <tt>respond_with</tt> methods.
# Adding it is quite simple, you just need to include the module in a specific
# controller or in <tt>ApplicationController</tt> 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 <tt>ActionController::Base</tt>
# available modules if you want to include any other functionality that is
# not provided by <tt>ActionController::API</tt> 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
14 changes: 14 additions & 0 deletions 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
4 changes: 4 additions & 0 deletions actionpack/test/abstract_unit.rb
Expand Up @@ -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)
Expand Down
57 changes: 57 additions & 0 deletions 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
26 changes: 26 additions & 0 deletions 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
20 changes: 20 additions & 0 deletions 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
19 changes: 19 additions & 0 deletions 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
38 changes: 38 additions & 0 deletions 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
20 changes: 20 additions & 0 deletions 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

0 comments on commit a8559ef

Please sign in to comment.