require 'hanami/validations'
require 'hanami/utils/attributes'
require 'set'
module Hanami
module Action
# A set of params requested by the client
#
# It's able to extract the relevant params from a Rack env of from an Hash.
#
# There are three scenarios:
# * When used with Hanami::Router: it contains only the params from the request
# * When used standalone: it contains all the Rack env
# * Default: it returns the given hash as it is. It's useful for testing purposes.
#
# @since 0.1.0
class Params
# The key that returns raw input from the Rack env
#
# @since 0.1.0
RACK_INPUT = 'rack.input'.freeze
# The key that returns router params from the Rack env
# This is a builtin integration for Hanami::Router
#
# @since 0.1.0
ROUTER_PARAMS = 'router.params'.freeze
# CSRF params key
#
# This key is shared with hanamirb and hanami-helpers
#
# @since 0.4.4
# @api private
CSRF_TOKEN = '_csrf_token'.freeze
# Set of params that are never filtered
#
# @since 0.4.4
# @api private
DEFAULT_PARAMS = Hash[CSRF_TOKEN => true].freeze
# Separator for #get
#
# @since 0.4.0
# @api private
#
# @see Hanami::Action::Params#get
GET_SEPARATOR = '.'.freeze
# Whitelist and validate a parameter
#
# @param name [#to_sym] The name of the param to whitelist
#
# @raise [ArgumentError] if one the validations is unknown, or if
# the size validator is used with an object that can't be coerced to
# integer.
#
# @return void
#
# @since 0.3.0
#
# @see http://rdoc.info/gems/hanami-validations/Hanami/Validations
#
# @example Whitelisting
# require 'hanami/controller'
#
# class SignupParams < Hanami::Action::Params
# param :email
# end
#
# params = SignupParams.new({id: 23, email: 'mjb@example.com'})
#
# params[:email] # => 'mjb@example.com'
# params[:id] # => nil
#
# @example Validation
# require 'hanami/controller'
#
# class SignupParams < Hanami::Action::Params
# param :email, presence: true
# end
#
# params = SignupParams.new({})
#
# params[:email] # => nil
# params.valid? # => false
#
# @example Unknown validation
# require 'hanami/controller'
#
# class SignupParams < Hanami::Action::Params
# param :email, unknown: true # => raise ArgumentError
# end
#
# @example Wrong size validation
# require 'hanami/controller'
#
# class SignupParams < Hanami::Action::Params
# param :email, size: 'twentythree'
# end
#
# params = SignupParams.new({})
# params.valid? # => raise ArgumentError
def self.param(name, options = {}, &block)
attribute name, options, &block
nil
end
include Hanami::Validations
def self.whitelisting?
defined_attributes.any?
end
# Overrides the method in Hanami::Validation to build a class that
# inherits from Params rather than only Hanami::Validations.
#
# @since 0.3.2
# @api private
def self.build_validation_class(&block)
kls = Class.new(Params) do
def hanami_nested_attributes?
true
end
end
kls.class_eval(&block)
kls
end
# @attr_reader env [Hash] the Rack env
#
# @since 0.2.0
# @api private
attr_reader :env
# @attr_reader raw [Hanami::Utils::Attributes] all request's attributes
#
# @since 0.3.2
attr_reader :raw
# Initialize the params and freeze them.
#
# @param env [Hash] a Rack env or an hash of params.
#
# @return [Params]
#
# @since 0.1.0
def initialize(env)
@env = env
super(_compute_params)
# freeze
end
# Returns the object associated with the given key
#
# @param key [Symbol] the key
#
# @return [Object,nil] return the associated object, if found
#
# @since 0.2.0
def [](key)
@attributes.get(key)
end
# Get an attribute value associated with the given key.
# Nested attributes are reached with a dot notation.
#
# @param key [String] the key
#
# @return [Object,NilClass] return the associated value, if found
#
# @since 0.4.0
#
# @example
# require 'hanami/controller'
#
# module Deliveries
# class Create
# include Hanami::Action
#
# params do
# param :customer_name
# param :address do
# param :city
# end
# end
#
# def call(params)
# params.get('customer_name') # => "Luca"
# params.get('uknown') # => nil
#
# params.get('address.city') # => "Rome"
# params.get('address.unknown') # => nil
#
# params.get(nil) # => nil
# end
# end
# end
def get(key)
key, *keys = key.to_s.split(GET_SEPARATOR)
result = self[key]
Array(keys).each do |k|
break if result.nil?
result = result[k]
end
result
end
# Serialize params to Hash
#
# @return [::Hash]
#
# @since 0.3.0
def to_h
@attributes.to_h
end
alias_method :to_hash, :to_h
# Assign CSRF Token.
# This method is here for compatibility with Hanami::Validations.
#
# NOTE: When we will not support indifferent access anymore, we can probably
# remove this method.
#
# @since 0.4.4
# @api private
def _csrf_token=(value)
@attributes.set(CSRF_TOKEN, value)
end
private
# @since 0.3.1
# @api private
def _compute_params
if self.class.whitelisting?
_whitelisted_params
else
@attributes = _raw
end
end
# @since 0.3.2
# @api private
def _raw
@raw ||= Utils::Attributes.new(_params)
end
# @since 0.3.1
# @api private
def _params
{}.tap do |result|
if env.has_key?(RACK_INPUT)
result.merge! ::Rack::Request.new(env).params
result.merge! env.fetch(ROUTER_PARAMS, {})
else
result.merge! env.fetch(ROUTER_PARAMS, env)
end
end
end
# @since 0.3.1
# @api private
def _whitelisted_params
{}.tap do |result|
_raw.to_h.each do |k, v|
next unless assign_attribute?(k)
result[k] = v
end
end
end
# Override Hanami::Validations method
#
# @since 0.4.4
# @api private
def assign_attribute?(key)
DEFAULT_PARAMS[key.to_s] || super
end
end
end
end