cattri 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +34 -0
- data/.gitignore +72 -0
- data/.rubocop.yml +6 -3
- data/CHANGELOG.md +41 -0
- data/Gemfile +12 -0
- data/README.md +163 -151
- 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 -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 +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 -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: 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,44 @@
|
|
1
|
+
## [0.2.0] - 2025-05-01
|
2
|
+
|
3
|
+
### Changed
|
4
|
+
|
5
|
+
- Replaced `cattr` and `iattr` with unified `cattri` DSL
|
6
|
+
- All attributes now use `cattri`, with `scope: :class` or `scope: :instance`
|
7
|
+
- `iattr` and `cattr` are no longer public API
|
8
|
+
|
9
|
+
- Attribute behavior is now centralized via:
|
10
|
+
- `Cattri::Attribute` and `Cattri::AttributeOptions`
|
11
|
+
- `Cattri::Context` and `ContextRegistry`
|
12
|
+
- `Cattri::InternalStore` for safe write-once value storage
|
13
|
+
|
14
|
+
- Final attributes (`final: true`) now enforced at the store level, with safe write-once semantics
|
15
|
+
- Visibility and exposure are fully separated:
|
16
|
+
- `visibility: :public|:protected|:private` sets method scope
|
17
|
+
- `expose: :read_write|:read|:write|:none` controls which methods are generated
|
18
|
+
- New predicate handling via `predicate: true`, with visibility inheritance
|
19
|
+
|
20
|
+
### Added
|
21
|
+
|
22
|
+
- Support for `scope:` to explicitly declare attribute scope
|
23
|
+
- `InitializerPatch` to apply default values for `final` instance attributes
|
24
|
+
- `memoize_default_value` helper to simplify accessor generation
|
25
|
+
- 100% RSpec coverage and branch coverage
|
26
|
+
- Steep RBS type signatures for public and internal API
|
27
|
+
- Full introspection via `.attributes`, `.attribute`, `.attribute_methods`, `.attribute_source`
|
28
|
+
|
29
|
+
### Removed
|
30
|
+
|
31
|
+
- `iattr`, `cattr`, `iattr_alias`, `cattr_alias`, and setter helpers (`*_setter`)
|
32
|
+
- Legacy inheritance hook logic and module-style patching
|
33
|
+
|
34
|
+
### Notes
|
35
|
+
|
36
|
+
This release consolidates and simplifies the attribute system into a modern, safer, and more flexible DSL. All existing functionality is preserved through the `cattri` interface.
|
37
|
+
|
38
|
+
This version introduces **breaking changes** to the DSL. Migration guide available in the README.
|
39
|
+
|
40
|
+
---
|
41
|
+
|
1
42
|
## [0.1.3] - 2025-04-22
|
2
43
|
|
3
44
|
### Added
|
data/Gemfile
ADDED
data/README.md
CHANGED
@@ -1,244 +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
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
|
67
53
|
|
68
|
-
|
54
|
+
# Final instance-level attribute
|
55
|
+
cattri :id, -> { SecureRandom.uuid }, final: true
|
69
56
|
|
70
|
-
|
57
|
+
# Writable instance-level attributes
|
58
|
+
cattri :name, "anonymous"
|
59
|
+
cattri :admin, false, predicate: true
|
71
60
|
|
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
|
61
|
+
def initialize(id)
|
62
|
+
self.id = id # set the value for `cattri :id`
|
63
|
+
end
|
80
64
|
end
|
81
|
-
```
|
82
65
|
|
83
|
-
|
66
|
+
# Class-level access
|
67
|
+
User.type # => :standard
|
68
|
+
User.config # => {}
|
84
69
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
70
|
+
# Instance-level access
|
71
|
+
user = User.new
|
72
|
+
user.name # => "anonymous"
|
73
|
+
user.admin? # => false
|
74
|
+
user.id # => uuid
|
90
75
|
```
|
91
76
|
|
92
|
-
|
77
|
+
---
|
93
78
|
|
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)`
|
79
|
+
## 👇 Accessing Attributes Within the Class
|
102
80
|
|
103
|
-
|
81
|
+
```ruby
|
82
|
+
class User
|
83
|
+
include Cattri
|
104
84
|
|
105
|
-
|
85
|
+
cattri :id, -> { SecureRandom.uuid }, final: true
|
86
|
+
cattri :type, :standard, final: true, scope: :class
|
106
87
|
|
107
|
-
|
88
|
+
def initialize(id)
|
89
|
+
self.id = id # Sets instance-level attribute
|
90
|
+
end
|
108
91
|
|
109
|
-
|
92
|
+
def summary
|
93
|
+
"#{self.class.type}-#{id}" # Accesses class-level and instance-level attributes
|
94
|
+
end
|
110
95
|
|
111
|
-
|
112
|
-
|
96
|
+
def self.default_type
|
97
|
+
type # Same as self.type — resolves on the singleton
|
98
|
+
end
|
99
|
+
end
|
113
100
|
```
|
114
101
|
|
115
|
-
|
102
|
+
---
|
116
103
|
|
117
|
-
|
118
|
-
- `iattr_setter` for instance attributes
|
104
|
+
## 🧭 Attribute Scope
|
119
105
|
|
120
|
-
|
106
|
+
By default, attributes are defined per-instance. You can change this behavior using `scope:`.
|
121
107
|
|
122
108
|
```ruby
|
123
109
|
class Config
|
124
110
|
include Cattri
|
125
111
|
|
126
|
-
|
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
|
112
|
+
cattri :global_timeout, 30, scope: :class
|
113
|
+
cattri :retries, 3 # implicitly scope: :instance
|
135
114
|
end
|
115
|
+
|
116
|
+
Config.global_timeout # => 30
|
117
|
+
|
118
|
+
instance = Config.new
|
119
|
+
instance.retries # => 3
|
120
|
+
instance.global_timeout # => NoMethodError
|
136
121
|
```
|
137
122
|
|
138
|
-
|
123
|
+
- `scope: :class` defines the attribute on the class (i.e., the singleton).
|
124
|
+
- `scope: :instance` (or omitting scope) defines the attribute per instance.
|
139
125
|
|
140
|
-
|
126
|
+
---
|
141
127
|
|
142
|
-
|
143
|
-
- `Cattri::AttributeDefinitionError` – the attribute is marked as readonly
|
128
|
+
## 🛡 Final Attributes
|
144
129
|
|
145
|
-
|
130
|
+
```ruby
|
131
|
+
class Settings
|
132
|
+
include Cattri
|
133
|
+
cattri :version, -> { "1.0.0" }, final: true, scope: :class
|
134
|
+
end
|
135
|
+
|
136
|
+
Settings.version # => "1.0.0"
|
137
|
+
Settings.version = "2.0" # => Raises Cattri::AttributeError
|
138
|
+
```
|
139
|
+
|
140
|
+
- `final: true, scope: :class` defines a constant class-level attribute. It cannot be reassigned and uses the value provided at definition.
|
141
|
+
- `final: true` (with instance scope) defines a write-once attribute. If not explicitly set during initialization, the default value will be used.
|
142
|
+
|
143
|
+
> Note: `final_cattri` is a shorthand for `cattri(..., final: true)`, included for API symmetry but not required.
|
146
144
|
|
147
145
|
---
|
148
146
|
|
149
|
-
##
|
147
|
+
## 👁 Attribute Exposure
|
150
148
|
|
151
|
-
|
149
|
+
The `expose:` option controls what public methods are generated for an attribute. You can fine-tune whether the reader, writer, or neither is available.
|
152
150
|
|
153
151
|
```ruby
|
154
|
-
class
|
152
|
+
class Profile
|
155
153
|
include Cattri
|
156
154
|
|
157
|
-
|
158
|
-
|
155
|
+
cattri :name, "guest", expose: :read_write
|
156
|
+
cattri :token, "secret", expose: :read
|
157
|
+
cattri :attempts, 0, expose: :write
|
158
|
+
cattri :internal_flag, true, expose: :none
|
159
159
|
end
|
160
|
-
|
161
|
-
Secrets.private_methods.include?(:api_key) # => true
|
162
160
|
```
|
163
161
|
|
164
|
-
|
162
|
+
### Exposure Levels
|
163
|
+
|
164
|
+
- `:read_write` — defines both reader and writer
|
165
|
+
- `:read` — defines a reader only
|
166
|
+
- `:write` — defines a writer only
|
167
|
+
- `:none` — defines no public methods (internal only)
|
168
|
+
|
169
|
+
> Predicate methods (`admin?`, etc.) are enabled via `predicate: true`.
|
165
170
|
|
166
171
|
---
|
167
172
|
|
168
|
-
##
|
173
|
+
## 🔐 Visibility
|
169
174
|
|
170
|
-
|
175
|
+
Cattri respects Ruby's `public`, `protected`, and `private` scoping when defining methods. You can also explicitly override visibility using `visibility:`.
|
171
176
|
|
172
177
|
```ruby
|
173
|
-
class
|
178
|
+
class Document
|
174
179
|
include Cattri
|
175
|
-
cattr :settings, default: {}
|
176
|
-
end
|
177
180
|
|
178
|
-
|
181
|
+
private
|
182
|
+
cattri :token
|
183
|
+
|
184
|
+
protected
|
185
|
+
cattri :internal_flag
|
186
|
+
|
187
|
+
public
|
188
|
+
cattri :title
|
179
189
|
|
180
|
-
|
181
|
-
|
190
|
+
cattri :owner, "system", visibility: :protected
|
191
|
+
end
|
182
192
|
```
|
183
193
|
|
194
|
+
- If defined inside a visibility scope, Cattri applies that visibility automatically
|
195
|
+
- Use `visibility:` to override the inferred scope
|
196
|
+
- Applies only to generated methods (reader, writer, predicate), not internal store access
|
197
|
+
|
184
198
|
---
|
185
199
|
|
186
|
-
## Introspection
|
200
|
+
## 🔍 Introspection
|
187
201
|
|
188
|
-
|
202
|
+
Enable introspection with:
|
189
203
|
|
190
204
|
```ruby
|
191
|
-
|
192
|
-
instance.snapshot_iattrs # => { name: "bob", age: 42 }
|
193
|
-
```
|
205
|
+
User.with_cattri_introspection
|
194
206
|
|
195
|
-
|
207
|
+
User.attributes # => [:type, :name, :admin]
|
208
|
+
User.attribute(:type).final? # => true
|
209
|
+
User.attribute_methods # => { type: [:type], name: [:name], admin: [:admin, :admin?] }
|
210
|
+
User.attribute_source(:name) # => User
|
211
|
+
```
|
196
212
|
|
197
213
|
---
|
198
214
|
|
199
|
-
##
|
215
|
+
## 📦 Installation
|
200
216
|
|
201
|
-
|
217
|
+
Add to your Gemfile:
|
202
218
|
|
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 |
|
219
|
+
```ruby
|
220
|
+
gem "cattri"
|
221
|
+
```
|
209
222
|
|
210
|
-
|
223
|
+
Or via Bundler:
|
211
224
|
|
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
|
225
|
+
```sh
|
226
|
+
bundle add cattri
|
224
227
|
```
|
225
228
|
|
226
229
|
---
|
227
230
|
|
228
|
-
##
|
231
|
+
## 🧱 Design Overview
|
229
232
|
|
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.
|
233
|
+
Cattri includes:
|
233
234
|
|
234
|
-
|
235
|
+
- `InternalStore` for final-safe value tracking
|
236
|
+
- `ContextRegistry` and `Context` for method definition logic
|
237
|
+
- `Attribute` and `AttributeOptions` for metadata handling
|
238
|
+
- `Visibility` tracking for DSL-defined methods
|
239
|
+
- `InitializerPatch` for final attribute enforcement on `#initialize`
|
240
|
+
- `Dsl` for `cattri` and `final_cattri`
|
241
|
+
- `Inheritance` to ensure subclass copying
|
235
242
|
|
236
243
|
---
|
237
244
|
|
238
|
-
##
|
245
|
+
## 🧪 Test Coverage
|
246
|
+
|
247
|
+
Cattri is tested with 100% line and branch coverage. All dynamic definitions are validated via RSpec, and edge cases are covered, including:
|
239
248
|
|
240
|
-
|
241
|
-
|
249
|
+
- Predicate methods
|
250
|
+
- Final value enforcement
|
251
|
+
- Class vs. instance scope
|
252
|
+
- Attribute inheritance
|
253
|
+
- Visibility and expose interaction
|
242
254
|
|
243
255
|
---
|
244
256
|
|
@@ -247,13 +259,13 @@ Cattri sits in the sweet spot: **micro‑sized (~300 LOC)**, dependency‑free,
|
|
247
259
|
1. Fork the repo
|
248
260
|
2. `bundle install`
|
249
261
|
3. Run the test suite with `bundle exec rake`
|
250
|
-
4. Submit a pull request – ensure new code is covered and **rubocop** passes
|
262
|
+
4. Submit a pull request – ensure new code is covered and **rubocop** passes
|
251
263
|
|
252
264
|
---
|
253
265
|
|
254
266
|
## License
|
255
267
|
|
256
|
-
This gem is released under the MIT License – see
|
268
|
+
This gem is released under the MIT License – see [LICENSE](LICENSE) for details.
|
257
269
|
|
258
270
|
## 🙏 Credits
|
259
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__)
|