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
@@ -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 "
|
4
|
-
require_relative "
|
3
|
+
require_relative "attribute"
|
4
|
+
require_relative "context"
|
5
|
+
require_relative "attribute_definer"
|
6
|
+
require_relative "visibility"
|
5
7
|
|
6
8
|
module Cattri
|
7
|
-
#
|
9
|
+
# Mixin that provides support for defining class-level attributes.
|
8
10
|
#
|
9
|
-
#
|
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
|
-
# -
|
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
|
-
#
|
32
|
-
#
|
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
|
-
|
40
|
-
|
41
|
-
#
|
42
|
-
|
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
|
-
# @
|
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
|
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
|
-
# @
|
52
|
-
# @
|
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(
|
55
|
-
|
47
|
+
def class_attribute(*names, **options, &block)
|
48
|
+
raise Cattri::AmbiguousBlockError if names.size > 1 && block_given?
|
56
49
|
|
57
|
-
|
58
|
-
|
50
|
+
names.each { |name| define_class_attribute(name, options, block) }
|
51
|
+
end
|
59
52
|
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
#
|
71
|
+
# Updates the setter behavior of an existing class-level attribute.
|
66
72
|
#
|
67
|
-
#
|
68
|
-
#
|
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
|
71
|
-
|
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
|
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
|
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
|
116
|
+
# Returns the full attribute definition object.
|
90
117
|
#
|
91
118
|
# @param name [Symbol]
|
92
|
-
# @return [
|
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
|
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
|
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
|
136
|
+
# @see #class_attribute_reader
|
131
137
|
alias cattr_reader class_attribute_reader
|
132
138
|
|
133
139
|
# @!method cattrs
|
134
|
-
#
|
135
|
-
# @
|
140
|
+
# Alias for {.class_attributes}
|
141
|
+
# @return [Array<Symbol>]
|
136
142
|
alias cattrs class_attributes
|
137
143
|
|
138
144
|
# @!method cattr_defined?(name)
|
139
|
-
#
|
140
|
-
# @
|
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
|
144
|
-
#
|
145
|
-
# @
|
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
|
158
|
+
# Defines a single class-level attribute.
|
161
159
|
#
|
162
|
-
#
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
186
|
-
|
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
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
195
|
-
return if definition[:readonly]
|
178
|
+
begin
|
179
|
+
__cattri_class_attributes[name] = attribute
|
196
180
|
|
197
|
-
|
198
|
-
|
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
|
-
#
|
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
|
-
# @
|
213
|
-
|
214
|
-
|
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
|
-
#
|
195
|
+
# Context object used to define accessors with scoped visibility.
|
223
196
|
#
|
224
|
-
# @
|
225
|
-
#
|
226
|
-
|
227
|
-
|
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
|