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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +34 -0
- data/.gitignore +72 -0
- data/.rubocop.yml +6 -3
- data/CHANGELOG.md +63 -0
- data/Gemfile +12 -0
- data/README.md +166 -151
- data/Steepfile +6 -0
- data/bin/console +8 -0
- data/bin/setup +8 -0
- data/cattri.gemspec +5 -5
- data/lib/cattri/attribute.rb +119 -155
- data/lib/cattri/attribute_compiler.rb +104 -0
- data/lib/cattri/attribute_options.rb +183 -0
- data/lib/cattri/attribute_registry.rb +155 -0
- data/lib/cattri/context.rb +124 -106
- data/lib/cattri/context_registry.rb +36 -0
- data/lib/cattri/deferred_attributes.rb +73 -0
- data/lib/cattri/dsl.rb +54 -0
- data/lib/cattri/error.rb +17 -90
- data/lib/cattri/inheritance.rb +35 -0
- data/lib/cattri/initializer_patch.rb +38 -0
- data/lib/cattri/internal_store.rb +104 -0
- data/lib/cattri/introspection.rb +56 -49
- data/lib/cattri/version.rb +3 -1
- data/lib/cattri.rb +38 -99
- data/sig/lib/cattri/attribute.rbs +115 -0
- data/sig/lib/cattri/attribute_compiler.rbs +61 -0
- data/sig/lib/cattri/attribute_options.rbs +150 -0
- data/sig/lib/cattri/attribute_registry.rbs +101 -0
- data/sig/lib/cattri/context.rbs +130 -0
- data/sig/lib/cattri/context_registry.rbs +31 -0
- data/sig/lib/cattri/deferred_attributes.rbs +53 -0
- data/sig/lib/cattri/dsl.rbs +66 -0
- data/sig/lib/cattri/error.rbs +28 -0
- data/sig/lib/cattri/inheritance.rbs +21 -0
- data/sig/lib/cattri/initializer_patch.rbs +26 -0
- data/sig/lib/cattri/internal_store.rbs +75 -0
- data/sig/lib/cattri/introspection.rbs +61 -0
- data/sig/lib/cattri/types.rbs +9 -0
- data/sig/lib/cattri/visibility.rbs +55 -0
- data/sig/lib/cattri.rbs +37 -0
- data/spec/cattri/attribute_compiler_spec.rb +179 -0
- data/spec/cattri/attribute_options_spec.rb +267 -0
- data/spec/cattri/attribute_registry_spec.rb +257 -0
- data/spec/cattri/attribute_spec.rb +297 -0
- data/spec/cattri/context_registry_spec.rb +45 -0
- data/spec/cattri/context_spec.rb +346 -0
- data/spec/cattri/deferred_attrributes_spec.rb +117 -0
- data/spec/cattri/dsl_spec.rb +69 -0
- data/spec/cattri/error_spec.rb +37 -0
- data/spec/cattri/inheritance_spec.rb +60 -0
- data/spec/cattri/initializer_patch_spec.rb +35 -0
- data/spec/cattri/internal_store_spec.rb +139 -0
- data/spec/cattri/introspection_spec.rb +90 -0
- data/spec/cattri/visibility_spec.rb +68 -0
- data/spec/cattri_spec.rb +54 -0
- data/spec/simplecov_helper.rb +21 -0
- data/spec/spec_helper.rb +16 -0
- metadata +79 -6
- data/lib/cattri/attribute_definer.rb +0 -143
- data/lib/cattri/class_attributes.rb +0 -277
- data/lib/cattri/instance_attributes.rb +0 -276
- data/sig/cattri.rbs +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6570d6da579ae6a15c865884a895a1ef0923eafd610d2a97a474d4a888af5778
|
4
|
+
data.tar.gz: 6f01dcc2490c4c4ef159d5fcf88c87ab50204c293ce403e87440b87130add491
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/README.md
CHANGED
@@ -1,244 +1,259 @@
|
|
1
1
|
# Cattri
|
2
2
|
|
3
|
-
|
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
|
-
|
7
|
+
---
|
8
8
|
|
9
|
-
|
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
|
-
|
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
|
-
##
|
26
|
-
|
27
|
-
```bash
|
28
|
-
bundle add cattri # Ruby ≥ 2.7
|
29
|
-
```
|
23
|
+
## 💡 Why Use Cattri?
|
30
24
|
|
31
|
-
|
25
|
+
Ruby's built-in attribute helpers and Rails' `class_attribute` are either too limited or too invasive. Cattri offers:
|
32
26
|
|
33
|
-
|
34
|
-
|
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
|
-
##
|
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
|
43
|
-
include Cattri
|
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
|
-
|
60
|
-
|
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
|
-
|
57
|
+
# Writable instance-level attributes
|
58
|
+
cattri :name, "anonymous" do |value|
|
59
|
+
value.to_s.capitalize # custom setter/coercer
|
60
|
+
end
|
69
61
|
|
70
|
-
|
62
|
+
cattri :admin, false, predicate: true
|
71
63
|
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
69
|
+
# Class-level access
|
70
|
+
User.type # => :standard
|
71
|
+
User.config # => {}
|
84
72
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
80
|
+
---
|
93
81
|
|
94
|
-
|
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
|
-
|
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
|
-
|
91
|
+
def initialize(id)
|
92
|
+
self.id = id # Sets instance-level attribute
|
93
|
+
end
|
108
94
|
|
109
|
-
|
95
|
+
def summary
|
96
|
+
"#{self.class.type}-#{id}" # Accesses class-level and instance-level attributes
|
97
|
+
end
|
110
98
|
|
111
|
-
|
112
|
-
|
99
|
+
def self.default_type
|
100
|
+
type # Same as self.type — resolves on the singleton
|
101
|
+
end
|
102
|
+
end
|
113
103
|
```
|
114
104
|
|
115
|
-
|
105
|
+
---
|
116
106
|
|
117
|
-
|
118
|
-
- `iattr_setter` for instance attributes
|
107
|
+
## 🧭 Attribute Scope
|
119
108
|
|
120
|
-
|
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
|
-
|
127
|
-
|
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
|
-
|
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
|
-
|
139
|
+
Settings.version # => "1.0.0"
|
140
|
+
Settings.version = "2.0" # => Raises Cattri::AttributeError
|
141
|
+
```
|
141
142
|
|
142
|
-
- `
|
143
|
-
- `
|
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
|
-
|
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
|
-
##
|
150
|
+
## 👁 Attribute Exposure
|
150
151
|
|
151
|
-
|
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
|
155
|
+
class Profile
|
155
156
|
include Cattri
|
156
157
|
|
157
|
-
|
158
|
-
|
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
|
-
|
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
|
-
##
|
176
|
+
## 🔐 Visibility
|
169
177
|
|
170
|
-
|
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
|
181
|
+
class Document
|
174
182
|
include Cattri
|
175
|
-
cattr :settings, default: {}
|
176
|
-
end
|
177
183
|
|
178
|
-
|
184
|
+
private
|
185
|
+
cattri :token
|
186
|
+
|
187
|
+
protected
|
188
|
+
cattri :internal_flag
|
189
|
+
|
190
|
+
public
|
191
|
+
cattri :title
|
179
192
|
|
180
|
-
|
181
|
-
|
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
|
203
|
+
## 🔍 Introspection
|
187
204
|
|
188
|
-
|
205
|
+
Enable introspection with:
|
189
206
|
|
190
207
|
```ruby
|
191
|
-
|
192
|
-
instance.snapshot_iattrs # => { name: "bob", age: 42 }
|
193
|
-
```
|
208
|
+
User.with_cattri_introspection
|
194
209
|
|
195
|
-
|
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
|
-
##
|
218
|
+
## 📦 Installation
|
200
219
|
|
201
|
-
|
220
|
+
Add to your Gemfile:
|
202
221
|
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
226
|
+
Or via Bundler:
|
211
227
|
|
212
|
-
```
|
213
|
-
|
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
|
-
##
|
234
|
+
## 🧱 Design Overview
|
229
235
|
|
230
|
-
|
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
|
-
|
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
|
-
##
|
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
|
-
|
241
|
-
|
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
|
271
|
+
This gem is released under the MIT License – see [LICENSE](LICENSE) for details.
|
257
272
|
|
258
273
|
## 🙏 Credits
|
259
274
|
|
data/Steepfile
ADDED
data/bin/console
ADDED