class Roda::RodaPlugins::TypecastParams::Params

Class handling conversion of submitted parameters to desired types.

def self.handle_type(type, opts=OPTS, &block)

types.
in ones. It can be called in subclasses to setup subclass-specific
This method is used to define all type conversions, even the built

* _convert_array_foo(value) # private
* convert_foo(value) # private
* foo!(key)
* foo(key, default=nil)

For a type named +foo+, this will create the following methods:
Handle conversions for the given type using the given block.
def self.handle_type(type, opts=OPTS, &block)
  convert_meth = :"convert_#{type}"
  define_method(convert_meth, &block)
  max_input_bytesize = opts[:max_input_bytesize]
  max_input_bytesize_meth = :"_max_input_bytesize_for_#{type}"
  define_method(max_input_bytesize_meth){max_input_bytesize}
  convert_array_meth = :"_convert_array_#{type}"
  define_method(convert_array_meth) do |v|
    raise Error, "expected array but received #{v.inspect}" unless v.is_a?(Array)
    v.map! do |val|
      check_allowed_bytesize(val, send(max_input_bytesize_meth))
      check_null_byte(val)
      send(convert_meth, val)
    end
  end
  private convert_meth, convert_array_meth, max_input_bytesize_meth
  alias_method max_input_bytesize_meth, max_input_bytesize_meth
  define_method(type) do |key, default=nil|
    process_arg(convert_meth, key, default, send(max_input_bytesize_meth)) if require_hash!
  end
  define_method(:"#{type}!") do |key|
    send(type, key, CHECK_NIL)
  end
end

def self.max_input_bytesize(type, bytesize)

for overriding the sizes for the default input types.
Override the maximum input bytesize for the given type. This is mostly useful
def self.max_input_bytesize(type, bytesize)
  max_input_bytesize_meth = :"_max_input_bytesize_for_#{type}"
  define_method(max_input_bytesize_meth){bytesize}
  private max_input_bytesize_meth
  alias_method max_input_bytesize_meth, max_input_bytesize_meth
end

def self.nest(obj, nesting)

external code.
array. Designed for internal use, should not be called by
+obj+ should be an array or hash, and +nesting+ should be an
Create a new instance with the given object and nesting level.
def self.nest(obj, nesting)
  v = allocate
  v.instance_variable_set(:@nesting, nesting)
  v.send(:initialize, obj)
  v
end

def [](key)

if +key+ is an integer, or hash otherwise.
Return a new Params instance for the given +key+. The value of +key+ should be an array
def [](key)
  @subs ||= {}
  if sub = @subs[key]
    return sub
  end
  if @obj.is_a?(Array)
    unless key.is_a?(Integer)
      handle_error(key, :invalid_type, "invalid use of non-integer key for accessing array: #{key.inspect}", true)
    end
  else
    if key.is_a?(Integer)
      handle_error(key, :invalid_type, "invalid use of integer key for accessing hash: #{key}", true)
    end
  end
  v = @obj[key]
  v = yield if v.nil? && defined?(yield)
  begin
    sub = self.class.nest(v, Array(@nesting) + [key])
  rescue => e
    handle_error(key, :invalid_type, e, true)
  end
  @subs[key] = sub
  sub.sub_capture(@capture, @symbolize, @skip_missing)
  sub
end

def _capture!(ret, opts)

Internals of convert! and convert_each!.
def _capture!(ret, opts)
  previous_symbolize = @symbolize
  previous_skip_missing = @skip_missing
  unless cap = @capture
    @params = @obj.class.new
    @subs.clear if @subs
    capturing_started = true
    cap = @capture = []
  end
  if opts.has_key?(:symbolize)
    @symbolize = !!opts[:symbolize]
  end
  if opts.has_key?(:skip_missing)
    @skip_missing = !!opts[:skip_missing]
  end
  begin
    v = yield
  rescue Error => e
    cap << e unless cap.last == e
  end
  if capturing_started
    unless cap.empty?
      e = cap[0]
      e.all_errors = cap
      raise e
    end
    if ret == :nested_params
      nested_params
    else
      v
    end
  end
