cattri 0.1.1 → 0.1.3
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/CHANGELOG.md +32 -0
- data/README.md +63 -12
- data/lib/cattri/attribute.rb +4 -2
- data/lib/cattri/attribute_definer.rb +32 -1
- data/lib/cattri/class_attributes.rb +159 -17
- data/lib/cattri/context.rb +17 -1
- data/lib/cattri/error.rb +37 -4
- data/lib/cattri/instance_attributes.rb +148 -22
- data/lib/cattri/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2495a8756d30c301267ad2db2dfbd4c9669e817119fa316b608408665988b747
|
4
|
+
data.tar.gz: 47d682eaf5465e4ab6cc4bdb7dcc74d053c93c6b8d1ee3a8cfda0925cf68662a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aae777877b7576a76b0be11a529aafaaac9c59066efd873fc9020689ba3ccefb78688411907b2246cb603dafa5e6868e1841e4724f296f634eac821a1bc27bc9
|
7
|
+
data.tar.gz: 8e919b6782d0cce71ed1a78bb46ebb92df995471238080a4e20604914d0fe958c041aec201eb598f9764598384fd776aaff03f5b131908082428c2d546c97cd9
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,35 @@
|
|
1
|
+
## [0.1.3] - 2025-04-22
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
- ✅ Support for `predicate: true` on both `iattr` and `cattr` — defines a `:name?` method returning `!!send(:name)`
|
6
|
+
- ✅ `iattr_alias` and `cattr_alias` — define alias methods that delegate to existing attributes (e.g., `:foo?` for `:foo`)
|
7
|
+
- Predicate methods inherit visibility from the original attribute and are excluded from introspection (`iattrs`, `cattrs`)
|
8
|
+
- Raised error when attempting to define an attribute ending in `?`, with guidance to use `predicate: true` or `*_alias`
|
9
|
+
|
10
|
+
## [0.1.2] - 2025-04-22
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Support for defining multiple attributes in a single call to `cattr` or `iattr`.
|
15
|
+
- Example: `cattr :foo, :bar, default: 1`
|
16
|
+
- Shared options apply to all attributes.
|
17
|
+
- Adds `cattr_setter` and `iattr_setter` for defining setters on attributes, useful when defining multiple attributes since ambiguous blocks are not allow.
|
18
|
+
```ruby
|
19
|
+
class Config
|
20
|
+
include Cattri
|
21
|
+
|
22
|
+
cattr :a, :b # new functionality, does not allow setter blocks.
|
23
|
+
# creates writers as def a=(val); @a = val; end
|
24
|
+
|
25
|
+
cattr_setter :a do |val| # redefines a= as def a=(val); val.to_s.downcase.to_sym; end
|
26
|
+
val.to_s.downcase.to_sym
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
- Validation to prevent use of a block when defining multiple attributes.
|
31
|
+
- Raises `Cattri::AmbiguousBlockError` if `&block` is passed with more than one attribute.
|
32
|
+
|
1
33
|
## [0.1.1] - 2025-04-22
|
2
34
|
|
3
35
|
### Added
|
data/README.md
CHANGED
@@ -43,19 +43,24 @@ class Config
|
|
43
43
|
include Cattri # exposes `cattr` & `iattr`
|
44
44
|
|
45
45
|
# -- class‑level ----------------------------------
|
46
|
-
cattr :
|
47
|
-
cattr :
|
46
|
+
cattr :flag_a, :flag_b, default: true
|
47
|
+
cattr :enabled, default: true, predicate: true
|
48
|
+
cattr :timeout, default: -> { 5.0 }, instance_reader: false
|
48
49
|
|
49
50
|
# -- instance‑level -------------------------------
|
50
|
-
iattr :
|
51
|
-
iattr :
|
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
|
52
55
|
Integer(val)
|
53
56
|
end
|
54
57
|
end
|
55
58
|
|
56
59
|
Config.enabled # => true
|
57
60
|
Config.enabled = false
|
61
|
+
Config.enabled? # => false (created with predicate: true flag)
|
58
62
|
Config.new.age = "42" # => 42
|
63
|
+
Config.new.username # proxy to Config.new.name
|
59
64
|
```
|
60
65
|
|
61
66
|
---
|
@@ -65,12 +70,14 @@ Config.new.age = "42" # => 42
|
|
65
70
|
### Class attributes (`cattr`)
|
66
71
|
|
67
72
|
```ruby
|
68
|
-
cattr :log_level,
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
73
|
+
cattr :log_level,
|
74
|
+
default: :info,
|
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
|
80
|
+
end
|
74
81
|
```
|
75
82
|
|
76
83
|
### Instance attributes (`iattr`)
|
@@ -78,7 +85,8 @@ cattr :log_level, default: :info,
|
|
78
85
|
```ruby
|
79
86
|
iattr :token, default: -> { SecureRandom.hex(8) },
|
80
87
|
reader: true,
|
81
|
-
writer: false # read‑only
|
88
|
+
writer: false, # read‑only
|
89
|
+
predicate: true
|
82
90
|
```
|
83
91
|
|
84
92
|
Both forms accept:
|
@@ -90,11 +98,54 @@ Both forms accept:
|
|
90
98
|
| `reader:` / `writer:` | Disable reader or writer for instance attributes. |
|
91
99
|
| `readonly:` | Shorthand for class attributes (`writer` is always present). |
|
92
100
|
| `instance_reader:` | Expose class attribute as instance reader (default: **true**). |
|
101
|
+
| `predicate` | Define a `:name?` method that calls `!!send(name)`
|
93
102
|
|
94
103
|
If you pass a block, it’s treated as a **coercion setter** and receives the incoming value.
|
95
104
|
|
96
105
|
---
|
97
106
|
|
107
|
+
## Post-definition coercion with `*_setter`
|
108
|
+
|
109
|
+
If you define multiple attributes at once, you can't provide a coercion block inline:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
cattr :foo, :bar, default: nil # ❌ cannot use block here
|
113
|
+
```
|
114
|
+
|
115
|
+
Instead, define them first, then apply a coercion later using:
|
116
|
+
|
117
|
+
- `cattr_setter` for class attributes
|
118
|
+
- `iattr_setter` for instance attributes
|
119
|
+
|
120
|
+
These allow you to attach or override the setter logic after the fact:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
class Config
|
124
|
+
include Cattri
|
125
|
+
|
126
|
+
cattr :log_level
|
127
|
+
cattr_setter :log_level do |val|
|
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
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
Coercion is only applied when the attribute is written (via `=` or callable form), not when read.
|
139
|
+
|
140
|
+
Attempting to use `*_setter` on an undefined attribute or one without a writer will raise:
|
141
|
+
|
142
|
+
- `Cattri::AttributeNotDefinedError` – the attribute doesn't exist or wasn't fully defined
|
143
|
+
- `Cattri::AttributeDefinitionError` – the attribute is marked as readonly
|
144
|
+
|
145
|
+
These APIs ensure your DSL stays consistent and extensible, even when bulk-declaring attributes up front.
|
146
|
+
|
147
|
+
---
|
148
|
+
|
98
149
|
## Visibility tracking
|
99
150
|
|
100
151
|
Cattri watches calls to `public`, `protected`, and `private` while you define methods:
|
@@ -180,7 +231,7 @@ end
|
|
180
231
|
* **ActiveSupport** extends the API but still relies on mutable class variables and offers no visibility control.
|
181
232
|
* **Dry‑configurable** is robust yet heavyweight when you only need a handful of attributes outside a full config object.
|
182
233
|
|
183
|
-
Cattri sits in the sweet spot: **micro‑sized (~
|
234
|
+
Cattri sits in the sweet spot: **micro‑sized (~300 LOC)**, dependency‑free, and purpose‑built for attribute declaration.
|
184
235
|
|
185
236
|
---
|
186
237
|
|
data/lib/cattri/attribute.rb
CHANGED
@@ -23,13 +23,15 @@ module Cattri
|
|
23
23
|
# Default options for class-level attributes.
|
24
24
|
DEFAULT_CLASS_ATTRIBUTE_OPTIONS = {
|
25
25
|
readonly: false,
|
26
|
-
instance_reader: true
|
26
|
+
instance_reader: true,
|
27
|
+
predicate: false
|
27
28
|
}.freeze
|
28
29
|
|
29
30
|
# Default options for instance-level attributes.
|
30
31
|
DEFAULT_INSTANCE_ATTRIBUTE_OPTIONS = {
|
31
32
|
reader: true,
|
32
|
-
writer: true
|
33
|
+
writer: true,
|
34
|
+
predicate: false
|
33
35
|
}.freeze
|
34
36
|
|
35
37
|
# @return [Symbol] the attribute name
|
@@ -47,13 +47,32 @@ module Cattri
|
|
47
47
|
def define_instance_level_reader(attribute, context)
|
48
48
|
return unless attribute.class_level?
|
49
49
|
|
50
|
-
|
50
|
+
define_instance_level_method(attribute, context) do
|
51
51
|
self.class.__send__(attribute.name)
|
52
52
|
end
|
53
53
|
|
54
54
|
context.send(:apply_access, attribute.name, attribute)
|
55
55
|
end
|
56
56
|
|
57
|
+
# Defines an instance-level method for a class-level attribute.
|
58
|
+
#
|
59
|
+
# This is a shared utility for defining instance methods that delegate to class attributes,
|
60
|
+
# including both regular readers and predicate-style readers (`predicate: true`).
|
61
|
+
#
|
62
|
+
# Visibility is inherited from the attribute and applied to the defined method.
|
63
|
+
#
|
64
|
+
# @param attribute [Cattri::Attribute] the associated attribute metadata
|
65
|
+
# @param context [Cattri::Context] the context in which to define the method
|
66
|
+
# @param name [Symbol, nil] optional override for the method name (defaults to `attribute.name`)
|
67
|
+
# @yield the method body to define
|
68
|
+
# @return [void]
|
69
|
+
def define_instance_level_method(attribute, context, name: nil, &block)
|
70
|
+
name = (name || attribute.name).to_sym
|
71
|
+
context.target.define_method(name, &block)
|
72
|
+
|
73
|
+
context.send(:apply_access, name, attribute)
|
74
|
+
end
|
75
|
+
|
57
76
|
# Defines standard reader and writer methods for instance-level attributes.
|
58
77
|
#
|
59
78
|
# Skips definition if `reader: false` or `writer: false` is specified.
|
@@ -92,6 +111,18 @@ module Cattri
|
|
92
111
|
end
|
93
112
|
end
|
94
113
|
|
114
|
+
# Defines, or redefines, a writer method (`foo=`) that sets and coerces a value via the attribute setter.
|
115
|
+
#
|
116
|
+
# @param attribute [Cattri::Attribute]
|
117
|
+
# @param context [Cattri::Context]
|
118
|
+
# @return [void]
|
119
|
+
def define_writer!(attribute, context)
|
120
|
+
context.define_method!(attribute, name: :"#{attribute.name}=") do |value|
|
121
|
+
coerced_value = attribute.setter.call(value)
|
122
|
+
instance_variable_set(attribute.ivar, coerced_value)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
95
126
|
private
|
96
127
|
|
97
128
|
# Returns the memoized value for an attribute or computes it from the default.
|
@@ -20,31 +20,43 @@ module Cattri
|
|
20
20
|
# Class attributes are stored internally as `Cattri::Attribute` instances and
|
21
21
|
# values are memoized using class-level instance variables.
|
22
22
|
module ClassAttributes
|
23
|
-
# Defines
|
23
|
+
# Defines one or more class-level attributes with optional default, coercion, and reader access.
|
24
24
|
#
|
25
|
-
#
|
25
|
+
# This method supports defining multiple attributes at once, provided they share the same options.
|
26
|
+
# If a block is given, only one attribute may be defined to avoid ambiguity.
|
27
|
+
#
|
28
|
+
# @example Define multiple attributes with shared options
|
29
|
+
# class_attribute :foo, :bar, default: 42
|
30
|
+
#
|
31
|
+
# @example Define a single attribute with a coercion block
|
32
|
+
# class_attribute :path do |val|
|
33
|
+
# Pathname(val)
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# @param names [Array<Symbol | String>] the names of the attributes to define
|
26
37
|
# @param options [Hash] additional attribute options
|
27
38
|
# @option options [Object, Proc] :default the default value or lambda
|
28
39
|
# @option options [Boolean] :readonly whether the attribute is read-only
|
29
40
|
# @option options [Boolean] :instance_reader whether to define an instance-level reader (default: true)
|
30
41
|
# @option options [Symbol] :access visibility level (:public, :protected, :private)
|
42
|
+
# @option options [Boolean] :predicate whether to define a predicate-style alias method
|
43
|
+
# (e.g., `foo?`) for the attribute
|
31
44
|
# @yieldparam value [Object] an optional custom setter block
|
32
45
|
# @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
|
33
46
|
# `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
|
34
47
|
# already defined or an error occurs while defining methods)
|
35
|
-
|
36
|
-
|
37
|
-
|
48
|
+
# @return [void]
|
49
|
+
def class_attribute(*names, **options, &block)
|
50
|
+
raise Cattri::AmbiguousBlockError if names.size > 1 && block_given?
|
38
51
|
|
39
|
-
|
52
|
+
names.each do |name|
|
53
|
+
if name.end_with?("?")
|
54
|
+
raise Cattri::AttributeError,
|
55
|
+
"Attribute names ending in '?' are not allowed. Use `predicate: true` or `cattr_alias` instead."
|
40
56
|
|
41
|
-
|
42
|
-
__cattri_class_attributes[name] = attribute
|
57
|
+
end
|
43
58
|
|
44
|
-
|
45
|
-
Cattri::AttributeDefiner.define_instance_level_reader(attribute, context) if attribute[:instance_reader]
|
46
|
-
rescue StandardError => e
|
47
|
-
raise Cattri::AttributeDefinitionError.new(self, attribute, e)
|
59
|
+
define_class_attribute(name, options, block)
|
48
60
|
end
|
49
61
|
end
|
50
62
|
|
@@ -52,18 +64,79 @@ module Cattri
|
|
52
64
|
#
|
53
65
|
# Equivalent to calling `class_attribute(name, readonly: true, ...)`
|
54
66
|
#
|
55
|
-
# @param
|
56
|
-
# @param options [Hash]
|
67
|
+
# @param names [Array<Symbol | String>] the names of the attributes to define
|
68
|
+
# @param options [Hash] additional attribute options
|
69
|
+
# @option options [Object, Proc] :default the default value or lambda
|
70
|
+
# @option options [Boolean] :readonly whether the attribute is read-only
|
71
|
+
# @option options [Boolean] :instance_reader whether to define an instance-level reader (default: true)
|
72
|
+
# @option options [Symbol] :access visibility level (:public, :protected, :private)
|
73
|
+
# @option options [Boolean] :predicate whether to define a predicate-style alias method
|
74
|
+
# (e.g., `foo?`) for the attribute
|
75
|
+
# @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
|
76
|
+
# `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
|
77
|
+
# already defined or an error occurs while defining methods)
|
78
|
+
# @return [void]
|
79
|
+
def class_attribute_reader(*names, **options)
|
80
|
+
class_attribute(*names, **options, readonly: true)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Updates the setter behavior of an existing class-level attribute.
|
84
|
+
#
|
85
|
+
# This allows coercion logic to be defined or overridden after the attribute
|
86
|
+
# has been declared using `cattr`, as long as the writer method exists.
|
87
|
+
#
|
88
|
+
# @example Add coercion to an existing attribute
|
89
|
+
# cattr :format
|
90
|
+
# cattr_setter :format do |val|
|
91
|
+
# val.to_s.downcase.to_sym
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# @param name [Symbol, String] the name of the attribute
|
95
|
+
# @yieldparam value [Object] the value passed to the setter
|
96
|
+
# @yieldreturn [Object] the coerced value to be assigned
|
97
|
+
# @raise [Cattri::AttributeNotDefinedError] if the attribute is not defined or the writer method does not exist
|
98
|
+
# @raise [Cattri::AttributeDefinitionError] if method redefinition fails
|
99
|
+
# @return [void]
|
100
|
+
def class_attribute_setter(name, &block)
|
101
|
+
name = name.to_sym
|
102
|
+
attribute = __cattri_class_attributes[name]
|
103
|
+
puts "<<< #{attribute} = #{name}>"
|
104
|
+
|
105
|
+
if attribute.nil? || !context.method_defined?(:"#{name}=")
|
106
|
+
raise Cattri::AttributeNotDefinedError.new(:class, name)
|
107
|
+
end
|
108
|
+
|
109
|
+
attribute.instance_variable_set(:@setter, attribute.send(:normalize_setter, block))
|
110
|
+
Cattri::AttributeDefiner.define_writer!(attribute, context)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Defines an alias method for an existing class-level attribute.
|
114
|
+
#
|
115
|
+
# This does **not** register a new attribute; it simply defines a method
|
116
|
+
# (e.g., a predicate-style alias like `foo?`) that delegates to an existing one.
|
117
|
+
#
|
118
|
+
# The alias method inherits the visibility of the original attribute.
|
119
|
+
#
|
120
|
+
# @param alias_name [Symbol, String] the new method name (e.g., `:foo?`)
|
121
|
+
# @param original [Symbol, String] the name of the existing attribute to delegate to (e.g., `:foo`)
|
122
|
+
# @raise [Cattri::AttributeNotDefinedError] if the original attribute is not defined
|
57
123
|
# @return [void]
|
58
|
-
def
|
59
|
-
|
124
|
+
def class_attribute_alias(alias_name, original)
|
125
|
+
attribute = __cattri_class_attributes[original.to_sym]
|
126
|
+
raise Cattri::AttributeNotDefinedError.new(:class, original) if attribute.nil?
|
127
|
+
|
128
|
+
context.define_method(attribute, name: alias_name) { public_send(original) }
|
60
129
|
end
|
61
130
|
|
62
131
|
# Returns a list of defined class attribute names.
|
63
132
|
#
|
64
133
|
# @return [Array<Symbol>]
|
65
134
|
def class_attributes
|
66
|
-
|
135
|
+
([self] + ancestors + singleton_class.included_modules)
|
136
|
+
.uniq
|
137
|
+
.select { |mod| mod.respond_to?(:__cattri_class_attributes, true) }
|
138
|
+
.flat_map { |mod| mod.send(:__cattri_class_attributes).keys }
|
139
|
+
.uniq
|
67
140
|
end
|
68
141
|
|
69
142
|
# Checks whether a class attribute has been defined.
|
@@ -97,6 +170,16 @@ module Cattri
|
|
97
170
|
# @see #class_attribute_reader
|
98
171
|
alias cattr_reader class_attribute_reader
|
99
172
|
|
173
|
+
# @!method cattr_setter(name, **options)
|
174
|
+
# Alias for {.class_attribute_setter}
|
175
|
+
# @see #class_attribute_setter
|
176
|
+
alias cattr_setter class_attribute_setter
|
177
|
+
|
178
|
+
# @!method cattr_alias(name, **options)
|
179
|
+
# Alias for {.class_attribute_alias}
|
180
|
+
# @see #class_attribute_alias
|
181
|
+
alias cattr_alias class_attribute_alias
|
182
|
+
|
100
183
|
# @!method cattrs
|
101
184
|
# Alias for {.class_attributes}
|
102
185
|
# @return [Array<Symbol>]
|
@@ -116,6 +199,65 @@ module Cattri
|
|
116
199
|
|
117
200
|
private
|
118
201
|
|
202
|
+
# Defines a single class-level attribute.
|
203
|
+
#
|
204
|
+
# This is the internal implementation used by {.class_attribute} and its aliases.
|
205
|
+
# It constructs a `Cattri::Attribute`, registers it, and defines the necessary
|
206
|
+
# class and instance methods.
|
207
|
+
#
|
208
|
+
# @param name [Symbol] the name of the attribute to define
|
209
|
+
# @param options [Hash] additional attribute options (e.g., :default, :readonly)
|
210
|
+
# @param block [Proc, nil] an optional setter block for coercion
|
211
|
+
#
|
212
|
+
# @raise [Cattri::AttributeDefinedError] if the attribute has already been defined
|
213
|
+
# @raise [Cattri::AttributeDefinitionError] if method definition fails
|
214
|
+
#
|
215
|
+
# @return [void]
|
216
|
+
def define_class_attribute(name, options, block) # rubocop:disable Metrics/AbcSize
|
217
|
+
options[:access] ||= __cattri_visibility
|
218
|
+
attribute = Cattri::Attribute.new(name, :class, options, block)
|
219
|
+
|
220
|
+
raise Cattri::AttributeDefinedError.new(:class, name) if class_attribute_defined?(attribute.name)
|
221
|
+
|
222
|
+
begin
|
223
|
+
__cattri_class_attributes[name] = attribute
|
224
|
+
|
225
|
+
Cattri::AttributeDefiner.define_callable_accessor(attribute, context)
|
226
|
+
Cattri::AttributeDefiner.define_instance_level_reader(attribute, context) if attribute[:instance_reader]
|
227
|
+
rescue StandardError => e
|
228
|
+
raise Cattri::AttributeDefinitionError.new(self, attribute, e)
|
229
|
+
end
|
230
|
+
|
231
|
+
define_predicate_methods(attribute) if attribute[:predicate]
|
232
|
+
end
|
233
|
+
|
234
|
+
# Defines predicate-style (`:name?`) methods for a class-level attribute.
|
235
|
+
#
|
236
|
+
# If `attribute[:predicate]` is true, this defines a method named `:name?` that returns
|
237
|
+
# a boolean based on the truthiness of the attribute's value (`!!value`).
|
238
|
+
#
|
239
|
+
# If `attribute[:instance_reader]` is also true, an instance-level predicate method
|
240
|
+
# is defined that delegates to the class-level value.
|
241
|
+
#
|
242
|
+
# Visibility is inherited from the original attribute.
|
243
|
+
#
|
244
|
+
# @param attribute [Cattri::Attribute] the attribute for which to define predicate methods
|
245
|
+
# @return [void]
|
246
|
+
def define_predicate_methods(attribute)
|
247
|
+
return unless attribute[:predicate]
|
248
|
+
|
249
|
+
predicate_name = :"#{attribute.name}?"
|
250
|
+
|
251
|
+
# rubocop:disable Style/DoubleNegation
|
252
|
+
context.define_method(attribute, name: predicate_name) { !!send(attribute.name) }
|
253
|
+
return unless attribute[:instance_reader]
|
254
|
+
|
255
|
+
Cattri::AttributeDefiner.define_instance_level_method(attribute, context, name: predicate_name) do
|
256
|
+
!!self.class.__send__(attribute.name)
|
257
|
+
end
|
258
|
+
# rubocop:enable Style/DoubleNegation
|
259
|
+
end
|
260
|
+
|
119
261
|
# Internal registry of defined class-level attributes.
|
120
262
|
#
|
121
263
|
# @return [Hash{Symbol => Cattri::Attribute}]
|
data/lib/cattri/context.rb
CHANGED
@@ -63,11 +63,27 @@ module Cattri
|
|
63
63
|
name = (name || attribute.name).to_sym
|
64
64
|
return if method_defined?(name)
|
65
65
|
|
66
|
+
define_method!(attribute, name: name, &block)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Defines a method on the target class or singleton class, regardless of whether it already exists.
|
70
|
+
#
|
71
|
+
# This bypasses any checks for prior definition and forcibly installs the method using `define_method`.
|
72
|
+
# The method will be assigned visibility based on the attribute's `:access` setting.
|
73
|
+
#
|
74
|
+
# Used internally by attribute definers to (re)define writers, readers, or callables.
|
75
|
+
#
|
76
|
+
# @param attribute [Cattri::Attribute] the attribute whose context and access rules apply
|
77
|
+
# @param name [Symbol, nil] the method name to define (defaults to attribute name)
|
78
|
+
# @yield the method body to define
|
79
|
+
# @raise [Cattri::AttributeDefinitionError] if method definition fails
|
80
|
+
# @return [void]
|
81
|
+
def define_method!(attribute, name: nil, &block)
|
66
82
|
target = target_for(attribute)
|
67
83
|
|
68
84
|
begin
|
69
85
|
target.define_method(name, &block)
|
70
|
-
@defined_methods << name
|
86
|
+
@defined_methods << name unless method_defined?(name)
|
71
87
|
apply_access(name, attribute)
|
72
88
|
rescue StandardError => e
|
73
89
|
raise Cattri::AttributeDefinitionError.new(target, attribute, e)
|
data/lib/cattri/error.rb
CHANGED
@@ -30,9 +30,31 @@ module Cattri
|
|
30
30
|
# rescue Cattri::AttributeDefinedError => e
|
31
31
|
# puts e.message
|
32
32
|
class AttributeDefinedError < Cattri::AttributeError
|
33
|
-
# @param
|
34
|
-
|
35
|
-
|
33
|
+
# @param type [Symbol, String] either :class or :instance
|
34
|
+
# @param name [Symbol, String] the name of the missing attribute
|
35
|
+
def initialize(type, name)
|
36
|
+
super("#{type.capitalize} attribute :#{name} has already been defined")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Raised when attempting to access or modify an attribute that has not been defined.
|
41
|
+
#
|
42
|
+
# This applies to both class-level and instance-level attributes.
|
43
|
+
# It is typically raised when calling `.class_attribute_setter` or `.instance_attribute_setter`
|
44
|
+
# on a name that does not exist or lacks the expected method (e.g., writer).
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# raise Cattri::AttributeNotDefinedError.new(:class, :foo)
|
48
|
+
# # => Class attribute :foo has not been defined
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# rescue Cattri::AttributeNotDefinedError => e
|
52
|
+
# puts e.message
|
53
|
+
class AttributeNotDefinedError < Cattri::AttributeError
|
54
|
+
# @param type [Symbol, String] either :class or :instance
|
55
|
+
# @param name [Symbol, String] the name of the missing attribute
|
56
|
+
def initialize(type, name)
|
57
|
+
super("#{type.capitalize} attribute :#{name} has not been defined")
|
36
58
|
end
|
37
59
|
end
|
38
60
|
|
@@ -63,10 +85,21 @@ module Cattri
|
|
63
85
|
# @example
|
64
86
|
# raise Cattri::UnsupportedTypeError.new(:foo)
|
65
87
|
# # => Attribute type :foo is not supported
|
66
|
-
class UnsupportedTypeError <
|
88
|
+
class UnsupportedTypeError < Cattri::AttributeError
|
67
89
|
# @param type [Symbol] the invalid type that triggered the error
|
68
90
|
def initialize(type)
|
69
91
|
super("Attribute type :#{type} is not supported")
|
70
92
|
end
|
71
93
|
end
|
94
|
+
|
95
|
+
# Raised when a block is provided when defining a group of attributes `cattr :attr_a, :attr_b do ... end`
|
96
|
+
#
|
97
|
+
# @example
|
98
|
+
# raise Cattri::AmbiguousBlockError
|
99
|
+
# # => Cannot define multiple attributes with a block
|
100
|
+
class AmbiguousBlockError < Cattri::AttributeError
|
101
|
+
def initialize
|
102
|
+
super("Cannot define multiple attributes with a block")
|
103
|
+
end
|
104
|
+
end
|
72
105
|
end
|
@@ -26,29 +26,43 @@ module Cattri
|
|
26
26
|
|
27
27
|
# Defines instance-level attribute DSL methods.
|
28
28
|
module ClassMethods
|
29
|
-
# Defines
|
29
|
+
# Defines one or more instance-level attributes with optional default and coercion.
|
30
30
|
#
|
31
|
-
#
|
31
|
+
# This method supports defining multiple attributes at once, provided they share the same options.
|
32
|
+
# If a block is given, only one attribute may be defined to avoid ambiguity.
|
33
|
+
#
|
34
|
+
# @example Define multiple attributes with shared defaults
|
35
|
+
# iattr :foo, :bar, default: []
|
36
|
+
#
|
37
|
+
# @example Define a single attribute with coercion
|
38
|
+
# iattr :level do |val|
|
39
|
+
# Integer(val)
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# @param names [Array<Symbol | String>] the names of the attributes to define
|
32
43
|
# @param options [Hash] additional options like `:default`, `:reader`, `:writer`
|
33
44
|
# @option options [Object, Proc] :default the default value or lambda
|
34
45
|
# @option options [Boolean] :reader whether to define a reader method (default: true)
|
35
46
|
# @option options [Boolean] :writer whether to define a writer method (default: true)
|
36
47
|
# @option options [Symbol] :access method visibility (:public, :protected, :private)
|
48
|
+
# @option options [Boolean] :predicate whether to define a predicate-style alias method
|
49
|
+
# (e.g., `foo?`) for the attribute
|
37
50
|
# @yieldparam value [Object] optional custom coercion logic for the setter
|
38
51
|
# @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
|
39
52
|
# `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
|
40
53
|
# already defined or an error occurs while defining methods)
|
41
|
-
|
42
|
-
|
43
|
-
|
54
|
+
# @return [void]
|
55
|
+
def instance_attribute(*names, **options, &block)
|
56
|
+
raise Cattri::AmbiguousBlockError if names.size > 1 && block_given?
|
44
57
|
|
45
|
-
|
58
|
+
names.each do |name|
|
59
|
+
if name.end_with?("?")
|
60
|
+
raise Cattri::AttributeError,
|
61
|
+
"Attribute names ending in '?' are not allowed. Use `predicate: true` or `iattr_alias` instead."
|
46
62
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
rescue StandardError => e
|
51
|
-
raise Cattri::AttributeDefinitionError.new(self, attribute, e)
|
63
|
+
end
|
64
|
+
|
65
|
+
define_instance_attribute(name, options, block)
|
52
66
|
end
|
53
67
|
end
|
54
68
|
|
@@ -56,30 +70,95 @@ module Cattri
|
|
56
70
|
#
|
57
71
|
# Equivalent to `instance_attribute(..., writer: false)`
|
58
72
|
#
|
59
|
-
# @param
|
60
|
-
# @param options [Hash]
|
73
|
+
# @param names [Array<Symbol | String>] the names of the attributes to define
|
74
|
+
# @param options [Hash] additional options like `:default`, `:reader`, `:writer`
|
75
|
+
# @option options [Object, Proc] :default the default value or lambda
|
76
|
+
# @option options [Boolean] :reader whether to define a reader method (default: true)
|
77
|
+
# @option options [Symbol] :access method visibility (:public, :protected, :private)
|
78
|
+
# @option options [Boolean] :predicate whether to define a predicate-style alias method
|
79
|
+
# (e.g., `foo?`) for the attribute
|
80
|
+
# @yieldparam value [Object] optional custom coercion logic for the setter
|
81
|
+
# @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
|
82
|
+
# `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
|
83
|
+
# already defined or an error occurs while defining methods)
|
61
84
|
# @return [void]
|
62
|
-
def instance_attribute_reader(
|
63
|
-
instance_attribute(
|
85
|
+
def instance_attribute_reader(*names, **options)
|
86
|
+
instance_attribute(*names, **options, writer: false)
|
64
87
|
end
|
65
88
|
|
66
89
|
# Defines a write-only instance-level attribute.
|
67
90
|
#
|
68
|
-
# Equivalent to `instance_attribute(..., reader: false)
|
91
|
+
# Equivalent to `instance_attribute(..., reader: false)`. The predicate: option is not allowed
|
92
|
+
# when defining writer methods.
|
69
93
|
#
|
70
|
-
# @param
|
71
|
-
# @param options [Hash]
|
72
|
-
# @
|
94
|
+
# @param names [Array<Symbol | String>] the names of the attributes to define
|
95
|
+
# @param options [Hash] additional options like `:default`, `:reader`, `:writer`
|
96
|
+
# @option options [Object, Proc] :default the default value or lambda
|
97
|
+
# @option options [Boolean] :writer whether to define a writer method (default: true)
|
98
|
+
# @option options [Symbol] :access method visibility (:public, :protected, :private)
|
99
|
+
# @yieldparam value [Object] optional custom coercion logic for the setter
|
100
|
+
# @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
|
101
|
+
# `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
|
102
|
+
# already defined or an error occurs while defining methods)
|
73
103
|
# @return [void]
|
74
|
-
def instance_attribute_writer(
|
75
|
-
instance_attribute(
|
104
|
+
def instance_attribute_writer(*names, **options, &block)
|
105
|
+
instance_attribute(*names, **options.merge(reader: false, predicate: false), &block)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Updates the setter behavior of an existing instance-level attribute.
|
109
|
+
#
|
110
|
+
# This allows coercion logic to be defined or overridden after the attribute
|
111
|
+
# has been declared using `iattr`, as long as the writer method exists.
|
112
|
+
#
|
113
|
+
# @example Add coercion to an existing attribute
|
114
|
+
# iattr :format
|
115
|
+
# iattr_setter :format do |val|
|
116
|
+
# val.to_s.downcase.to_sym
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# @param name [Symbol, String] the name of the attribute
|
120
|
+
# @yieldparam value [Object] the value passed to the setter
|
121
|
+
# @yieldreturn [Object] the coerced value to be assigned
|
122
|
+
# @raise [Cattri::AttributeNotDefinedError] if the attribute is not defined or the writer method does not exist
|
123
|
+
# @raise [Cattri::AttributeDefinitionError] if method redefinition fails
|
124
|
+
# @return [void]
|
125
|
+
def instance_attribute_setter(name, &block)
|
126
|
+
attribute = __cattri_instance_attributes[name.to_sym]
|
127
|
+
|
128
|
+
raise Cattri::AttributeNotDefinedError.new(:instance, name) if attribute.nil?
|
129
|
+
raise Cattri::AttributeError, "Cannot define setter for readonly attribute :#{name}" unless attribute[:writer]
|
130
|
+
|
131
|
+
attribute.instance_variable_set(:@setter, attribute.send(:normalize_setter, block))
|
132
|
+
Cattri::AttributeDefiner.define_writer!(attribute, context)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Defines an alias method for an existing instance-level attribute.
|
136
|
+
#
|
137
|
+
# This does **not** register a new attribute; it simply defines a method
|
138
|
+
# (e.g., a predicate-style alias like `foo?`) that delegates to an existing one.
|
139
|
+
#
|
140
|
+
# The alias method inherits the visibility of the original attribute.
|
141
|
+
#
|
142
|
+
# @param alias_name [Symbol, String] the new method name (e.g., `:foo?`)
|
143
|
+
# @param original [Symbol, String] the name of the existing attribute to delegate to (e.g., `:foo`)
|
144
|
+
# @raise [Cattri::AttributeNotDefinedError] if the original attribute is not defined
|
145
|
+
# @return [void]
|
146
|
+
def instance_attribute_alias(alias_name, original)
|
147
|
+
attribute = __cattri_instance_attributes[original.to_sym]
|
148
|
+
raise Cattri::AttributeNotDefinedError.new(:instance, original) if attribute.nil?
|
149
|
+
|
150
|
+
context.define_method(attribute, name: alias_name) { public_send(original) }
|
76
151
|
end
|
77
152
|
|
78
153
|
# Returns a list of defined instance-level attribute names.
|
79
154
|
#
|
80
155
|
# @return [Array<Symbol>]
|
81
156
|
def instance_attributes
|
82
|
-
|
157
|
+
([self] + ancestors + singleton_class.included_modules)
|
158
|
+
.uniq
|
159
|
+
.select { |mod| mod.respond_to?(:__cattri_instance_attributes, true) }
|
160
|
+
.flat_map { |mod| mod.send(:__cattri_instance_attributes).keys }
|
161
|
+
.uniq
|
83
162
|
end
|
84
163
|
|
85
164
|
# Checks if an instance-level attribute has been defined.
|
@@ -100,34 +179,81 @@ module Cattri
|
|
100
179
|
|
101
180
|
# @!method iattr(name, **options, &block)
|
102
181
|
# Alias for {#instance_attribute}
|
182
|
+
# @see #instance_attribute
|
103
183
|
alias iattr instance_attribute
|
104
184
|
|
105
185
|
# @!method iattr_accessor(name, **options, &block)
|
106
186
|
# Alias for {#instance_attribute}
|
187
|
+
# @see #instance_attribute
|
107
188
|
alias iattr_accessor instance_attribute
|
108
189
|
|
109
190
|
# @!method iattr_reader(name, **options)
|
110
191
|
# Alias for {#instance_attribute_reader}
|
192
|
+
# @see #instance_attribute_reader
|
111
193
|
alias iattr_reader instance_attribute_reader
|
112
194
|
|
113
195
|
# @!method iattr_writer(name, **options, &block)
|
114
196
|
# Alias for {#instance_attribute_writer}
|
197
|
+
# @see #instance_attribute_writer
|
115
198
|
alias iattr_writer instance_attribute_writer
|
116
199
|
|
200
|
+
# @!method iattr_setter(name, &block)
|
201
|
+
# Alias for {#instance_attribute_setter}
|
202
|
+
# @see #instance_attribute_setter
|
203
|
+
alias iattr_setter instance_attribute_setter
|
204
|
+
|
205
|
+
# @!method iattr_alias(name, &block)
|
206
|
+
# Alias for {#instance_attribute_alias}
|
207
|
+
# @see #instance_attribute_alias
|
208
|
+
alias iattr_alias instance_attribute_alias
|
209
|
+
|
117
210
|
# @!method iattrs
|
118
211
|
# Alias for {#instance_attributes}
|
212
|
+
# @see #instance_attributes
|
119
213
|
alias iattrs instance_attributes
|
120
214
|
|
121
215
|
# @!method iattr_defined?(name)
|
122
216
|
# Alias for {#instance_attribute_defined?}
|
217
|
+
# @see #instance_attribute_defined?
|
123
218
|
alias iattr_defined? instance_attribute_defined?
|
124
219
|
|
125
220
|
# @!method iattr_definition(name)
|
126
221
|
# Alias for {#instance_attribute_definition}
|
222
|
+
# @see #instance_attribute_definition
|
127
223
|
alias iattr_definition instance_attribute_definition
|
128
224
|
|
129
225
|
private
|
130
226
|
|
227
|
+
# Defines a single instance-level attribute.
|
228
|
+
#
|
229
|
+
# This is the internal implementation used by {.instance_attribute} and its aliases.
|
230
|
+
# It creates a `Cattri::Attribute`, registers it, and defines the appropriate
|
231
|
+
# reader and/or writer methods on the class.
|
232
|
+
#
|
233
|
+
# @param name [Symbol, String] the attribute name
|
234
|
+
# @param options [Hash] additional options for the attribute
|
235
|
+
# @param block [Proc, nil] optional setter coercion logic
|
236
|
+
#
|
237
|
+
# @raise [Cattri::AttributeDefinedError] if the attribute has already been defined
|
238
|
+
# @raise [Cattri::AttributeDefinitionError] if method definition fails
|
239
|
+
#
|
240
|
+
# @return [void]
|
241
|
+
def define_instance_attribute(name, options, block) # rubocop:disable Metrics/AbcSize
|
242
|
+
options[:access] ||= __cattri_visibility
|
243
|
+
attribute = Cattri::Attribute.new(name, :instance, options, block)
|
244
|
+
|
245
|
+
raise Cattri::AttributeDefinedError.new(:instance, name) if instance_attribute_defined?(attribute.name)
|
246
|
+
|
247
|
+
begin
|
248
|
+
__cattri_instance_attributes[name.to_sym] = attribute
|
249
|
+
Cattri::AttributeDefiner.define_accessor(attribute, context)
|
250
|
+
rescue StandardError => e
|
251
|
+
raise Cattri::AttributeDefinitionError.new(self, attribute, e)
|
252
|
+
end
|
253
|
+
|
254
|
+
context.define_method(attribute, name: :"#{name}?") { !!send(attribute.name) } if options[:predicate]
|
255
|
+
end
|
256
|
+
|
131
257
|
# Internal registry of instance attributes defined on the class.
|
132
258
|
#
|
133
259
|
# @return [Hash{Symbol => Cattri::Attribute}]
|
data/lib/cattri/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cattri
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Lucas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-04-
|
11
|
+
date: 2025-04-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|