Cattri
Cattri is a minimal-footprint Ruby DSL for defining class-level and instance-level attributes with clarity, safety, and full visibility control โ without relying on ActiveSupport.
It offers subclass-safe inheritance, lazy or static defaults, optional coercion, and write-once (final
) semantics, while remaining lightweight and idiomatic.
โจ Features
- โ
Unified
cattri
API for both class and instance attributes - ๐ Tracks visibility:
public
,protected
,private
- ๐ Inheritance-safe attribute copying
- ๐งผ Lazy defaults or static values
- ๐ Write-once
final: true
support - ๐ Predicate support (
admin?
, etc.) - ๐ Introspection: list all attributes and methods
- ๐งช 100% test and branch coverage
- ๐ Zero runtime dependencies
๐ก Why Use Cattri?
Ruby's built-in attribute helpers and Rails' class_attribute
are either too limited or too invasive. Cattri offers:
Capability | Cattri |
attr_* / cattr_*
|
class_attribute (Rails) |
---|---|---|---|
Single DSL for class & instance attributes | โ | โ | โ |
Subclass-safe value & metadata inheritance | โ | โ | โ ๏ธ |
Visibility-aware (private , protected ) |
โ | โ | โ |
Lazy or static defaults | โ | โ ๏ธ | โ |
Optional coercion or transformation | โ | โ | โ ๏ธ |
Write-once (final: true ) semantics |
โ | โ | โ |
๐ Usage Examples
Cattri uses a single DSL method, cattri
, to define both class-level and instance-level attributes.
Use the scope:
option to indicate whether the attribute belongs to the class (:class
) or the instance (:instance
). If omitted, it defaults to :instance
.
class User
include Cattri
# Final class-level attribute
cattri :type, :standard, final: true, scope: :class
# Writable class-level attribute
cattri :config, -> { {} }, scope: :class
# Final instance-level attribute
cattri :id, -> { SecureRandom.uuid }, final: true
# Writable instance-level attributes
cattri :name, "anonymous" do |value|
value.to_s.capitalize # custom setter/coercer
end
cattri :admin, false, predicate: true
def initialize(id)
self.id = id # set the value for `cattri :id`
end
end
# Class-level access
User.type # => :standard
User.config # => {}
# Instance-level access
user = User.new
user.name # => "anonymous"
user.admin? # => false
user.id # => uuid
๐ Accessing Attributes Within the Class
class User
include Cattri
cattri :id, -> { SecureRandom.uuid }, final: true
cattri :type, :standard, final: true, scope: :class
def initialize(id)
self.id = id # Sets instance-level attribute
end
def summary
"#{self.class.type}-#{id}" # Accesses class-level and instance-level attributes
end
def self.default_type
type # Same as self.type โ resolves on the singleton
end
end
๐งญ Attribute Scope
By default, attributes are defined per-instance. You can change this behavior using scope:
.
class Config
include Cattri
cattri :global_timeout, 30, scope: :class
cattri :retries, 3 # implicitly scope: :instance
end
Config.global_timeout # => 30
instance = Config.new
instance.retries # => 3
instance.global_timeout # => NoMethodError
-
scope: :class
defines the attribute on the class (i.e., the singleton). -
scope: :instance
(or omitting scope) defines the attribute per instance.
๐ก Final Attributes
class Settings
include Cattri
cattri :version, -> { "1.0.0" }, final: true, scope: :class
end
Settings.version # => "1.0.0"
Settings.version = "2.0" # => Raises Cattri::AttributeError
-
final: true, scope: :class
defines a constant class-level attribute. It cannot be reassigned and uses the value provided at definition. -
final: true
(with instance scope) defines a write-once attribute. If not explicitly set during initialization, the default value will be used.
Note:
final_cattri
is a shorthand forcattri(..., final: true)
, included for API symmetry but not required.
๐ Attribute Exposure
The expose:
option controls what public methods are generated for an attribute. You can fine-tune whether the reader, writer, or neither is available.
class Profile
include Cattri
cattri :name, "guest", expose: :read_write
cattri :token, "secret", expose: :read
cattri :attempts, 0, expose: :write
cattri :internal_flag, true, expose: :none
end
Exposure Levels
-
:read_write
โ defines both reader and writer -
:read
โ defines a reader only -
:write
โ defines a writer only -
:none
โ defines no public methods (internal only)
Predicate methods (
admin?
, etc.) are enabled viapredicate: true
.
๐ Visibility
Cattri respects Ruby's public
, protected
, and private
scoping when defining methods. You can also explicitly override visibility using visibility:
.
class Document
include Cattri
private
cattri :token
protected
cattri :internal_flag
public
cattri :title
cattri :owner, "system", visibility: :protected
end
- If defined inside a visibility scope, Cattri applies that visibility automatically
- Use
visibility:
to override the inferred scope - Applies only to generated methods (reader, writer, predicate), not internal store access
๐ Introspection
Enable introspection with:
User.with_cattri_introspection
User.attributes # => [:type, :name, :admin]
User.attribute(:type).final? # => true
User.attribute_methods # => { type: [:type], name: [:name], admin: [:admin, :admin?] }
User.attribute_source(:name) # => User
๐ฆ Installation
Add to your Gemfile:
gem "cattri"
Or via Bundler:
bundle add cattri
๐งฑ Design Overview
Cattri includes:
-
InternalStore
for final-safe value tracking -
ContextRegistry
andContext
for method definition logic -
Attribute
andAttributeOptions
for metadata handling -
Visibility
tracking for DSL-defined methods -
InitializerPatch
for final attribute enforcement on#initialize
-
Dsl
forcattri
andfinal_cattri
-
Inheritance
to ensure subclass copying
๐งช Test Coverage
Cattri is tested with 100% line and branch coverage. All dynamic definitions are validated via RSpec, and edge cases are covered, including:
- Predicate methods
- Final value enforcement
- Class vs. instance scope
- Attribute inheritance
- Visibility and expose interaction
Contributing
- Fork the repo
bundle install
- Run the test suite with
bundle exec rake
- Submit a pull request โ ensure new code is covered and rubocop passes
License
This gem is released under the MIT License โ see LICENSE for details.
๐ Credits
Created with โค๏ธ by Nathan Lucas