Rasti::Model

Gem Version CI Coverage Status Code Climate

Domain models with typed attributes

Installation

Add this line to your application's Gemfile:

gem 'rasti-model'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rasti-model

Usage

Basic models

class Point < Rasti::Model
  attribute :x
  attribute :y
end

point = Point.new x: 1, y: 2
point.x # => 1
point.y # => 2

# Unexpected attributes
Point.new z: 3 # => Rasti::Model::UnexpectedAttributesError: Unexpected attributes: z

Typed models

T = Rasti::Types

class TypedPoint < Rasti::Model
  attribute :x, T::Integer
  attribute :y, T::Integer
end

point = TypedPoint.new x: '1', y: '2'
point.x # => 1
point.y # => 2

Inline definition

Point = Rasti::Model[:x, :y]

TypedPoint = Rasti::Model[x: T::Integer, y: T::Integer]

Serialization and deserialization

City = Rasti::Model[name: T::String]
Country = Rasti::Model[name: T::String, cities: T::Array[T::Model[City]]]

attributes = {
  name: 'Argentina',
  cities: [
    {name: 'Buenos Aires'},
    {name: 'Córdoba'},
    {name: 'Rosario'}
  ]
}

country = Country.new attributes
country.name # => 'Argentina'
country.cities # => [City[name: "Buenos Aires"], City[name: "Córdoba"], City[name: "Rosario"]]

country.to_h # => attributes

# Attribute filtering
country.to_h(only: [:name]) # => {name: "Argentina"}
country.to_h(except: [:cities]) # => {name: "Argentina"}

Default values

class User < Rasti::Model
  attribute :name, T::String
  attribute :admin, T::Boolean, default: false
  attribute :created_at, T::Time, default: ->(m) { Time.now }
end

user = User.new name: 'John'
user.admin # => false
user.created_at # => 2026-01-02 23:19:15 -0300

Merging models

point_1 = Point.new x: 1, y: 2
point_2 = point_1.merge x: 10
point_2.to_h # => {x: 10, y: 2}

Custom attribute options

You can add custom metadata to attributes that can be used later (e.g., for UI generation):

class User < Rasti::Model
  attribute :name, T::String, description: 'The user full name'
end

attribute = User.attributes.first
attribute.option(:description) # => 'The user full name'

These options are also included in the schema representation.

Equality

point_1 = Point.new x: 1, y: 2
point_2 = Point.new x: 1, y: 2
point_3 = Point.new x: 2, y: 1

point_1 == point_2 # => true
point_1 == point_3 # => false

Error handling

TypedPoint = Rasti::Model[x: T::Integer, y: T::Integer]

point = TypedPoint.new x: true
point.x # => Rasti::Types::CastError: Invalid cast: true -> Rasti::Types::Integer
point.y # => Rasti::Model::NotAssignedAttributeError: Not assigned attribute y

# Bulk validation
point = TypedPoint.new x: 'invalid', y: 'invalid'
point.cast_attributes! # => Rasti::Types::CompoundError: x: ["Invalid cast: \"invalid\" -> Rasti::Types::Integer"], y: ["Invalid cast: \"invalid\" -> Rasti::Types::Integer"]

Model Schema

It is possible to obtain a serializable representation of the model structure (schema).

Point = Rasti::Model[x: T::Integer, y: T::Integer]
Point.to_schema
# => {
#      model: "Point",
#      attributes: [
#        {name: :x, type: :integer},
#        {name: :y, type: :integer}
#      ]
#    }

Custom type serializers

You can register custom serializers for your types to be used in the schema generation:

Rasti::Model::Schema.register_type_serializer(MyCustomType, :custom)

# Or with a block for more details
Rasti::Model::Schema.register_type_serializer(MyCustomType) do |type|
  {type: :custom, details: type.some_info}
end

Also, if a type responds to to_schema, it will be used.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/gabynaiman/rasti-model.

License

The gem is available as open source under the terms of the MIT License.