cattri 0.1.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4558aae18122f29cebf5ddb34fe452ee6b00898d994b4cd8f2b0a825c5a599aa
4
- data.tar.gz: '092f86968324144a229510426858c9a4cee0245267503a160e27b6087384e88e'
3
+ metadata.gz: 2495a8756d30c301267ad2db2dfbd4c9669e817119fa316b608408665988b747
4
+ data.tar.gz: 47d682eaf5465e4ab6cc4bdb7dcc74d053c93c6b8d1ee3a8cfda0925cf68662a
5
5
  SHA512:
6
- metadata.gz: 61ac08cebe59853a0a4c163052bdabef6e70db7476bf0d93ff4a1184b706e326fab8583ad6b77533f7c297e5be15cd15cd3414ef8c751443d4f1ed8a06b768a6
7
- data.tar.gz: dc18a8a83dc56d9a4c2b122c3f824808f8037f0d1cb08a2d71e82ef6aa933d73c75efa083df12cb4b5a7dfb30a0ce22c0558871fe07365d73b601b2e32b537fd
6
+ metadata.gz: aae777877b7576a76b0be11a529aafaaac9c59066efd873fc9020689ba3ccefb78688411907b2246cb603dafa5e6868e1841e4724f296f634eac821a1bc27bc9
7
+ data.tar.gz: 8e919b6782d0cce71ed1a78bb46ebb92df995471238080a4e20604914d0fe958c041aec201eb598f9764598384fd776aaff03f5b131908082428c2d546c97cd9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
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
+
1
10
  ## [0.1.2] - 2025-04-22
2
11
 
3
12
  ### Added
data/README.md CHANGED
@@ -44,12 +44,13 @@ class Config
44
44
 
45
45
  # -- class‑level ----------------------------------
46
46
  cattr :flag_a, :flag_b, default: true
47
- cattr :enabled, default: true
47
+ cattr :enabled, default: true, predicate: true
48
48
  cattr :timeout, default: -> { 5.0 }, instance_reader: false
49
49
 
50
50
  # -- instance‑level -------------------------------
51
51
  iattr :item_a, :item_b, default: true
52
52
  iattr :name, default: "anonymous"
53
+ iattr_alias :username, :name
53
54
  iattr :age, default: 0 do |val| # coercion block
54
55
  Integer(val)
55
56
  end
@@ -57,7 +58,9 @@ end
57
58
 
58
59
  Config.enabled # => true
59
60
  Config.enabled = false
61
+ Config.enabled? # => false (created with predicate: true flag)
60
62
  Config.new.age = "42" # => 42
63
+ Config.new.username # proxy to Config.new.name
61
64
  ```
62
65
 
63
66
  ---
@@ -67,12 +70,14 @@ Config.new.age = "42" # => 42
67
70
  ### Class attributes (`cattr`)
68
71
 
69
72
  ```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
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
76
81
  ```
77
82
 
78
83
  ### Instance attributes (`iattr`)
@@ -80,7 +85,8 @@ cattr :log_level, default: :info,
80
85
  ```ruby
81
86
  iattr :token, default: -> { SecureRandom.hex(8) },
82
87
  reader: true,
83
- writer: false # read‑only
88
+ writer: false, # read‑only
89
+ predicate: true
84
90
  ```
85
91
 
86
92
  Both forms accept:
@@ -92,6 +98,7 @@ Both forms accept:
92
98
  | `reader:` / `writer:` | Disable reader or writer for instance attributes. |
93
99
  | `readonly:` | Shorthand for class attributes (`writer` is always present). |
94
100
  | `instance_reader:` | Expose class attribute as instance reader (default: **true**). |
101
+ | `predicate` | Define a `:name?` method that calls `!!send(name)`
95
102
 
96
103
  If you pass a block, it’s treated as a **coercion setter** and receives the incoming value.
97
104
 
@@ -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
- context.target.define_method(attribute.name) do
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.
@@ -39,6 +39,8 @@ module Cattri
39
39
  # @option options [Boolean] :readonly whether the attribute is read-only
40
40
  # @option options [Boolean] :instance_reader whether to define an instance-level reader (default: true)
41
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
42
44
  # @yieldparam value [Object] an optional custom setter block
43
45
  # @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
44
46
  # `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
