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.
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cattri
4
+ # Defines attribute accessors on a given target class using Cattri's Context.
5
+ #
6
+ # This class provides a set of utility methods to generate reader and writer
7
+ # methods dynamically, with support for default values, coercion, and memoization.
8
+ #
9
+ # All accessors are defined through the Cattri::Context abstraction to ensure
10
+ # consistent scoping, visibility, and method tracking.
11
+ class AttributeDefiner
12
+ class << self
13
+ # Defines a callable accessor for class-level attributes.
14
+ #
15
+ # The generated method:
16
+ # - Returns the memoized default value when called with no args or if readonly
17
+ # - Otherwise, calls the attribute’s setter and memoizes the result
18
+ #
19
+ # If the attribute is not readonly, a writer (`foo=`) is also defined.
20
+ #
21
+ # @param attribute [Cattri::Attribute]
22
+ # @param context [Cattri::Context]
23
+ # @return [void]
24
+ # @raise [Cattri::AttributeError] if the setter raises an error
25
+ def define_callable_accessor(attribute, context)
26
+ return unless attribute.class_level?
27
+
28
+ context.define_method(attribute) do |*args, **kwargs|
29
+ readonly = (args.empty? && kwargs.empty?) || attribute[:readonly]
30
+ return AttributeDefiner.send(:memoize_default_value, self, attribute) if readonly
31
+
32
+ value = attribute.invoke_setter(*args, **kwargs)
33
+ instance_variable_set(attribute.ivar, value)
34
+ end
35
+
36
+ define_writer(attribute, context) unless attribute[:readonly]
37
+ end
38
+
39
+ # Defines an instance-level reader for class-level attributes.
40
+ #
41
+ # This method delegates the instance-level call to the class method.
42
+ # It is used when `instance_reader: true` is specified.
43
+ #
44
+ # @param attribute [Cattri::Attribute]
45
+ # @param context [Cattri::Context]
46
+ # @return [void]
47
+ def define_instance_level_reader(attribute, context)
48
+ return unless attribute.class_level?
49
+
50
+ context.target.define_method(attribute.name) do
51
+ self.class.__send__(attribute.name)
52
+ end
53
+
54
+ context.send(:apply_access, attribute.name, attribute)
55
+ end
56
+
57
+ # Defines standard reader and writer methods for instance-level attributes.
58
+ #
59
+ # Skips definition if `reader: false` or `writer: false` is specified.
60
+ #
61
+ # @param attribute [Cattri::Attribute]
62
+ # @param context [Cattri::Context]
63
+ # @return [void]
64
+ def define_accessor(attribute, context)
65
+ define_reader(attribute, context) if attribute[:reader]
66
+ define_writer(attribute, context) if attribute[:writer]
67
+ end
68
+
69
+ # Defines a memoizing reader for the given attribute.
70
+ #
71
+ # This is used for both class and instance attributes, and ensures that
72
+ # the default value is computed only once and stored in the ivar.
73
+ #
74
+ # @param attribute [Cattri::Attribute]
75
+ # @param context [Cattri::Context]
76
+ # @return [void]
77
+ def define_reader(attribute, context)
78
+ context.define_method(attribute) do
79
+ AttributeDefiner.send(:memoize_default_value, self, attribute)
80
+ end
81
+ end
82
+
83
+ # Defines a writer method (`foo=`) that sets and coerces a value via the attribute setter.
84
+ #
85
+ # @param attribute [Cattri::Attribute]
86
+ # @param context [Cattri::Context]
87
+ # @return [void]
88
+ def define_writer(attribute, context)
89
+ context.define_method(attribute, name: :"#{attribute.name}=") do |value|
90
+ coerced_value = attribute.setter.call(value)
91
+ instance_variable_set(attribute.ivar, coerced_value)
92
+ end
93
+ end
94
+
95
+ # Defines, or redefines, a writer method (`foo=`) that sets and coerces a value via the attribute setter.
96
+ #
97
+ # @param attribute [Cattri::Attribute]
98
+ # @param context [Cattri::Context]
99
+ # @return [void]
100
+ def define_writer!(attribute, context)
101
+ context.define_method!(attribute, name: :"#{attribute.name}=") do |value|
102
+ coerced_value = attribute.setter.call(value)
103
+ instance_variable_set(attribute.ivar, coerced_value)
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ # Returns the memoized value for an attribute or computes it from the default.
110
+ #
111
+ # This helper ensures lazy initialization while guarding against errors in the default proc.
112
+ #
113
+ # @param receiver [Object]
114
+ # @param attribute [Cattri::Attribute]
115
+ # @return [Object]
116
+ # @raise [Cattri::AttributeError] if the default block raises an error
117
+ def memoize_default_value(receiver, attribute)
118
+ return receiver.instance_variable_get(attribute.ivar) if receiver.instance_variable_defined?(attribute.ivar)
119
+
120
+ receiver.instance_variable_set(attribute.ivar, attribute.invoke_default)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -1,84 +1,111 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "error"
4
- require_relative "helpers"
3
+ require_relative "attribute"
4
+ require_relative "context"
5
+ require_relative "attribute_definer"
6
+ require_relative "visibility"
5
7
 
