cattri 0.1.2 → 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 +50 -0
  6. data/Gemfile +12 -0
  7. data/README.md +163 -144
  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 -153
  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 -124
  62. data/lib/cattri/class_attributes.rb +0 -204
  63. data/lib/cattri/instance_attributes.rb +0 -226
  64. data/sig/cattri.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4558aae18122f29cebf5ddb34fe452ee6b00898d994b4cd8f2b0a825c5a599aa
4
- data.tar.gz: '092f86968324144a229510426858c9a4cee0245267503a160e27b6087384e88e'
3
+ metadata.gz: 9de3a522be8c55a1b05edcc432f04d0352f0661290d6e7947b20e9399dde1462
4
+ data.tar.gz: d5b090040ea3e975c10342c8ac6d0ba3bfbd6b9bc1b3b8b50107c5ba12a13640
5
5
  SHA512:
6
- metadata.gz: 61ac08cebe59853a0a4c163052bdabef6e70db7476bf0d93ff4a1184b706e326fab8583ad6b77533f7c297e5be15cd15cd3414ef8c751443d4f1ed8a06b768a6
7
- data.tar.gz: dc18a8a83dc56d9a4c2b122c3f824808f8037f0d1cb08a2d71e82ef6aa933d73c75efa083df12cb4b5a7dfb30a0ce22c0558871fe07365d73b601b2e32b537fd
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,53 @@
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
+
42
+ ## [0.1.3] - 2025-04-22
43
+
44
+ ### Added
45
+
46
+ - ✅ Support for `predicate: true` on both `iattr` and `cattr` — defines a `:name?` method returning `!!send(:name)`
47
+ - ✅ `iattr_alias` and `cattr_alias` — define alias methods that delegate to existing attributes (e.g., `:foo?` for `:foo`)
48
+ - Predicate methods inherit visibility from the original attribute and are excluded from introspection (`iattrs`, `cattrs`)
49
+ - Raised error when attempting to define an attribute ending in `?`, with guidance to use `predicate: true` or `*_alias`
50
+
1
51
  ## [0.1.2] - 2025-04-22
2
52
 
3
53
  ### 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,237 +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
+
5
+ It offers subclass-safe inheritance, lazy or static defaults, optional coercion, and write-once (`final`) semantics, while remaining lightweight and idiomatic.
6
+
7
+ ---
8
+
9
+ ## ✨ Features
10
+
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
4
20
 
5
21
  ---
6
22
 
7
- ## Why another attribute DSL?
23
+ ## 💡 Why Use Cattri?
8
24
 
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) | ✅ |
25
+ Ruby's built-in attribute helpers and Rails' `class_attribute` are either too limited or too invasive. Cattri offers:
20
26
 
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.
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 | ✅ | ❌ | ❌ |
22
35
 
23
36
  ---
24
37
 
25
- ## Installation
38
+ ## 🚀 Usage Examples
26
39
 
27
- ```bash
28
- bundle add cattri # Ruby ≥ 2.7
29
- ```
40
+ Cattri uses a single DSL method, `cattri`, to define both class-level and instance-level attributes.
30
41
 
31
- Or in your Gemfile:
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`.
32
43
 
33
44
  ```ruby
34
- gem "cattri"
35
- ```
45
+ class User
46
+ include Cattri
36
47
 
37
- ---
48
+ # Final class-level attribute
49
+ cattri :type, :standard, final: true, scope: :class
38
50
 
39
- ## Quick start
51
+ # Writable class-level attribute
52
+ cattri :config, -> { {} }, scope: :class
40
53
 
41
- ```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
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 :age, default: 0 do |val| # coercion block
54
- Integer(val)
54
+ # Final instance-level attribute
55
+ cattri :id, -> { SecureRandom.uuid }, final: true
56
+
57
+ # Writable instance-level attributes
58
+ cattri :name, "anonymous"
59
+ cattri :admin, false, predicate: true
60
+
61
+ def initialize(id)
62
+ self.id = id # set the value for `cattri :id`
55
63
  end
56
64
  end
57
65
 
58
- Config.enabled # => true
59
- Config.enabled = false
60
- Config.new.age = "42" # => 42
66
+ # Class-level access
67
+ User.type # => :standard
68
+ User.config # => {}
69
+
70
+ # Instance-level access
71
+ user = User.new
72
+ user.name # => "anonymous"
73
+ user.admin? # => false
74
+ user.id # => uuid
61
75
  ```
62
76
 
63
77
  ---
64
78
 
65
- ## Defining attributes
66
-
67
- ### Class attributes (`cattr`)
79
+ ## 👇 Accessing Attributes Within the Class
68
80
 
69
81
  ```ruby