@@ -47,7 +49,15 @@ module Cattri
47
49
  def class_attribute(*names, **options, &block)
48
50
  raise Cattri::AmbiguousBlockError if names.size > 1 && block_given?
49
51
 
50
- names.each { |name| define_class_attribute(name, options, block) }
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."
56
+
57
+ end
58
+
59
+ define_class_attribute(name, options, block)
60
+ end
51
61
  end
52
62
 
53
63
  # Defines a read-only class attribute.
@@ -60,6 +70,8 @@ module Cattri
60
70
  # @option options [Boolean] :readonly whether the attribute is read-only
61
71
  # @option options [Boolean] :instance_reader whether to define an instance-level reader (default: true)
62
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
63
75
  # @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
64
76
  # `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
65
77
  # already defined or an error occurs while defining methods)
@@ -98,11 +110,33 @@ module Cattri
98
110
  Cattri::AttributeDefiner.define_writer!(attribute, context)
99
111
  end
100
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
123
+ # @return [void]
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) }
129
+ end
130
+
101
131
  # Returns a list of defined class attribute names.
102
132
  #
103
133
  # @return [Array<Symbol>]
104
134
  def class_attributes
105
- __cattri_class_attributes.keys
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
106
140
  end
107
141
 
108
142
  # Checks whether a class attribute has been defined.
@@ -136,6 +170,16 @@ module Cattri
136
170
  # @see #class_attribute_reader
137
171
  alias cattr_reader class_attribute_reader
138
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
+
139
183
  # @!method cattrs
140
184
  # Alias for {.class_attributes}
141
185
  # @return [Array<Symbol>]
@@ -183,6 +227,35 @@ module Cattri
183
227
  rescue StandardError => e
184
228
  raise Cattri::AttributeDefinitionError.new(self, attribute, e)
185
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
186
259
  end
187
260
 
188
261
  # Internal registry of defined class-level attributes.
@@ -45,6 +45,8 @@ module Cattri
45
45
  # @option options [Boolean] :reader whether to define a reader method (default: true)
46
46
  # @option options [Boolean] :writer whether to define a writer method (default: true)
47
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
48
50
  # @yieldparam value [Object] optional custom coercion logic for the setter
49
51
  # @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
50
52
  # `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
@@ -53,7 +55,15 @@ module Cattri
53
55
  def instance_attribute(*names, **options, &block)
54
56
  raise Cattri::AmbiguousBlockError if names.size > 1 && block_given?
55
57
 
56
- names.each { |name| define_instance_attribute(name, options, block) }
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."
62
+
63
+ end
64
+
65
+ define_instance_attribute(name, options, block)
66
+ end
57
67
  end
58
68
 
59
69
  # Defines a read-only instance-level attribute.
@@ -65,6 +75,8 @@ module Cattri
65
75
  # @option options [Object, Proc] :default the default value or lambda
66
76
  # @option options [Boolean] :reader whether to define a reader method (default: true)
67
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
68
80
  # @yieldparam value [Object] optional custom coercion logic for the setter
69
81
  # @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
70
82
  # `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
@@ -76,7 +88,8 @@ module Cattri
76
88
 
77
89
  # Defines a write-only instance-level attribute.
78
90
  #
79
- # Equivalent to `instance_attribute(..., reader: false)`
91
+ # Equivalent to `instance_attribute(..., reader: false)`. The predicate: option is not allowed
92
+ # when defining writer methods.
80
93
  #
81
94
  # @param names [Array<Symbol | String>] the names of the attributes to define