6
8
  module Cattri
7
- # Provides a DSL for defining class-level attributes with support for:
9
+ # Mixin that provides support for defining class-level attributes.
8
10
  #
9
- # - Static or dynamic default values
11
+ # This module is intended to be extended onto a class and provides a DSL
12
+ # for defining configuration-style attributes at the class level using `cattr`.
13
+ #
14
+ # Features:
15
+ # - Default values (static, frozen, or callable)
10
16
  # - Optional coercion via setter blocks
11
17
  # - Optional instance-level readers
12
- # - Read-only attribute enforcement
13
- # - Inheritance-safe duplication
14
- # - Attribute locking to prevent mutation in subclasses
15
- #
16
- # This module is designed for advanced metaprogramming needs such as DSL builders,
17
- # configuration objects, and plugin systems that require reusable and introspectable
18
- # class-level state.
19
- #
20
- # @example
21
- # class MyClass
22
- # extend Cattri::ClassAttributes
23
- #
24
- # cattr :format, default: :json
25
- # cattr_reader :version, default: "1.0.0"
26
- # cattr :enabled, default: true do |value|
27
- # !!value
28
- # end
29
- # end
18
+ # - Visibility enforcement (`:public`, `:protected`, `:private`)
30
19
  #
31
- # MyClass.format # => :json
32
- # MyClass.format :xml
33
- # MyClass.format # => :xml
34
- # MyClass.version # => "1.0.0"
35
- #
36
- # instance = MyClass.new
37
- # instance.format # => :xml
20
+ # Class attributes are stored internally as `Cattri::Attribute` instances and
21
+ # values are memoized using class-level instance variables.
38
22
  module ClassAttributes
39
- include Cattri::Helpers
40
-
41
- # Default options applied to all class-level attributes.
42
- DEFAULT_OPTIONS = { default: nil, readonly: false }.freeze
43
-
44
- # Defines a class-level attribute with optional default, coercion, and reader access.
23
+ # Defines one or more class-level attributes with optional default, coercion, and reader access.
24
+ #
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.
45
27
  #
46
- # @param name [Symbol] the attribute name
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
47
37
  # @param options [Hash] additional attribute options
48
- # @option options [Object, Proc] :default the default value or callable
38
+ # @option options [Object, Proc] :default the default value or lambda
49
39
  # @option options [Boolean] :readonly whether the attribute is read-only
50
- # @option options [Boolean] :instance_reader whether to define an instance-level reader
51
- # @yield [*args] Optional setter block for custom coercion
52
- # @raise [Cattri::Error] if attribute is already defined
40
+ # @option options [Boolean] :instance_reader whether to define an instance-level reader (default: true)
41
+ # @option options [Symbol] :access visibility level (:public, :protected, :private)
42
+ # @yieldparam value [Object] an optional custom setter block
43
+ # @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
44
+ # `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
45
+ # already defined or an error occurs while defining methods)
53
46
  # @return [void]
54
- def class_attribute(name, **options, &block)
55
- define_inheritance unless respond_to?(:__cattri_class_attributes)
47
+ def class_attribute(*names, **options, &block)
48
+ raise Cattri::AmbiguousBlockError if names.size > 1 && block_given?
56
49
 
57
- name, definition = define_attribute(name, options, block, DEFAULT_OPTIONS)
58
- raise Cattri::Error, "Class attribute `#{name}` already defined" if class_attribute_defined?(name)
50
+ names.each { |name| define_class_attribute(name, options, block) }
51
+ end
59
52
 
