cattri 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +34 -0
  3. data/.gitignore +72 -0
  4. data/.rubocop.yml +6 -3
  5. data/CHANGELOG.md +41 -0
  6. data/Gemfile +12 -0
  7. data/README.md +163 -151
  8. data/Steepfile +6 -0
  9. data/bin/console +33 -0
  10. data/bin/setup +8 -0
  11. data/cattri.gemspec +5 -5
  12. data/lib/cattri/attribute.rb +119 -155
  13. data/lib/cattri/attribute_compiler.rb +104 -0
  14. data/lib/cattri/attribute_options.rb +183 -0
  15. data/lib/cattri/attribute_registry.rb +155 -0
  16. data/lib/cattri/context.rb +124 -106
  17. data/lib/cattri/context_registry.rb +36 -0
  18. data/lib/cattri/deferred_attributes.rb +73 -0
  19. data/lib/cattri/dsl.rb +54 -0
  20. data/lib/cattri/error.rb +17 -90
  21. data/lib/cattri/inheritance.rb +35 -0
  22. data/lib/cattri/initializer_patch.rb +37 -0
  23. data/lib/cattri/internal_store.rb +104 -0
  24. data/lib/cattri/introspection.rb +56 -49
  25. data/lib/cattri/version.rb +3 -1
  26. data/lib/cattri.rb +38 -99
  27. data/sig/lib/cattri/attribute.rbs +105 -0
  28. data/sig/lib/cattri/attribute_compiler.rbs +61 -0
  29. data/sig/lib/cattri/attribute_options.rbs +150 -0
  30. data/sig/lib/cattri/attribute_registry.rbs +95 -0
  31. data/sig/lib/cattri/context.rbs +130 -0
  32. data/sig/lib/cattri/context_registry.rbs +31 -0
  33. data/sig/lib/cattri/deferred_attributes.rbs +53 -0
  34. data/sig/lib/cattri/dsl.rbs +55 -0
  35. data/sig/lib/cattri/error.rbs +28 -0
  36. data/sig/lib/cattri/inheritance.rbs +21 -0
  37. data/sig/lib/cattri/initializer_patch.rbs +26 -0
  38. data/sig/lib/cattri/internal_store.rbs +75 -0
  39. data/sig/lib/cattri/introspection.rbs +61 -0
  40. data/sig/lib/cattri/types.rbs +19 -0
  41. data/sig/lib/cattri/visibility.rbs +55 -0
  42. data/sig/lib/cattri.rbs +37 -0
  43. data/spec/cattri/attribute_compiler_spec.rb +179 -0
  44. data/spec/cattri/attribute_options_spec.rb +267 -0
  45. data/spec/cattri/attribute_registry_spec.rb +257 -0
  46. data/spec/cattri/attribute_spec.rb +297 -0
  47. data/spec/cattri/context_registry_spec.rb +45 -0
  48. data/spec/cattri/context_spec.rb +346 -0
  49. data/spec/cattri/deferred_attrributes_spec.rb +117 -0
  50. data/spec/cattri/dsl_spec.rb +69 -0
  51. data/spec/cattri/error_spec.rb +37 -0
  52. data/spec/cattri/inheritance_spec.rb +60 -0
  53. data/spec/cattri/initializer_patch_spec.rb +35 -0
  54. data/spec/cattri/internal_store_spec.rb +139 -0
  55. data/spec/cattri/introspection_spec.rb +90 -0
  56. data/spec/cattri/visibility_spec.rb +68 -0
  57. data/spec/cattri_spec.rb +54 -0
  58. data/spec/simplecov_helper.rb +21 -0
  59. data/spec/spec_helper.rb +16 -0
  60. metadata +79 -6
  61. data/lib/cattri/attribute_definer.rb +0 -143
  62. data/lib/cattri/class_attributes.rb +0 -277
  63. data/lib/cattri/instance_attributes.rb +0 -276
  64. data/sig/cattri.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2495a8756d30c301267ad2db2dfbd4c9669e817119fa316b608408665988b747
