# frozen_string_literal: true 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 # 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.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 #to_xml on the given resource; # # 3) if the responder does not respond_to :to_xml, call #to_format 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", status: :unprocessable_entity } # 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 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.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 # project_task_url instead of task_url. # # 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 # # respond_with 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 # respond_with: # # 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", status: :unprocessable_entity } # end # end # end # # Using respond_with with a block follows the same syntax as respond_to. class Responder class_attribute :error_status, default: :ok, instance_writer: false, instance_predicate: false class_attribute :redirect_status, default: :found, instance_writer: false, instance_predicate: false 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) if options[:location].respond_to?(:call) location = options.delete(:location) options[:location] = location.call unless has_errors? end 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? display_errors elsif has_view_rendering? || response_overridden? default_render else api_behavior end rescue ActionView::MissingTemplate api_behavior 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 error_rendering_options else redirect_to navigation_location, status: redirect_status end end # This is the common behavior for formats associated with APIs, such as :xml and :json. def api_behavior 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 # 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) elsif !get? && has_errors? controller.render({ status: error_status }.merge!(options)) else controller.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 # TODO: use `error_status` once we switch the default to be `unprocessable_entity`, # otherwise we'd be changing this behavior here now. 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 def has_view_rendering? controller.class.include? ActionView::Rendering end # By default, render the :edit 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 def error_rendering_options if options[:render] options[:render] else { action: default_action, status: error_status } end end end end