Custom Type Creation¶
You can create your own advanced types by extending Ralph::Types::BaseType.
Type System Architecture¶
The type system uses a three-phase transformation pipeline:
- Cast - Convert user input (strings, hashes, arrays) into domain types
- Dump - Serialize domain types into database-compatible formats
- Load - Deserialize database values back into domain types
This approach ensures type safety and enables features like dirty tracking and automatic serialization.
Example: Money Type¶
require "ralph/types/base"
module Ralph
module Types
# Money type that stores cents as integer
class MoneyType < BaseType
def type_symbol : Symbol
:money
end
# Cast external value to cents (Int64)
def cast(value) : Value
case value
when Int32, Int64
value.to_i64
when Float64
(value * 100).to_i64
when String
# Parse "$10.50" -> 1050 cents
if match = value.match(/^\$?(\d+)\.(\d{2})$/)
dollars = match[1].to_i64
cents = match[2].to_i64
(dollars * 100) + cents
else
nil
end
else
nil
end
end
# Dump cents to database
def dump(value) : DB::Any
case value
when Int64, Int32
value.to_i64
else
nil
end
end
# Load cents from database
def load(value : DB::Any) : Value
case value
when Int64, Int32
value.to_i64
else
nil
end
end
# SQL type
def sql_type(dialect : Symbol) : String?
"BIGINT"
end
end
# Factory method
def self.money_type : MoneyType
MoneyType.new
end
end
end
Register Custom Type¶
# Register globally
Ralph::Types::Registry.register(:money, Ralph::Types::MoneyType.new)
# Or register per backend
Ralph::Types::Registry.register_for_backend(
:postgres,
:money,
Ralph::Types::MoneyType.new
)
Use Custom Type in Migration¶
class AddPriceToProducts < Ralph::Migrations::Migration
def up : Nil
add_column :products, :price, :money, default: 0
end
def down : Nil
remove_column :products, :price
end
end
Example: Email Type¶
module Ralph
module Types
# Email type with validation
class EmailType < BaseType
EMAIL_REGEX = /\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
def type_symbol : Symbol
:email
end
def cast(value) : Value
case value
when String
value.strip.downcase if valid_email?(value)
else
nil
end
end
def dump(value) : DB::Any
case value
when String
value
else
nil
end
end
def load(value : DB::Any) : Value
case value
when String
value
else
nil
end
end
def sql_type(dialect : Symbol) : String?
"VARCHAR(255)"
end
# Optional: CHECK constraint for database-level validation
def check_constraint(column_name : String) : String?
# SQLite/PostgreSQL regex check
"\"#{column_name}\" ~ '#{EMAIL_REGEX.source}'"
end
private def valid_email?(email : String) : Bool
!!(email =~ EMAIL_REGEX)
end
end
def self.email_type : EmailType
EmailType.new
end
end
end
Type Registry¶
Check registered types:
# List all registered types
Ralph::Types::Registry.all_types # => [:json, :jsonb, :uuid, :enum, :array, ...]
# Check if type is registered
Ralph::Types::Registry.registered?(:json) # => true
# Lookup type (with optional backend)
Ralph::Types::Registry.lookup(:json) # Global registration
Ralph::Types::Registry.lookup(:uuid, :postgres) # Backend-specific