70
- cattr :log_level, default: :info,
71
- access: :protected, # respects current visibility by default
72
- readonly: false,
73
- instance_reader: true do |value|
74
- value.to_sym
75
- end
76
- ```
77
-
78
- ### Instance attributes (`iattr`)
82
+ class User
83
+ include Cattri
79
84
 
80
- ```ruby
81
- iattr :token, default: -> { SecureRandom.hex(8) },
82
- reader: true,
83
- writer: false # read‑only
84
- ```
85
+ cattri :id, -> { SecureRandom.uuid }, final: true
86
+ cattri :type, :standard, final: true, scope: :class
85
87
 
86
- Both forms accept:
88
+ def initialize(id)
89
+ self.id = id # Sets instance-level attribute
90
+ end
87
91
 
88
- | Option | Purpose |
89
- | ------ | ------- |
90
- | `default:` | Static value or callable (`Proc`) evaluated lazily. |
91
- | `access:` | Override inferred visibility (`:public`, `:protected`, `:private`). |
92
- | `reader:` / `writer:` | Disable reader or writer for instance attributes. |
93
- | `readonly:` | Shorthand for class attributes (`writer` is always present). |
94
- | `instance_reader:` | Expose class attribute as instance reader (default: **true**). |
92
+ def summary
93
+ "#{self.class.type}-#{id}" # Accesses class-level and instance-level attributes
94
+ end
95
95
 
96
- If you pass a block, it’s treated as a **coercion setter** and receives the incoming value.
96
+ def self.default_type
97
+ type # Same as self.type — resolves on the singleton
98
+ end
99
+ end
100
+ ```
97
101
 
98
102
  ---
99
103
 
100
- ## Post-definition coercion with `*_setter`
104
+ ## 🧭 Attribute Scope
101
105
 
102
- If you define multiple attributes at once, you can't provide a coercion block inline:
106
+ By default, attributes are defined per-instance. You can change this behavior using `scope:`.
103
107
 
104
108
  ```ruby
105
- cattr :foo, :bar, default: nil # ❌ cannot use block here
109
+ class Config
110
+ include Cattri
111
+
112
+ cattri :global_timeout, 30, scope: :class
113
+ cattri :retries, 3 # implicitly scope: :instance
114
+ end
115
+
116
+ Config.global_timeout # => 30
117
+
118
+ instance = Config.new
119
+ instance.retries # => 3
120
+ instance.global_timeout # => NoMethodError
106
121
  ```
107
122
 
108
- Instead, define them first, then apply a coercion later using:
123
+ - `scope: :class` defines the attribute on the class (i.e., the singleton).
124
+ - `scope: :instance` (or omitting scope) defines the attribute per instance.
109
125
 
110
- - `cattr_setter` for class attributes
111
- - `iattr_setter` for instance attributes
126
+ ---
112
127
 
113
- These allow you to attach or override the setter logic after the fact:
128
+ ## 🛡 Final Attributes
114
129
 
115
130
  ```ruby
116
- class Config
131
+ class Settings
117
132
  include Cattri
118
-
119
- cattr :log_level
120
- cattr_setter :log_level do |val|
121
- val.to_s.downcase.to_sym
122
- end
123
-
124
- iattr_writer :token
125
- iattr_setter :token do |val|
126
- val.strip
127
- end
133
+ cattri :version, -> { "1.0.0" }, final: true, scope: :class
128
134
  end
129
- ```
130
-
131
- Coercion is only applied when the attribute is written (via `=` or callable form), not when read.
132
135
 
133
- Attempting to use `*_setter` on an undefined attribute or one without a writer will raise:
136
+ Settings.version # => "1.0.0"
137
+ Settings.version = "2.0" # => Raises Cattri::AttributeError
138
+ ```
134
139
 
135
- - `Cattri::AttributeNotDefinedError` the attribute doesn't exist or wasn't fully defined
136
- - `Cattri::AttributeDefinitionError` the attribute is marked as readonly
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.
137
142
 
138
- These APIs ensure your DSL stays consistent and extensible, even when bulk-declaring attributes up front.
143
+ > Note: `final_cattri` is a shorthand for `cattri(..., final: true)`, included for API symmetry but not required.
139
144
 
140
145
  ---
141
146
 
142
- ## Visibility tracking
147
+ ## 👁 Attribute Exposure
143
148
 
144
- 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.
145
150
 
146
151
  ```ruby
