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_typeOr 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] # => 123Routing 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
typeto transform into - the
defaultvalue when the result isnil -
requiredindicates whether empty values are allowed
strict vs default
The primitive types in SafeType provide default and strict mode, which are
SafeType::BooleanSafeType::DateSafeType::DateTimeSafeType::FloatSafeType::IntegerSafeType::StringSafeType::SymbolSafeType::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::coercereturns 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") # => 1Note 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
initializeto change the default values, types, or add more attributes. - Override
beforeto 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 returntrueorfalse. - Override
afterto update the input after validate. This method should take the input and return it after processing. - Override
handle_exceptionsto change the behavior of exceptions handling (e.g: send to the logger, or no exception) - Override
defaultorstrictto 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.