60
- __cattri_class_attributes[name] = definition
61
- define_accessor(name, definition)
62
- define_instance_reader(name) if options.fetch(:instance_reader, true)
53
+ # Defines a read-only class attribute.
54
+ #
55
+ # Equivalent to calling `class_attribute(name, readonly: true, ...)`
56
+ #
57
+ # @param names [Array<Symbol | String>] the names of the attributes to define
58
+ # @param options [Hash] additional attribute options
59
+ # @option options [Object, Proc] :default the default value or lambda
60
+ # @option options [Boolean] :readonly whether the attribute is read-only
61
+ # @option options [Boolean] :instance_reader whether to define an instance-level reader (default: true)
62
+ # @option options [Symbol] :access visibility level (:public, :protected, :private)
63
+ # @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
64
+ # `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
65
+ # already defined or an error occurs while defining methods)
66
+ # @return [void]
67
+ def class_attribute_reader(*names, **options)
68
+ class_attribute(*names, **options, readonly: true)
63
69
  end
64
70
 
65
- # Defines a read-only class attribute (no writer).
71
+ # Updates the setter behavior of an existing class-level attribute.
66
72
  #
67
- # @param name [Symbol]
68
- # @param options [Hash]
73
+ # This allows coercion logic to be defined or overridden after the attribute
74
+ # has been declared using `cattr`, as long as the writer method exists.
75
+ #
76
+ # @example Add coercion to an existing attribute
77
+ # cattr :format
78
+ # cattr_setter :format do |val|
79
+ # val.to_s.downcase.to_sym
80
+ # end
81
+ #
82
+ # @param name [Symbol, String] the name of the attribute
83
+ # @yieldparam value [Object] the value passed to the setter
84
+ # @yieldreturn [Object] the coerced value to be assigned
85
+ # @raise [Cattri::AttributeNotDefinedError] if the attribute is not defined or the writer method does not exist
86
+ # @raise [Cattri::AttributeDefinitionError] if method redefinition fails
69
87
  # @return [void]
70
- def class_attribute_reader(name, **options)
71
- class_attribute(name, readonly: true, **options)
88
+ def class_attribute_setter(name, &block)
89
+ name = name.to_sym
90
+ attribute = __cattri_class_attributes[name]
91
+ puts "<<< #{attribute} = #{name}>"
92
+
93
+ if attribute.nil? || !context.method_defined?(:"#{name}=")
94
+ raise Cattri::AttributeNotDefinedError.new(:class, name)
95
+ end
96
+
97
+ attribute.instance_variable_set(:@setter, attribute.send(:normalize_setter, block))
98
+ Cattri::AttributeDefiner.define_writer!(attribute, context)
72
99
  end
73
100
 
74
- # Returns all defined class-level attribute names.
101
+ # Returns a list of defined class attribute names.
75
102
  #
76
103
  # @return [Array<Symbol>]
77
104
  def class_attributes
78
105
  __cattri_class_attributes.keys
79
106
  end
80
107
 
81
- # Checks whether a class-level attribute is defined.
108
+ # Checks whether a class attribute has been defined.
82
109
  #
83
110
  # @param name [Symbol]
84
111
  # @return [Boolean]
@@ -86,161 +113,92 @@ module Cattri
86
113
  __cattri_class_attributes.key?(name.to_sym)
87
114
  end
88
115
 
89
- # Returns metadata for a given class-level attribute.
116
+ # Returns the full attribute definition object.
90
117
  #
91
118
  # @param name [Symbol]
92
- # @return [Hash, nil]
119
+ # @return [Cattri::Attribute, nil]
93
120
  def class_attribute_definition(name)
94
121
  __cattri_class_attributes[name.to_sym]
95
122
  end
96
123
 
97
- # Resets all defined class attributes to their default values.
98
- #
99
- # @return [void]
100
- def reset_class_attributes!
101
- reset_attributes!(self, __cattri_class_attributes.values)
102
- end
103
-
104
- # Resets a single class attribute to its default value.
105
- #
106
- # @param name [Symbol]
107
- # @return [void]
108
- def reset_class_attribute!(name)
109
- definition = __cattri_class_attributes[name]
110
- return unless definition
111
-
112
- reset_attributes!(self, [definition])
113
- end
114
-
115
- # alias lock_cattrs! lock_class_attributes!
116
- # alias cattrs_locked? class_attributes_locked?
117
-
118
124
  # @!method cattr(name, **options, &block)
