From bd6b61be88dfe6eb1ff1dcc5c17542d804a842c7 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 6 Aug 2009 19:52:11 -0300 Subject: Rename /base to /metal and make base.rb and metal.rb top-level to reflect their module locations --- .../lib/action_controller/metal/mime_responds.rb | 353 +++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 actionpack/lib/action_controller/metal/mime_responds.rb (limited to 'actionpack/lib/action_controller/metal/mime_responds.rb') diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb new file mode 100644 index 0000000000..f4a4007a43 --- /dev/null +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -0,0 +1,353 @@ +module ActionController #:nodoc: + module MimeResponds #:nodoc: + extend ActiveSupport::Concern + + included do + class_inheritable_reader :mimes_for_respond_to + clear_respond_to + end + + module ClassMethods + # Defines mimes that are rendered by default when invoking respond_with. + # + # Examples: + # + # respond_to :html, :xml, :json + # + # All actions on your controller will respond to :html, :xml and :json. + # + # But if you want to specify it based on your actions, you can use only and + # except: + # + # respond_to :html + # respond_to :xml, :json, :except => [ :edit ] + # + # The definition above explicits that all actions respond to :html. And all + # actions except :edit respond to :xml and :json. + # + # You can specify also only parameters: + # + # respond_to :rjs, :only => :create + # + def respond_to(*mimes) + options = mimes.extract_options! + + only_actions = Array(options.delete(:only)) + except_actions = Array(options.delete(:except)) + + mimes.each do |mime| + mime = mime.to_sym + mimes_for_respond_to[mime] = {} + mimes_for_respond_to[mime][:only] = only_actions unless only_actions.empty? + mimes_for_respond_to[mime][:except] = except_actions unless except_actions.empty? + end + end + + # Clear all mimes in respond_to. + # + def clear_respond_to + write_inheritable_attribute(:mimes_for_respond_to, ActiveSupport::OrderedHash.new) + end + end + + # Without web-service support, an action which collects the data for displaying a list of people + # might look something like this: + # + # def index + # @people = Person.find(:all) + # end + # + # Here's the same action, with web-service support baked in: + # + # def index + # @people = Person.find(:all) + # + # respond_to do |format| + # format.html + # format.xml { render :xml => @people.to_xml } + # end + # end + # + # What that says is, "if the client wants HTML in response to this action, just respond as we + # would have before, but if the client wants XML, return them the list of people in XML format." + # (Rails determines the desired response format from the HTTP Accept header submitted by the client.) + # + # Supposing you have an action that adds a new person, optionally creating their company + # (by name) if it does not already exist, without web-services, it might look like this: + # + # def create + # @company = Company.find_or_create_by_name(params[:company][:name]) + # @person = @company.people.create(params[:person]) + # + # redirect_to(person_list_url) + # end + # + # Here's the same action, with web-service support baked in: + # + # def create + # company = params[:person].delete(:company) + # @company = Company.find_or_create_by_name(company[:name]) + # @person = @company.people.create(params[:person]) + # + # respond_to do |format| + # format.html { redirect_to(person_list_url) } + # format.js + # format.xml { render :xml => @person.to_xml(:include => @company) } + # end + # end + # + # If the client wants HTML, we just redirect them back to the person list. If they want Javascript + # (format.js), then it is an RJS request and we render the RJS template associated with this action. + # Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also + # include the person's company in the rendered XML, so you get something like this: + # + # + # ... + # ... + # + # ... + # ... + # ... + # + # + # + # Note, however, the extra bit at the top of that action: + # + # company = params[:person].delete(:company) + # @company = Company.find_or_create_by_name(company[:name]) + # + # This is because the incoming XML document (if a web-service request is in process) can only contain a + # single root-node. So, we have to rearrange things so that the request looks like this (url-encoded): + # + # person[name]=...&person[company][name]=...&... + # + # And, like this (xml-encoded): + # + # + # ... + # + # ... + # + # + # + # In other words, we make the request so that it operates on a single entity's person. Then, in the action, + # we extract the company data from the request, find or create the company, and then create the new person + # with the remaining data. + # + # Note that you can define your own XML parameter parser which would allow you to describe multiple entities + # in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow + # and accept Rails' defaults, life will be much easier. + # + # If you need to use a MIME type which isn't supported by default, you can register your own handlers in + # environment.rb as follows. + # + # Mime::Type.register "image/jpg", :jpg + # + # Respond to also allows you to specify a common block for different formats by using any: + # + # def index + # @people = Person.find(:all) + # + # respond_to do |format| + # format.html + # format.any(:xml, :json) { render request.format.to_sym => @people } + # end + # end + # + # In the example above, if the format is xml, it will render: + # + # render :xml => @people + # + # Or if the format is json: + # + # render :json => @people + # + # Since this is a common pattern, you can use the class method respond_to + # with the respond_with method to have the same results: + # + # class PeopleController < ApplicationController + # respond_to :html, :xml, :json + # + # def index + # @people = Person.find(:all) + # respond_with(@person) + # end + # end + # + # Be sure to check respond_with and respond_to documentation for more examples. + # + def respond_to(*mimes, &block) + options = mimes.extract_options! + raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? + + resource = options.delete(:with) + responder = Responder.new + + mimes = collect_mimes_from_class_level if mimes.empty? + mimes.each { |mime| responder.send(mime) } + block.call(responder) if block_given? + + if format = request.negotiate_mime(responder.order) + respond_to_block_or_template_or_resource(format, resource, + options, &responder.response_for(format)) + else + head :not_acceptable + end + end + + # respond_with allows you to respond an action with a given resource. It + # requires that you set your class with a :respond_to method with the + # formats allowed: + # + # class PeopleController < ApplicationController + # respond_to :html, :xml, :json + # + # def index + # @people = Person.find(:all) + # respond_with(@person) + # end + # end + # + # When a request comes with format :xml, the respond_with will first search + # for a template as person/index.xml, if the template is not available, it + # will see if the given resource responds to :to_xml. + # + # If neither are available, it will raise an error. + # + # Extra parameters given to respond_with are used when :to_format is invoked. + # This allows you to set status and location for several formats at the same + # time. Consider this restful controller response on create for both xml + # and json formats: + # + # class PeopleController < ApplicationController + # respond_to :xml, :json + # + # def create + # @person = Person.new(params[:person]) + # + # if @person.save + # respond_with(@person, :status => :ok, :location => person_url(@person)) + # else + # respond_with(@person.errors, :status => :unprocessable_entity) + # end + # end + # end + # + # Finally, respond_with also accepts blocks, as in respond_to. Let's take + # the same controller and create action above and add common html behavior: + # + # class PeopleController < ApplicationController + # respond_to :html, :xml, :json + # + # def create + # @person = Person.new(params[:person]) + # + # if @person.save + # options = { :status => :ok, :location => person_url(@person) } + # + # respond_with(@person, options) do |format| + # format.html { redirect_to options[:location] } + # end + # else + # respond_with(@person.errors, :status => :unprocessable_entity) do + # format.html { render :action => :new } + # end + # end + # end + # end + # + def respond_with(resource, options={}, &block) + respond_to(options.merge!(:with => resource), &block) + end + + protected + + def respond_to_block_or_template_or_resource(format, resource, options) + self.formats = [format.to_sym] + return yield if block_given? + + begin + default_render + rescue ActionView::MissingTemplate => e + if resource && resource.respond_to?(:"to_#{format.to_sym}") + render options.merge(format.to_sym => resource) + else + raise e + end + end + end + + # Collect mimes declared in the class method respond_to valid for the + # current action. + # + def collect_mimes_from_class_level #:nodoc: + action = action_name.to_sym + + mimes_for_respond_to.keys.select do |mime| + config = mimes_for_respond_to[mime] + + if config[:except] + !config[:except].include?(action) + elsif config[:only] + config[:only].include?(action) + else + true + end + end + end + + class Responder #:nodoc: + attr_accessor :order + + def initialize + @order, @responses = [], {} + end + + def any(*args, &block) + if args.any? + args.each { |type| send(type, &block) } + else + custom(Mime::ALL, &block) + end + end + alias :all :any + + def custom(mime_type, &block) + mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s) + + @order << mime_type + @responses[mime_type] ||= block + end + + def response_for(mime) + @responses[mime] || @responses[Mime::ALL] + end + + def self.generate_method_for_mime(mime) + sym = mime.is_a?(Symbol) ? mime : mime.to_sym + const = sym.to_s.upcase + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{sym}(&block) # def html(&block) + custom(Mime::#{const}, &block) # custom(Mime::HTML, &block) + end # end + RUBY + end + + Mime::SET.each do |mime| + generate_method_for_mime(mime) + end + + def method_missing(symbol, &block) + mime_constant = Mime.const_get(symbol.to_s.upcase) + + if Mime::SET.include?(mime_constant) + self.class.generate_method_for_mime(mime_constant) + send(symbol, &block) + else + super + end + end + + end + end +end -- cgit v1.2.3 From f59984cc81bd1a64a53a2480a9b4e05fe7357d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 1 Aug 2009 15:29:39 +0200 Subject: Add nagivational behavior to respond_with. --- .../lib/action_controller/metal/mime_responds.rb | 157 +++++++++++++++------ 1 file changed, 110 insertions(+), 47 deletions(-) (limited to 'actionpack/lib/action_controller/metal/mime_responds.rb') diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 9a6c8aa58b..837496e477 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -198,11 +198,11 @@ module ActionController #:nodoc: end # respond_with allows you to respond an action with a given resource. It - # requires that you set your class with a :respond_to method with the + # requires that you set your class with a respond_to method with the # formats allowed: # # class PeopleController < ApplicationController - # respond_to :xml, :json + # respond_to :html, :xml, :json # # def index # @people = Person.find(:all) @@ -210,14 +210,43 @@ module ActionController #:nodoc: # end # end # - # When a request comes with format :xml, the respond_with will first search - # for a template as person/index.xml, if the template is not available, it - # will see if the given resource responds to :to_xml. + # 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 check if the given + # resource responds to :to_xml. + # + # 3) if a :location option was provided, redirect to the location with + # redirect status if a string was given, or render an action if a + # symbol was given. + # + # If all steps fail, a missing template error will be raised. + # + # === Supported options + # + # [status] + # Sets the response status. + # + # [head] + # Tell respond_with to set the content type, status and location header, + # but do not render the object, leaving the response body empty. This + # option only has effect if the resource is being rendered. If a + # template was found, it's going to be rendered anyway. + # + # [location] + # Sets the location header with the given value. It accepts a string, + # representing the location header value, or a symbol representing an + # action name. + # + # === Builtin HTTP verb semantics # - # If neither are available, it will raise an error. + # respond_with holds semantics for each HTTP verb. Depending on the verb + # and the resource status, respond_with will automatically set the options + # above. # - # respond_with holds semantics for each HTTP verb. The example above cover - # GET requests. Let's check a POST request example: + # Above we saw an example for GET requests, where actually no option is + # configured. A create action for POST requests, could be written as: # # def create # @person = Person.new(params[:person]) @@ -225,34 +254,40 @@ module ActionController #:nodoc: # respond_with(@person) # end # - # Since the request is a POST, respond_with will check wether @people - # resource have errors or not. If it has errors, it will render the error - # object with unprocessable entity status (422). + # respond_with will inspect the @person object and check if we have any + # error. If errors are empty, it will add status and location to the options + # hash. Then the create action in case of success, is equivalent to this: # - # If no error was found, it will render the @people resource, with status - # created (201) and location header set to person_url(@people). + # respond_with(@person, :status => :created, :location => @person) # - # If you also want to provide html behavior in the method above, you can - # supply a block to customize it: + # From them on, the lookup happens as described above. Let's suppose a :xml + # request and we don't have a people/create.xml template. But since the + # @person object responds to :to_xml, it will render the newly created + # resource and set status and location. # - # class PeopleController < ApplicationController - # respond_to :html, :xml, :json # Add :html to respond_to definition - # - # def create - # @person = Person.new(params[:pe]) - # - # respond_with(@person) do |format| - # if @person.save - # flash[:notice] = 'Person was successfully created.' - # format.html { redirect_to @person } - # else - # format.html { render :action => "new" } - # end - # end + # However, if the request is :html, a template is not available and @person + # does not respond to :to_html. But since a :location options was provided, + # it will redirect to it. + # + # In case of failures (when the @person could not be saved and errors are + # not empty), respond_with can be expanded as this: + # + # respond_with(@person.errors, :status => :unprocessable_entity, :location => :new) + # + # In other words, respond_with(@person) for POST requests is expanded + # internally into this: + # + # def create + # @person = Person.new(params[:person]) + # + # if @person.save + # respond_with(@person, :status => :created, :location => @person) + # else + # respond_with(@person.errors, :status => :unprocessable_entity, :location => :new) # end # end # - # It works similarly for PUT requests: + # For an update action for PUT requests, we would have: # # def update # @person = Person.find(params[:id]) @@ -260,10 +295,23 @@ module ActionController #:nodoc: # respond_with(@person) # end # - # In case of failures, it works as POST requests, but in success failures - # it just reply status ok (200) to the client. + # Which, in face of success and failure scenarios, can be expanded as: # - # A DELETE request also works in the same way: + # def update + # @person = Person.find(params[:id]) + # @person.update_attributes(params[:person]) + # + # if @person.save + # respond_with(@person, :status => :ok, :location => @person, :head => true) + # else + # respond_with(@person.errors, :status => :unprocessable_entity, :location => :edit) + # end + # end + # + # Notice that in case of success, we just need to reply :ok to the client. + # The option :head ensures that the object is not rendered. + # + # Finally, we have the destroy action with DELETE verb: # # def destroy # @person = Person.find(params[:id]) @@ -271,21 +319,30 @@ module ActionController #:nodoc: # respond_with(@person) # end # - # It just replies with status ok, indicating the record was successfuly - # destroyed. + # Which is expanded as: + # + # def destroy + # @person = Person.find(params[:id]) + # @person.destroy + # respond_with(@person, :status => :ok, :location => @person, :head => true) + # end + # + # In this case, since @person.destroyed? returns true, polymorphic urls will + # redirect to the collection url, instead of the resource url. # def respond_with(resource, options={}, &block) respond_to(&block) rescue ActionView::MissingTemplate => e format = self.formats.first resource = normalize_resource_options_by_verb(resource, options) + action = options.delete(:location) if options[:location].is_a?(Symbol) if resource.respond_to?(:"to_#{format}") - if options.delete(:no_content) - head options - else - render options.merge(format => resource) - end + options.delete(:head) ? head(options) : render(options.merge(format => resource)) + elsif action + render :action => action + elsif options[:location] + redirect_to options[:location] else raise e end @@ -296,16 +353,22 @@ module ActionController #:nodoc: # Change respond with behavior based on the HTTP verb. # def normalize_resource_options_by_verb(resource_or_array, options) - resource = resource_or_array.is_a?(Array) ? resource_or_array.last : resource_or_array - has_errors = resource.respond_to?(:errors) && !resource.errors.empty? + resource = resource_or_array.is_a?(Array) ? resource_or_array.last : resource_or_array - if has_errors && (request.post? || request.put?) - options.reverse_merge!(:status => :unprocessable_entity) + if resource.respond_to?(:errors) && !resource.errors.empty? + options[:status] ||= :unprocessable_entity + options[:location] ||= :new if request.post? + options[:location] ||= :edit if request.put? return resource.errors - elsif request.post? - options.reverse_merge!(:status => :created, :location => resource_or_array) elsif !request.get? - options.reverse_merge!(:status => :ok, :no_content => true) + options[:location] ||= resource_or_array + + if request.post? + options[:status] ||= :created + else + options[:status] ||= :ok + options[:head] = true unless options.key?(:head) + end end return resource -- cgit v1.2.3 From 7034272354ad41dd4d1c90138a79e7f14ebc2bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 1 Aug 2009 16:47:44 +0200 Subject: Add destroyed? to ActiveRecord, include tests for polymorphic urls for destroyed objects and refactor mime responds tests and documentation. --- .../lib/action_controller/metal/mime_responds.rb | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'actionpack/lib/action_controller/metal/mime_responds.rb') diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 837496e477..02a88437e3 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -330,6 +330,37 @@ module ActionController #:nodoc: # In this case, since @person.destroyed? returns true, polymorphic urls will # redirect to the collection url, instead of the resource url. # + # === Nested resources + # + # respond_with also works with nested resources, you just need to pass them + # 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]) + # @task.save + # respond_with([@project, @task]) + # end + # + # Namespaced and singleton resources requires a symbol to be given, as in + # polymorphic urls. If a project has one manager with has many tasks, it + # should be invoked as: + # + # respond_with([@project, :manager, @task]) + # + # Be sure to check polymorphic_url documentation. The only occasion you will + # need to give clear input to respond_with is in DELETE verbs for singleton + # resources. In such cases, the collection url does not exist, so you need + # to supply the destination url after delete: + # + # def destroy + # @project = Project.find(params[:project_id]) + # @manager = @project.manager + # @manager.destroy + # respond_with([@project, @manager], :location => root_url) + # end + # def respond_with(resource, options={}, &block) respond_to(&block) rescue ActionView::MissingTemplate => e -- cgit v1.2.3 From 1fd65c80fcdc6080b9efa27f41dbb7f7d95a17c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 5 Aug 2009 12:36:43 +0200 Subject: Encapsulate respond_with behavior in a presenter. --- .../lib/action_controller/metal/mime_responds.rb | 355 ++++++++++----------- 1 file changed, 165 insertions(+), 190 deletions(-) (limited to 'actionpack/lib/action_controller/metal/mime_responds.rb') diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 02a88437e3..d43f940774 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -1,4 +1,146 @@ 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 @@ -197,214 +339,47 @@ module ActionController #:nodoc: end end - # respond_with allows you to respond an action with a given resource. It - # requires that you set your class with a respond_to method with the - # formats allowed: - # - # 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 check if the given - # resource responds to :to_xml. - # - # 3) if a :location option was provided, redirect to the location with - # redirect status if a string was given, or render an action if a - # symbol was given. - # - # If all steps fail, a missing template error will be raised. - # - # === Supported options - # - # [status] - # Sets the response status. - # - # [head] - # Tell respond_with to set the content type, status and location header, - # but do not render the object, leaving the response body empty. This - # option only has effect if the resource is being rendered. If a - # template was found, it's going to be rendered anyway. - # - # [location] - # Sets the location header with the given value. It accepts a string, - # representing the location header value, or a symbol representing an - # action name. - # - # === Builtin HTTP verb semantics - # - # respond_with holds semantics for each HTTP verb. Depending on the verb - # and the resource status, respond_with will automatically set the options - # above. - # - # Above we saw an example for GET requests, where actually no option is - # configured. A create action for POST requests, could be written as: - # - # def create - # @person = Person.new(params[:person]) - # @person.save - # respond_with(@person) - # end + # respond_with wraps a resource around a presenter 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. # - # respond_with will inspect the @person object and check if we have any - # error. If errors are empty, it will add status and location to the options - # hash. Then the create action in case of success, is equivalent to this: + # ==== Example # - # respond_with(@person, :status => :created, :location => @person) - # - # From them on, the lookup happens as described above. Let's suppose a :xml - # request and we don't have a people/create.xml template. But since the - # @person object responds to :to_xml, it will render the newly created - # resource and set status and location. - # - # However, if the request is :html, a template is not available and @person - # does not respond to :to_html. But since a :location options was provided, - # it will redirect to it. - # - # In case of failures (when the @person could not be saved and errors are - # not empty), respond_with can be expanded as this: - # - # respond_with(@person.errors, :status => :unprocessable_entity, :location => :new) - # - # In other words, respond_with(@person) for POST requests is expanded - # internally into this: - # - # def create - # @person = Person.new(params[:person]) - # - # if @person.save - # respond_with(@person, :status => :created, :location => @person) - # else - # respond_with(@person.errors, :status => :unprocessable_entity, :location => :new) - # end - # end - # - # For an update action for PUT requests, we would have: - # - # def update - # @person = Person.find(params[:id]) - # @person.update_attributes(params[:person]) - # respond_with(@person) - # end - # - # Which, in face of success and failure scenarios, can be expanded as: - # - # def update - # @person = Person.find(params[:id]) - # @person.update_attributes(params[:person]) - # - # if @person.save - # respond_with(@person, :status => :ok, :location => @person, :head => true) - # else - # respond_with(@person.errors, :status => :unprocessable_entity, :location => :edit) - # end - # end - # - # Notice that in case of success, we just need to reply :ok to the client. - # The option :head ensures that the object is not rendered. - # - # Finally, we have the destroy action with DELETE verb: - # - # def destroy - # @person = Person.find(params[:id]) - # @person.destroy - # respond_with(@person) + # def index + # @users = User.all + # respond_with(@users) # end # - # Which is expanded as: + # It also accepts a block to be given. It's used to overwrite a default + # response: # # def destroy - # @person = Person.find(params[:id]) - # @person.destroy - # respond_with(@person, :status => :ok, :location => @person, :head => true) - # end - # - # In this case, since @person.destroyed? returns true, polymorphic urls will - # redirect to the collection url, instead of the resource url. + # @user = User.find(params[:id]) + # flash[:notice] = "User was successfully created." if @user.save # - # === Nested resources - # - # respond_with also works with nested resources, you just need to pass them - # 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]) - # @task.save - # respond_with([@project, @task]) + # respond_with(@user) do |format| + # format.html { render } + # end # end # - # Namespaced and singleton resources requires a symbol to be given, as in - # polymorphic urls. If a project has one manager with has many tasks, it - # should be invoked as: - # - # respond_with([@project, :manager, @task]) - # - # Be sure to check polymorphic_url documentation. The only occasion you will - # need to give clear input to respond_with is in DELETE verbs for singleton - # resources. In such cases, the collection url does not exist, so you need - # to supply the destination url after delete: - # - # def destroy - # @project = Project.find(params[:project_id]) - # @manager = @project.manager - # @manager.destroy - # respond_with([@project, @manager], :location => root_url) - # end + # All options given to respond_with are sent to the underlying presenter. # def respond_with(resource, options={}, &block) respond_to(&block) - rescue ActionView::MissingTemplate => e - format = self.formats.first - resource = normalize_resource_options_by_verb(resource, options) - action = options.delete(:location) if options[:location].is_a?(Symbol) - - if resource.respond_to?(:"to_#{format}") - options.delete(:head) ? head(options) : render(options.merge(format => resource)) - elsif action - render :action => action - elsif options[:location] - redirect_to options[:location] + rescue ActionView::MissingTemplate + presenter = ActionController::Presenter.new(self, resource, options) + format_method = :"to_#{self.formats.first}" + + if presenter.respond_to?(format_method) + presenter.send(format_method) else - raise e + presenter.to_format end end protected - # Change respond with behavior based on the HTTP verb. - # - def normalize_resource_options_by_verb(resource_or_array, options) - resource = resource_or_array.is_a?(Array) ? resource_or_array.last : resource_or_array - - if resource.respond_to?(:errors) && !resource.errors.empty? - options[:status] ||= :unprocessable_entity - options[:location] ||= :new if request.post? - options[:location] ||= :edit if request.put? - return resource.errors - elsif !request.get? - options[:location] ||= resource_or_array - - if request.post? - options[:status] ||= :created - else - options[:status] ||= :ok - options[:head] = true unless options.key?(:head) - end - end - - return resource - end - # Collect mimes declared in the class method respond_to valid for the # current action. # -- cgit v1.2.3 From aed135d3e261cbee153a35fcfbeb47e2e02b12e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 6 Aug 2009 23:48:48 +0200 Subject: Renamed presenter to renderer, added some documentation and defined its API. --- .../lib/action_controller/metal/mime_responds.rb | 162 ++------------------- 1 file changed, 10 insertions(+), 152 deletions(-) (limited to 'actionpack/lib/action_controller/metal/mime_responds.rb') diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index d43f940774..d823dd424a 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -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 @@ module ActionController #:nodoc: 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 @@ module ActionController #:nodoc: # 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 -- cgit v1.2.3 From 6e0ac748e41e8de8111095f7188c75f673779f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 8 Aug 2009 11:07:07 +0200 Subject: Renamed ActionController::Renderer to ActionController::Responder and ActionController::MimeResponds::Responder to ActionController::MimeResponds::Collector. --- .../lib/action_controller/metal/mime_responds.rb | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'actionpack/lib/action_controller/metal/mime_responds.rb') diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index d823dd424a..c8d042acb5 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -179,15 +179,15 @@ module ActionController #:nodoc: def respond_to(*mimes, &block) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? - responder = Responder.new + collector = Collector.new mimes = collect_mimes_from_class_level if mimes.empty? - mimes.each { |mime| responder.send(mime) } - block.call(responder) if block_given? + mimes.each { |mime| collector.send(mime) } + block.call(collector) if block_given? - if format = request.negotiate_mime(responder.order) + if format = request.negotiate_mime(collector.order) self.formats = [format.to_sym] - if response = responder.response_for(format) + if response = collector.response_for(format) response.call else default_render @@ -197,10 +197,10 @@ module ActionController #:nodoc: end end - # respond_with wraps a resource around a renderer for default representation. + # respond_with wraps a resource around a responder 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::Renderer with the controller and resource. + # an ActionController::Responder with the controller and resource. # # ==== Example # @@ -221,19 +221,19 @@ module ActionController #:nodoc: # end # end # - # All options given to respond_with are sent to the underlying renderer, - # except for the option :renderer itself. Since the renderer interface + # All options given to respond_with are sent to the underlying responder, + # except for the option :responder itself. Since the responder 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 - (options.delete(:renderer) || renderer).call(self, resource, options) + (options.delete(:responder) || responder).call(self, resource, options) end - def renderer - ActionController::Renderer + def responder + ActionController::Responder end protected @@ -257,7 +257,7 @@ module ActionController #:nodoc: end end - class Responder #:nodoc: + class Collector #:nodoc: attr_accessor :order def initialize -- cgit v1.2.3