4
- data.tar.gz: 47d682eaf5465e4ab6cc4bdb7dcc74d053c93c6b8d1ee3a8cfda0925cf68662a
3
+ metadata.gz: 9de3a522be8c55a1b05edcc432f04d0352f0661290d6e7947b20e9399dde1462
4
+ data.tar.gz: d5b090040ea3e975c10342c8ac6d0ba3bfbd6b9bc1b3b8b50107c5ba12a13640
5
5
  SHA512:
6
- metadata.gz: aae777877b7576a76b0be11a529aafaaac9c59066efd873fc9020689ba3ccefb78688411907b2246cb603dafa5e6868e1841e4724f296f634eac821a1bc27bc9
7
- data.tar.gz: 8e919b6782d0cce71ed1a78bb46ebb92df995471238080a4e20604914d0fe958c041aec201eb598f9764598384fd776aaff03f5b131908082428c2d546c97cd9
6
+ metadata.gz: 43fb43c99d80be87ebf0b966093252980be960a1893e77f86f0344d8ba44f54d24bdf85f1a766b417b62f4a74a307c477f1ecde5fbb724baedb3bd6d65e858fb
7
+ data.tar.gz: 07b2830c5fbe6b805ceb191b2f8a1aacad6f3b3cd6ef03dca10e26b8b83dc8d708483756b4c46cac54e4d3f713b802957e9173bd959ea5b45c3b18fba5118869
@@ -0,0 +1,34 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ pull_request:
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ name: Ruby ${{ matrix.ruby }}
14
+ strategy:
15
+ matrix:
16
+ ruby:
17
+ - '2.7.0'
18
+ - '3.1.4'
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - name: Set up Ruby
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby }}
26
+ bundler-cache: true
27
+
28
+ - name: Run the default task
29
+ run: bundle exec rake
30
+
31
+ - name: Upload coverage to Codecov
32
+ uses: codecov/codecov-action@v5
33
+ env:
34
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
data/.gitignore ADDED
@@ -0,0 +1,72 @@
1
+ # Ignore bundler config.
2
+ /.bundle
3
+ vendor/
4
+
5
+ # Ignore the default SQLite database.
6
+ /db/*.sqlite3
7
+
8
+ # Ignore all logfiles and tempfiles.
9
+ /log/*.log
10
+ /tmp
11
+
12
+ # Ignore coverage reports.
13
+ /coverage/
14
+
15
+ # Ignore system specific files.
16
+ /.byebug
17
+ /.yardoc
18
+ /._*
19
+
20
+ # Ignore bundler binstubs.
21
+ /bin/
22
+
23
+ # Ignore Gemfile.lock
24
+ Gemfile.lock
25
+
26
+ # Ignore .env file containing sensitive information.
27
+ .env
28
+
29
+ # Ignore any local .env files.
30
+ .env.*
31
+
32
+ # VSCode settings
33
+ .vscode/
34
+
35
+ # RubyMine settings
36
+ .idea/
37
+
38
+ # Ignore VSCode workspace settings
39
+ *.code-workspace
40
+
41
+ # Ignore VSCode user-specific settings
42
+ .vscode-server/
43
+
44
+ # Ignore VSCode logs
45
+ .vscode-logs/
46
+
47
+ # Ignore the generated RDoc directory.
48
+ /doc/
49
+
50
+ # Ignore the default RVM or rbenv config.
51
+ .rvmrc
52
+ .rbenv-vars
53
+
54
+ # Ignore macOS metadata directories.
55
+ .DS_Store
56
+ .AppleDouble
57
+ .LSOverride
58
+
59
+ # Ignore Windows thumbnail database.
60
+ Thumbs.db
61
+
62
+ # Ignore the Rubocop auto-generated configuration file.
63
+ .rubocop_todo.yml
64
+
65
+ # Ignore the coverage analysis directory.
66
+ /coverage/
67
+
68
+ # Ignore the local config file for Overcommit.
69
+ .overcommit.yml
70
+
71
+ *.gem
72
+ .rspec_status
data/.rubocop.yml CHANGED
@@ -28,13 +28,16 @@ Layout/LineLength:
28
28
  Lint/MissingSuper:
29
29
  Enabled: false
30
30
 
31
- Metrics/ClassLength:
32
- Max: 200
33
-
34
31
  Metrics/BlockLength:
35
32
  Exclude:
36
33
  - cattri.gemspec
37
34
  - spec/**/*.rb
38
35
 