119
125
  # Alias for {.class_attribute}
120
- # @see .class_attribute
126
+ # @see #class_attribute
121
127
  alias cattr class_attribute
122
128
 
123
129
  # @!method cattr_accessor(name, **options, &block)
124
130
  # Alias for {.class_attribute}
125
- # @see .class_attribute
131
+ # @see #class_attribute
126
132
  alias cattr_accessor class_attribute
127
133
 
128
134
  # @!method cattr_reader(name, **options)
129
135
  # Alias for {.class_attribute_reader}
130
- # @see .class_attribute_reader
136
+ # @see #class_attribute_reader
131
137
  alias cattr_reader class_attribute_reader
132
138
 
133
139
  # @!method cattrs
134
- # @return [Array<Symbol>] all defined class attribute names
135
- # @see .class_attributes
140
+ # Alias for {.class_attributes}
141
+ # @return [Array<Symbol>]
136
142
  alias cattrs class_attributes
137
143
 
138
144
  # @!method cattr_defined?(name)
139
- # @return [Boolean] whether the given attribute has been defined
140
- # @see .class_attribute_defined?
145
+ # Alias for {.class_attribute_defined?}
146
+ # @param name [Symbol]
147
+ # @return [Boolean]
141
148
  alias cattr_defined? class_attribute_defined?
142
149
 
143
- # @!method cattr_for(name)
144
- # @return [Hash, nil] the internal metadata hash for a defined attribute
145
- # @see .class_attribute_for
150
+ # @!method cattr_definition(name)
151
+ # Alias for {.class_attribute_definition}
152
+ # @param name [Symbol]
153
+ # @return [Cattri::Attribute, nil]
146
154
  alias cattr_definition class_attribute_definition
147
155
 
148
- # @!method reset_cattrs!
149
- # Resets all class attributes to their default values.
150
- # @see .reset_class_attributes!
151
- alias reset_cattrs! reset_class_attributes!
152
-
153
- # @!method reset_cattr!(name)
154
- # Resets a specific class attribute to its default value.
155
- # @see .reset_class_attribute!
156
- alias reset_cattr! reset_class_attribute!
157
-
158
156
  private
159
157
 
160
- # Defines class-level inheritance behavior for declared attributes.
158
+ # Defines a single class-level attribute.
161
159
  #
162
- # @return [void]
163
- def define_inheritance
164
- unless singleton_class.method_defined?(:__cattri_class_attributes)
165
- define_singleton_method(:__cattri_class_attributes) { @__cattri_class_attributes ||= {} }
166
- end
167
-
168
- define_singleton_method(:inherited) do |subclass|
169
- super(subclass)
170
- subclass_attributes = {}
171
-
172
- __cattri_class_attributes.each do |name, definition|
173
- apply_attribute!(subclass, subclass_attributes, name, definition)
174
- end
175
-
176
- subclass.instance_variable_set(:@__cattri_class_attributes, subclass_attributes)
177
- end
178
- end
179
-
180
- # Defines the primary accessor method on the class.
160
+ # This is the internal implementation used by {.class_attribute} and its aliases.
161
+ # It constructs a `Cattri::Attribute`, registers it, and defines the necessary
162
+ # class and instance methods.
163
+ #
164
+ # @param name [Symbol] the name of the attribute to define
165
+ # @param options [Hash] additional attribute options (e.g., :default, :readonly)
166
+ # @param block [Proc, nil] an optional setter block for coercion
167
+ #
168
+ # @raise [Cattri::AttributeDefinedError] if the attribute has already been defined
169
+ # @raise [Cattri::AttributeDefinitionError] if method definition fails
181
170
  #
182
- # @param name [Symbol]
183
- # @param definition [Hash]
184
171
  # @return [void]
185
- def define_accessor(name, definition)
186
- ivar = definition[:ivar]
172
+ def define_class_attribute(name, options, block) # rubocop:disable Metrics/AbcSize
173
+ options[:access] ||= __cattri_visibility
174
+ attribute = Cattri::Attribute.new(name, :class, options, block)
187
175
 
