The typecast_params plugin allows for type conversion of submitted parameters. Submitted parameters should be considered untrusted input, and in standard use with browsers, parameters are # submitted as strings (or a hash/array containing strings). In most # cases it makes sense to explicitly convert the parameter to the desired type. While this can be done via manual conversion:
val = request.params['key'].to_i val = nil unless val > 0
the typecast_params plugin adds a friendlier interface:
val = typecast_params.pos_int('key')
As typecast_params
is a fairly long method name, and may be a method you call frequently, you may want to consider aliasing it to something more terse in your application, such as tp
.
typecast_params offers support for default values:
val = typecast_params.pos_int('key', 1)
The default value is only used if no value has been submitted for the parameter, or if the conversion of the value results in nil
. Handling defaults for parameter conversion manually is more difficult, since the parameter may not be present at all, or it may be present but an empty string because the user did not enter a value on the related form. Use of typecast_params for the conversion handles both cases.
In many cases, parameters should be required, and if they aren’t submitted, that should be considered an error. typecast_params handles this with ! methods:
val = typecast_params.pos_int!('key')
These ! methods raise an error instead of returning nil
, and do not allow defaults.
The errors raised by this plugin use a specific exception class, Roda::RodaPlugins::TypecastParams::Error
. This allows you to handle this specific exception class globally and return an appropriate 4xx response to the client. You can use the Error#param_name
and Error#reason
methods to get more information about the error.
To make it easy to handle cases where many parameters need the same conversion done, you can pass an array of keys to a conversion method, and it will return an array of converted values:
val1, val2 = typecast_params.pos_int(['key1', 'key2'])
This is equivalent to:
val1 = typecast_params.pos_int('key1') val2 = typecast_params.pos_int('key2')
The ! methods also support arrays, ensuring that all parameters have a value:
val1, val2 = typecast_params.pos_int!(['key1', 'key2'])
For handling of array parameters, where all entries in the array use the same conversion, there is an array
method which takes the type as the first argument and the keys to convert as the second argument:
vals = typecast_params.array(:pos_int, 'keys')
If you want to ensure that all entries in the array are converted successfully and that there is a value for the array itself, you can use array!
:
vals = typecast_params.array!(:pos_int, 'keys')
This will raise an exception if any of the values in the array for parameter keys
cannot be converted to integer.
Both array
and array!
support default values which are used if no value is present for the parameter:
vals1 = typecast_params.array(:pos_int, 'keys1', []) vals2 = typecast_params.array!(:pos_int, 'keys2', [])
You can also pass an array of keys to array
or array!
, if you would like to perform the same conversion on multiple arrays:
foo_ids, bar_ids = typecast_params.array!(:pos_int, ['foo_ids', 'bar_ids'])
The previous examples have shown use of the pos_int
method, which uses to_i
to convert the value to an integer, but returns nil
if the resulting integer is not positive. Unless you need to handle negative numbers, it is recommended to use pos_int
instead of int
as int
will convert invalid values to 0 (since that is how String#to_i
works).
There are many built in methods for type conversion:
any |
Returns the value as is without conversion | ||||||
str |
Raises if value is not already a string | ||||||
nonempty_str |
Raises if value is not already a string, and converts the empty string or string containing only whitespace to | ||||||
bool |
Converts entry to boolean if in one of the recognized formats:
If not in one of those formats, raises an error. | ||||||
int |
Converts value to integer using | ||||||
pos_int |
Converts value using | ||||||
Integer |
Converts value to integer using | ||||||
float |
Converts value to float using | ||||||
Float |
Converts value to float using | ||||||
Hash |
Raises if value is not already a hash | ||||||
date |
Converts value to Date using | ||||||
time |
Converts value to Time using | ||||||
datetime |
Converts value to DateTime using | ||||||
file |
Raises if value is not already a hash with a :tempfile key whose value responds to |
All of these methods also support ! methods (e.g. pos_int!
), and all of them can be used in the array
and array!
methods to support arrays of values.
Since parameter hashes can be nested, the []
method can be used to access nested hashes:
# params: {'key'=>{'sub_key'=>'1'}} typecast_params['key'].pos_int!('sub_key') # => 1
This works to an arbitrary depth:
# params: {'key'=>{'sub_key'=>{'sub_sub_key'=>'1'}}} typecast_params['key']['sub_key'].pos_int!('sub_sub_key') # => 1
And also works with arrays at any depth, if those arrays contain hashes:
# params: {'key'=>[{'sub_key'=>{'sub_sub_key'=>'1'}}]} typecast_params['key'][0]['sub_key'].pos_int!('sub_sub_key') # => 1 # params: {'key'=>[{'sub_key'=>['1']}]} typecast_params['key'][0].array!(:pos_int, 'sub_key') # => [1]
To allow easier access to nested data, there is a dig
method:
typecast_params.dig(:pos_int, 'key', 'sub_key') typecast_params.dig(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key')
dig
will return nil
if any access while looking up the nested value returns nil
. There is also a dig!
method, which will raise an Error
if dig
would return nil
:
typecast_params.dig!(:pos_int, 'key', 'sub_key') typecast_params.dig!(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key')
Note that none of these conversion methods modify request.params
. They purely do the conversion and return the converted value. However, in some cases it is useful to do all the conversion up front, and then pass a hash of converted parameters to an internal method that expects to receive values in specific types. The convert!
method does this, and there is also a convert_each!
method designed for converting multiple values using the same block:
converted_params = typecast_params.convert! do |tp| tp.int('page') tp.pos_int!('artist_id') tp.array!(:pos_int, 'album_ids') tp.convert!('sales') do |stp| stp.pos_int!(['num_sold', 'num_shipped']) end tp.convert!('members') do |mtp| mtp.convert_each! do |stp| stp.str!(['first_name', 'last_name']) end end end # converted_params: # { # 'page' => 1, # 'artist_id' => 2, # 'album_ids' => [3, 4], # 'sales' => { # 'num_sold' => 5, # 'num_shipped' => 6 # }, # 'members' => [ # {'first_name' => 'Foo', 'last_name' => 'Bar'}, # {'first_name' => 'Baz', 'last_name' => 'Quux'} # ] # }
convert!
and convert_each!
only return values you explicitly specify for conversion inside the passed block.
You can specify the :symbolize
option to convert!
or convert_each!
, which will symbolize the resulting hash keys:
converted_params = typecast_params.convert!(symbolize: true) do |tp| tp.int('page') tp.pos_int!('artist_id') tp.array!(:pos_int, 'album_ids') tp.convert!('sales') do |stp| stp.pos_int!(['num_sold', 'num_shipped']) end tp.convert!('members') do |mtp| mtp.convert_each! do |stp| stp.str!(['first_name', 'last_name']) end end end # converted_params: # { # :page => 1, # :artist_id => 2, # :album_ids => [3, 4], # :sales => { # :num_sold => 5, # :num_shipped => 6 # }, # :members => [ # {:first_name => 'Foo', :last_name => 'Bar'}, # {:first_name => 'Baz', :last_name => 'Quux'} # ] # }
Using the :symbolize
option makes it simpler to transition from untrusted external data (string keys), to semitrusted data that can be used internally (trusted in the sense that the expected types are used, not that you trust the values).
Note that if there are multiple conversion errors raised inside a convert!
or convert_each!
block, they are recorded and a single TypecastParams::Error
instance is raised after processing the block. TypecastParams::Error#param_names
can be called on the exception to get an array of all parameter names with conversion issues, and TypecastParams::Error#all_errors
can be used to get an array of all Error
instances.
Because of how convert!
and convert_each!
work, you should avoid calling TypecastParams::Params#[]
inside the block you pass to these methods, because if the [] call fails, it will skip the reminder of the block.
Be aware that when you use convert!
and convert_each!
, the conversion methods called inside the block may return nil if there is a error raised, and nested calls to convert!
and convert_each!
may not return values.
When loading the typecast_params plugin, a subclass of TypecastParams::Params
is created specific to the Roda
application. You can add support for custom types by passing a block when loading the typecast_params plugin. This block is executed in the context of the subclass, and calling handle_type
in the block can be used to add conversion methods. handle_type
accepts a type name, an options hash, and the block used to convert the type. The only currently supported option is :max_input_bytesize
, specifying the maximum bytesize of string input. You can also override the max input bytesize of an existing type using the max_input_bytesize
method.
plugin :typecast_params do handle_type(:album, max_input_bytesize: 100) do |value| if id = convert_pos_int(val) Album[id] end end max_input_bytesize(:date, 256) end
By default, the typecast_params conversion procs are passed the parameter value directly from request.params
without modification. In some cases, it may be beneficial to strip leading and trailing whitespace from parameter string values before processing, which you can do by passing the strip: :all
option when loading the plugin.
By default, the typecasting methods for some types check whether the bytesize of input strings is over the maximum expected values, and raise an error in such cases. The input bytesize is checked prior to any type conversion. If you would like to skip this check and allow any bytesize when doing type conversion for param string values, you can do so by passing the # :skip_bytesize_checking
option when loading the plugin. By default, there is an 100 byte limit on integer input, an 1000 byte input on float input, and a 128 byte limit on date/time input.
By default, the typecasting methods check whether input strings have null bytes, and raise an error in such cases. This check for null bytes occurs prior to any type conversion. If you would like to skip this check and allow null bytes in param string values, you can do so by passing the :allow_null_bytes
option when loading the plugin.
You can use the :date_parse_input_handler option to specify custom handling of date parsing input. Modern versions of Ruby and the date gem internally raise if the input to date parsing methods is too large to prevent denial of service. If you are using an older version of Ruby, you can use this option to enforce the same check:
plugin :typecast_params, date_parse_input_handler: proc {|string| raise ArgumentError, "too big" if string.bytesize > 128 string }
You can also use this option to modify the input, such as truncating it to the first 128 bytes:
plugin :typecast_params, date_parse_input_handler: proc {|string| string.b[0, 128] }
The date_parse_input_handler
is only called if the value is under the max input bytesize, so you may need to call max_input_bytesize
for the :date
, :time
, and :datetime
methods to override the max input bytesize if you want to use this option for input strings over 128 bytes.
By design, typecast_params only deals with string keys, it is not possible to use symbol keys as arguments to the conversion methods and have them converted.
Included modules
Classes and Modules
- Roda::RodaPlugins::TypecastParams::AllowNullByte
- Roda::RodaPlugins::TypecastParams::ClassMethods
- Roda::RodaPlugins::TypecastParams::DateParseInputHandler
- Roda::RodaPlugins::TypecastParams::InstanceMethods
- Roda::RodaPlugins::TypecastParams::SkipBytesizeChecking
- Roda::RodaPlugins::TypecastParams::StringStripper
- Roda::RodaPlugins::TypecastParams::Error
- Roda::RodaPlugins::TypecastParams::Params
- Roda::RodaPlugins::TypecastParams::ProgrammerError
Constants
CHECK_NIL | = | Object.new.freeze |
Sentinal value for whether to raise exception during process |
Public Class methods
Set application-specific Params
subclass unless one has been set, and if a block is passed, eval it in the context of the subclass. Respect the strip: :all
to strip all parameter strings before processing them.
# File lib/roda/plugins/typecast_params.rb 1091 def self.configure(app, opts=OPTS, &block) 1092 app.const_set(:TypecastParams, Class.new(RodaPlugins::TypecastParams::Params)) unless app.const_defined?(:TypecastParams) 1093 app::TypecastParams.class_eval(&block) if block 1094 if opts[:strip] == :all 1095 app::TypecastParams.send(:include, StringStripper) 1096 end 1097 if opts[:allow_null_bytes] 1098 app::TypecastParams.send(:include, AllowNullByte) 1099 end 1100 if opts[:skip_bytesize_checking] 1101 app::TypecastParams.send(:include, SkipBytesizeChecking) 1102 end 1103 if opts[:date_parse_input_handler] 1104 app::TypecastParams.class_eval do 1105 include DateParseInputHandler 1106 define_method(:handle_date_parse_input, &opts[:date_parse_input_handler]) 1107 private :handle_date_parse_input 1108 alias handle_date_parse_input handle_date_parse_input 1109 end 1110 end 1111 end