Renamed presenter to renderer, added some documentation and defined its API.

This commit is contained in:
José Valim 2009-08-06 23:48:48 +02:00
parent 1fd65c80fc
commit aed135d3e2
18 changed files with 296 additions and 243 deletions

@ -6,7 +6,7 @@
class Kaigi < ActionController::Metal
include AbstractController::Callbacks
include ActionController::RackConvenience
include ActionController::Renderer
include ActionController::RenderingController
include ActionController::Layouts
include ActionView::Context
@ -47,4 +47,4 @@ def set_name
map("/kaigi/alt") { run Kaigi.action(:alt) }
end.to_app
Rack::Handler::Mongrel.run app, :Port => 3000
Rack::Handler::Mongrel.run app, :Port => 3000

@ -2,15 +2,15 @@
require "active_support/core_ext/module/delegation"
module AbstractController
autoload :Base, "abstract_controller/base"
autoload :Benchmarker, "abstract_controller/benchmarker"
autoload :Callbacks, "abstract_controller/callbacks"
autoload :Helpers, "abstract_controller/helpers"
autoload :Layouts, "abstract_controller/layouts"
autoload :Logger, "abstract_controller/logger"
autoload :Renderer, "abstract_controller/renderer"
autoload :Base, "abstract_controller/base"
autoload :Benchmarker, "abstract_controller/benchmarker"
autoload :Callbacks, "abstract_controller/callbacks"
autoload :Helpers, "abstract_controller/helpers"
autoload :Layouts, "abstract_controller/layouts"
autoload :Logger, "abstract_controller/logger"
autoload :RenderingController, "abstract_controller/rendering_controller"
# === Exceptions
autoload :ActionNotFound, "abstract_controller/exceptions"
autoload :DoubleRenderError, "abstract_controller/exceptions"
autoload :Error, "abstract_controller/exceptions"
autoload :ActionNotFound, "abstract_controller/exceptions"
autoload :DoubleRenderError, "abstract_controller/exceptions"
autoload :Error, "abstract_controller/exceptions"
end

@ -2,7 +2,7 @@ module AbstractController
module Helpers
extend ActiveSupport::Concern
include Renderer
include RenderingController
included do
extlib_inheritable_accessor(:_helpers) { Module.new }

@ -2,7 +2,7 @@ module AbstractController
module Layouts
extend ActiveSupport::Concern
include Renderer
include RenderingController
included do
extlib_inheritable_accessor(:_layout_conditions) { Hash.new }

@ -1,7 +1,7 @@
require "abstract_controller/logger"
module AbstractController
module Renderer
module RenderingController
extend ActiveSupport::Concern
include AbstractController::Logger
@ -67,7 +67,7 @@ def render_to_body(options = {})
#
# :api: plugin
def render_to_string(options = {})
AbstractController::Renderer.body_to_s(render_to_body(options))
AbstractController::RenderingController.body_to_s(render_to_body(options))
end
# Renders the template from an object.

@ -8,8 +8,8 @@ module ActionController
autoload :Rails2Compatibility, "action_controller/metal/compatibility"
autoload :Redirector, "action_controller/metal/redirector"
autoload :Renderer, "action_controller/metal/renderer"
autoload :RenderingController, "action_controller/metal/rendering_controller"
autoload :RenderOptions, "action_controller/metal/render_options"
autoload :Renderers, "action_controller/metal/render_options"
autoload :Rescue, "action_controller/metal/rescuable"
autoload :Testing, "action_controller/metal/testing"
autoload :UrlFor, "action_controller/metal/url_for"
@ -69,4 +69,4 @@ module ActionController
require 'active_support/core_ext/module/attr_internal'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/name_error'
require 'active_support/inflector'
require 'active_support/inflector'

@ -10,8 +10,8 @@ class Base < Metal
include ActionController::HideActions
include ActionController::UrlFor
include ActionController::Redirector
include ActionController::Renderer
include ActionController::Renderers::All
include ActionController::RenderingController
include ActionController::RenderOptions::All
include ActionController::Layouts
include ActionController::ConditionalGet
include ActionController::RackConvenience

