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
data/lib/cattri/error.rb
CHANGED
@@ -1,5 +1,105 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Cattri
|
4
|
+
# Base error class for all exceptions raised by Cattri.
|
5
|
+
#
|
6
|
+
# All Cattri-specific errors inherit from this class.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# rescue Cattri::Error => e
|
10
|
+
# puts "Something went wrong with Cattri: #{e.message}"
|
4
11
|
class Error < StandardError; end
|
12
|
+
|
13
|
+
# Parent class for all attribute-related errors in Cattri.
|
14
|
+
#
|
15
|
+
# This includes definition conflicts, setter failures, or configuration issues.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# rescue Cattri::AttributeError => e
|
19
|
+
# puts "Attribute error: #{e.message}"
|
20
|
+
class AttributeError < Cattri::Error; end
|
21
|
+
|
22
|
+
# Raised when a class or instance attribute is defined more than once.
|
23
|
+
#
|
24
|
+
# This helps detect naming collisions during DSL usage.
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# raise Cattri::AttributeDefinedError.new(attribute)
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# rescue Cattri::AttributeDefinedError => e
|
31
|
+
# puts e.message
|
32
|
+
class AttributeDefinedError < Cattri::AttributeError
|
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")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Raised when a method definition (reader, writer, or callable) fails.
|
62
|
+
#
|
63
|
+
# This wraps the original error that occurred during `define_method` or visibility handling.
|
64
|
+
#
|
65
|
+
# @example
|
66
|
+
# raise Cattri::AttributeDefinitionError.new(target, attribute, error)
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
# rescue Cattri::AttributeDefinitionError => e
|
70
|
+
# puts e.message
|
71
|
+
class AttributeDefinitionError < Cattri::AttributeError
|
72
|
+
# @param target [Module] the class or module receiving the method
|
73
|
+
# @param attribute [Cattri::Attribute] the attribute being defined
|
74
|
+
# @param error [StandardError] the original raised exception
|
75
|
+
def initialize(target, attribute, error)
|
76
|
+
super("Failed to define method :#{attribute.name} on #{target}. Error: #{error.message}")
|
77
|
+
set_backtrace(error.backtrace)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Raised when an unsupported attribute type is passed to Cattri.
|
82
|
+
#
|
83
|
+
# Valid types are typically `:class` and `:instance`. Any other value is invalid.
|
84
|
+
#
|
85
|
+
# @example
|
86
|
+
# raise Cattri::UnsupportedTypeError.new(:foo)
|
87
|
+
# # => Attribute type :foo is not supported
|
88
|
+
class UnsupportedTypeError < Cattri::AttributeError
|
89
|
+
# @param type [Symbol] the invalid type that triggered the error
|
90
|
+
def initialize(type)
|
91
|
+
super("Attribute type :#{type} is not supported")
|
92
|
+
end
|
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
|
5
105
|
end
|
@@ -1,93 +1,132 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "attribute_definer"
|
4
|
+
|
3
5
|
module Cattri
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# - Default values (static or callable)
|
7
|
-
# - Optional reader/writer method generation
|
8
|
-
# - Custom coercion logic for writers
|
9
|
-
# - Full attribute metadata access and reset capabilities
|
10
|
-
#
|
11
|
-
# This module is designed for mixin into classes or modules that want to offer
|
12
|
-
# configurable instance attributes (e.g., plugin systems, DTOs, DSLs).
|
13
|
-
#
|
14
|
-
# Example:
|
6
|
+
# Mixin that provides support for defining instance-level attributes.
|
15
7
|
#
|
16
|
-
#
|
17
|
-
#
|
8
|
+
# This module is included into a class (via `include Cattri`) and exposes
|
9
|
+
# a DSL similar to `attr_accessor`, with enhancements:
|
18
10
|
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# end
|
11
|
+
# - Lazy or static default values
|
12
|
+
# - Coercion via custom setter blocks
|
13
|
+
# - Visibility control (`:public`, `:protected`, `:private`)
|
14
|
+
# - Read-only or write-only support
|
24
15
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
# obj.age = "42"
|
28
|
-
# obj.instance_variable_get(:@age) # => 42
|
16
|
+
# Each defined attribute is stored as metadata and linked to a reader and/or writer.
|
17
|
+
# Values are accessed and stored via standard instance variables.
|
29
18
|
module InstanceAttributes
|
30
|
-
|
31
|
-
|
19
|
+
# Hook called when this module is included into a class.
|
20
|
+
#
|
21
|
+
# @param base [Class]
|
22
|
+
# @return [void]
|
32
23
|
def self.included(base)
|
33
24
|
base.extend(ClassMethods)
|
34
25
|
end
|
35
26
|
|
36
|
-
#
|
27
|
+
# Defines instance-level attribute DSL methods.
|
37
28
|
module ClassMethods
|
38
|
-
|
39
|
-
|
40
|
-
# Default options for all instance attributes
|
41
|
-
DEFAULT_OPTIONS = { default: nil, reader: true, writer: true }.freeze
|
42
|
-
|
43
|
-
# Defines a new instance-level attribute with optional default and coercion.
|
29
|
+
# Defines one or more instance-level attributes with optional default and coercion.
|
44
30
|
#
|
45
|
-
#
|
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
|
46
43
|
# @param options [Hash] additional options like `:default`, `:reader`, `:writer`
|
47
|
-
# @option options [Object, Proc] :default the default value or
|
44
|
+
# @option options [Object, Proc] :default the default value or lambda
|
48
45
|
# @option options [Boolean] :reader whether to define a reader method (default: true)
|
49
46
|
# @option options [Boolean] :writer whether to define a writer method (default: true)
|
50
|
-
# @
|
47
|
+
# @option options [Symbol] :access method visibility (:public, :protected, :private)
|
48
|
+
# @yieldparam value [Object] optional custom coercion logic for the setter
|
49
|
+
# @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
|
50
|
+
# `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
|
51
|
+
# already defined or an error occurs while defining methods)
|
51
52
|
# @return [void]
|
52
|
-
def instance_attribute(
|
53
|
-
|
54
|
-
__cattri_instance_attributes[name] = definition
|
53
|
+
def instance_attribute(*names, **options, &block)
|
54
|
+
raise Cattri::AmbiguousBlockError if names.size > 1 && block_given?
|
55
55
|
|
56
|
-
|
57
|
-
define_writer(name, definition) if options.fetch(:writer, true)
|
56
|
+
names.each { |name| define_instance_attribute(name, options, block) }
|
58
57
|
end
|
59
58
|
|
60
59
|
# Defines a read-only instance-level attribute.
|
61
60
|
#
|
62
|
-
#
|
63
|
-
#
|
61
|
+
# Equivalent to `instance_attribute(..., writer: false)`
|
62
|
+
#
|
63
|
+
# @param names [Array<Symbol | String>] the names of the attributes to define
|
64
|
+
# @param options [Hash] additional options like `:default`, `:reader`, `:writer`
|
65
|
+
# @option options [Object, Proc] :default the default value or lambda
|
66
|
+
# @option options [Boolean] :reader whether to define a reader method (default: true)
|
67
|
+
# @option options [Symbol] :access method visibility (:public, :protected, :private)
|
68
|
+
# @yieldparam value [Object] optional custom coercion logic for the setter
|
69
|
+
# @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
|
70
|
+
# `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
|
71
|
+
# already defined or an error occurs while defining methods)
|
64
72
|
# @return [void]
|
65
|
-
def instance_attribute_reader(
|
66
|
-
instance_attribute(
|
73
|
+
def instance_attribute_reader(*names, **options)
|
74
|
+
instance_attribute(*names, **options, writer: false)
|
67
75
|
end
|
68
76
|
|
69
77
|
# Defines a write-only instance-level attribute.
|
70
78
|
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
# @
|
79
|
+
# Equivalent to `instance_attribute(..., reader: false)`
|
80
|
+
#
|
81
|
+
# @param names [Array<Symbol | String>] the names of the attributes to define
|
82
|
+
# @param options [Hash] additional options like `:default`, `:reader`, `:writer`
|
83
|
+
# @option options [Object, Proc] :default the default value or lambda
|
84
|
+
# @option options [Boolean] :writer whether to define a writer method (default: true)
|
85
|
+
# @option options [Symbol] :access method visibility (:public, :protected, :private)
|
86
|
+
# @yieldparam value [Object] optional custom coercion logic for the setter
|
87
|
+
# @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
|
88
|
+
# `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
|
89
|
+
# already defined or an error occurs while defining methods)
|
74
90
|
# @return [void]
|
75
|
-
def instance_attribute_writer(
|
76
|
-
instance_attribute(
|
91
|
+
def instance_attribute_writer(*names, **options, &block)
|
92
|
+
instance_attribute(*names, **options.merge(reader: false), &block)
|
77
93
|
end
|
78
94
|
|
79
|
-
|
80
|
-
|
95
|
+
# Updates the setter behavior of an existing instance-level attribute.
|
96
|
+
#
|
97
|
+
# This allows coercion logic to be defined or overridden after the attribute
|
98
|
+
# has been declared using `iattr`, as long as the writer method exists.
|
99
|
+
#
|
100
|
+
# @example Add coercion to an existing attribute
|
101
|
+
# iattr :format
|
102
|
+
# iattr_setter :format do |val|
|
103
|
+
# val.to_s.downcase.to_sym
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# @param name [Symbol, String] the name of the attribute
|
107
|
+
# @yieldparam value [Object] the value passed to the setter
|
108
|
+
# @yieldreturn [Object] the coerced value to be assigned
|
109
|
+
# @raise [Cattri::AttributeNotDefinedError] if the attribute is not defined or the writer method does not exist
|
110
|
+
# @raise [Cattri::AttributeDefinitionError] if method redefinition fails
|
111
|
+
# @return [void]
|
112
|
+
def instance_attribute_setter(name, &block)
|
113
|
+
attribute = __cattri_instance_attributes[name.to_sym]
|
114
|
+
|
115
|
+
raise Cattri::AttributeNotDefinedError.new(:instance, name) if attribute.nil?
|
116
|
+
raise Cattri::AttributeError, "Cannot define setter for readonly attribute :#{name}" unless attribute[:writer]
|
117
|
+
|
118
|
+
attribute.instance_variable_set(:@setter, attribute.send(:normalize_setter, block))
|
119
|
+
Cattri::AttributeDefiner.define_writer!(attribute, context)
|
81
120
|
end
|
82
121
|
|
83
|
-
# Returns
|
122
|
+
# Returns a list of defined instance-level attribute names.
|
84
123
|
#
|
85
124
|
# @return [Array<Symbol>]
|
86
125
|
def instance_attributes
|
87
126
|
__cattri_instance_attributes.keys
|
88
127
|
end
|
89
128
|
|
90
|
-
# Checks
|
129
|
+
# Checks if an instance-level attribute has been defined.
|
91
130
|
#
|
92
131
|
# @param name [Symbol, String]
|
93
132
|
# @return [Boolean]
|
@@ -95,104 +134,93 @@ module Cattri
|
|
95
134
|
__cattri_instance_attributes.key?(name.to_sym)
|
96
135
|
end
|
97
136
|
|
98
|
-
#
|
137
|
+
# Returns the full attribute definition for a given name.
|
99
138
|
#
|
100
139
|
# @param name [Symbol, String]
|
101
|
-
# @return [
|
140
|
+
# @return [Cattri::Attribute, nil]
|
102
141
|
def instance_attribute_definition(name)
|
103
142
|
__cattri_instance_attributes[name.to_sym]
|
104
143
|
end
|
105
144
|
|
106
145
|
# @!method iattr(name, **options, &block)
|
107
|
-
# Alias for {
|
108
|
-
# @see .instance_attribute
|
146
|
+
# Alias for {#instance_attribute}
|
109
147
|
alias iattr instance_attribute
|
110
148
|
|
111
149
|
# @!method iattr_accessor(name, **options, &block)
|
112
|
-
# Alias for {
|
113
|
-
# @see .instance_attribute
|
150
|
+
# Alias for {#instance_attribute}
|
114
151
|
alias iattr_accessor instance_attribute
|
115
152
|
|
116
153
|
# @!method iattr_reader(name, **options)
|
117
|
-
# Alias for {
|
118
|
-
# @see .instance_attribute_reader
|
154
|
+
# Alias for {#instance_attribute_reader}
|
119
155
|
alias iattr_reader instance_attribute_reader
|
120
156
|
|
121
157
|
# @!method iattr_writer(name, **options, &block)
|
122
|
-
# Alias for {
|
123
|
-
# @see .instance_attribute_writer
|
158
|
+
# Alias for {#instance_attribute_writer}
|
124
159
|
alias iattr_writer instance_attribute_writer
|
125
160
|
|
161
|
+
# @!method iattr_setter(name, &block)
|
162
|
+
# Alias for {#instance_attribute_setter}
|
163
|
+
alias iattr_setter instance_attribute_setter
|
164
|
+
|
126
165
|
# @!method iattrs
|
127
|
-
#
|
128
|
-
# @see .instance_attributes
|
166
|
+
# Alias for {#instance_attributes}
|
129
167
|
alias iattrs instance_attributes
|
130
168
|
|
131
169
|
# @!method iattr_defined?(name)
|
132
|
-
#
|
133
|
-
# @see .instance_attribute_defined?
|
170
|
+
# Alias for {#instance_attribute_defined?}
|
134
171
|
alias iattr_defined? instance_attribute_defined?
|
135
172
|
|
136
|
-
# @!method
|
137
|
-
#
|
138
|
-
# @see .instance_attribute_definition
|
173
|
+
# @!method iattr_definition(name)
|
174
|
+
# Alias for {#instance_attribute_definition}
|
139
175
|
alias iattr_definition instance_attribute_definition
|
140
176
|
|
141
177
|
private
|
142
178
|
|
143
|
-
# Defines
|
179
|
+
# Defines a single instance-level attribute.
|
180
|
+
#
|
181
|
+
# This is the internal implementation used by {.instance_attribute} and its aliases.
|
182
|
+
# It creates a `Cattri::Attribute`, registers it, and defines the appropriate
|
183
|
+
# reader and/or writer methods on the class.
|
184
|
+
#
|
185
|
+
# @param name [Symbol, String] the attribute name
|
186
|
+
# @param options [Hash] additional options for the attribute
|
187
|
+
# @param block [Proc, nil] optional setter coercion logic
|
144
188
|
#
|
145
|
-
# @
|
146
|
-
# @
|
147
|
-
|
148
|
-
|
189
|
+
# @raise [Cattri::AttributeDefinedError] if the attribute has already been defined
|
190
|
+
# @raise [Cattri::AttributeDefinitionError] if method definition fails
|
191
|
+
#
|
192
|
+
# @return [void]
|
193
|
+
def define_instance_attribute(name, options, block)
|
194
|
+
options[:access] ||= __cattri_visibility
|
195
|
+
attribute = Cattri::Attribute.new(name, :instance, options, block)
|
149
196
|
|
150
|
-
|
151
|
-
return instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
197
|
+
raise Cattri::AttributeDefinedError.new(:instance, name) if instance_attribute_defined?(attribute.name)
|
152
198
|
|
153
|
-
|
154
|
-
|
199
|
+
begin
|
200
|
+
__cattri_instance_attributes[name.to_sym] = attribute
|
201
|
+
Cattri::AttributeDefiner.define_accessor(attribute, context)
|
202
|
+
rescue StandardError => e
|
203
|
+
raise Cattri::AttributeDefinitionError.new(self, attribute, e)
|
155
204
|
end
|
156
205
|
end
|
157
206
|
|
158
|
-
#
|
207
|
+
# Internal registry of instance attributes defined on the class.
|
159
208
|
#
|
160
|
-
# @
|
161
|
-
|
162
|
-
|
163
|
-
define_method("#{name}=") do |value|
|
164
|
-
coerced_value = definition[:setter].call(value)
|
165
|
-
instance_variable_set(definition[:ivar], coerced_value)
|
166
|
-
end
|
209
|
+
# @return [Hash{Symbol => Cattri::Attribute}]
|
210
|
+
def __cattri_instance_attributes
|
211
|
+
@__cattri_instance_attributes ||= {}
|
167
212
|
end
|
168
|
-
end
|
169
213
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
# @return [void]
|
181
|
-
def reset_instance_attribute!(name)
|
182
|
-
definition = self.class.__cattri_instance_attributes[name]
|
183
|
-
return unless definition
|
184
|
-
|
185
|
-
reset_attributes!(self, [definition])
|
214
|
+
# Returns the context used to define methods for this class.
|
215
|
+
#
|
216
|
+
# Used internally to encapsulate method definition and visibility rules.
|
217
|
+
#
|
218
|
+
# @return [Cattri::Context]
|
219
|
+
# :nocov:
|
220
|
+
def context
|
221
|
+
@context ||= Context.new(self)
|
222
|
+
end
|
223
|
+
# :nocov:
|
186
224
|
end
|
187
|
-
|
188
|
-
# @!method reset_iattrs!
|
189
|
-
# @return [void]
|
190
|
-
# @see .reset_instance_attributes!
|
191
|
-
alias reset_iattrs! reset_instance_attributes!
|
192
|
-
|
193
|
-
# @!method reset_iattr!(name)
|
194
|
-
# @return [void]
|
195
|
-
# @see .reset_instance_attribute!
|
196
|
-
alias reset_iattr! reset_instance_attribute!
|
197
225
|
end
|
198
226
|
end
|
data/lib/cattri/introspection.rb
CHANGED
data/lib/cattri/version.rb
CHANGED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cattri
|
4
|
+
# Cattri::Visibility tracks the current method visibility context (`public`, `protected`, `private`)
|
5
|
+
# when defining methods dynamically. It mimics Ruby's native visibility behavior so that
|
6
|
+
# `cattr` and `iattr` definitions can automatically infer the intended access level
|
7
|
+
# based on the current context in the source file.
|
8
|
+
#
|
9
|
+
# This module is intended to be extended by classes that include or extend Cattri.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# class MyClass
|
13
|
+
# include Cattri
|
14
|
+
#
|
15
|
+
# private
|
16
|
+
# cattr :sensitive_data
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # => :sensitive_data will be defined as a private method
|
20
|
+
module Visibility
|
21
|
+
# Returns the currently active visibility scope on the class or module.
|
22
|
+
#
|
23
|
+
# Defaults to `:public` unless changed explicitly via `public`, `protected`, or `private`.
|
24
|
+
#
|
25
|
+
# @return [Symbol] :public, :protected, or :private
|
26
|
+
def __cattri_visibility
|
27
|
+
@__cattri_visibility ||= :public
|
28
|
+
end
|
29
|
+
|
30
|
+
# Intercepts calls to `public` to update the visibility tracker.
|
31
|
+
#
|
32
|
+
# If no method names are passed, this sets the current visibility scope for future methods.
|
33
|
+
# Otherwise, delegates to Ruby’s native `Module#public`.
|
34
|
+
#
|
35
|
+
# @param args [Array<Symbol>] method names to make public, or empty to set context
|
36
|
+
# @return [void]
|
37
|
+
def public(*args)
|
38
|
+
@__cattri_visibility = :public if args.empty?
|
39
|
+
Module.instance_method(:public).bind(self).call(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Intercepts calls to `protected` to update the visibility tracker.
|
43
|
+
#
|
44
|
+
# If no method names are passed, this sets the current visibility scope for future methods.
|
45
|
+
# Otherwise, delegates to Ruby’s native `Module#protected`.
|
46
|
+
#
|
47
|
+
# @param args [Array<Symbol>] method names to make protected, or empty to set context
|
48
|
+
# @return [void]
|
49
|
+
def protected(*args)
|
50
|
+
@__cattri_visibility = :protected if args.empty?
|
51
|
+
Module.instance_method(:protected).bind(self).call(*args)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Intercepts calls to `private` to update the visibility tracker.
|
55
|
+
#
|
56
|
+
# If no method names are passed, this sets the current visibility scope for future methods.
|
57
|
+
# Otherwise, delegates to Ruby’s native `Module#private`.
|
58
|
+
#
|
59
|
+
# @param args [Array<Symbol>] method names to make private, or empty to set context
|
60
|
+
# @return [void]
|
61
|
+
def private(*args)
|
62
|
+
@__cattri_visibility = :private if args.empty?
|
63
|
+
Module.instance_method(:private).bind(self).call(*args)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/cattri.rb
CHANGED
@@ -1,36 +1,119 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "cattri/version"
|
4
|
+
require_relative "cattri/visibility"
|
4
5
|
require_relative "cattri/class_attributes"
|
5
6
|
require_relative "cattri/instance_attributes"
|
6
7
|
require_relative "cattri/introspection"
|
7
8
|
|
8
9
|
# The primary entry point for the Cattri gem.
|
9
10
|
#
|
10
|
-
# When included, it
|
11
|
-
#
|
11
|
+
# When included, it enables both class-level and instance-level attribute definitions
|
12
|
+
# via `cattr` and `iattr`-style DSLs, providing a lightweight alternative to traditional
|
13
|
+
# `attr_*` and `cattr_*` patterns.
|
12
14
|
#
|
13
|
-
#
|
14
|
-
#
|
15
|
+
# The module includes:
|
16
|
+
# - `Cattri::ClassAttributes` (for class-level configuration)
|
17
|
+
# - `Cattri::InstanceAttributes` (for instance-level configuration)
|
18
|
+
# - `Cattri::Visibility` (for default access control)
|
19
|
+
#
|
20
|
+
# It also installs a custom `.inherited` hook to ensure that subclassed classes
|
21
|
+
# receive deep copies of attribute metadata and current values.
|
22
|
+
#
|
23
|
+
# Note: The `Cattri::Introspection` module must be included manually if needed.
|
15
24
|
#
|
16
25
|
# @example Using both class and instance attributes
|
17
|
-
# class
|
26
|
+
# class Config
|
18
27
|
# include Cattri
|
19
28
|
#
|
20
29
|
# cattr :enabled, default: true
|
21
30
|
# iattr :name, default: "anonymous"
|
22
31
|
# end
|
23
32
|
#
|
24
|
-
#
|
25
|
-
#
|
33
|
+
# Config.enabled # => true
|
34
|
+
# Config.new.name # => "anonymous"
|
26
35
|
module Cattri
|
27
36
|
# Hook triggered when `include Cattri` is called.
|
28
|
-
#
|
37
|
+
#
|
38
|
+
# Installs core attribute DSLs and visibility settings into the host class.
|
39
|
+
# Also injects `.inherited` logic to propagate attribute metadata to subclasses.
|
29
40
|
#
|
30
41
|
# @param base [Class, Module] the receiving class or module
|
31
42
|
# @return [void]
|
32
43
|
def self.included(base)
|
44
|
+
base.extend(Cattri::Visibility)
|
33
45
|
base.extend(Cattri::ClassAttributes)
|
34
46
|
base.include(Cattri::InstanceAttributes)
|
47
|
+
|
48
|
+
base.singleton_class.define_method(:inherited) do |subclass|
|
49
|
+
super(subclass) if defined?(super)
|
50
|
+
|
51
|
+
%i[class instance].each do |type|
|
52
|
+
Cattri.send(:copy_attributes_to, self, subclass, type)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class << self
|
58
|
+
private
|
59
|
+
|
60
|
+
# Copies attribute definitions and backing values from one class to a subclass.
|
61
|
+
#
|
62
|
+
# This is invoked automatically via the `.inherited` hook to ensure
|
63
|
+
# subclass isolation and metadata integrity.
|
64
|
+
#
|
65
|
+
# @param origin [Class] the parent class
|
66
|
+
# @param subclass [Class] the child class inheriting the attributes
|
67
|
+
# @param type [Symbol] either `:class` or `:instance`
|
68
|
+
# @return [void]
|
69
|
+
# @raise [Cattri::AttributeError] if an ivar copy operation fails
|
70
|
+
def copy_attributes_to(origin, subclass, type)
|
71
|
+
ivar = :"@__cattri_#{type}_attributes"
|
72
|
+
attributes = origin.instance_variable_get(ivar) || {}
|
73
|
+
|
74
|
+
subclass_attributes = attributes.transform_values do |attribute|
|
75
|
+
copy_ivar_to(origin, subclass, attribute)
|
76
|
+
attribute.dup
|
77
|
+
end
|
78
|
+
|
79
|
+
subclass.instance_variable_set(ivar, subclass_attributes)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Duplicates the current value of an attribute's backing ivar to the subclass.
|
83
|
+
#
|
84
|
+
# Falls back to raw assignment if duplication is not supported.
|
85
|
+
#
|
86
|
+
# @param origin [Class] the parent class
|
87
|
+
# @param subclass [Class] the receiving subclass
|
88
|
+
# @param attribute [Cattri::Attribute]
|
89
|
+
# @return [void]
|
90
|
+
# @raise [Cattri::AttributeError] if the value cannot be safely duplicated
|
91
|
+
def copy_ivar_to(origin, subclass, attribute)
|
92
|
+
value = duplicate_value(origin, attribute)
|
93
|
+
subclass.instance_variable_set(attribute.ivar, value)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Attempts to duplicate the value of an attribute's backing ivar.
|
97
|
+
#
|
98
|
+
# This method first tries to duplicate the value stored in the ivar.
|
99
|
+
# If duplication is not supported due to the object's nature (e.g., it is frozen or immutable),
|
100
|
+
# it will fall back to returning the original value.
|
101
|
+
#
|
102
|
+
# @param origin [Class] the parent class from which the ivar value is being retrieved
|
103
|
+
# @param attribute [Cattri::Attribute] the attribute for which the ivar value is being duplicated
|
104
|
+
# @return [Object] the duplicated value or the original value if duplication is not possible
|
105
|
+
# @raise [Cattri::AttributeError] if duplication fails due to unsupported object types
|
106
|
+
def duplicate_value(origin, attribute)
|
107
|
+
value = origin.instance_variable_get(attribute.ivar)
|
108
|
+
|
109
|
+
begin
|
110
|
+
value.dup
|
111
|
+
rescue TypeError, FrozenError
|
112
|
+
puts "HERE"
|
113
|
+
value
|
114
|
+
end
|
115
|
+
rescue StandardError => e
|
116
|
+
raise Cattri::AttributeError, "Failed to duplicate value for attribute #{attribute}. Error: #{e.message}"
|
117
|
+
end
|
35
118
|
end
|
36
119
|
end
|