From 8c9e4d520291871e5319bc0e0a890527d8aea099 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Thu, 28 Apr 2011 15:56:11 +0700 Subject: Add `ActionController::ParamsWrapper` to wrap parameters into a nested hash This will allow us to do a rootless JSON/XML request to server. --- .../lib/action_controller/metal/params_wrapper.rb | 197 +++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 actionpack/lib/action_controller/metal/params_wrapper.rb (limited to 'actionpack/lib/action_controller/metal') diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb new file mode 100644 index 0000000000..29ff546139 --- /dev/null +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -0,0 +1,197 @@ +require 'active_support/core_ext/class/attribute' +require 'action_dispatch/http/mime_types' + +module ActionController + # Wraps parameters hash into nested hash. This will allow client to submit + # POST request without having to specify a root element in it. + # + # By default, this functionality won't be enabled by default. You can enable + # it globally by setting +ActionController::Base.wrap_parameters+: + # + # ActionController::Base.wrap_parameters = [:json] + # + # You could also turn it on per controller by setting the format array to + # non-empty array: + # + # class UsersController < ApplicationController + # wrap_parameters :format => [:json, :xml] + # end + # + # If you enable +ParamsWrapper+ for +:json+ format. Instead of having to + # send JSON parameters like this: + # + # {"user": {"name": "Konata"}} + # + # You can now just send a parameters like this: + # + # {"name": "Konata"} + # + # And it will be wrapped into a nested hash with the key name matching + # controller's name. For example, if you're posting to +UsersController+, + # your new +params+ hash will look like this: + # + # {"name" => "Konata", "user" => {"name" => "Konata"}} + # + # You can also specify the key in which the parameters should be wrapped to, + # and also the list of attributes it should wrap by using either +:only+ or + # +:except+ options like this: + # + # class UsersController < ApplicationController + # wrap_parameters :person, :only => [:username, :password] + # end + # + # If you're going to pass the parameters to an +ActiveModel+ object (such as + # +User.new(params[:user])+), you might consider passing the model class to + # the method instead. The +ParamsWrapper+ will actually try to determine the + # list of attribute names from the model and only wrap those attributes: + # + # class UsersController < ApplicationController + # wrap_parameters Person + # end + # + # You still could pass +:only+ and +:except+ to set the list of attributes + # you want to wrap. + # + # By default, if you don't specify the key in which the parameters would be + # wrapped to, +ParamsWrapper+ will actually try to determine if there's + # a model related to it or not. This controller, for example: + # + # class Admin::UsersController < ApplicationController + # end + # + # will try to check if +Admin::User+ or +User+ model exists, and use it to + # determine the wrapper key respectively. If both of the model doesn't exists, + # it will then fallback to use +user+ as the key. + module ParamsWrapper + extend ActiveSupport::Concern + + EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8) + + included do + class_attribute :_wrapper_options + self._wrapper_options = {:format => []} + end + + module ClassMethods + # Sets the name of the wrapper key, or the model which +ParamsWrapper+ + # would use to determine the attribute names from. + # + # ==== Examples + # wrap_parameters :format => :xml + # # enables the parmeter wrappes for XML format + # + # wrap_parameters :person + # # wraps parameters into +params[:person]+ hash + # + # wrap_parameters Person + # # wraps parameters by determine the wrapper key from Person class + # (+person+, in this case) and the list of attribute names + # + # wrap_parameters :only => [:username, :title] + # # wraps only +:username+ and +:title+ attributes from parameters. + # + # wrap_parameters false + # # disable parameters wrapping for this controller altogether. + # + # ==== Options + # * :format - The list of formats in which the parameters wrapper + # will be enabled. + # * :only - The list of attribute names which parmeters wrapper + # will wrap into a nested hash. + # * :only - The list of attribute names which parmeters wrapper + # will exclude from a nested hash. + def wrap_parameters(name_or_model_or_options, options = {}) + if !name_or_model_or_options.is_a? Hash + if name_or_model_or_options != false + options = options.merge(:name_or_model => name_or_model_or_options) + else + options = opions.merge(:format => []) + end + else + options = name_or_model_or_options + end + + options[:name_or_model] ||= _default_wrap_model + self._wrapper_options = self._wrapper_options.merge(options) + end + + # Sets the default wrapper key or model which will be used to determine + # wrapper key and attribute names. Will be called automatically when the + # module is inherited. + def inherited(klass) + if klass._wrapper_options[:format].present? + klass._wrapper_options = klass._wrapper_options.merge(:name_or_model => klass._default_wrap_model) + end + super + end + + # Determine the wrapper model from the controller's name. By convention, + # this could be done by trying to find the defined model that has the + # same singularize name as the controller. For example, +UsersController+ + # will try to find if the +User+ model exists. + def _default_wrap_model + model_name = self.name.sub(/Controller$/, '').singularize + + begin + model_klass = model_name.constantize + rescue NameError => e + unscoped_model_name = model_name.split("::", 2).last + break if unscoped_model_name == model_name + model_name = unscoped_model_name + end until model_klass + + model_klass + end + end + + # Performs parameters wrapping upon the request. Will be called automatically + # by the metal call stack. + def process_action(*args) + if _wrapper_enabled? + wrapped_hash = { _wrapper_key => request.request_parameters.slice(*_wrapped_keys) } + wrapped_filtered_hash = { _wrapper_key => request.filtered_parameters.slice(*_wrapped_keys) } + + # This will make the wrapped hash accessible from controller and view + request.parameters.merge! wrapped_hash + request.request_parameters.merge! wrapped_hash + + # This will make the wrapped hash displayed in the log file + request.filtered_parameters.merge! wrapped_filtered_hash + end + super + end + + private + # Returns the wrapper key which will use to stored wrapped parameters. + def _wrapper_key + @_wrapper_key ||= if _wrapper_options[:name_or_model] + _wrapper_options[:name_or_model].to_s.demodulize.underscore + else + self.class.controller_name.singularize + end + end + + # Returns the list of parameters which will be selected for wrapped. + def _wrapped_keys + @_wrapped_keys ||= if _wrapper_options[:only] + Array(_wrapper_options[:only]).collect(&:to_s) + elsif _wrapper_options[:except] + request.request_parameters.keys - Array(_wrapper_options[:except]).collect(&:to_s) - EXCLUDE_PARAMETERS + elsif _wrapper_options[:name_or_model].respond_to?(:column_names) + _wrapper_options[:name_or_model].column_names + else + request.request_parameters.keys - EXCLUDE_PARAMETERS + end + end + + # Returns the list of enabled formats. + def _wrapper_formats + Array(_wrapper_options[:format]) + end + + # Checks if we should perform parameters wrapping. + def _wrapper_enabled? + _wrapper_formats.any?{ |format| format == request.content_mime_type.try(:ref) } && request.request_parameters[_wrapper_key].nil? + end + end +end -- cgit v1.2.3