188
- define_singleton_method(name) do |*args, **kwargs|
189
- readonly = readonly_call?(args, kwargs) || definition[:readonly]
190
- return apply_readonly(ivar, definition[:default]) if readonly
176
+ raise Cattri::AttributeDefinedError.new(:class, name) if class_attribute_defined?(attribute.name)
191
177
 
192
- instance_variable_set(ivar, definition[:setter].call(*args, **kwargs))
193
- end
194
-
195
- return if definition[:readonly]
178
+ begin
179
+ __cattri_class_attributes[name] = attribute
196
180
 
197
- define_singleton_method("#{name}=") do |value|
198
- instance_variable_set(ivar, definition[:setter].call(value))
181
+ Cattri::AttributeDefiner.define_callable_accessor(attribute, context)
182
+ Cattri::AttributeDefiner.define_instance_level_reader(attribute, context) if attribute[:instance_reader]
183
+ rescue StandardError => e
184
+ raise Cattri::AttributeDefinitionError.new(self, attribute, e)
199
185
  end
200
186
  end
201
187
 
202
- # Defines an instance-level reader that delegates to the class-level method.
203
- #
204
- # @param name [Symbol]
205
- # @return [void]
206
- def define_instance_reader(name)
207
- define_method(name) { self.class.__send__(name) }
208
- end
209
-
210
- # Applies the default value for a read-only call.
188
+ # Internal registry of defined class-level attributes.
211
189
  #
212
- # @param ivar [Symbol]
213
- # @param default [Proc]
214
- # @return [Object]
215
- def apply_readonly(ivar, default)
216
- return instance_variable_get(ivar) if instance_variable_defined?(ivar)
217
-
218
- value = default.call
219
- instance_variable_set(ivar, value)
190
+ # @return [Hash{Symbol => Cattri::Attribute}]
191
+ def __cattri_class_attributes
192
+ @__cattri_class_attributes ||= {}
220
193
  end
221
194
 
222
- # Applies inherited attribute definitions to a subclass.
195
+ # Context object used to define accessors with scoped visibility.
223
196
  #
224
- # @param subclass [Class]
225
- # @param attributes [Hash]
226
- # @param name [Symbol]
227
- # @param definition [Hash]
228
- # @return [void]
229
- def apply_attribute!(subclass, attributes, name, definition)
230
- value = instance_variable_get(definition[:ivar])
231
- value = value.dup rescue value # rubocop:disable Style/RescueModifier
232
-
233
- subclass.instance_variable_set(definition[:ivar], value)
234
- attributes[name] = definition
235
- end
236
-
237
- # Determines if the method call should be treated as read-only access.
238
- #
239
- # @param args [Array]
240
- # @param kwargs [Hash]
241
- # @return [Boolean]
242
- def readonly_call?(args, kwargs)
243
- args.empty? && kwargs.empty?
197
+ # @return [Cattri::Context]
198
+ # :nocov:
199
+ def context
200
+ @context ||= Context.new(self)
244
201
  end
202
+ # :nocov:
245
203
  end
246
204
  end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Cattri
