class Roda::RodaPlugins::TypecastParams::Params
Class handling conversion of submitted parameters to desired types.
def self.handle_type(type, opts=OPTS, &block)
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)
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)
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)
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)
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)
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)
def _string_parse!(klass, v) klass.parse(v) end
def array(type, key, default=nil)
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)
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)
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)
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)
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)
: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)
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)
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)
def dig!(type, *nest, key) _dig(true, type, nest, key) end
def fetch(key)
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)
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)
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)
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
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)
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)
def param_value(key) @obj[key] end
def parse!(klass, v)
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)
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)
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)
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!
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)
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)
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)
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