36
+ Metrics/ClassLength:
37
+ Max: 200
38
+
39
39
  Metrics/MethodLength:
40
40
  Max: 20
41
+
42
+ Metrics/ParameterLists:
43
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,3 +1,44 @@
1
+ ## [0.2.0] - 2025-05-01
2
+
3
+ ### Changed
4
+
5
+ - Replaced `cattr` and `iattr` with unified `cattri` DSL
6
+ - All attributes now use `cattri`, with `scope: :class` or `scope: :instance`
7
+ - `iattr` and `cattr` are no longer public API
8
+
9
+ - Attribute behavior is now centralized via:
10
+ - `Cattri::Attribute` and `Cattri::AttributeOptions`
11
+ - `Cattri::Context` and `ContextRegistry`
12
+ - `Cattri::InternalStore` for safe write-once value storage
13
+
14
+ - Final attributes (`final: true`) now enforced at the store level, with safe write-once semantics
15
+ - Visibility and exposure are fully separated:
16
+ - `visibility: :public|:protected|:private` sets method scope
17
+ - `expose: :read_write|:read|:write|:none` controls which methods are generated
18
+ - New predicate handling via `predicate: true`, with visibility inheritance
19
+
20
+ ### Added
21
+
22
+ - Support for `scope:` to explicitly declare attribute scope
23
+ - `InitializerPatch` to apply default values for `final` instance attributes
24
+ - `memoize_default_value` helper to simplify accessor generation
25
+ - 100% RSpec coverage and branch coverage
26
+ - Steep RBS type signatures for public and internal API
27
+ - Full introspection via `.attributes`, `.attribute`, `.attribute_methods`, `.attribute_source`
28
+
29
+ ### Removed
30
+
31
+ - `iattr`, `cattr`, `iattr_alias`, `cattr_alias`, and setter helpers (`*_setter`)
32
+ - Legacy inheritance hook logic and module-style patching
33
+
34
+ ### Notes
35
+
36
+ This release consolidates and simplifies the attribute system into a modern, safer, and more flexible DSL. All existing functionality is preserved through the `cattri` interface.
37
+
38
+ This version introduces **breaking changes** to the DSL. Migration guide available in the README.
39
+
40
+ ---
41
+
1
42
  ## [0.1.3] - 2025-04-22
2
43
 
3
44
  ### Added
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in cattri.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+
12
+ gem "rubocop", "~> 1.21"
data/README.md CHANGED
@@ -1,244 +1,256 @@
1
1
  # Cattri
2
2
 
3
- A **minimalfootprint** DSL for defining **classlevel** and **instancelevel** attributes in Ruby, with first‑class support for custom defaults, coercion, visibility tracking, and safety‑first error handling.
3
+ **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.
4
4
 
5
- ---
5
+ It offers subclass-safe inheritance, lazy or static defaults, optional coercion, and write-once (`final`) semantics, while remaining lightweight and idiomatic.
6
6
 
7
- ## Why another attribute DSL?
7
+ ---
8
8
 
9
- | Capability | Cattri | `attr_*` / `cattr_*` (Ruby / ActiveSupport) | `dry-configurable` |
10
- |------------|:------:|:-------------------------------------------:|:------------------:|
11
- | **Single DSL for class *and* instance attributes** | ✅ | ❌ (separate APIs) | ⚠️ (config only) |
12
- | **Per‑subclass deep copy of attribute metadata & values** | ✅ | ❌ | ❌ |
13
- | **Built‑in visibility tracking (`public` / `protected` / `private`)** | ✅ | ❌ | ❌ |
14
- | **Lazy or static *default* values** | ✅ | ⚠️ (writer‑based) | ✅ |
15
- | **Optional *coercion* via custom setter block** | ✅ | ❌ | ✅ |
16
- | **Read‑only / write‑only flags** | ✅ | ⚠️ (reader / writer macros) | ❌ |
17
- | **Introspection helpers (`snapshot_*`)** | ✅ | ❌ | ⚠️ via internals |
18
- | **Clear, granular error hierarchy** | ✅ | ❌ | ✅ |
19
- | **Zero runtime dependencies** | ✅ | ⚠️ (ActiveSupport) | ✅ |
9
+ ## Features
20
10
 
21
- > **TL;DR** If you need lightweight, _Rails‑agnostic_ attribute helpers that play nicely with inheritance and don’t leak state between subclasses, Cattri is for you.
11
+ - Unified `cattri` API for both class and instance attributes
12
+ - 🔍 Tracks visibility: `public`, `protected`, `private`
13
+ - 🔁 Inheritance-safe attribute copying
14
+ - 🧼 Lazy defaults or static values
15
+ - 🔒 Write-once `final: true` support
16
+ - 👁 Predicate support (`admin?`, etc.)
17
+ - 🔍 Introspection: list all attributes and methods
18
+ - 🧪 100% test and branch coverage
19
+ - 🔌 Zero runtime dependencies
22
20
 
23
21
  ---
24
22
 
25
- ## Installation
26
-
27
- ```bash
28
- bundle add cattri # Ruby ≥ 2.7
29
- ```
23
+ ## 💡 Why Use Cattri?
30
24
 
31
- Or in your Gemfile:
25
+ Ruby's built-in attribute helpers and Rails' `class_attribute` are either too limited or too invasive. Cattri offers:
32
26
 
33
- ```ruby
34
- gem "cattri"
35
- ```
27
+ | Capability | Cattri | `attr_*` / `cattr_*` | `class_attribute` (Rails) |
28
+ |-----------------------------------------------|--------|----------------------|---------------------------|
29
+ | Single DSL for class & instance attributes | ✅ | ❌ | ❌ |
30
+ | Subclass-safe value & metadata inheritance | ✅ | ❌ | ⚠️ |
31
+ | Visibility-aware (`private`, `protected`) | ✅ | ❌ | ❌ |
32
+ | Lazy or static defaults | ✅ | ⚠️ | ✅ |
33
+ | Optional coercion or transformation | ✅ | ❌ | ⚠️ |
34
+ | Write-once (`final: true`) semantics | ✅ | ❌ | ❌ |
36
35
 
37
36
  ---
38
37
 
39
- ## Quick start
38
+ ## 🚀 Usage Examples
39
+
40
+ Cattri uses a single DSL method, `cattri`, to define both class-level and instance-level attributes.
41
+
42
+ Use the `scope:` option to indicate whether the attribute belongs to the class (`:class`) or the instance (`:instance`). If omitted, it defaults to `:instance`.
40
43
 
41
44
  ```ruby
42
- class Config
43
- include Cattri # exposes `cattr` & `iattr`
44
-
45
- # -- class‑level ----------------------------------
46
- cattr :flag_a, :flag_b, default: true
47
- cattr :enabled, default: true, predicate: true
48
- cattr :timeout, default: -> { 5.0 }, instance_reader: false
49
-
50
- # -- instance‑level -------------------------------
51
- iattr :item_a, :item_b, default: true
52
- iattr :name, default: "anonymous"
53
- iattr_alias :username, :name
54
- iattr :age, default: 0 do |val| # coercion block
55
- Integer(val)
56
- end
57
- end
45
+ class User
46
+ include Cattri
58
47
 
59
- Config.enabled # => true
60
- Config.enabled = false
61
- Config.enabled? # => false (created with predicate: true flag)
62
- Config.new.age = "42" # => 42
63
- Config.new.username # proxy to Config.new.name
64
- ```
48
+ # Final class-level attribute
49
+ cattri :type, :standard, final: true, scope: :class
65
50
 
66
- ---
51
+ # Writable class-level attribute
52
+ cattri :config, -> { {} }, scope: :class
67
53
 
68
- ## Defining attributes
54
+ # Final instance-level attribute
55
+ cattri :id, -> { SecureRandom.uuid }, final: true
69
56
 
70
- ### Class attributes (`cattr`)
57
+ # Writable instance-level attributes
58
+ cattri :name, "anonymous"
59
+ cattri :admin, false, predicate: true
71
60
 
72
- ```ruby
73
- cattr :log_level,
74
- default: :info,
75
- access: :protected, # respects current visibility by default
76
- readonly: false,
77
- predicate: true, # defines #{name}? predicate method that respects visibility
78
- instance_reader: true do |value|
79
- value.to_sym
61
+ def initialize(id)
62
+ self.id = id # set the value for `cattri :id`
63
+ end
80
64
  end
81
- ```
82
65
 
83
- ### Instance attributes (`iattr`)
66
+ # Class-level access
67
+ User.type # => :standard
68
+ User.config # => {}
84
69
 
85
- ```ruby
86
- iattr :token, default: -> { SecureRandom.hex(8) },
87
- reader: true,
88
- writer: false, # read‑only
89
- predicate: true
70
+ # Instance-level access
71
+ user = User.new
72
+ user.name # => "anonymous"
73
+ user.admin? # => false
74
+ user.id # => uuid
90
75
  ```
91
76
 
92
- Both forms accept:
77
+ ---
93
78
 
94
- | Option | Purpose |
95
- | ------ | ------- |
96
- | `default:` | Static value or callable (`Proc`) evaluated lazily. |
97
- | `access:` | Override inferred visibility (`:public`, `:protected`, `:private`). |
98
- | `reader:` / `writer:` | Disable reader or writer for instance attributes. |
99
- | `readonly:` | Shorthand for class attributes (`writer` is always present). |
100
- | `instance_reader:` | Expose class attribute as instance reader (default: **true**). |
101
- | `predicate` | Define a `:name?` method that calls `!!send(name)`
79
+ ## 👇 Accessing Attributes Within the Class
102
80
 
103
- If you pass a block, it’s treated as a **coercion setter** and receives the incoming value.
81
+ ```ruby
82
+ class User
83
+ include Cattri
104
84
 
105
- ---
85
+ cattri :id, -> { SecureRandom.uuid }, final: true
86
+ cattri :type, :standard, final: true, scope: :class
106
87
 
107
- ## Post-definition coercion with `*_setter`
88
+ def initialize(id)
89
+ self.id = id # Sets instance-level attribute
90
+ end
108
91
 
109
- If you define multiple attributes at once, you can't provide a coercion block inline:
92
+ def summary
93
+ "#{self.class.type}-#{id}" # Accesses class-level and instance-level attributes
94
+ end
110
95
 
111
- ```ruby
112
- cattr :foo, :bar, default: nil # cannot use block here
96
+ def self.default_type
97
+ type # Same as self.type resolves on the singleton
98
+ end
99
+ end
113
100
  ```
114
101
 
115
- Instead, define them first, then apply a coercion later using:
102
+ ---
116
103
 
117
- - `cattr_setter` for class attributes
118
- - `iattr_setter` for instance attributes
104
+ ## 🧭 Attribute Scope
119
105
 
120
- These allow you to attach or override the setter logic after the fact:
106
+ By default, attributes are defined per-instance. You can change this behavior using `scope:`.
121
107
 
122
108
  ```ruby
123
109
  class Config
124
110
  include Cattri
125
111
 
126
- cattr :log_level
127
- cattr_setter :log_level do |val|
128
- val.to_s.downcase.to_sym
129
- end
130
-
131
- iattr_writer :token
132
- iattr_setter :token do |val|
133
- val.strip
134
- end
112
+ cattri :global_timeout, 30, scope: :class
113
+ cattri :retries, 3 # implicitly scope: :instance
135
114
  end
115
+
116
+ Config.global_timeout # => 30
117
+
118
+ instance = Config.new
119
+ instance.retries # => 3
120
+ instance.global_timeout # => NoMethodError
136
121
  ```
137
122
 
138
- Coercion is only applied when the attribute is written (via `=` or callable form), not when read.
123
+ - `scope: :class` defines the attribute on the class (i.e., the singleton).
124
+ - `scope: :instance` (or omitting scope) defines the attribute per instance.
139
125
 
140
- Attempting to use `*_setter` on an undefined attribute or one without a writer will raise:
126
+ ---
141
127
 
142
- - `Cattri::AttributeNotDefinedError` the attribute doesn't exist or wasn't fully defined
143
- - `Cattri::AttributeDefinitionError` – the attribute is marked as readonly
128
+ ## 🛡 Final Attributes
144
129
 
145
- These APIs ensure your DSL stays consistent and extensible, even when bulk-declaring attributes up front.
130
+ ```ruby
131
+ class Settings
132
+ include Cattri
133
+ cattri :version, -> { "1.0.0" }, final: true, scope: :class
134
+ end
135
+
136
+ Settings.version # => "1.0.0"
137
+ Settings.version = "2.0" # => Raises Cattri::AttributeError
138
+ ```
139
+
140
+ - `final: true, scope: :class` defines a constant class-level attribute. It cannot be reassigned and uses the value provided at definition.
141
+ - `final: true` (with instance scope) defines a write-once attribute. If not explicitly set during initialization, the default value will be used.
142
+
143
+ > Note: `final_cattri` is a shorthand for `cattri(..., final: true)`, included for API symmetry but not required.
146
144
 
147
145
  ---
148
146
 
149
- ## Visibility tracking
147
+ ## 👁 Attribute Exposure
150
148
 
151
- Cattri watches calls to `public`, `protected`, and `private` while you define methods:
149
+ The `expose:` option controls what public methods are generated for an attribute. You can fine-tune whether the reader, writer, or neither is available.
152
150
 
153
151
  ```ruby