6
+ # Provides a controlled interface for defining methods and instance variables
7
+ # on a target class or module. Used internally by Cattri to define attribute
8
+ # readers and writers while preserving access visibility and tracking which
9
+ # methods were explicitly created.
10
+ #
11
+ # This abstraction allows class and instance attribute logic to be composed
12
+ # consistently and safely across both standard and singleton contexts.
13
+ class Context
14
+ # Allowed Ruby visibility levels for methods.
15
+ ACCESS_LEVELS = %i[public protected private].freeze
16
+ private_constant :ACCESS_LEVELS
17
+
18
+ # @return [Module, Class] the receiver to which accessors will be added
19
+ attr_reader :target
20
+
21
+ # Initializes a new context wrapper around the given target.
22
+ #
23
+ # @param target [Module, Class] the object to define methods or ivars on
24
+ def initialize(target)
25
+ @target = target
26
+ @defined_methods = Set.new
27
+ end
28
+
29
+ # Returns the singleton class of the target.
30
+ #
31
+ # This is used to define methods on the class itself (not its instances).
32
+ #
33
+ # @return [Class]
34
+ def singleton
35
+ @target.singleton_class
36
+ end
37
+
38
+ # Checks whether a method is already defined on the target.
39
+ #
40
+ # This includes public, protected, private, and previously defined methods
41
+ # via this context (tracked in `@defined_methods`).
42
+ #
43
+ # @param method [String, Symbol]
44
+ # @return [Boolean]
45
+ def method_defined?(method)
46
+ @target.method_defined?(method) ||
47
+ @target.private_method_defined?(method) ||
48
+ @target.protected_method_defined?(method) ||
49
+ @defined_methods.include?(method.to_sym)
50
+ end
51
+
52
+ # Defines a method on the appropriate context (class or singleton).
53
+ #
54
+ # If the method already exists, it is not redefined. Visibility is applied
55
+ # according to the attribute's `:access` setting (defaulting to `:public`).
56
+ #
57
+ # @param attribute [Cattri::Attribute]
58
+ # @param name [Symbol, nil] optional method name override
59
+ # @yield the method implementation
60
+ # @raise [Cattri::AttributeDefinitionError] if method definition fails
61
+ # @return [void]
62
+ def define_method(attribute, name: nil, &block)
63
+ name = (name || attribute.name).to_sym
64
+ return if method_defined?(name)
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)
82
+ target = target_for(attribute)
83
+
84
+ begin
85
+ target.define_method(name, &block)
86
+ @defined_methods << name unless method_defined?(name)
87
+ apply_access(name, attribute)
88
+ rescue StandardError => e
89
+ raise Cattri::AttributeDefinitionError.new(target, attribute, e)
90
+ end
91
+ end
92
+
93
+ # Checks whether the target has the given instance variable.
94
+ #
95
+ # @param name [Symbol, String]
96
+ # @return [Boolean]
97
+ def ivar_defined?(name)
98
+ @target.instance_variable_defined?(sanitize_ivar(name))
99
+ end
100
+
101
+ # Retrieves the value of the specified instance variable on the target.
102
+ #
103
+ # @param name [Symbol, String]
104
+ # @return [Object]
105
+ def ivar_get(name)
106
+ @target.instance_variable_get(sanitize_ivar(name))
107
+ end
108
+
109
+ # Assigns a value to the specified instance variable on the target.
110
+ #
111
+ # @param name [Symbol, String]
112
+ # @param value [Object]
113
+ # @return [void]
114
+ def ivar_set(name, value)
115
+ @target.instance_variable_set(sanitize_ivar(name), value)
116
+ end
117
+
118
+ # Memoizes a value in the instance variable only if not already defined.
119
+ #
120
+ # @param name [Symbol, String]
121
+ # @param value [Object]
122
+ # @return [Object] the existing or assigned value
123
+ def ivar_memoize(name, value)
124
+ return ivar_get(name) if ivar_defined?(name)
125
+
126
+ ivar_set(name, value)
127
+ end
128
+
129
+ private
130
+
131
+ # Selects the correct definition target based on attribute type.
132
+ #
133
+ # @param attribute [Cattri::Attribute]
134
+ # @return [Module] either the target or its singleton class
135
+ def target_for(attribute)
136
+ attribute.class_level? ? singleton : @target
137
+ end
138
+
139
+ # Validates and normalizes access level.
140
+ #
141
+ # @param access [Symbol, String, nil]
142
+ # @return [Symbol] a valid visibility level
143
+ def validate_access(access)
144
+ access = (access || :public).to_sym
145
+ return access if ACCESS_LEVELS.include?(access)
146
+
147
+ warn "[Cattri] `#{access.inspect}` is not a supported access level, defaulting to :public"
148
+ :public
149
+ end
150
+
151
+ # Applies method visibility to a newly defined method.
152
+ #
153
+ # @param method_name [Symbol]
154
+ # @param attribute [Cattri::Attribute]
155
+ # @return [void]
156
+ def apply_access(method_name, attribute)
157
+ return if attribute.public?
158
+
159
+ access = validate_access(attribute[:access])
160
+ Module.instance_method(access).bind(@target).call(method_name)
161
+ end
162
+
163
+ # Ensures consistent formatting for ivar keys.
164
+ #
165
+ # @param name [Symbol, String]
166
+ # @return [Symbol]
167
+ def sanitize_ivar(name)
168
+ :"@#{name.to_s.delete_prefix("@")}"
169
+ end
170
+ end
171
+ end