Low commit activity in last 3 years
Define data classes with typed fields, default values, validation rules, and pattern matching support. Immutable by default with keyword-only construction, JSON/Hash serialization, and runtime type checking.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-struct_kit

Tests Gem Version Last updated

Enhanced struct builder with typed fields, defaults, validation, and pattern matching

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem 'philiprehberger-struct_kit'

Or install directly:

gem install philiprehberger-struct_kit

Usage

require 'philiprehberger/struct_kit'

User = Philiprehberger::StructKit.define do
  field :name, String
  field :age, Integer, default: 0
  field :role, Symbol, default: :user
  validate :age, range: 0..150
end

user = User.new(name: 'Alice', age: 30)
user.name   # => "Alice"
user.age    # => 30
user.frozen? # => true

Type Checking

Point = Philiprehberger::StructKit.define do
  field :x, Integer
  field :y, Integer
  field :active, [TrueClass, FalseClass], default: true
end

Point.new(x: 1, y: 2)            # OK
Point.new(x: 'a', y: 2)          # TypeError!
Point.new(x: 1, y: 2, active: 0) # TypeError!

Default Values

Config = Philiprehberger::StructKit.define do
  field :timeout, Integer, default: 30
  field :tags, Array, default: -> { [] }  # lambda for mutable defaults
end

Validation

Email = Philiprehberger::StructKit.define do
  field :address, String
  validate :address, format: /@/
end

Mutable Structs

MutableUser = Philiprehberger::StructKit.define(mutable: true) do
  field :name, String
  field :age, Integer, default: 0
end

user = MutableUser.new(name: 'Alice')
user.name = 'Bob'  # OK, not frozen

Serialization

user = User.new(name: 'Alice', age: 30)

user.to_h    # => { name: "Alice", age: 30, role: :user }
user.to_json # => '{"name":"Alice","age":30,"role":"user"}'

User.from_h({ 'name' => 'Bob', 'age' => 25 })  # string keys OK

Coercion

require 'philiprehberger/struct_kit'

User = Philiprehberger::StructKit.define do
  field :age, Integer, coerce: ->(v) { Integer(v) }
  field :status, Symbol, coerce: ->(v) { v.to_sym }
  validate :age, range: 0..150
end

user = User.new(age: "25", status: "active")
user.age    # => 25 (Integer)
user.status # => :active (Symbol)

Pattern Matching

case user
in { role: :admin }
  puts 'Admin user'
in { role: :user }
  puts 'Regular user'
end

Pattern-Style Match

user = User.new(name: 'Alice', age: 30, role: :user)
user.match?(role: :user)         # => true
user.match?(age: 18..30)         # => true (uses === for case equality)
user.match?(name: /^A/)          # => true

Non-destructive Updates

require 'philiprehberger/struct_kit'

User = Philiprehberger::StructKit.define do
  field :name, String
  field :age, Integer, default: 0
end

alice = User.new(name: 'Alice', age: 30)
older = alice.with(age: 31)

alice.age # => 30 (unchanged)
older.age # => 31

Copy With Changes

require 'philiprehberger/struct_kit'

User = Philiprehberger::StructKit.define do
  field :name, String
  field :age, Integer, default: 0
end

alice = User.new(name: 'Alice', age: 30)
bob   = alice.with(name: 'Bob')

alice.name # => "Alice" (unchanged)
bob.name   # => "Bob"
bob.age    # => 30 (retained)

# Overrides are re-validated through the existing type/validation system:
alice.with(age: 'oops') # TypeError: age must be Integer, got String
alice.with(nope: 1)     # ArgumentError: unknown keyword: nope

Presence Validation

Account = Philiprehberger::StructKit.define do
  field :email, String
  field :tags, Array, default: -> { [] }
  validate :email, presence: true
  validate :tags, presence: true
end

Account.new(email: '', tags: ['a']) # ArgumentError: email must be present
Account.new(email: 'a@b', tags: []) # ArgumentError: tags must be present

Positional constructor

require 'philiprehberger/struct_kit'

Point = Philiprehberger::StructKit.define do
  field :x, Integer
  field :y, Integer
end

p = Point.from_a([1, 2])
p.x # => 1
p.y # => 2

# Roundtrip with #to_a
Point.from_a(p.to_a) == p # => true

Introspection

User = Philiprehberger::StructKit.define do
  field :name, String
  field :age, Integer, default: 0
end

User.field_names        # => [:name, :age]
User.new(name: 'Alice', age: 30).to_a  # => ["Alice", 30]

API

Philiprehberger::StructKit.define(mutable: false, &block)

Define a new struct class. Evaluates the block in DSL context.

DSL Methods

Method Description
field(name, type = nil, default: UNSET, coerce: nil) Declare a typed field with optional default and coercion
validate(name, range: nil, format: nil, presence: nil, &block) Add validation rule to a field

Instance Methods

Method Description
#to_h Convert to a plain hash
#to_a Convert to an array of values in field-declaration order
#to_json Convert to JSON string
#with(**changes) Return a new instance with the given fields changed
#with(**overrides) Immutable copy-with: return a new instance with selected fields replaced (re-validated)
#deconstruct_keys(keys) Pattern matching support
#match?(**pattern) Returns true when every key in pattern matches via ===
#== Value equality
#inspect Human-readable string representation

Class Methods

Method Description
.from_h(hash) Construct from hash (string or symbol keys)
.from_a(array) Construct from an array of values in field-declaration order (inverse of #to_a)
.field_names Return the declared field names in order

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