0.0
No release in over 3 years
Low commit activity in last 3 years
Type casting
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 0.8
~> 5.0, < 5.11
~> 0.2
~> 12.0
~> 0.12

Runtime

 Project Readme

Rasti::Types

Gem Version Build Status Coverage Status

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::Types

Then call .cast(value) on any type:

T::Integer.cast '10'     # => 10
T::Boolean.cast 'TRUE'   # => true
T::Symbol.cast 'hello'   # => :hello

For 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 nil if value is nil (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::CastError if 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'   # => 42

Parameterized 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        # => nil

String (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'    # => CastError

Integer

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       # => nil

Float

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       # => nil

Boolean

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       # => nil

Symbol

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        # => nil

Time

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                   # => nil

UUID

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                                      # => nil

Regexp

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        # => nil

IO

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                    # => nil

Enum

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.new

Array

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                   # => nil

Multi-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"] }
end

Hash

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              # => nil

Multi-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"] }
end

Model

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       # => nil

With 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"] }
end

Errors

CastError

Raised when a single value cannot be cast to a type:

Rasti::Types::CastError < StandardError

Attributes:

  • 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"
end

CompoundError

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 < StandardError

Attributes:

  • 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"] }
end

MultiCastError

Raised when casting an Array or Hash and multiple elements fail. Inherits from CompoundError.

Rasti::Types::MultiCastError < Rasti::Types::CompoundError

Attributes:

  • 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"] }
end

MultiCaster

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) — returns true if 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       # => nil

Parameterized 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'   # => CastError

Development

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.