cattri 0.1.0 → 0.1.2
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/.rubocop.yml +5 -1
- data/CHANGELOG.md +54 -1
- data/README.md +182 -82
- data/lib/cattri/attribute.rb +202 -0
- data/lib/cattri/attribute_definer.rb +124 -0
- data/lib/cattri/class_attributes.rb +126 -168
- data/lib/cattri/context.rb +171 -0
- data/lib/cattri/error.rb +100 -0
- data/lib/cattri/instance_attributes.rb +141 -113
- data/lib/cattri/introspection.rb +1 -1
- data/lib/cattri/version.rb +1 -1
- data/lib/cattri/visibility.rb +66 -0
- data/lib/cattri.rb +91 -8
- metadata +6 -5
- data/.idea/workspace.xml +0 -350
- data/.rspec_status +0 -75
- data/lib/cattri/helpers.rb +0 -75
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4558aae18122f29cebf5ddb34fe452ee6b00898d994b4cd8f2b0a825c5a599aa
|
4
|
+
data.tar.gz: '092f86968324144a229510426858c9a4cee0245267503a160e27b6087384e88e'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61ac08cebe59853a0a4c163052bdabef6e70db7476bf0d93ff4a1184b706e326fab8583ad6b77533f7c297e5be15cd15cd3414ef8c751443d4f1ed8a06b768a6
|
7
|
+
data.tar.gz: dc18a8a83dc56d9a4c2b122c3f824808f8037f0d1cb08a2d71e82ef6aa933d73c75efa083df12cb4b5a7dfb30a0ce22c0558871fe07365d73b601b2e32b537fd
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,57 @@
|
|
1
|
-
## [
|
1
|
+
## [0.1.2] - 2025-04-22
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
- Support for defining multiple attributes in a single call to `cattr` or `iattr`.
|
6
|
+
- Example: `cattr :foo, :bar, default: 1`
|
7
|
+
- Shared options apply to all attributes.
|
8
|
+
- Adds `cattr_setter` and `iattr_setter` for defining setters on attributes, useful when defining multiple attributes since ambiguous blocks are not allow.
|
9
|
+
```ruby
|
10
|
+
class Config
|
11
|
+
include Cattri
|
12
|
+
|
13
|
+
cattr :a, :b # new functionality, does not allow setter blocks.
|
14
|
+
# creates writers as def a=(val); @a = val; end
|
15
|
+
|
16
|
+
cattr_setter :a do |val| # redefines a= as def a=(val); val.to_s.downcase.to_sym; end
|
17
|
+
val.to_s.downcase.to_sym
|
18
|
+
end
|
19
|
+
end
|
20
|
+
```
|
21
|
+
- Validation to prevent use of a block when defining multiple attributes.
|
22
|
+
- Raises `Cattri::AmbiguousBlockError` if `&block` is passed with more than one attribute.
|
23
|
+
|
24
|
+
## [0.1.1] - 2025-04-22
|
25
|
+
|
26
|
+
### Added
|
27
|
+
|
28
|
+
- `Cattri::Context`: new class encapsulating all class-level and instance-level attribute metadata.
|
29
|
+
- `Cattri::Attribute`: formal representation of a defined attribute, with support for default values, coercion, and visibility.
|
30
|
+
- `Cattri::AttributeDefiner`: internal abstraction for building attributes and assigning behavior based on visibility and type.
|
31
|
+
- `Cattri::Visibility`: tracks current method visibility (`public`, `protected`, `private`) to ensure dynamically defined methods (e.g., via `cattr`, `iattr`) respect the active access scope during declaration.
|
32
|
+
|
33
|
+
### Changed
|
34
|
+
|
35
|
+
- Internal architecture now uses `Context` to manage attribute storage and duplication logic across inheritance chains.
|
36
|
+
- Class and instance attribute definitions (`cattr`, `iattr`) now delegate to `AttributeDefiner`, improving consistency and reducing duplication.
|
37
|
+
- Visibility handling is now centralized through `Cattri::Visibility`, which intercepts `public`, `protected`, and `private` to track and apply the current access level when defining methods.
|
38
|
+
- Subclass inheritance now copies attribute metadata and current values using a consistent, visibility-aware strategy.
|
39
|
+
|
40
|
+
### Removed
|
41
|
+
|
42
|
+
- Legacy handling of attribute hashes and manual copying in `inherited` hooks.
|
43
|
+
- Ad hoc attribute construction logic in `ClassAttributes` and `InstanceAttributes`.
|
44
|
+
|
45
|
+
### Improved
|
46
|
+
|
47
|
+
- Clear separation of concerns between metadata (`Attribute`), context (`Context`), and definition logic (`AttributeDefiner`).
|
48
|
+
- More robust error messages and consistent failure behavior when defining attributes with invalid configuration.
|
49
|
+
|
50
|
+
---
|
51
|
+
|
52
|
+
No breaking changes – the public DSL (cattr, iattr) remains identical to v0.1.0.
|
53
|
+
|
54
|
+
---
|
2
55
|
|
3
56
|
## [0.1.0] - 2025-04-17
|
4
57
|
|
data/README.md
CHANGED
@@ -1,153 +1,253 @@
|
|
1
1
|
# Cattri
|
2
2
|
|
3
|
-
|
3
|
+
A **minimal‑footprint** DSL for defining **class‑level** and **instance‑level** attributes in Ruby, with first‑class support for custom defaults, coercion, visibility tracking, and safety‑first error handling.
|
4
4
|
|
5
|
-
|
5
|
+
---
|
6
|
+
|
7
|
+
## Why another attribute DSL?
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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) | ✅ |
|
11
20
|
|
12
|
-
|
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.
|
13
22
|
|
14
|
-
|
15
|
-
- 🧱 Static or callable default values
|
16
|
-
- 🌀 Optional coercion logic via blocks
|
17
|
-
- 🧼 Reset attributes to default
|
18
|
-
- 🔒 Lock class attribute definitions in base class
|
19
|
-
- 🔍 Introspect attribute definitions and values (optional)
|
23
|
+
---
|
20
24
|
|
21
|
-
##
|
25
|
+
## Installation
|
22
26
|
|
23
27
|
```bash
|
24
|
-
bundle add cattri
|
28
|
+
bundle add cattri # Ruby ≥ 2.7
|
25
29
|
```
|
26
30
|
|
27
|
-
Or
|
31
|
+
Or in your Gemfile:
|
28
32
|
|
29
33
|
```ruby
|
30
34
|
gem "cattri"
|
31
35
|
```
|
32
36
|
|
33
|
-
|
37
|
+
---
|
34
38
|
|
35
|
-
|
39
|
+
## Quick start
|
36
40
|
|
37
41
|
```ruby
|
38
|
-
class
|
39
|
-
include Cattri
|
40
|
-
|
41
|
-
|
42
|
-
|
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)
|
55
|
+
end
|
43
56
|
end
|
44
57
|
|
45
|
-
|
46
|
-
|
58
|
+
Config.enabled # => true
|
59
|
+
Config.enabled = false
|
60
|
+
Config.new.age = "42" # => 42
|
47
61
|
```
|
48
62
|
|
49
|
-
|
50
|
-
|
51
|
-
```ruby
|
52
|
-
class MyConfig
|
53
|
-
extend Cattri::ClassAttributes
|
63
|
+
---
|
54
64
|
|
55
|
-
|
56
|
-
cattr_reader :version, default: "1.0.0"
|
57
|
-
cattr :enabled, default: true do |value|
|
58
|
-
!!value
|
59
|
-
end
|
60
|
-
end
|
65
|
+
## Defining attributes
|
61
66
|
|
62
|
-
|
63
|
-
MyConfig.format :xml
|
64
|
-
MyConfig.format # => :xml
|
67
|
+
### Class attributes (`cattr`)
|
65
68
|
|
66
|
-
|
69
|
+
```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
|
67
76
|
```
|
68
77
|
|
69
|
-
|
78
|
+
### Instance attributes (`iattr`)
|
70
79
|
|
71
80
|
```ruby
|
72
|
-
|
81
|
+
iattr :token, default: -> { SecureRandom.hex(8) },
|
82
|
+
reader: true,
|
83
|
+
writer: false # read‑only
|
73
84
|
```
|
74
85
|
|
75
|
-
|
86
|
+
Both forms accept:
|
87
|
+
|
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**). |
|
95
|
+
|
96
|
+
If you pass a block, it’s treated as a **coercion setter** and receives the incoming value.
|
97
|
+
|
98
|
+
---
|
99
|
+
|
100
|
+
## Post-definition coercion with `*_setter`
|
101
|
+
|
102
|
+
If you define multiple attributes at once, you can't provide a coercion block inline:
|
76
103
|
|
77
104
|
```ruby
|
78
|
-
|
105
|
+
cattr :foo, :bar, default: nil # ❌ cannot use block here
|
79
106
|
```
|
80
107
|
|
81
|
-
|
108
|
+
Instead, define them first, then apply a coercion later using:
|
82
109
|
|
83
|
-
|
110
|
+
- `cattr_setter` for class attributes
|
111
|
+
- `iattr_setter` for instance attributes
|
112
|
+
|
113
|
+
These allow you to attach or override the setter logic after the fact:
|
84
114
|
|
85
115
|
```ruby
|
86
|
-
class
|
87
|
-
include Cattri
|
116
|
+
class Config
|
117
|
+
include Cattri
|
88
118
|
|
89
|
-
|
90
|
-
|
91
|
-
val.to_s.
|
119
|
+
cattr :log_level
|
120
|
+
cattr_setter :log_level do |val|
|
121
|
+
val.to_s.downcase.to_sym
|
92
122
|
end
|
93
|
-
end
|
94
123
|
|
95
|
-
|
96
|
-
|
97
|
-
|
124
|
+
iattr_writer :token
|
125
|
+
iattr_setter :token do |val|
|
126
|
+
val.strip
|
127
|
+
end
|
128
|
+
end
|
98
129
|
```
|
99
130
|
|
100
|
-
|
131
|
+
Coercion is only applied when the attribute is written (via `=` or callable form), not when read.
|
132
|
+
|
133
|
+
Attempting to use `*_setter` on an undefined attribute or one without a writer will raise:
|
134
|
+
|
135
|
+
- `Cattri::AttributeNotDefinedError` – the attribute doesn't exist or wasn't fully defined
|
136
|
+
- `Cattri::AttributeDefinitionError` – the attribute is marked as readonly
|
137
|
+
|
138
|
+
These APIs ensure your DSL stays consistent and extensible, even when bulk-declaring attributes up front.
|
139
|
+
|
140
|
+
---
|
141
|
+
|
142
|
+
## Visibility tracking
|
143
|
+
|
144
|
+
Cattri watches calls to `public`, `protected`, and `private` while you define methods:
|
101
145
|
|
102
146
|
```ruby
|
103
|
-
|
104
|
-
|
147
|
+
class Secrets
|
148
|
+
include Cattri
|
105
149
|
|
106
|
-
|
150
|
+
private
|
151
|
+
cattr :api_key
|
152
|
+
end
|
153
|
+
|
154
|
+
Secrets.private_methods.include?(:api_key) # => true
|
107
155
|
```
|
108
156
|
|
109
|
-
|
157
|
+
No boilerplate—attributes inherit the visibility that was in effect at the call site.
|
158
|
+
|
159
|
+
---
|
160
|
+
|
161
|
+
## Safe inheritance
|
110
162
|
|
111
|
-
|
163
|
+
Subclassing copies both **metadata** and **current values**, using defensive `#dup` where possible and falling back safely when objects are frozen or not duplicable:
|
112
164
|
|
113
165
|
```ruby
|
114
|
-
class
|
166
|
+
class Base
|
115
167
|
include Cattri
|
116
|
-
|
117
|
-
|
118
|
-
cattr :items, default: []
|
168
|
+
cattr :settings, default: {}
|
119
169
|
end
|
120
170
|
|
121
|
-
|
122
|
-
|
171
|
+
class Child < Base; end
|
172
|
+
|
173
|
+
Base.settings[:foo] = 1
|
174
|
+
Child.settings # => {} (isolated copy)
|
123
175
|
```
|
124
176
|
|
125
|
-
|
177
|
+
---
|
126
178
|
|
127
|
-
|
128
|
-
|----------------------------------|--------------------------------------------|
|
129
|
-
| `cattr`, `cattr_reader` | Define class-level attributes |
|
130
|
-
| `iattr`, `iattr_reader`, `iattr_writer` | Define instance-level attributes |
|
131
|
-
| `reset_cattr!`, `reset_iattr!` | Reset specific attributes |
|
132
|
-
| `cattr_definition(:name)` | Get attribute metadata |
|
133
|
-
| `lock_cattrs!` | Prevent redefinition in subclasses |
|
179
|
+
## Introspection helpers
|
134
180
|
|
135
|
-
|
181
|
+
Add `include Cattri::Introspection` (or `extend` for class‑only use) to snapshot live values:
|
136
182
|
|
137
|
-
```
|
138
|
-
|
183
|
+
```ruby
|
184
|
+
Config.snapshot_cattrs # => { enabled: false, timeout: 5.0 }
|
185
|
+
instance.snapshot_iattrs # => { name: "bob", age: 42 }
|
186
|
+
```
|
187
|
+
|
188
|
+
Great for debugging or test assertions.
|
189
|
+
|
190
|
+
---
|
191
|
+
|
192
|
+
## Error handling
|
193
|
+
|
194
|
+
All errors inherit from `Cattri::Error`, allowing a single rescue for any gem‑specific issue.
|
195
|
+
|
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 |
|
202
|
+
|
203
|
+
Example:
|
204
|
+
|
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
|
139
217
|
```
|
140
218
|
|
141
|
-
|
219
|
+
---
|
220
|
+
|
221
|
+
## Comparison with standard patterns
|
222
|
+
|
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.
|
142
226
|
|
143
|
-
Cattri
|
227
|
+
Cattri sits in the sweet spot: **micro‑sized (~300 LOC)**, dependency‑free, and purpose‑built for attribute declaration.
|
144
228
|
|
145
|
-
|
229
|
+
---
|
230
|
+
|
231
|
+
## Testing tips
|
232
|
+
|
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.
|
235
|
+
|
236
|
+
---
|
237
|
+
|
238
|
+
## Contributing
|
146
239
|
|
147
|
-
|
240
|
+
1. Fork the repo
|
241
|
+
2. `bundle install`
|
242
|
+
3. Run the test suite with `bundle exec rake`
|
243
|
+
4. Submit a pull request – ensure new code is covered and **rubocop** passes.
|
148
244
|
|
149
245
|
---
|
150
246
|
|
247
|
+
## License
|
248
|
+
|
249
|
+
This gem is released under the MIT License – see See [LICENSE](LICENSE) for details.
|
250
|
+
|
151
251
|
## 🙏 Credits
|
152
252
|
|
153
253
|
Created with ❤️ by [Nathan Lucas](https://github.com/bnlucas)
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "error"
|
4
|
+
|
5
|
+
module Cattri
|
6
|
+
# Represents a single attribute definition in Cattri.
|
7
|
+
#
|
8
|
+
# This class encapsulates metadata and behavior for a declared attribute,
|
9
|
+
# including name, visibility, default value, and setter coercion logic.
|
10
|
+
#
|
11
|
+
# It is used internally by the DSL to configure how accessors are defined,
|
12
|
+
# memoized, and resolved at runtime.
|
13
|
+
class Attribute
|
14
|
+
# Supported attribute scopes within Cattri.
|
15
|
+
ATTRIBUTE_TYPES = %i[class instance].freeze
|
16
|
+
|
17
|
+
# Supported Ruby method visibility levels.
|
18
|
+
ACCESS_LEVELS = %i[public protected private].freeze
|
19
|
+
|
20
|
+
# Ruby value types considered safe to reuse as-is (no `#dup` needed).
|
21
|
+
SAFE_VALUE_TYPES = [Numeric, Symbol, TrueClass, FalseClass, NilClass].freeze
|
22
|
+
|
23
|
+
# Default options for class-level attributes.
|
24
|
+
DEFAULT_CLASS_ATTRIBUTE_OPTIONS = {
|
25
|
+
readonly: false,
|
26
|
+
instance_reader: true
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
# Default options for instance-level attributes.
|
30
|
+
DEFAULT_INSTANCE_ATTRIBUTE_OPTIONS = {
|
31
|
+
reader: true,
|
32
|
+
writer: true
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
# @return [Symbol] the attribute name
|
36
|
+
attr_reader :name
|
37
|
+
|
38
|
+
# @return [Symbol] the attribute type (:class or :instance)
|
39
|
+
attr_reader :type
|
40
|
+
|
41
|
+
# @return [Symbol] the associated instance variable (e.g., :@items)
|
42
|
+
attr_reader :ivar
|
43
|
+
|
44
|
+
# @return [Symbol] the access level (:public, :protected, :private)
|
45
|
+
attr_reader :access
|
46
|
+
|
47
|
+
# @return [Proc] the normalized default value block
|
48
|
+
attr_reader :default
|
49
|
+
|
50
|
+
# @return [Proc] the setter function used to assign values
|
51
|
+
attr_reader :setter
|
52
|
+
|
53
|
+
# Initializes a new attribute definition.
|
54
|
+
#
|
55
|
+
# @param name [String, Symbol] the name of the attribute
|
56
|
+
# @param type [Symbol] either :class or :instance
|
57
|
+
# @param options [Hash] additional attribute configuration
|
58
|
+
# @param block [Proc, nil] optional block for setter coercion
|
59
|
+
#
|
60
|
+
# @raise [Cattri::UnsupportedTypeError] if an invalid type is provided
|
61
|
+
def initialize(name, type, options, block)
|
62
|
+
@type = type.to_sym
|
63
|
+
raise Cattri::UnsupportedTypeError, type unless ATTRIBUTE_TYPES.include?(@type)
|
64
|
+
|
65
|
+
@name = name.to_sym
|
66
|
+
@ivar = normalize_ivar(options[:ivar])
|
67
|
+
@access = options[:access] || :public
|
68
|
+
@default = normalize_default(options[:default])
|
69
|
+
@setter = normalize_setter(block)
|
70
|
+
@options = typed_options(options)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Hash-like access to option values or metadata.
|
74
|
+
#
|
75
|
+
# @param key [Symbol, String]
|
76
|
+
# @return [Object]
|
77
|
+
def [](key)
|
78
|
+
to_hash[key.to_sym]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Serializes this attribute to a hash, including core properties and type-specific flags.
|
82
|
+
#
|
83
|
+
# @return [Hash]
|
84
|
+
def to_hash
|
85
|
+
@to_hash ||= {
|
86
|
+
name: @name,
|
87
|
+
ivar: @ivar,
|
88
|
+
type: @type,
|
89
|
+
access: @access,
|
90
|
+
default: @default,
|
91
|
+
setter: @setter
|
92
|
+
}.merge(@options)
|
93
|
+
end
|
94
|
+
|
95
|
+
alias to_h to_hash
|
96
|
+
|
97
|
+
# @return [Boolean] true if the attribute is class-scoped
|
98
|
+
def class_level?
|
99
|
+
type == :class
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [Boolean] true if the attribute is instance-scoped
|
103
|
+
def instance_level?
|
104
|
+
type == :instance
|
105
|
+
end
|
106
|
+
|
107
|
+
# @return [Boolean] whether the attribute is public
|
108
|
+
def public?
|
109
|
+
access == :public
|
110
|
+
end
|
111
|
+
|
112
|
+
# @return [Boolean] whether the attribute is protected
|
113
|
+
def protected?
|
114
|
+
access == :protected
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [Boolean] whether the attribute is private
|
118
|
+
def private?
|
119
|
+
access == :private
|
120
|
+
end
|
121
|
+
|
122
|
+
# Invokes the default value logic for the attribute.
|
123
|
+
#
|
124
|
+
# @return [Object] the default value for the attribute
|
125
|
+
# @raise [Cattri::AttributeError] if the default value logic raises an error
|
126
|
+
def invoke_default
|
127
|
+
default.call
|
128
|
+
rescue StandardError => e
|
129
|
+
raise Cattri::AttributeError, "Failed to evaluate the default value for :#{name}. Error: #{e.message}"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Invokes the setter function with error handling
|
133
|
+
#
|
134
|
+
# @param args [Array] the positional arguments
|
135
|
+
# @param kwargs [Hash] the keyword arguments
|
136
|
+
# @raise [Cattri::AttributeError] if setter raises an error
|
137
|
+
# @return [Object] the value returned by the setter
|
138
|
+
def invoke_setter(*args, **kwargs)
|
139
|
+
setter.call(*args, **kwargs)
|
140
|
+
rescue StandardError => e
|
141
|
+
raise Cattri::AttributeError, "Failed to evaluate the setter for :#{name}. Error: #{e.message}"
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# Applies class- or instance-level defaults and filters valid option keys.
|
147
|
+
#
|
148
|
+
# @param options [Hash]
|
149
|
+
# @return [Hash]
|
150
|
+
def typed_options(options)
|
151
|
+
defaults = type == :class ? DEFAULT_CLASS_ATTRIBUTE_OPTIONS : DEFAULT_INSTANCE_ATTRIBUTE_OPTIONS
|
152
|
+
defaults.merge(options.slice(*defaults.keys))
|
153
|
+
end
|
154
|
+
|
155
|
+
# Normalizes the instance variable name for the attribute.
|
156
|
+
#
|
157
|
+
# @param ivar [String, Symbol, nil]
|
158
|
+
# @return [Symbol]
|
159
|
+
def normalize_ivar(ivar)
|
160
|
+
ivar ||= name
|
161
|
+
:"@#{ivar.to_s.delete_prefix("@")}"
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns the setter proc. If no block is provided, uses default logic:
|
165
|
+
# - Returns kwargs if given
|
166
|
+
# - Returns the single positional argument if one
|
167
|
+
# - Returns all args as an array otherwise
|
168
|
+
#
|
169
|
+
# @param block [Proc, nil]
|
170
|
+
# @return [Proc]
|
171
|
+
def normalize_setter(block)
|
172
|
+
block || lambda { |*args, **kwargs|
|
173
|
+
return kwargs unless kwargs.empty?
|
174
|
+
return args.first if args.length == 1
|
175
|
+
|
176
|
+
args
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
# Wraps the default value in a memoized lambda.
|
181
|
+
#
|
182
|
+
# If value is already callable, returns it.
|
183
|
+
# If immutable, wraps it in a lambda.
|
184
|
+
# If mutable, wraps it in a lambda that calls `#dup`.
|
185
|
+
#
|
186
|
+
# @param default [Object, Proc, nil]
|
187
|
+
# @return [Proc]
|
188
|
+
def normalize_default(default)
|
189
|
+
return default if default.respond_to?(:call)
|
190
|
+
return -> { default } if default.frozen? || SAFE_VALUE_TYPES.any? { |type| default.is_a?(type) }
|
191
|
+
|
192
|
+
lambda {
|
193
|
+
begin
|
194
|
+
default.dup
|
195
|
+
rescue StandardError => e
|
196
|
+
raise Cattri::AttributeError,
|
197
|
+
"Failed to duplicate default value for :#{name}. Error: #{e.message}"
|
198
|
+
end
|
199
|
+
}
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|