diff options
Diffstat (limited to 'actionpack/lib/action_controller/metal/responder.rb')
-rw-r--r-- | actionpack/lib/action_controller/metal/responder.rb | 297 |
1 files changed, 297 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb new file mode 100644 index 0000000000..5096558c67 --- /dev/null +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -0,0 +1,297 @@ +require 'active_support/json' + +module ActionController #:nodoc: + # Responsible for exposing a resource to different mime requests, + # usually depending on the HTTP verb. The responder is triggered when + # <code>respond_with</code> is called. The simplest case to study is a GET request: + # + # class PeopleController < ApplicationController + # respond_to :html, :xml, :json + # + # def index + # @people = Person.all + # respond_with(@people) + # end + # end + # + # When a request comes in, for example for an XML response, three steps happen: + # + # 1) the responder searches for a template at people/index.xml; + # + # 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource; + # + # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it. + # + # === Built-in HTTP verb semantics + # + # The default \Rails responder holds semantics for each HTTP verb. Depending on the + # content type, verb and the resource status, it will behave differently. + # + # Using \Rails default responder, 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 PATCH/PUT and DELETE requests. + # + # === Nested resources + # + # You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>. + # 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.tasks.build(params[:task]) + # flash[:notice] = 'Task was successfully created.' if @task.save + # respond_with(@project, @task) + # end + # + # Giving several resources ensures that the responder will redirect to + # <code>project_task_url</code> instead of <code>task_url</code>. + # + # Namespaced and singleton resources require 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) + # + # Note that if you give an array, it will be treated as a collection, + # so the following is not equivalent: + # + # respond_with [@project, :manager, @task] + # + # === Custom options + # + # <code>respond_with</code> also allows you to pass options that are forwarded + # to the underlying render call. Those options are only applied for success + # scenarios. For instance, you can do the following in the create method above: + # + # def create + # @project = Project.find(params[:project_id]) + # @task = @project.tasks.build(params[:task]) + # flash[:notice] = 'Task was successfully created.' if @task.save + # respond_with(@project, @task, status: 201) + # end + # + # This will return status 201 if the task was saved successfully. If not, + # it will simply ignore the given options and return status 422 and the + # resource errors. You can also override the location to redirect to: + # + # respond_with(@project, location: root_path) + # + # To customize the failure scenario, you can pass a block to + # <code>respond_with</code>: + # + # def create + # @project = Project.find(params[:project_id]) + # @task = @project.tasks.build(params[:task]) + # respond_with(@project, @task, status: 201) do |format| + # if @task.save + # flash[:notice] = 'Task was successfully created.' + # else + # format.html { render "some_special_template" } + # end + # end + # end + # + # Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>. + class Responder + attr_reader :controller, :request, :format, :resource, :resources, :options + + DEFAULT_ACTIONS_FOR_VERBS = { + :post => :new, + :patch => :edit, + :put => :edit + } + + def initialize(controller, resources, options={}) + @controller = controller + @request = @controller.request + @format = @controller.formats.first + @resource = resources.last + @resources = resources + @options = options + @action = options.delete(:action) + @default_response = options.delete(:default_response) + end + + delegate :head, :render, :redirect_to, :to => :controller + delegate :get?, :post?, :patch?, :put?, :delete?, :to => :request + + # Undefine :to_json and :to_yaml since it's defined on Object + undef_method(:to_json) if method_defined?(:to_json) + undef_method(:to_yaml) if method_defined?(:to_yaml) + + # Initializes a new responder and invokes the proper format. If the format is + # not defined, call to_format. + # + def self.call(*args) + new(*args).respond + end + + # Main entry point for responder responsible to dispatch to the proper format. + # + def respond + method = "to_#{format}" + respond_to?(method) ? send(method) : to_format + end + + # HTML format does not render the resource, it always attempt to render a + # template. + # + def to_html + default_render + rescue ActionView::MissingTemplate => e + navigation_behavior(e) + end + + # to_js simply tries to render a template. If no template is found, raises the error. + def to_js + default_render + end + + # All other formats follow the procedure below. First we try to render a + # template, if the template is not available, we verify if the resource + # responds to :to_format and display it. + # + def to_format + if get? || !has_errors? || response_overridden? + default_render + else + display_errors + end + rescue ActionView::MissingTemplate => e + api_behavior(e) + end + + protected + + # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth. + def navigation_behavior(error) + if get? + raise error + elsif has_errors? && default_action + render :action => default_action + else + redirect_to navigation_location + end + end + + # 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 + elsif post? + display resource, :status => :created, :location => api_location + else + head :no_content + end + end + + # Checks whether the resource responds to the current format or not. + # + def resourceful? + resource.respond_to?("to_#{format}") + end + + # Returns the resource location by retrieving it from the options or + # returning the resources array. + # + def resource_location + options[:location] || resources + end + alias :navigation_location :resource_location + alias :api_location :resource_location + + # If a response block was given, use it, otherwise call render on + # controller. + # + def default_render + if @default_response + @default_response.call(options) + else + controller.default_render(options) + end + end + + # Display is just a shortcut to render a resource with the current format. + # + # display @user, status: :ok + # + # For XML requests it's 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={}) + controller.render given_options.merge!(options).merge!(format => resource) + end + + def display_errors + controller.render format => resource_errors, :status => :unprocessable_entity + end + + # Check whether the resource has errors. + # + def has_errors? + resource.respond_to?(:errors) && !resource.errors.empty? + end + + # Check whether the necessary 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. + # + def default_action + @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol] + end + + def resource_errors + respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors + end + + def json_resource_errors + {:errors => resource.errors} + end + + def response_overridden? + @default_response.present? + end + end +end |