ensure
  @nested_params = nil
  if capturing_started
    # Unset capturing if capturing was already started.
    @capture = nil
  else
    # If capturing was not already started, update cached nested params
    # before resetting symbolize setting. 
    @nested_params = nested_params
  end
  @symbolize = previous_symbolize
  @skip_missing = previous_skip_missing
end

def _dig(force, type, nest, key)

Internals of dig/dig!
def _dig(force, type, nest, key)
  if type == :array || type == :array!
    conv_type = nest.shift
    unless conv_type.is_a?(Symbol)
      raise ProgrammerError, "incorrect subtype given when using #{type} as argument for dig/dig!: #{conv_type.inspect}"
    end
    meth = type
    type = conv_type
    args = [meth, type]
  else
    meth = type
    args = [type]
  end
  unless respond_to?("_convert_array_#{type}", true)
    raise ProgrammerError, "no typecast_params type registered for #{meth.inspect}"
  end
  if v = subkey(nest, force)
    v.send(*args, key, (CHECK_NIL if force))
  end
end

def _string_parse!(klass, v)

Handle parsing for string values passed to parse!.
def _string_parse!(klass, v)
  klass.parse(v)
end

def array(type, key, default=nil)

no value for +key+, nil is returned instead of an array.
then this returns an array of arrays, one for each respective value of +key+. If there is
given, any +nil+ values in the array are replaced with +default+. If +key+ is an array
Convert the value of +key+ to an array of values of the given +type+. If +default+ is
def array(type, key, default=nil)
  meth = :"_convert_array_#{type}"
  raise ProgrammerError, "no typecast_params type registered for #{type.inspect}" unless respond_to?(meth, true)
  process_arg(meth, key, default, send(:"_max_input_bytesize_for_#{type}")) if require_hash!
end

def array!(type, key, default=nil)

the returned array is +nil+, raise an Error.
Call +array+ with the +type+, +key+, and +default+, but if the return value is nil or any value in
def array!(type, key, default=nil)
  v = array(type, key, default)
  if key.is_a?(Array)
    key.zip(v).each do |k, arr|
      check_array!(k, arr)
    end
  else
    check_array!(key, v)
  end
  v
end

def check_allowed_bytesize(v, max)

Raise an Error if the value is a string with bytesize over max (if max is given)
def check_allowed_bytesize(v, max)
  if max && v.is_a?(String) && v.bytesize > max
    handle_error(nil, :too_long, "string parameter is too long for type", true)
  end
end

def check_array!(key, arr)

Raise an error if the array given does contains nil values.
def check_array!(key, arr)
  if arr
    if arr.any?{|val| val.nil?}
      handle_error(key, :invalid_type, "invalid value in array parameter #{param_name(key)}")
    end
  else
    handle_error(key, :missing, "missing parameter for #{param_name(key)}")
  end
end

def check_null_byte(v)

Raise an Error if the value is a string containing a null byte.
def check_null_byte(v)
  if v.is_a?(String) && v.index("\0")
    handle_error(nil, :null_byte, "string parameter contains null byte", true)
  end
end

def convert!(keys=nil, opts=OPTS)

conversions below
:symbolize :: Convert any string keys in the resulting hash and for any
present in the params.
:skip_missing :: If set to true, does not store values if the key is not
:raise :: If set to false, do not raise errors for missing keys

the options hash. Options:
or nil to convert the current object. If +keys+ is given as a hash, it is used as
including conversions of subkeys. +keys+ should be an array of subkeys to access,
Captures conversions inside the given block, and returns a hash of all conversions,
def convert!(keys=nil, opts=OPTS)
  if keys.is_a?(Hash)
    opts = keys
    keys = nil
  end
  _capture!(:nested_params, opts) do
    if sub = subkey(Array(keys).dup, opts.fetch(:raise, true))
      yield sub
    end
  end
end

def convert_each!(opts=OPTS, &block)

to use.
calls the value with the current object, which should return the array of keys
:keys :: The keys to extract from the object. If a proc or method,

