Rasti::Types
Type casting library for Ruby. Provides a set of built-in types with casting, validation and transformation via a unified .cast API.
Table of Contents
- Installation
- Usage
- Core Concepts
- Built-in Types
- String
- String (Formatted)
- Integer
- Float
- Boolean
- Symbol
- Time
- UUID
- Regexp
- IO
- Enum
- TypedEnum
- Array
- Hash
- Model
- Errors
- MultiCaster
- Custom / Pluggable Types
- Development
- Contributing
- License
Installation
Add this line to your application's Gemfile:
gem 'rasti-types'And then execute:
$ bundle
Or install it yourself as:
$ gem install rasti-types
Usage
require 'rasti-types'
T = Rasti::TypesThen call .cast(value) on any type:
T::Integer.cast '10' # => 10
T::Boolean.cast 'TRUE' # => true
T::Symbol.cast 'hello' # => :helloFor parameterized types, use [] to specify parameters before casting:
T::Time['%Y-%m-%d'].cast '2016-10-22' # => 2016-10-22 00:00:00 -0300
T::Array[T::Integer].cast ['1', '2', '3'] # => [1, 2, 3]
T::Hash[T::Symbol, T::Integer].cast('a' => '1') # => { a: 1 }Core Concepts
Castable
All types include the Castable module, which provides the unified .cast method:
cast(value)- Returns
nilifvalueisnil(nil passes through for all types). - Validates the value against the type rules.
- Transforms the value to the target type if valid.
- Raises
Rasti::Types::CastErrorif the value cannot be cast.
Simple vs Parameterized Types
Simple types (e.g., Integer, String, Boolean) expose cast as a class method:
Rasti::Types::Integer.cast '42' # => 42Parameterized types (e.g., Time, Array, Hash, Enum, Model) require parameters via [] and return an instance that responds to cast:
type = Rasti::Types::Array[Rasti::Types::String] # => instance
type.cast [1, 2, 3] # => ["1", "2", "3"]Built-in Types
String
Converts any value to its string representation via #to_s.
T::String.cast 'text' # => "text"
T::String.cast :symbol # => "symbol"
T::String.cast 100 # => "100"
T::String.cast true # => "true"
T::String.cast Time.now # => "2024-01-15 10:00:00 -0300"
T::String.cast [1, 2] # => "[1, 2]"
T::String.cast nil # => nilString (Formatted)
Restricts string values to match a given Regexp pattern. Created with String[regexp].
Converts the value via #to_s and validates it matches the pattern.
email_type = T::String[/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i]
email_type.cast 'user@mail.com' # => "user@mail.com"
email_type.cast :user_name # => CastError (does not match email pattern)
email_type.cast 'not-an-email' # => CastErrorInteger
Casts values to Integer via #to_i. Accepts strings with numeric content, Time objects, and non-string values that respond to #to_i.
Strings must not be purely alphabetic. The value is valid if it's a numeric-looking string or a non-string that responds to #to_i.
T::Integer.cast '10' # => 10
T::Integer.cast '10.5' # => 10
T::Integer.cast 2.1 # => 2
T::Integer.cast Time.now # => 1705312800 (integer representation)
T::Integer.cast 'text' # => CastError
T::Integer.cast :symbol # => CastError
T::Integer.cast nil # => nilFloat
Casts values to Float via #to_f. String values must match the format /^(\d+\.)?\d+$/ (numeric digits with optional decimal part).
Invalid string formats include "1.", ".2", ".", and non-numeric strings.
T::Float.cast '10' # => 10.0
T::Float.cast '12.5' # => 12.5
T::Float.cast 2.0 # => 2.0
T::Float.cast Time.now # => 1705312800.123
T::Float.cast 'text' # => CastError
T::Float.cast '.2' # => CastError
T::Float.cast '1.' # => CastError
T::Float.cast nil # => nilBoolean
Accepts true, false, and their string representations (case-insensitive).
Valid true values: true, "true", "True", "TRUE", "T", "t", "tr", "tru" (matches /^t(rue)?$/i).
Valid false values: false, "false", "False", "FALSE", "F", "f", "fa", "fal", "fals" (matches /^f(alse)?$/i).
T::Boolean.cast true # => true
T::Boolean.cast 'TRUE' # => true
T::Boolean.cast 'T' # => true
T::Boolean.cast false # => false
T::Boolean.cast 'FALSE' # => false
T::Boolean.cast 'F' # => false
T::Boolean.cast 'text' # => CastError
T::Boolean.cast 123 # => CastError
T::Boolean.cast :true # => CastError
T::Boolean.cast nil # => nilSymbol
Converts any value to a Symbol. Uses #to_sym if available, otherwise falls back to #to_s.to_sym.
T::Symbol.cast 'text' # => :text
T::Symbol.cast :existing # => :existing
T::Symbol.cast 100 # => :"100"
T::Symbol.cast true # => :true
T::Symbol.cast nil # => nilTime
Parameterized type that parses time strings using Time.strptime with a given format. Also accepts objects that respond to #to_time.
T::Time['%Y-%m-%d'].cast '2016-10-22' # => 2016-10-22 00:00:00 -0300
T::Time['%d/%m/%y'].cast '18/08/16' # => 2016-08-18 00:00:00 -0300
time = Time.new 2016, 8, 18
T::Time['%F %T %z'].cast time # => 2016-08-18 00:00:00 -0300 (passes through)
T::Time['%Y-%m-%d'].cast '2016-10' # => CastError (format mismatch)
T::Time['%Y-%m-%d'].cast 'text' # => CastError
T::Time['%Y-%m-%d'].cast nil # => nilUUID
A predefined String type restricted to UUID format (RFC 4122). Defined as:
Rasti::Types::UUID = Rasti::Types::String[/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/]T::UUID.cast 'f09b7716-81a9-11e6-a549-bb8f165bcf02' # => "f09b7716-81a9-11e6-a549-bb8f165bcf02"
T::UUID.cast 'not-a-uuid' # => CastError
T::UUID.cast nil # => nilRegexp
Accepts Regexp objects or strings with valid regular expressions. Returns the source of the compiled regexp.
T::Regexp.cast '[a-z]' # => "[a-z]"
T::Regexp.cast /[a-z]/ # => "[a-z]"
T::Regexp.cast '[a-z' # => CastError (invalid regexp)
T::Regexp.cast 5 # => CastError
T::Regexp.cast nil # => nilIO
Validates that the value implements both #read and #write. Returns the value unchanged (no transformation).
T::IO.cast StringIO.new('data') # => #<StringIO:...>
T::IO.cast File.new('test.txt') # => #<File:test.txt>
T::IO.cast 'not-io' # => CastError
T::IO.cast 123 # => CastError
T::IO.cast nil # => nilEnum
Parameterized type that restricts values to a predefined set. Values are compared as strings.
status = T::Enum[:active, :inactive, :pending]
status.cast :active # => "active"
status.cast 'inactive' # => "inactive"
status.cast 'pending' # => "pending"
status.cast 'unknown' # => CastError
status.cast nil # => nil
status.values # => ["active", "inactive", "pending"]Accessor methods: Each enum value gets an underscored getter method:
enum = T::Enum[:first_value, 'SecondValue', 'THIRD_VALUE']
enum.first_value # => "first_value"
enum.second_value # => "SecondValue"
enum.third_value # => "THIRD_VALUE"TypedEnum
Like Enum, but wraps a Rasti::Enum module. Casts values to the corresponding typed enum value (instances of Rasti::Enum::Value subclasses), not plain strings.
module Colors
extend Rasti::Enum
class Red < Rasti::Enum::Value; end
class Green < Rasti::Enum::Value; end
class Blue < Rasti::Enum::Value; end
end
color = T::TypedEnum[Colors]
color.cast 'RED' # => Colors::Red.new
color.cast 'BLUE' # => Colors::Blue.new
color.cast 'GREEN' # => Colors::Green.new
color.cast 'YELLOW' # => CastError
color.cast nil # => nil
color.values # => [Colors::Red.new, Colors::Green.new, Colors::Blue.new]
# Accessor methods
color.red # => Colors::Red.new
color.green # => Colors::Green.new
color.blue # => Colors::Blue.newArray
Parameterized type. Casts each element of an array to the given element type. Reports all invalid elements (not just the first).
T::Array[T::Integer].cast ['1', '2', '3'] # => [1, 2, 3]
T::Array[T::Symbol].cast [1, 'test', :sym] # => [:"1", :test, :sym]
# Elements within the array can be nil
T::Array[T::String].cast [nil, 'a', 'b'] # => [nil, "a", "b"]
# Non-array values raise CastError
T::Array[T::String].cast 'not-array' # => CastError
T::Array[T::Integer].cast nil # => nilMulti-cast errors: When multiple elements fail to cast, a MultiCastError is raised with all errors indexed by position (1-based):
begin
T::Array[T::Integer].cast [1, 2, 'a', 3, 'c', 4]
rescue Rasti::Types::MultiCastError => ex
ex.errors
# => { 3 => ["Invalid cast: 'a' -> Rasti::Types::Integer"],
# 5 => ["Invalid cast: 'c' -> Rasti::Types::Integer"] }
endHash
Parameterized type with both key and value types. Casts all keys and values of a hash.
T::Hash[T::Symbol, T::Integer].cast('a' => '123') # => { a: 123 }
T::Hash[T::Integer, T::String].cast('1' => :abc) # => { 1 => "abc" }
# nil keys/values within the hash are allowed
T::Hash[T::Integer, T::String].cast(nil => nil) # => { nil => nil }
# Non-hash values raise CastError
T::Hash[T::Symbol, T::Integer].cast 'text' # => CastError
T::Hash[T::Symbol, T::Integer].cast nil # => nilMulti-cast errors: When multiple keys or values fail, a MultiCastError reports all failures:
begin
T::Hash[T::Integer, T::Integer].cast(true => 1, 2 => false, false => true)
rescue Rasti::Types::MultiCastError => ex
ex.errors
# => { true => ["Invalid cast: true -> Rasti::Types::Integer"],
# 2 => ["Invalid cast: false -> Rasti::Types::Integer"],
# false => ["Invalid cast: false -> Rasti::Types::Integer",
# "Invalid cast: true -> Rasti::Types::Integer"] }
endModel
Parameterized type that builds model instances from hashes, or validates existing model instances.
The model class must accept a hash in its constructor and raise CompoundError if attributes are invalid.
class Point
attr_reader :x, :y
def initialize(attributes = {})
errors = {}
if attributes.key? :x
@x = attributes[:x]
else
errors[:x] = ['not present']
end
if attributes.key? :y
@y = attributes[:y]
else
errors[:y] = ['not present']
end
raise Rasti::Types::CompoundError, errors if errors.any?
end
end
# From Hash — creates model instance
point = T::Model[Point].cast(x: 1, y: 2)
point.class # => Point
point.x # => 1
point.y # => 2
# From existing model — returns as-is
existing = Point.new(x: 3, y: 4)
T::Model[Point].cast(existing).equal?(existing) # => true
# Invalid hash — raises CompoundError with all attribute errors
T::Model[Point].cast(z: 'text')
# => Rasti::Types::CompoundError: Errors:
# - x: ["not present"]
# - y: ["not present"]
# Invalid value type — raises CastError
T::Model[Point].cast 'text' # => CastError
T::Model[Point].cast nil # => nilWith Arrays/Hashes: MultiCastError aggregates errors from nested model casts with dotted keys:
begin
T::Array[T::Model[Point]].cast([
{ x: 1, y: 2 },
{ y: 2 }, # missing x
{ x: 1 }, # missing y
{ x: 3, y: 4 }
])
rescue Rasti::Types::MultiCastError => ex
ex.errors
# => { "2.x" => ["not present"],
# "3.y" => ["not present"] }
endErrors
CastError
Raised when a single value cannot be cast to a type:
Rasti::Types::CastError < StandardErrorAttributes:
-
type— the type that rejected the value -
value— the original value
begin
T::Integer.cast 'hello'
rescue Rasti::Types::CastError => ex
ex.type # => Rasti::Types::Integer
ex.value # => "hello"
ex.message # => "Invalid cast: 'hello' -> Rasti::Types::Integer"
endCompoundError
Raised by model constructors when multiple attributes are invalid. Not a casting error per se — it's raised by user-defined model #initialize methods and propagated through the cast pipeline.
Rasti::Types::CompoundError < StandardErrorAttributes:
-
errors— a hash of{ attribute => [messages] }
begin
T::Model[Point].cast(z: 'text')
rescue Rasti::Types::CompoundError => ex
ex.errors # => { x: ["not present"], y: ["not present"] }
endMultiCastError
Raised when casting an Array or Hash and multiple elements fail. Inherits from CompoundError.
Rasti::Types::MultiCastError < Rasti::Types::CompoundErrorAttributes:
-
type— the array/hash type -
value— the original value -
errors— hash mapping element positions (for arrays) or keys (for hashes) to error messages
begin
T::Array[T::Integer].cast [1, 'a', 2, 'b']
rescue Rasti::Types::MultiCastError => ex
ex.type # => Rasti::Types::Array[Rasti::Types::Integer]
ex.errors # => { 2 => ["Invalid cast: 'a' -> Rasti::Types::Integer"],
# 4 => ["Invalid cast: 'b' -> Rasti::Types::Integer"] }
endMultiCaster
Utility for performing multiple casts and aggregating errors. Used internally by Array and Hash types, but also available for custom use:
Rasti::Types::MultiCaster.cast!(type, value) do |multi_caster|
result_a = multi_caster.cast type: T::Integer, value: '10', error_key: :field_a
result_b = multi_caster.cast type: T::String, value: 'hi', error_key: :field_b
[result_a, result_b]
end
# => [10, "hi"]If any cast fails, a MultiCastError is raised with all errors collected:
Rasti::Types::MultiCaster.cast!(T::Integer, [1, 'a']) do |mc|
mc.cast type: T::Integer, value: 'a', error_key: :x
mc.cast type: T::String, value: 123, error_key: :y # This won't fail since String accepts anything
mc.cast type: T::Boolean, value: 'xyz', error_key: :z
end
# => raises MultiCastError with errors: { x: [...], z: [...] }Custom / Pluggable Types
Create custom types by building a class that includes Castable. Implement two private methods:
-
valid?(value)— returnstrueif the value can be cast. -
transform(value)— returns the transformed value.
Simple Custom Type
class UpcaseString
class << self
include Rasti::Types::Castable
private
def valid?(value)
value.is_a?(String)
end
def transform(value)
value.upcase
end
end
end
UpcaseString.cast 'hello' # => "HELLO"
UpcaseString.cast 123 # => CastError
UpcaseString.cast nil # => nilParameterized Custom Type
For types that require parameters, use an instance-based approach (similar to Time, Enum, Array):
class RangeType
include Rasti::Types::Castable
def self.[](min, max)
new(min, max)
end
def initialize(min, max)
@min = min
@max = max
end
private
def valid?(value)
value.respond_to?(:to_i) && value.to_i >= @min && value.to_i <= @max
end
def transform(value)
value.to_i
end
end
range = RangeType[1, 10]
range.cast '5' # => 5
range.cast '20' # => CastErrorDevelopment
After checking out the repo, run:
$ bundle install
To run the full test suite:
$ rake spec
To run tests for a specific directory or file:
$ rake spec DIR=spec
$ rake spec TEST=spec/integer_spec.rb
To start an interactive console with rasti-types loaded:
$ rake console
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/gabynaiman/rasti-types.
License
The gem is available as open source under the terms of the MIT License.