philiprehberger-typed_hash

Tests Gem Version Last updated

Hash with per-key type declarations, coercion, validation, nested schemas, and JSON serialization

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-typed_hash"

Or install directly:

gem install philiprehberger-typed_hash

Usage

require "philiprehberger/typed_hash"

UserSchema = Philiprehberger::TypedHash.define do
  key :name, String
  key :age, Integer
  key :email, String
end

user = UserSchema.new(name: 'Alice', age: 30, email: 'alice@example.com')
user[:name]   # => 'Alice'
user.valid?   # => true

Default Values

schema = Philiprehberger::TypedHash.define do
  key :name, String
  key :role, String, default: 'user'
end

instance = schema.new(name: 'Alice')
instance[:role]  # => 'user'

Optional Keys

schema = Philiprehberger::TypedHash.define do
  key :name, String
  key :nickname, String, optional: true
end

schema.new(name: 'Alice').valid?  # => true

Coercion

schema = Philiprehberger::TypedHash.define do
  key :count, Integer, coerce: ->(v) { Integer(v) }
  key :active, TrueClass, coerce: ->(v) { v == 'true' }
end

instance = schema.new(count: '42', active: 'true')
instance[:count]  # => 42

Strict Mode

schema = Philiprehberger::TypedHash.define(strict: true) do
  key :name, String
end

instance = schema.new(name: 'Alice', extra: 'value')
instance.valid?   # => false
instance.errors   # => ['unknown key: extra']

Nested Schemas

schema = Philiprehberger::TypedHash.define do
  key :name, String
  nested :address do
    key :street, String
    key :city, String
  end
end

user = schema.new(name: 'Alice', address: { street: '123 Main St', city: 'Springfield' })
user[:address][:street]  # => '123 Main St'

Pick and Omit

full = schema.new(name: 'Alice', age: 30, email: 'alice@example.com')
picked = full.pick(:name, :email)   # only :name and :email
omitted = full.omit(:email)         # everything except :email

JSON Serialization

instance = schema.new(name: 'Alice', age: 30)
json = instance.to_json              # => '{"name":"Alice","age":30}'
restored = schema.from_json(json)    # => Instance

Freeze

instance = schema.new(name: 'Alice', age: 30)
instance.freeze
instance[:name] = 'Bob'  # => raises Philiprehberger::TypedHash::FrozenError

Diff

a = schema.new(name: 'Alice', age: 30)
b = schema.new(name: 'Alice', age: 35)
a.diff(b)  # => { age: { old: 30, new: 35 } }

Merging

base = schema.new(name: 'Alice', age: 25)
updated = base.merge(age: 30)
updated[:age]  # => 30

Schema introspection

schema = Philiprehberger::TypedHash.define do
  key :name, String
  nested :address do
    key :street, String
  end
  key :age, Integer
end

schema.keys  # => [:name, :address, :age]

Schema#keys returns the declared top-level key names in definition order. Nested schemas contribute only their parent key — inner fields are not included.

API

TypedHash

Method Description
.define(strict:) { } Define a schema with a block DSL

Schema

Method Description
key :name, Type, opts Declare a typed key with options
nested :name, opts, &block Define a nested typed hash schema
#new(data) Create a typed hash instance
#from_json(str) Deserialize a JSON string into a typed hash instance
#keys Return the declared top-level key names in definition order

Instance

Method Description
#[key] Access a value by key
#[key] = value Set a value by key (raises if frozen)
#valid? Check if the instance passes validation
#errors Return validation error messages
#to_h Convert to a plain hash
#to_json Serialize to a JSON string
#merge(other) Merge with another hash or instance
#pick(*keys) Return new instance with only the specified keys
#omit(*keys) Return new instance without the specified keys
#freeze Make the instance immutable
#frozen? Check if the instance is frozen
#diff(other) Return hash of changed keys with old and new values

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT