cattri 0.1.3 → 0.2.1

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 +63 -0
  6. data/Gemfile +12 -0
  7. data/README.md +166 -151
  8. data/Steepfile +6 -0
  9. data/bin/console +8 -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 +38 -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 +115 -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 +101 -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 +66 -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 +9 -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: 6570d6da579ae6a15c865884a895a1ef0923eafd610d2a97a474d4a888af5778
4
+ data.tar.gz: 6f01dcc2490c4c4ef159d5fcf88c87ab50204c293ce403e87440b87130add491
5
5
  SHA512:
6
- metadata.gz: aae777877b7576a76b0be11a529aafaaac9c59066efd873fc9020689ba3ccefb78688411907b2246cb603dafa5e6868e1841e4724f296f634eac821a1bc27bc9
7
- data.tar.gz: 8e919b6782d0cce71ed1a78bb46ebb92df995471238080a4e20604914d0fe958c041aec201eb598f9764598384fd776aaff03f5b131908082428c2d546c97cd9
6
+ metadata.gz: 9dbe7c5e9a7a8f4edcc160dc6952f42e80fd0ee5c602b65113fda8059663634c9c3d7d418044a7584626e8c9294240ad78be77a32b5dcabfa94d8498d2f76178
7
+ data.tar.gz: 301099ebc23bc730ada63468003fe1c9cd462e0040cbc8bc194d4a015d03a59c2b777482a4a142d5c215d93bc4083b7d2f5e33dec172e97ba343b5c48085f1a5
@@ -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,66 @@
1
+ ## [0.2.1] - 2025-05-01
2
+
3
+ - Fixed an issue where only `final: true` instance variables defined on the current/class had their values applied.
4
+ - Now walks the ancestor tree to ensure all attributes get set.
5
+
6
+ ```ruby
7
+ module Options
8
+ include Cattri
9
+
10
+ cattri :enabled, true, final: true # wasn't being set previously
11
+ end
12
+
13
+ class Attribute
14
+ include Options
15
+
16
+ def initialize(enabled: true)
17
+ seld.enabled = enabled
18
+ end
19
+ end
20
+ ```
21
+ - Cleanup of `cattri.gemspec` and `bin/console`.
22
+
23
+ ## [0.2.0] - 2025-05-01
24
+
25
+ ### Changed
26
+
27
+ - Replaced `cattr` and `iattr` with unified `cattri` DSL
28
+ - All attributes now use `cattri`, with `scope: :class` or `scope: :instance`
29
+ - `iattr` and `cattr` are no longer public API
30
+
31
+ - Attribute behavior is now centralized via:
32
+ - `Cattri::Attribute` and `Cattri::AttributeOptions`
33
+ - `Cattri::Context` and `ContextRegistry`
34
+ - `Cattri::InternalStore` for safe write-once value storage
35
+
36
+ - Final attributes (`final: true`) now enforced at the store level, with safe write-once semantics
37
+ - Visibility and exposure are fully separated:
38
+ - `visibility: :public|:protected|:private` sets method scope
39
+ - `expose: :read_write|:read|:write|:none` controls which methods are generated
40
+ - New predicate handling via `predicate: true`, with visibility inheritance
41
+
42
+ ### Added
43
+
44
+ - Support for `scope:` to explicitly declare attribute scope
45
+ - `InitializerPatch` to apply default values for `final` instance attributes
46
+ - `memoize_default_value` helper to simplify accessor generation
47
+ - 100% RSpec coverage and branch coverage
48
+ - Steep RBS type signatures for public and internal API
49
+ - Full introspection via `.attributes`, `.attribute`, `.attribute_methods`, `.attribute_source`
50
+
51
+ ### Removed
52
+
53
+ - `iattr`, `cattr`, `iattr_alias`, `cattr_alias`, and setter helpers (`*_setter`)
54
+ - Legacy inheritance hook logic and module-style patching
55
+
56
+ ### Notes
57
+
58
+ 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.
59
+
60
+ This version introduces **breaking changes** to the DSL. Migration guide available in the README.
61
+
62
+ ---
63
+
1
64
  ## [0.1.3] - 2025-04-22
2
65
 
3
66
  ### 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,259 @@
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
53
+
54
+ # Final instance-level attribute
55
+ cattri :id, -> { SecureRandom.uuid }, final: true
67
56
 
68
- ## Defining attributes
57
+ # Writable instance-level attributes
58
+ cattri :name, "anonymous" do |value|
59
+ value.to_s.capitalize # custom setter/coercer
60
+ end
69
61
 
70
- ### Class attributes (`cattr`)
62
+ cattri :admin, false, predicate: true
71
63
 
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
64
+ def initialize(id)
65
+ self.id = id # set the value for `cattri :id`
66
+ end
80
67
  end
81
- ```
82
68
 
83
- ### Instance attributes (`iattr`)
69
+ # Class-level access
70
+ User.type # => :standard
71
+ User.config # => {}
84
72
 
85
- ```ruby
86
- iattr :token, default: -> { SecureRandom.hex(8) },
87
- reader: true,
88
- writer: false, # read‑only
89
- predicate: true
73
+ # Instance-level access
74
+ user = User.new
75
+ user.name # => "anonymous"
76
+ user.admin? # => false
77
+ user.id # => uuid
90
78
  ```
91
79
 
92
- Both forms accept:
80
+ ---
93
81
 
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)`
82
+ ## 👇 Accessing Attributes Within the Class
102
83
 
103
- If you pass a block, it’s treated as a **coercion setter** and receives the incoming value.
84
+ ```ruby
85
+ class User
86
+ include Cattri
104
87
 
105
- ---
88
+ cattri :id, -> { SecureRandom.uuid }, final: true
89
+ cattri :type, :standard, final: true, scope: :class
106
90
 
107
- ## Post-definition coercion with `*_setter`
91
+ def initialize(id)
92
+ self.id = id # Sets instance-level attribute
93
+ end
108
94
 
109
- If you define multiple attributes at once, you can't provide a coercion block inline:
95
+ def summary
96
+ "#{self.class.type}-#{id}" # Accesses class-level and instance-level attributes
97
+ end
110
98
 
111
- ```ruby
112
- cattr :foo, :bar, default: nil # cannot use block here
99
+ def self.default_type
100
+ type # Same as self.type resolves on the singleton
101
+ end
102
+ end
113
103
  ```
114
104
 
115
- Instead, define them first, then apply a coercion later using:
105
+ ---
116
106
 
117
- - `cattr_setter` for class attributes
118
- - `iattr_setter` for instance attributes
107
+ ## 🧭 Attribute Scope
119
108
 
120
- These allow you to attach or override the setter logic after the fact:
109
+ By default, attributes are defined per-instance. You can change this behavior using `scope:`.
121
110
 
122
111
  ```ruby
123
112
  class Config
124
113
  include Cattri
125
114
 
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
115
+ cattri :global_timeout, 30, scope: :class
116
+ cattri :retries, 3 # implicitly scope: :instance
135
117
  end
118
+
119
+ Config.global_timeout # => 30
120
+
121
+ instance = Config.new
122
+ instance.retries # => 3
123
+ instance.global_timeout # => NoMethodError
136
124
  ```
137
125
 
138
- Coercion is only applied when the attribute is written (via `=` or callable form), not when read.
126
+ - `scope: :class` defines the attribute on the class (i.e., the singleton).
127
+ - `scope: :instance` (or omitting scope) defines the attribute per instance.
128
+
129
+ ---
130
+
131
+ ## 🛡 Final Attributes
132
+
133
+ ```ruby
134
+ class Settings
135
+ include Cattri
136
+ cattri :version, -> { "1.0.0" }, final: true, scope: :class
137
+ end
139
138
 
140
- Attempting to use `*_setter` on an undefined attribute or one without a writer will raise:
139
+ Settings.version # => "1.0.0"
140
+ Settings.version = "2.0" # => Raises Cattri::AttributeError
141
+ ```
141
142
 
142
- - `Cattri::AttributeNotDefinedError` the attribute doesn't exist or wasn't fully defined
143
- - `Cattri::AttributeDefinitionError` the attribute is marked as readonly
143
+ - `final: true, scope: :class` defines a constant class-level attribute. It cannot be reassigned and uses the value provided at definition.
144
+ - `final: true` (with instance scope) defines a write-once attribute. If not explicitly set during initialization, the default value will be used.
144
145
 
145
- These APIs ensure your DSL stays consistent and extensible, even when bulk-declaring attributes up front.
146
+ > Note: `final_cattri` is a shorthand for `cattri(..., final: true)`, included for API symmetry but not required.
146
147
 
147
148
  ---
148
149
 
149
- ## Visibility tracking
150
+ ## 👁 Attribute Exposure
150
151
 
151
- Cattri watches calls to `public`, `protected`, and `private` while you define methods:
152
+ 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
153
 
153
154
  ```ruby
154
- class Secrets
155
+ class Profile
155
156
  include Cattri
156
157
 
157
- private
158
- cattr :api_key
158
+ cattri :name, "guest", expose: :read_write
159
+ cattri :token, "secret", expose: :read
160
+ cattri :attempts, 0, expose: :write
161
+ cattri :internal_flag, true, expose: :none
159
162
  end
160
-
161
- Secrets.private_methods.include?(:api_key) # => true
162
163
  ```
163
164
 
164
- No boilerplate—attributes inherit the visibility that was in effect at the call site.
165
+ ### Exposure Levels
166
+
167
+ - `:read_write` — defines both reader and writer
168
+ - `:read` — defines a reader only
169
+ - `:write` — defines a writer only
170
+ - `:none` — defines no public methods (internal only)
171
+
172
+ > Predicate methods (`admin?`, etc.) are enabled via `predicate: true`.
165
173
 
166
174
  ---
167
175
 
168
- ## Safe inheritance
176
+ ## 🔐 Visibility
169
177
 
170
- Subclassing copies both **metadata** and **current values**, using defensive `#dup` where possible and falling back safely when objects are frozen or not duplicable:
178
+ Cattri respects Ruby's `public`, `protected`, and `private` scoping when defining methods. You can also explicitly override visibility using `visibility:`.
171
179
 
172
180
  ```ruby
173
- class Base
181
+ class Document
174
182
  include Cattri
175
- cattr :settings, default: {}
176
- end
177
183
 
178
- class Child < Base; end
184
+ private
185
+ cattri :token
186
+
187
+ protected
188
+ cattri :internal_flag
189
+
190
+ public
191
+ cattri :title
179
192
 
180
- Base.settings[:foo] = 1
181
- Child.settings # => {} (isolated copy)
193
+ cattri :owner, "system", visibility: :protected
194
+ end
182
195
  ```
183
196
 
197
+ - If defined inside a visibility scope, Cattri applies that visibility automatically
198
+ - Use `visibility:` to override the inferred scope
199
+ - Applies only to generated methods (reader, writer, predicate), not internal store access
200
+
184
201
  ---
185
202
 
186
- ## Introspection helpers
203
+ ## 🔍 Introspection
187
204
 
188
- Add `include Cattri::Introspection` (or `extend` for class‑only use) to snapshot live values:
205
+ Enable introspection with:
189
206
 
190
207
  ```ruby
191
- Config.snapshot_cattrs # => { enabled: false, timeout: 5.0 }
192
- instance.snapshot_iattrs # => { name: "bob", age: 42 }
193
- ```
208
+ User.with_cattri_introspection
194
209
 
195
- Great for debugging or test assertions.
210
+ User.attributes # => [:type, :name, :admin]
211
+ User.attribute(:type).final? # => true
212
+ User.attribute_methods # => { type: [:type], name: [:name], admin: [:admin, :admin?] }
213
+ User.attribute_source(:name) # => User
214
+ ```
196
215
 
197
216
  ---
198
217
 
199
- ## Error handling
218
+ ## 📦 Installation
200
219
 
201
- All errors inherit from `Cattri::Error`, allowing a single rescue for any gem‑specific issue.
220
+ Add to your Gemfile:
202
221
 
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 |
222
+ ```ruby
223
+ gem "cattri"
224
+ ```
209
225
 
210
- Example:
226
+ Or via Bundler:
211
227
 
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
228
+ ```sh
229
+ bundle add cattri
224
230
  ```
225
231
 
226
232
  ---
227
233
 
228
- ## Comparison with standard patterns
234
+ ## 🧱 Design Overview
229
235
 
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.
236
+ Cattri includes:
233
237
 
234
- Cattri sits in the sweet spot: **micro‑sized (~300 LOC)**, dependency‑free, and purpose‑built for attribute declaration.
238
+ - `InternalStore` for final-safe value tracking
239
+ - `ContextRegistry` and `Context` for method definition logic
240
+ - `Attribute` and `AttributeOptions` for metadata handling
241
+ - `Visibility` tracking for DSL-defined methods
242
+ - `InitializerPatch` for final attribute enforcement on `#initialize`
243
+ - `Dsl` for `cattri` and `final_cattri`
244
+ - `Inheritance` to ensure subclass copying
235
245
 
236
246
  ---
237
247
 
238
- ## Testing tips
248
+ ## 🧪 Test Coverage
249
+
250
+ Cattri is tested with 100% line and branch coverage. All dynamic definitions are validated via RSpec, and edge cases are covered, including:
239
251
 
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.
252
+ - Predicate methods
253
+ - Final value enforcement
254
+ - Class vs. instance scope
255
+ - Attribute inheritance
256
+ - Visibility and expose interaction
242
257
 
243
258
  ---
244
259
 
@@ -247,13 +262,13 @@ Cattri sits in the sweet spot: **micro‑sized (~300 LOC)**, dependency‑free,
247
262
  1. Fork the repo
248
263
  2. `bundle install`
249
264
  3. Run the test suite with `bundle exec rake`
250
- 4. Submit a pull request – ensure new code is covered and **rubocop** passes.
265
+ 4. Submit a pull request – ensure new code is covered and **rubocop** passes
251
266
 
252
267
  ---
253
268
 
254
269
  ## License
255
270
 
256
- This gem is released under the MIT License – see See [LICENSE](LICENSE) for details.
271
+ This gem is released under the MIT License – see [LICENSE](LICENSE) for details.
257
272
 
258
273
  ## 🙏 Credits
259
274
 
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,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "cattri"
6
+
7
+ require "irb"
8
+ 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