82
95
  # @param options [Hash] additional options like `:default`, `:reader`, `:writer`
@@ -89,7 +102,7 @@ module Cattri
89
102
  # already defined or an error occurs while defining methods)
90
103
  # @return [void]
91
104
  def instance_attribute_writer(*names, **options, &block)
92
- instance_attribute(*names, **options.merge(reader: false), &block)
105
+ instance_attribute(*names, **options.merge(reader: false, predicate: false), &block)
93
106
  end
94
107
 
95
108
  # Updates the setter behavior of an existing instance-level attribute.
@@ -119,11 +132,33 @@ module Cattri
119
132
  Cattri::AttributeDefiner.define_writer!(attribute, context)
120
133
  end
121
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) }
151
+ end
152
+
122
153
  # Returns a list of defined instance-level attribute names.
123
154
  #
124
155
  # @return [Array<Symbol>]
125
156
  def instance_attributes
126
- __cattri_instance_attributes.keys
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
127
162
  end
128
163
 
129
164
  # Checks if an instance-level attribute has been defined.
@@ -144,34 +179,47 @@ module Cattri
144
179
 
145
180
  # @!method iattr(name, **options, &block)
146
181
  # Alias for {#instance_attribute}
182
+ # @see #instance_attribute
147
183
  alias iattr instance_attribute
148
184
 
149
185
  # @!method iattr_accessor(name, **options, &block)
150
186
  # Alias for {#instance_attribute}
187
+ # @see #instance_attribute
151
188
  alias iattr_accessor instance_attribute
152
189
 
153
190
  # @!method iattr_reader(name, **options)
154
191
  # Alias for {#instance_attribute_reader}
192
+ # @see #instance_attribute_reader
155
193
  alias iattr_reader instance_attribute_reader
156
194
 
157
195
  # @!method iattr_writer(name, **options, &block)
158
196
  # Alias for {#instance_attribute_writer}
197
+ # @see #instance_attribute_writer
159
198
  alias iattr_writer instance_attribute_writer
160
199
 
161
200
  # @!method iattr_setter(name, &block)
162
201
  # Alias for {#instance_attribute_setter}
202
+ # @see #instance_attribute_setter
163
203
  alias iattr_setter instance_attribute_setter
164
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
+
165
210
  # @!method iattrs
166
211
  # Alias for {#instance_attributes}
212
+ # @see #instance_attributes
167
213
  alias iattrs instance_attributes
168
214
 
169
215
  # @!method iattr_defined?(name)
170
216
  # Alias for {#instance_attribute_defined?}
217
+ # @see #instance_attribute_defined?
171
218
  alias iattr_defined? instance_attribute_defined?
172
219
 
173
220
  # @!method iattr_definition(name)
174
221
  # Alias for {#instance_attribute_definition}
222
+ # @see #instance_attribute_definition
175
223
  alias iattr_definition instance_attribute_definition
176
224
 
177
225
  private
@@ -190,7 +238,7 @@ module Cattri
190
238
  # @raise [Cattri::AttributeDefinitionError] if method definition fails
191
239
  #
192
240
  # @return [void]
193
- def define_instance_attribute(name, options, block)
241
+ def define_instance_attribute(name, options, block) # rubocop:disable Metrics/AbcSize
194
242
  options[:access] ||= __cattri_visibility
195
243
  attribute = Cattri::Attribute.new(name, :instance, options, block)
196
244
 
@@ -202,6 +250,8 @@ module Cattri
202
250
  rescue StandardError => e
203
251
  raise Cattri::AttributeDefinitionError.new(self, attribute, e)
204
252
  end
253
+
254
+ context.define_method(attribute, name: :"#{name}?") { !!send(attribute.name) } if options[:predicate]
205
255
  end
206
256
 
207
257
  # Internal registry of instance attributes defined on the class.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cattri
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cattri
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Lucas