safe_type
While working with environment variables, routing parameters, network responses, or other Hash-like objects that require parsing, we often need type coercion to assure expected behaviors.
safe_type provides an intuitive type coercion interface and type enhancement.
Installation
We can install safe_type
using gem install
:
gem install safe_type
Or we can add it as a dependency in the Gemfile
and run bundle install
:
gem 'safe_type'
Use Cases
Environment Variables
require 'safe_type/mixin/hash' # symbolize_keys
ENV["DISABLE_TASKS"] = "true"
ENV["API_KEY"] = ""
ENV["BUILD_NUM"] = "123"
SAFE_ENV = SafeType::coerce(
ENV,
{
"DISABLE_TASKS" => SafeType::Boolean.default(false),
"API_KEY" => SafeType::String.default("SECRET"),
"BUILD_NUM" => SafeType::Integer.strict,
}
).symbolize_keys
SAFE_ENV[:DISABLE_TASKS] # => true
SAFE_ENV[:API_KEY] # => SECRET
SAFE_ENV[:BUILD_NUM] # => 123
Routing Parameters
class FallSemesterStartDate < SafeType::Date
# implement `is_valid?` method
end
current_year = Date.today.year
params = {
"course_id" => "101",
"start_date" => "#{current_year}-10-01"
}
rules = {
"course_id" => SafeType::Integer.strict,
"start_date" => FallSemester.strict
}
SafeType::coerce!(params, rules)
params["course_id"] # => 101
params["start_date"] # => <Date: 2018-10-01 ((2458393j,0s,0n),+0s,2299161j)>
Ruby Hashes
json = {
"numbers" => [
["10", "100", "1000"],
["20", "200", "2000"],
["30", "300", "3000"]
],
"info" => [
{
"type" => "dog",
"pets_age" => "45",
"num_of_siblings" => "3"
},
{
"type" => "cat",
"pets_age" => "44",
"num_of_siblings" => "4"
},
{
"type" => "fish",
"pets_age" => "46",
"num_of_siblings" => "5"
}
]
}
SafeType::coerce!(json, {
"numbers" => [
[SafeType::Integer.strict, SafeType::String.strict],
[SafeType::Integer.strict],
[SafeType::String.strict]
],
"info" => [
{
"type" => SafeType::String.strict,
"pets_age" => SafeType::Integer.strict,
}
]
})
# ANSWER
answer = {
"numbers" => [
[10, "100", 1000],
[20, 200, 2000],
["30", "300", "3000"]
],
"info" => [
{
"type" => "dog",
"pets_age" => 45,
"num_of_siblings" => "3"
},
{
"type" => "cat",
"pets_age" => 44,
"num_of_siblings" => "4"
},
{
"type" => "fish",
"pets_age" => 46,
"num_of_siblings" => "5"
}
]
}
Network Responses
class ResponseType; end
class Response < SafeType::Rule
def initialize(type: ResponseType, default: "404")
super
end
def before(uri)
# make request
return ResponseType.new
end
end
Response.coerce("https://API_URI") # => #<ResponseType:0x000056005b3e7518>
Overview
A Rule
describes a single transformation pipeline. It's the core of this gem.
class Rule
def initialize(type:, default: nil, required: false)
The parameters are
- the
type
to transform into - the
default
value when the result isnil
-
required
indicates whether empty values are allowed
strict
vs default
The primitive types in SafeType
provide default
and strict
mode, which are
SafeType::Boolean
SafeType::Date
SafeType::DateTime
SafeType::Float
SafeType::Integer
SafeType::String
SafeType::Symbol
SafeType::Time
Under the hood, they are all just Rule
classes with parameters:
-
default
: a rule with default value specified -
strict
: a rule withrequired: true
, so no empty values are allowed, or it throwsEmptyValueError
Apply the Rules
As we've seen in the use cases, we can call coerce
to apply a set of SafeType::Rule
classes.
Rule
classes can be bundled together as elements in an array or values in a hash.
coerce
vs coerce!
-
SafeType::coerce
returns a new object, corresponding to the rules. The unspecified fields will not be included in the new object. -
SafeType::coerce!
coerces the object in place. The unspecified fields will not be modified. NoteSafeType::coerce!
cannot be used on a simple object, otherwise it will raiseSafeType::InvalidRuleError
.
To apply the rule on a simple object, we can call the coerce
method as well.
SafeType::Integer.default.coerce("1") # => 1
SafeType::Integer.coerce("1") # => 1
Note those two examples are equivalent:
SafeType::coerce(ENV["PORT"], SafeType::Integer.default(3000))
SafeType::Integer.default(3000).coerce(ENV["PORT"])
For the SafeType
primitive types, applying the rule on the class itself will use the default rule.
Customized Types
We can inherit from a SafeType::Rule
to create a customized type.
We can override following methods if needed:
- Override
initialize
to change the default values, types, or add more attributes. - Override
before
to update the input before convert. This method should take the input and return it after processing. - Override
is_valid?
to check the value after convert. This method should take the input and returntrue
orfalse
. - Override
after
to update the input after validate. This method should take the input and return it after processing. - Override
handle_exceptions
to change the behavior of exceptions handling (e.g: send to the logger, or no exception) - Override
default
orstrict
to modify the default and strict rule.
Prior Art
safe_type
pulls heavy inspiration from dry-types and rails_param.
The interface in safe_type
looks similar to the interface in dry-types
, however, safe_type
supports some additional features such as in-place coercion
using the Ruby bang method interface and the ability to define schemas using Ruby hashes and arrays. safe_type
also borrows some concepts from rails_param
,
but with some fundamental differences such as safe_type
not relying on Rails and safe_type
not having a focus on only typing Rails controller parameters.
The goal of safe_type
is to provide a good tradeoff between complexity and flexibility by enabling type checking through a clean and easy-to-use interface.
safe_type
should be useful when working with any string or hash where the values could be ambiguously typed, such as ENV
variables, rails params
, or network responses..
License
The safe_type
project is licensed and available open source under the terms of the MIT license.