@ -158,7 +158,7 @@ module ActionController
module Layouts
extend ActiveSupport::Concern
include ActionController::Renderer
include ActionController::RenderingController
include AbstractController::Layouts
module ClassMethods

@ -1,146 +1,4 @@
module ActionController #:nodoc:
# Presenter is responsible to expose a resource for different mime requests,
# usually depending on the HTTP verb. The presenter is triggered when
# respond_with is called. The simplest case to study is a GET request:
#
# class PeopleController < ApplicationController
# respond_to :html, :xml, :json
#
# def index
# @people = Person.find(:all)
# respond_with(@people)
# end
# end
#
# When a request comes, for example with format :xml, three steps happen:
#
# 1) respond_with searches for a template at people/index.xml;
#
# 2) if the template is not available, it will create a presenter, passing
# the controller and the resource, and invoke :to_xml on it;
#
# 3) if the presenter does not respond_to :to_xml, call to_format on it.
#
# === Builtin HTTP verb semantics
#
# Rails default presenter holds semantics for each HTTP verb. Depending on the
# content type, verb and the resource status, it will behave differently.
#
# Using Rails default presenter, a POST request could be written as:
#
# def create
# @user = User.new(params[:user])
# flash[:notice] = 'User was successfully created.' if @user.save
# respond_with(@user)
# end
#
# Which is exactly the same as:
#
# def create
# @user = User.new(params[:user])
#
# respond_to do |format|
# if @user.save
# flash[:notice] = 'User was successfully created.'
# format.html { redirect_to(@user) }
# format.xml { render :xml => @user, :status => :created, :location => @user }
# else
# format.html { render :action => "new" }
# format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
# end
# end
# end
#
# The same happens for PUT and DELETE requests. By default, it accepts just
# :location as parameter, which is used as redirect destination, in both
# POST, PUT, DELETE requests for HTML mime, as in the example below:
#
# def destroy
# @person = Person.find(params[:id])
# @person.destroy
# respond_with(@person, :location => root_url)
# end
#
# === Nested resources
#
# You can given nested resource as you do in form_for and polymorphic_url.
# Consider the project has many tasks example. The create action for
# TasksController would be like:
#
# def create
# @project = Project.find(params[:project_id])
# @task = @project.comments.build(params[:task])
# flash[:notice] = 'Task was successfully created.' if @task.save
# respond_with([@project, @task])
# end
#
# Given a nested resource, you ensure that the presenter will redirect to
# project_task_url instead of task_url.
#
# Namespaced and singleton resources requires a symbol to be given, as in
# polymorphic urls. If a project has one manager which has many tasks, it
# should be invoked as:
#
# respond_with([@project, :manager, @task])
#
# Check polymorphic_url documentation for more examples.
#
class Presenter
attr_reader :controller, :request, :format, :resource, :resource_location, :options
def initialize(controller, resource, options)
@controller = controller
@request = controller.request
@format = controller.formats.first
@resource = resource.is_a?(Array) ? resource.last : resource
@resource_location = options[:location] || resource
@options = options
end
delegate :head, :render, :redirect_to, :to => :controller
delegate :get?, :post?, :put?, :delete?, :to => :request
# Undefine :to_json since it's defined on Object
undef_method :to_json
def to_html
if get?
render
elsif has_errors?
render :action => default_action
else
redirect_to resource_location
end
end
def to_format
return render unless resourceful?
if get?
render format => resource
elsif has_errors?
render format => resource.errors, :status => :unprocessable_entity
elsif post?
render format => resource, :status => :created, :location => resource_location
else
head :ok
end
end
def resourceful?
resource.respond_to?(:"to_#{format}")
end
def has_errors?
resource.respond_to?(:errors) && !resource.errors.empty?
end
def default_action
request.post? ? :new : :edit
end
end
module MimeResponds #:nodoc:
extend ActiveSupport::Concern
@ -339,10 +197,10 @@ def respond_to(*mimes, &block)
end
end
# respond_with wraps a resource around a presenter for default representation.
# respond_with wraps a resource around a renderer for default representation.
# First it invokes respond_to, if a response cannot be found (ie. no block
# for the request was given and template was not available), it instantiates
# an ActionController::Presenter with the controller and resource.
# an ActionController::Renderer with the controller and resource.
#
# ==== Example
#
@ -363,19 +221,19 @@ def respond_to(*mimes, &block)
# end
# end
#
# All options given to respond_with are sent to the underlying presenter.
# All options given to respond_with are sent to the underlying renderer,
# except for the option :renderer itself. Since the renderer interface
# is quite simple (it just needs to respond to call), you can even give
# a proc to it.
#
def respond_with(resource, options={}, &block)
respond_to(&block)
rescue ActionView::MissingTemplate
presenter = ActionController::Presenter.new(self, resource, options)
format_method = :"to_#{self.formats.first}"
(options.delete(:renderer) || renderer).call(self, resource, options)
end
if presenter.respond_to?(format_method)
presenter.send(format_method)
else
presenter.to_format
end
def renderer
ActionController::Renderer
end
protected

