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