154
- class Secrets
152
+ class Profile
155
153
  include Cattri
156
154
 
157
- private
158
- cattr :api_key
155
+ cattri :name, "guest", expose: :read_write
156
+ cattri :token, "secret", expose: :read
157
+ cattri :attempts, 0, expose: :write
158
+ cattri :internal_flag, true, expose: :none
159
159
  end
160
-
161
- Secrets.private_methods.include?(:api_key) # => true
162
160
  ```
163
161
 
164
- No boilerplate—attributes inherit the visibility that was in effect at the call site.
162
+ ### Exposure Levels
163
+
164
+ - `:read_write` — defines both reader and writer
165
+ - `:read` — defines a reader only
166
+ - `:write` — defines a writer only
167
+ - `:none` — defines no public methods (internal only)
168
+
169
+ > Predicate methods (`admin?`, etc.) are enabled via `predicate: true`.
165
170
 
166
171
  ---
167
172
 
168
- ## Safe inheritance
173
+ ## 🔐 Visibility
169
174
 
170
- Subclassing copies both **metadata** and **current values**, using defensive `#dup` where possible and falling back safely when objects are frozen or not duplicable:
175
+ Cattri respects Ruby's `public`, `protected`, and `private` scoping when defining methods. You can also explicitly override visibility using `visibility:`.
171
176
 
172
177
  ```ruby
173
- class Base
178
+ class Document
174
179
  include Cattri
175
- cattr :settings, default: {}
176
- end
177
180
 
178
- class Child < Base; end
181
+ private
182
+ cattri :token
183
+
184
+ protected
185
+ cattri :internal_flag
186
+
187
+ public
188
+ cattri :title
179
189
 
180
- Base.settings[:foo] = 1
181
- Child.settings # => {} (isolated copy)
190
+ cattri :owner, "system", visibility: :protected
191
+ end
182
192
  ```
183
193
 
194
+ - If defined inside a visibility scope, Cattri applies that visibility automatically
195
+ - Use `visibility:` to override the inferred scope
196
+ - Applies only to generated methods (reader, writer, predicate), not internal store access
197
+
184
198
  ---
185
199
 
186
- ## Introspection helpers
200
+ ## 🔍 Introspection
187
201
 
188
- Add `include Cattri::Introspection` (or `extend` for class‑only use) to snapshot live values:
202
+ Enable introspection with:
189
203
 
190
204
  ```ruby
191
- Config.snapshot_cattrs # => { enabled: false, timeout: 5.0 }
192
- instance.snapshot_iattrs # => { name: "bob", age: 42 }
193
- ```
205
+ User.with_cattri_introspection
194
206
 
