Project

cattri

0.0
The project is in a healthy, maintained state
Cattri provides a clean DSL for defining class-level and instance-level attributes with optional defaults, coercion, accessors, and inheritance support.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies
 Project Readme

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 for cattri(..., 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 via predicate: 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 and Context for method definition logic
  • Attribute and AttributeOptions for metadata handling
  • Visibility tracking for DSL-defined methods
  • InitializerPatch for final attribute enforcement on #initialize
  • Dsl for cattri and final_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

  1. Fork the repo
  2. bundle install
  3. Run the test suite with bundle exec rake
  4. 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