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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +34 -0
- data/.gitignore +72 -0
- data/.rubocop.yml +6 -3
- data/CHANGELOG.md +50 -0
- data/Gemfile +12 -0
- data/README.md +163 -144
- data/Steepfile +6 -0
- data/bin/console +33 -0
- data/bin/setup +8 -0
- data/cattri.gemspec +5 -5
- data/lib/cattri/attribute.rb +119 -153
- 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 +37 -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 +105 -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 +95 -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 +55 -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 +19 -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 -124
- data/lib/cattri/class_attributes.rb +0 -204
- data/lib/cattri/instance_attributes.rb +0 -226
- 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: 9de3a522be8c55a1b05edcc432f04d0352f0661290d6e7947b20e9399dde1462
|
4
|
+
data.tar.gz: d5b090040ea3e975c10342c8ac6d0ba3bfbd6b9bc1b3b8b50107c5ba12a13640
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/README.md
CHANGED
@@ -1,237 +1,256 @@
|
|
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
|
+
|
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
|
23
|
+
## 💡 Why Use Cattri?
|
8
24
|
|
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) | ✅ |
|
25
|
+
Ruby's built-in attribute helpers and Rails' `class_attribute` are either too limited or too invasive. Cattri offers:
|
20
26
|
|
21
|
-
|
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
|
-
##
|
38
|
+
## 🚀 Usage Examples
|
26
39
|
|
27
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
51
|
+
# Writable class-level attribute
|
52
|
+
cattri :config, -> { {} }, scope: :class
|
40
53
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
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
|
-
##
|
66
|
-
|
67
|
-
### Class attributes (`cattr`)
|
79
|
+
## 👇 Accessing Attributes Within the Class
|
68
80
|
|
69
81
|
```ruby
|
70
|
-
|
71
|
-
|
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
|
-
|
81
|
-
|
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
|
-
|
88
|
+
def initialize(id)
|
89
|
+
self.id = id # Sets instance-level attribute
|
90
|
+
end
|
87
91
|
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
-
##
|
104
|
+
## 🧭 Attribute Scope
|
101
105
|
|
102
|
-
|
106
|
+
By default, attributes are defined per-instance. You can change this behavior using `scope:`.
|
103
107
|
|
104
108
|
```ruby
|
105
|
-
|
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
|
-
|
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
|
-
|
111
|
-
- `iattr_setter` for instance attributes
|
126
|
+
---
|
112
127
|
|
113
|
-
|
128
|
+
## 🛡 Final Attributes
|
114
129
|
|
115
130
|
```ruby
|
116
|
-
class
|
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
|
-
|
136
|
+
Settings.version # => "1.0.0"
|
137
|
+
Settings.version = "2.0" # => Raises Cattri::AttributeError
|
138
|
+
```
|
134
139
|
|
135
|
-
- `
|
136
|
-
- `
|
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
|
-
|
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
|
-
##
|
147
|
+
## 👁 Attribute Exposure
|
143
148
|
|
144
|
-
|
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
|
152
|
+
class Profile
|
148
153
|
include Cattri
|
149
154
|
|
150
|
-
|
151
|
-
|
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
|
-
|
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
|
-
##
|
173
|
+
## 🔐 Visibility
|
162
174
|
|
163
|
-
|
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
|
178
|
+
class Document
|
167
179
|
include Cattri
|
168
|
-
cattr :settings, default: {}
|
169
|
-
end
|
170
180
|
|
171
|
-
|
181
|
+
private
|
182
|
+
cattri :token
|
183
|
+
|
184
|
+
protected
|
185
|
+
cattri :internal_flag
|
186
|
+
|
187
|
+
public
|
188
|
+
cattri :title
|
172
189
|
|
173
|
-
|
174
|
-
|
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
|
200
|
+
## 🔍 Introspection
|
180
201
|
|
181
|
-
|
202
|
+
Enable introspection with:
|
182
203
|
|
183
204
|
```ruby
|
184
|
-
|
185
|
-
instance.snapshot_iattrs # => { name: "bob", age: 42 }
|
186
|
-
```
|
205
|
+
User.with_cattri_introspection
|
187
206
|
|
188
|
-
|
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
|
-
##
|
215
|
+
## 📦 Installation
|
193
216
|
|
194
|
-
|
217
|
+
Add to your Gemfile:
|
195
218
|
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
223
|
+
Or via Bundler:
|
204
224
|
|
205
|
-
```
|
206
|
-
|
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
|
-
##
|
231
|
+
## 🧱 Design Overview
|
222
232
|
|
223
|
-
|
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
|
-
|
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
|
-
##
|
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
|
-
|
234
|
-
|
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
|
268
|
+
This gem is released under the MIT License – see [LICENSE](LICENSE) for details.
|
250
269
|
|
251
270
|
## 🙏 Credits
|
252
271
|
|
data/Steepfile
ADDED
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__)
|