@ -47,7 +47,7 @@ def base.register_renderer(name)
end
end
module Renderers
module RenderOptions
module Json
extend RenderOption
register_renderer :json
@ -94,10 +94,10 @@ def render_update(proc, options)
module All
extend ActiveSupport::Concern
include ActionController::Renderers::Json
include ActionController::Renderers::Js
include ActionController::Renderers::Xml
include ActionController::Renderers::RJS
include ActionController::RenderOptions::Json
include ActionController::RenderOptions::Js
include ActionController::RenderOptions::Xml
include ActionController::RenderOptions::RJS
end
end
end

@ -1,66 +1,181 @@
module ActionController
module Renderer
extend ActiveSupport::Concern
module ActionController #:nodoc:
# Renderer is responsible to expose a resource for different mime requests,
# usually depending on the HTTP verb. The renderer is triggered when
# respond_with is called. The simplest case to study is a GET request:
#
# class PeopleController < ApplicationController
# respond_to :html, :xml, :json
#
# def index
# @people = Person.find(:all)
# respond_with(@people)
# end
# end
#
# When a request comes, for example with format :xml, three steps happen:
#
# 1) respond_with searches for a template at people/index.xml;
#
# 2) if the template is not available, it will create a renderer, passing
# the controller and the resource and invoke :to_xml on it;
#
# 3) if the renderer does not respond_to :to_xml, call to_format on it.
#
# === Builtin HTTP verb semantics
#
# Rails default renderer holds semantics for each HTTP verb. Depending on the
# content type, verb and the resource status, it will behave differently.
#
# Using Rails default renderer, a POST request for creating an object could
# be written as:
#
# def create
# @user = User.new(params[:user])
# flash[:notice] = 'User was successfully created.' if @user.save
# respond_with(@user)
# end
#
# Which is exactly the same as:
#
# def create
# @user = User.new(params[:user])
#
# respond_to do |format|
# if @user.save
# flash[:notice] = 'User was successfully created.'
# format.html { redirect_to(@user) }
# format.xml { render :xml => @user, :status => :created, :location => @user }
# else
# format.html { render :action => "new" }
# format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
# end
# end
# end
#
# The same happens for PUT and DELETE requests.
#
# === Nested resources
#
# You can given nested resource as you do in form_for and polymorphic_url.
# Consider the project has many tasks example. The create action for
# TasksController would be like:
#
# def create
# @project = Project.find(params[:project_id])
# @task = @project.comments.build(params[:task])
# flash[:notice] = 'Task was successfully created.' if @task.save
# respond_with([@project, @task])
# end
#
# Giving an array of resources, you ensure that the renderer will redirect to
# project_task_url instead of task_url.
#
# Namespaced and singleton resources requires a symbol to be given, as in
# polymorphic urls. If a project has one manager which has many tasks, it
# should be invoked as:
#
# respond_with([@project, :manager, @task])
#
# Check polymorphic_url documentation for more examples.
#
class Renderer
attr_reader :controller, :request, :format, :resource, :resource_location, :options
include AbstractController::Renderer
def process_action(*)
self.formats = request.formats.map {|x| x.to_sym}
super
def initialize(controller, resource, options={})
@controller = controller
@request = controller.request
@format = controller.formats.first
@resource = resource.is_a?(Array) ? resource.last : resource
@resource_location = options[:location] || resource
@options = options
end
def render(options)
super
self.content_type ||= begin
mime = options[:_template].mime_type
formats.include?(mime && mime.to_sym) || formats.include?(:all) ? mime : Mime::Type.lookup_by_extension(formats.first)
end.to_s
response_body
delegate :head, :render, :redirect_to, :to => :controller
delegate :get?, :post?, :put?, :delete?, :to => :request
# Undefine :to_json since it's defined on Object
undef_method :to_json
# Initializes a new renderer an invoke the proper format. If the format is
# not defined, call to_format.
#
def self.call(*args)
renderer = new(*args)
method = :"to_#{renderer.format}"
renderer.respond_to?(method) ? renderer.send(method) : renderer.to_format
end
def render_to_body(options)
_process_options(options)
if options.key?(:partial)
options[:partial] = action_name if options[:partial] == true
options[:_details] = {:formats => formats}
# HTML format does not render the resource, it always attempt to render a
# template.
#
def to_html
if get?
render
elsif has_errors?
render :action => default_action
else
redirect_to resource_location
end
super
end
private
def _prefix
controller_path
end
# All others formats try to render the resource given instead. For this
# purpose a helper called display as a shortcut to render a resource with
# the current format.
#
def to_format
return render unless resourceful?
def _determine_template(options)
if options.key?(:text)
options[:_template] = ActionView::TextTemplate.new(options[:text], formats.first)
elsif options.key?(:inline)
handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb")
template = ActionView::Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {})
options[:_template] = template
elsif options.key?(:template)
options[:_template_name] = options[:template]
elsif options.key?(:file)
options[:_template_name] = options[:file]
elsif !options.key?(:partial)
options[:_template_name] = (options[:action] || action_name).to_s
options[:_prefix] = _prefix
end
super
if get?
display resource
elsif has_errors?
display resource.errors, :status => :unprocessable_entity
elsif post?
display resource, :status => :created, :location => resource_location
else
head :ok
end
end
def _render_partial(partial, options)
end
protected
def _process_options(options)
status, content_type, location = options.values_at(:status, :content_type, :location)
self.status = status if status
self.content_type = content_type if content_type
self.headers["Location"] = url_for(location) if location
end
# Checks whether the resource responds to the current format or not.
#
def resourceful?
resource.respond_to?(:"to_#{format}")
end
# display is just a shortcut to render a resource with the current format.
#
# display @user, :status => :ok
#
# For xml request is equivalent to:
#
# render :xml => @user, :status => :ok
#
# Options sent by the user are also used:
#
# respond_with(@user, :status => :created)
# display(@user, :status => :ok)
#
# Results in:
#
# render :xml => @user, :status => :created
#
def display(resource, given_options={})
render given_options.merge!(options).merge!(format => resource)
end
# Check if the resource has errors or not.
#
def has_errors?
resource.respond_to?(:errors) && !resource.errors.empty?
end
# By default, render the :edit action for html requests with failure, unless
# the verb is post.
#
def default_action
request.post? ? :new : :edit
end
end
end