Supports options given to #convert!, and this additional option:
array of keys to use.
or a Method, calls the proc/method with the current object, which should return an
no skipped keys), runs conversions for all entries in the hash. If :keys option is a Proc
option is not given and the object is a Hash with string keys '0', '1', ..., 'N' (with
and the object is an array, runs conversions for all entries in the array. If the :keys
Runs conversions similar to convert! for each key specified by the :keys option. If :keys option is not given
def convert_each!(opts=OPTS, &block)
  np = !@capture
  _capture!(nil, opts) do
    case keys = opts[:keys]
    when nil
      keys = (0...@obj.length)
      valid = if @obj.is_a?(Array)
        true
      else
        keys = keys.map(&:to_s)
        keys.all?{|k| @obj.has_key?(k)}
      end
      unless valid
        handle_error(nil, :invalid_type, "convert_each! called on object not an array or hash with keys '0'..'N'")
        next 
      end
    when Array
      # nothing to do
    when Proc, Method
      keys = keys.call(@obj)
    else
      raise ProgrammerError, "unsupported convert_each! :keys option: #{keys.inspect}"
    end
    keys.map do |i|
      begin
        if v = subkey([i], opts.fetch(:raise, true))
          yield v
          v.nested_params if np 
        end
      rescue => e
        handle_error(i, :invalid_type, e)
      end
    end
  end
end

def dig(type, *nest, key)

tp.dig(:array, :pos_int, 'foo', 'bar', 'baz') # tp['foo']['bar'].array(:pos_int, 'baz')

the first argument and providing the type in the second argument:
You can use +dig+ to get access to nested arrays by using :array or :array! as

in an object that is not an array or hash, then raises an Error.
Returns nil if any of the values are not present or not the expected type. If the nest path results

tp.dig(:pos_int, 'foo', 'bar', 'baz') # tp['foo']['bar'].pos_int('baz')
tp.dig(:pos_int, 'foo', 'bar') # tp['foo'].pos_int('bar')
tp.dig(:pos_int, 'foo') # tp.pos_int('foo')

+key+ on that object using +type+:
Convert values nested under the current obj. Traverses the current object using +nest+, then converts
def dig(type, *nest, key)
  _dig(false, type, nest, key)
end

def dig!(type, *nest, key)

Similar to +dig+, but raises an Error instead of returning +nil+ if no value is found.
def dig!(type, *nest, key)
  _dig(true, type, nest, key)
end

def fetch(key)

calls the block to return the value, or returns nil if there is no block given.
Return the nested value for key. If there is no nested_value for +key+,
def fetch(key)
  send(:[], key){return(yield if defined?(yield))}
end

def handle_error(key, reason, e, do_raise=false)

converts ::ArgumentError instances to Error instances, and reraises other exceptions.
Handle any conversion errors. By default, reraises Error instances with the keys set,
def handle_error(key, reason, e, do_raise=false)
  case e
  when String
    handle_error(key, reason, Error.new(e), do_raise)
  when Error, ArgumentError
    if @capture && (le = @capture.last) && le == e
      raise e if do_raise
      return
    end
    e = Error.create(keys(key), reason, e)
    if @capture
      @capture << e
      raise e if do_raise
      nil
    else
      raise e
    end
  else
    raise e
  end
end

def initialize(obj)

the passed object.
Set the object used for converting. Conversion methods will convert members of
def initialize(obj)
  case @obj = obj
  when Hash, Array
    # nothing
  else
    if @nesting
      handle_error(nil, (@obj.nil? ? :missing : :invalid_type), "value of #{param_name(nil)} parameter not an array or hash: #{obj.inspect}", true)
    else
      handle_error(nil, :invalid_type, "parameters given not an array or hash: #{obj.inspect}", true)
    end
  end
end

def keys(key)

Designed for use in setting the +keys+ values in raised exceptions.
If +key+ is not +nil+, add it to the given nesting. Otherwise, just return the given nesting.
def keys(key)
  Array(@nesting) + Array(key)
end

def nested_params

Recursively descendent into all known subkeys and get the converted params from each.
def nested_params
  return @nested_params if @nested_params
  params = @params
  if @subs
    @subs.each do |key, v|
      if key.is_a?(String) && symbolize?
        key = key.to_sym
      end
      params[key] = v.nested_params
    end
  end
  
  params
end

def param_name(key)

Format a reasonable parameter name value, for use in exception messages.
def param_name(key)
  first, *rest = keys(key)
  if first
    v = first.dup
    rest.each do |param|
      v << "[#{param}]"
    end
    v
  end
end

def param_value(key)

Get the value for the given key in the object.
def param_value(key)
  @obj[key]
end

def parse!(klass, v)

the given +klass+.
and only String values should be converted by calling +parse+ on
Helper for conversion methods where '' should be considered nil,
def parse!(klass, v)
  case v
  when ''
    nil
  when String
    _string_parse!(klass, v)
  else
    raise Error, "unexpected value received: #{v.inspect}"
  end
end

def present?(key)

If key is a String Return whether the key is present in the object,
def present?(key)
  case key
  when String
    !any(key).nil?
  when Array
    key.all? do |k|
      raise ProgrammerError, "non-String element in array argument passed to present?: #{k.inspect}" unless k.is_a?(String)
      !any(k).nil?
    end
  else
    raise ProgrammerError, "unexpected argument passed to present?: #{key.inspect}"
  end
end

def process(meth, key, default, max_input_bytesize=nil)

If the value either before or after conversion is nil, return the +default+ value.
Get the value of +key+ for the object, and convert it to the expected type using +meth+.
def process(meth, key, default, max_input_bytesize=nil)
  v = param_value(key)
  unless v.nil?
    check_allowed_bytesize(v, max_input_bytesize)
    check_null_byte(v)
    v = send(meth, v)
  end
  if v.nil?
    if default == CHECK_NIL
      handle_error(key, :missing, "missing parameter for #{param_name(key)}")
    end
    default
  else
    v
  end
rescue => e
  handle_error(key, meth.to_s.sub(/\A_?convert_/, '').to_sym, e)
end

def process_arg(meth, key, default, max_input_bytesize=nil)

value. If +key+ is an array, return an array with the conversion done for each respective member of +key+.
If +key+ is not an array, convert the value at the given +key+ using the +meth+ method and +default+
def process_arg(meth, key, default, max_input_bytesize=nil)
  case key
  when String
    v = process(meth, key, default, max_input_bytesize)
    if @capture
      key = key.to_sym if symbolize?
      if !@skip_missing || @obj.has_key?(key)
        @params[key] = v
      end
    end
    v
  when Array
    key.map do |k|
      raise ProgrammerError, "non-String element in array argument passed to typecast_params: #{k.inspect}" unless k.is_a?(String)
      process_arg(meth, k, default, max_input_bytesize)
    end
  else
    raise ProgrammerError, "Unsupported argument for typecast_params conversion method: #{key.inspect}"
  end
end

def require_hash!

entries if the current object is an array.
Issue an error unless the current object is a hash. Used to ensure we don't try to access
def require_hash!
  @obj.is_a?(Hash) || handle_error(nil, :invalid_type, "expected hash object in #{param_name(nil)} but received array object")
end

def string_or_numeric!(v)

and only String or Numeric values should be converted.
Helper for conversion methods where '' should be considered nil,
def string_or_numeric!(v)
  case v
  when ''
    nil
  when String, Numeric
    true
  else
    raise Error, "unexpected value received: #{v.inspect}"
  end
end

def sub_capture(capture, symbolize, skip_missing)

Inherit given capturing and symbolize setting from parent object.
def sub_capture(capture, symbolize, skip_missing)
  if @capture = capture
    @symbolize = symbolize
    @skip_missing = skip_missing
    @params = @obj.class.new
  end
end

def subkey(keys, do_raise)

Recursive method to get subkeys.
def subkey(keys, do_raise)
  unless key = keys.shift
    return self
  end
  reason = :invalid_type
  case key
  when String
    unless @obj.is_a?(Hash)
      raise Error, "parameter #{param_name(nil)} is not a hash" if do_raise
      return
    end
    present = !@obj[key].nil?
  when Integer
    unless @obj.is_a?(Array)
      raise Error, "parameter #{param_name(nil)} is not an array" if do_raise
      return
    end
    present = key < @obj.length
  else
    raise ProgrammerError, "invalid argument used to traverse parameters: #{key.inspect}"
  end
  unless present
    reason = :missing
    raise Error, "parameter #{param_name(key)} is not present" if do_raise
    return
  end
  self[key].subkey(keys, do_raise)
rescue => e
  handle_error(key, reason, e)
end