147
- class Secrets
152
+ class Profile
148
153
  include Cattri
149
154
 
150
- private
151
- 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
152
159
  end
153
-
154
- Secrets.private_methods.include?(:api_key) # => true
155
160
  ```
156
161
 
157
- 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`.
158
170
 
159
171
  ---
160
172
 
161
- ## Safe inheritance
173
+ ## 🔐 Visibility
162
174
 
163
- 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:`.
164
176
 
165
177
  ```ruby
166
- class Base
178
+ class Document
167
179
  include Cattri
168
- cattr :settings, default: {}
169
- end
170
180
 
171
- class Child < Base; end
181
+ private
182
+ cattri :token
183
+
184
+ protected
185
+ cattri :internal_flag
186
+
187
+ public
188
+ cattri :title
172
189
 
173
- Base.settings[:foo] = 1
174
- Child.settings # => {} (isolated copy)
190
+ cattri :owner, "system", visibility: :protected
191
+ end
175
192
  ```
176
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
+
177
198
  ---
178
199
 
179
- ## Introspection helpers
200
+ ## 🔍 Introspection
180
201
 
181
- Add `include Cattri::Introspection` (or `extend` for class‑only use) to snapshot live values:
202
+ Enable introspection with:
182
203
 
183
204
  ```ruby
184
- Config.snapshot_cattrs # => { enabled: false, timeout: 5.0 }
185
- instance.snapshot_iattrs # => { name: "bob", age: 42 }
186
- ```
205
+ User.with_cattri_introspection
187
206
 
188
- 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
+ ```
189
212
 
190
213
  ---
191
214
 
192
- ## Error handling
215
+ ## 📦 Installation
193
216
 
194
- All errors inherit from `Cattri::Error`, allowing a single rescue for any gem‑specific issue.
217
+ Add to your Gemfile:
195
218
 
196
- | Error class | Raised when… |
197
- |-------------|--------------|
198
- | `Cattri::AttributeDefinedError` | an attribute is declared twice on the same level |
199
- | `Cattri::AttributeDefinitionError` | method generation (`define_method`) fails |
200
- | `Cattri::UnsupportedTypeError` | an internal API receives an unknown type |
201
- | `Cattri::AttributeError` | generic superclass for attribute‑related issues |
219
+ ```ruby
220
+ gem "cattri"
221
+ ```
202
222
 
203
- Example:
223
+ Or via Bundler:
204
224
 
205
- ```ruby
206
- begin
207
- class Foo
208
- include Cattri
209
- cattr :foo
210
- cattr :foo # duplicate
211
- end
212
- rescue Cattri::AttributeDefinedError => e
213
- warn e.message # => "Class attribute :foo has already been defined"
214
- rescue Cattri::AttributeError => e
215
- warn e.message # => Catch-all for any error raised within attributes
216
- end
225
+ ```sh
226
+ bundle add cattri
217
227
  ```
218
228
 
219
229
  ---
220
230
 
221
- ## Comparison with standard patterns
231
+ ## 🧱 Design Overview
222
232
 
223
- * **Core Ruby macros** (`attr_accessor`, `cattr_accessor`) are simple but global—attributes bleed into subclasses and lack defaults or coercion.
224
- * **ActiveSupport** extends the API but still relies on mutable class variables and offers no visibility control.
225
- * **Dry‑configurable** is robust yet heavyweight when you only need a handful of attributes outside a full config object.
233
+ Cattri includes:
226
234
 
227
- 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
228
242
 
229
243
  ---
230
244
 
231
- ## 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:
232
248
 
233
- * Use `include Cattri::Introspection` in spec helper files to capture snapshots before/after mutations.
234
- * 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
235
254
 
236
255
  ---
237
256
 
@@ -240,13 +259,13 @@ Cattri sits in the sweet spot: **micro‑sized (~300 LOC)**, dependency‑free,
240
259
  1. Fork the repo
241
260
  2. `bundle install`
242
261
  3. Run the test suite with `bundle exec rake`
243
- 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
244
263
 
245
264
  ---
246
265
 
247
266
  ## License
248
267
 
249
- 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.
250
269
 
251
270
  ## 🙏 Credits
252
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