@ -0,0 +1,66 @@
module ActionController
module RenderingController
extend ActiveSupport::Concern
include AbstractController::RenderingController
def process_action(*)
self.formats = request.formats.map {|x| x.to_sym}
super
end
def render(options)
super
self.content_type ||= begin
mime = options[:_template].mime_type
formats.include?(mime && mime.to_sym) || formats.include?(:all) ? mime : Mime::Type.lookup_by_extension(formats.first)
end.to_s
response_body
end
def render_to_body(options)
_process_options(options)
if options.key?(:partial)
options[:partial] = action_name if options[:partial] == true
options[:_details] = {:formats => formats}
end
super
end
private
def _prefix
controller_path
end
def _determine_template(options)
if options.key?(:text)
options[:_template] = ActionView::TextTemplate.new(options[:text], formats.first)
elsif options.key?(:inline)
handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb")
template = ActionView::Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {})
options[:_template] = template
elsif options.key?(:template)
options[:_template_name] = options[:template]
elsif options.key?(:file)
options[:_template_name] = options[:file]
elsif !options.key?(:partial)
options[:_template_name] = (options[:action] || action_name).to_s
options[:_prefix] = _prefix
end
super
end
def _render_partial(partial, options)
end
def _process_options(options)
status, content_type, location = options.values_at(:status, :content_type, :location)
self.status = status if status
self.content_type = content_type if content_type
self.headers["Location"] = url_for(location) if location
end
end
end