195
- Great for debugging or test assertions.
207
+ User.attributes # => [:type, :name, :admin]
208
+ User.attribute(:type).final? # => true
209
+ User.attribute_methods # => { type: [:type], name: [:name], admin: [:admin, :admin?] }
210
+ User.attribute_source(:name) # => User
211
+ ```
196
212
 
197
213
  ---
198
214
 
199
- ## Error handling
215
+ ## 📦 Installation
200
216
 
201
- All errors inherit from `Cattri::Error`, allowing a single rescue for any gem‑specific issue.
217
+ Add to your Gemfile:
202
218
 
203
- | Error class | Raised when… |
204
- |-------------|--------------|
205
- | `Cattri::AttributeDefinedError` | an attribute is declared twice on the same level |
206
- | `Cattri::AttributeDefinitionError` | method generation (`define_method`) fails |
207
- | `Cattri::UnsupportedTypeError` | an internal API receives an unknown type |
208
- | `Cattri::AttributeError` | generic superclass for attribute‑related issues |
219
+ ```ruby
220
+ gem "cattri"
221
+ ```
209
222
 
210
- Example:
223
+ Or via Bundler:
211
224
 
212
- ```ruby
213
- begin
214
- class Foo
215
- include Cattri
216
- cattr :foo
217
- cattr :foo # duplicate
218
- end
219
- rescue Cattri::AttributeDefinedError => e
220
- warn e.message # => "Class attribute :foo has already been defined"
221
- rescue Cattri::AttributeError => e
222
- warn e.message # => Catch-all for any error raised within attributes
223
- end
225
+ ```sh
226
+ bundle add cattri
224
227
  ```
225
228
 
226
229
  ---
227
230
 
228
- ## Comparison with standard patterns
231
+ ## 🧱 Design Overview
229
232
 
230
- * **Core Ruby macros** (`attr_accessor`, `cattr_accessor`) are simple but global—attributes bleed into subclasses and lack defaults or coercion.
231
- * **ActiveSupport** extends the API but still relies on mutable class variables and offers no visibility control.
232
- * **Dry‑configurable** is robust yet heavyweight when you only need a handful of attributes outside a full config object.
233
+ Cattri includes:
233
234
 
234
- Cattri sits in the sweet spot: **micro‑sized (~300 LOC)**, dependency‑free, and purpose‑built for attribute declaration.
235
+ - `InternalStore` for final-safe value tracking
236
+ - `ContextRegistry` and `Context` for method definition logic
237
+ - `Attribute` and `AttributeOptions` for metadata handling
238
+ - `Visibility` tracking for DSL-defined methods
239
+ - `InitializerPatch` for final attribute enforcement on `#initialize`
240
+ - `Dsl` for `cattri` and `final_cattri`
241
+ - `Inheritance` to ensure subclass copying
235
242
 
236
243
  ---
237
244
 
238
- ## Testing tips
245
+ ## 🧪 Test Coverage
246
+
247
+ Cattri is tested with 100% line and branch coverage. All dynamic definitions are validated via RSpec, and edge cases are covered, including:
239
248
 
240
- * Use `include Cattri::Introspection` in spec helper files to capture snapshots before/after mutations.
241
- * Rescue `Cattri::Error` in high‑level test helpers to assert failures without coupling to sub‑class names.
249
+ - Predicate methods
250
+ - Final value enforcement
251
+ - Class vs. instance scope
252
+ - Attribute inheritance
253
+ - Visibility and expose interaction
242
254
 
243
255
  ---
244
256
 
@@ -247,13 +259,13 @@ Cattri sits in the sweet spot: **micro‑sized (~300 LOC)**, dependency‑free,
247
259
  1. Fork the repo
248
260
  2. `bundle install`
249
261
  3. Run the test suite with `bundle exec rake`
250
- 4. Submit a pull request – ensure new code is covered and **rubocop** passes.
262
+ 4. Submit a pull request – ensure new code is covered and **rubocop** passes
251
263
 
252
264
  ---
253
265
 
254
266
  ## License
255
267
 
256
- This gem is released under the MIT License – see See [LICENSE](LICENSE) for details.
268
+ This gem is released under the MIT License – see [LICENSE](LICENSE) for details.
257
269
 
258
270
  ## 🙏 Credits
259
271
 
data/Steepfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ target :lib do
4
+ check "lib"
5
+ signature "sig"
6
+ end
data/bin/console ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "cattri"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # class Tester
11
+ # include Cattri
12
+ #
13
+ # cattr :public_attr, default: 1
14
+ #
15
+ # protected
16
+ # cattr :protected_attr, default: 2
17
+ #
18
+ # private
19
+ # cattr :private_attr, default: 3
20
+ #
21
+ # class << self
22
+ # def protected_proxy
23
+ # self.protected_attr
24
+ # end
25
+ #
26
+ # def private_proxy
27
+ # self.private_attr
28
+ # end
29
+ # end
30
+ # end
31
+
32
+ require "irb"
33
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here