@ -6,7 +6,7 @@ module ActionController #:nodoc:
module Streaming
extend ActiveSupport::Concern
include ActionController::Renderer
include ActionController::RenderingController
DEFAULT_SEND_FILE_OPTIONS = {
:type => 'application/octet-stream'.freeze,

@ -2,7 +2,7 @@ module ActionController #:nodoc:
module Verification #:nodoc:
extend ActiveSupport::Concern
include AbstractController::Callbacks, Session, Flash, Renderer
include AbstractController::Callbacks, Session, Flash, RenderingController
# This module provides a class-level method for specifying that certain
# actions are guarded against being called without certain prerequisites
@ -127,4 +127,4 @@ def apply_remaining_actions(options) # :nodoc:
end
end
end
end
end

@ -27,7 +27,7 @@ class TestBasic < ActiveSupport::TestCase
# Test Render mixin
# ====
class RenderingController < AbstractController::Base
include Renderer
include ::AbstractController::RenderingController
def _prefix() end
@ -65,8 +65,8 @@ def rendering_to_string
self.response_body = render_to_string :_template_name => "naked_render.erb"
end
end
class TestRenderer < ActiveSupport::TestCase
class TestRenderingController < ActiveSupport::TestCase
test "rendering templates works" do
result = Me2.new.process(:index)
assert_equal "Hello from index.erb", result.response_body

@ -4,7 +4,7 @@ module AbstractController
module Testing
class ControllerWithHelpers < AbstractController::Base
include Renderer
include RenderingController
include Helpers
def render(string)
@ -40,4 +40,4 @@ def test_helpers
end
end
end
end

@ -6,7 +6,7 @@ module Layouts
# Base controller for these tests
class Base < AbstractController::Base
include AbstractController::Renderer
include AbstractController::RenderingController
include AbstractController::Layouts
self.view_paths = [ActionView::FixtureResolver.new(

@ -501,8 +501,13 @@ def using_resource_with_parent
respond_with([Quiz::Store.new("developer?", 11), Customer.new("david", 13)])
end
def using_resource_with_location
respond_with(Customer.new("david", 13), :location => "http://test.host/")
def using_resource_with_status_and_location
respond_with(Customer.new("david", 13), :location => "http://test.host/", :status => :created)
end
def using_resource_with_renderer
renderer = proc { |c, r, o| c.render :text => "Resource name is #{r.name}" }
respond_with(Customer.new("david", 13), :renderer => renderer)
end
protected
@ -727,11 +732,20 @@ def test_no_double_render_is_raised
end
end
def test_using_resource_with_location
def test_using_resource_with_status_and_location
@request.accept = "text/html"
post :using_resource_with_location
post :using_resource_with_status_and_location
assert @response.redirect?
assert_equal "http://test.host/", @response.location
@request.accept = "application/xml"
get :using_resource_with_status_and_location
assert_equal 201, @response.status
end
def test_using_resource_with_renderer
get :using_resource_with_renderer
assert_equal "Resource name is david", @response.body
end
def test_not_acceptable