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.
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
- # Provides a DSL for defining instance-level attributes with support for:
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
- # class MyObject
17
- # extend Cattri::InstanceAttributes
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
- # iattr :name, default: "anonymous"
20
- # iattr_writer :age, default: 0 do |v|
21
- # Integer(v)
22
- # end
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
- # obj = MyObject.new
26
- # obj.name # => "anonymous"
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
- include Cattri::Helpers
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
- # Class-level methods for defining instance attributes
27
+ # Defines instance-level attribute DSL methods.
37
28
  module ClassMethods
38
- include Cattri::Helpers
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
- # @param name [Symbol, String] the name of the attribute
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 proc returning the value
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
- # @yield [value] optional coercion logic for the writer
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(name, **options, &block)
53
- name, definition = define_attribute(name, options, block, DEFAULT_OPTIONS)
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
- define_reader(name, definition) if options.fetch(:reader, true)
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
- # @param name [Symbol, String]
63
- # @param options [Hash]
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(name, **options)
66
- instance_attribute(name, writer: false, **options)
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
- # @param name [Symbol, String]
72
- # @param options [Hash]
73
- # @yield [value] coercion logic
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(name, **options, &block)
76
- instance_attribute(name, reader: false, **options, &block)
91
+ def instance_attribute_writer(*names, **options, &block)
92
+ instance_attribute(*names, **options.merge(reader: false), &block)
77
93
  end
78
94
 
79
- def __cattri_instance_attributes
80
- @__cattri_instance_attributes ||= {}
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 all defined instance-level attribute names.
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 whether an instance attribute is defined.
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
- # Fetches the full definition hash for a specific attribute.
137
+ # Returns the full attribute definition for a given name.
99
138
  #
100
139
  # @param name [Symbol, String]
101
- # @return [Hash, nil]
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 {.instance_attribute}
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 {.instance_attribute}
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 {.instance_attribute_reader}
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 {.instance_attribute_writer}
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
- # @return [Hash<Symbol, Hash>] all defined attributes
128
- # @see .instance_attributes
166
+ # Alias for {#instance_attributes}
129
167
  alias iattrs instance_attributes
130
168
 
131
169
  # @!method iattr_defined?(name)
132
- # @return [Boolean]
133
- # @see .instance_attribute_defined?
170
+ # Alias for {#instance_attribute_defined?}
134
171
  alias iattr_defined? instance_attribute_defined?
135
172
 
136
- # @!method iattr_for(name)
137
- # @return [Hash, nil]
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 the reader method for an instance attribute.
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
- # @param name [Symbol] attribute name
146
- # @param definition [Hash] full attribute definition
147
- def define_reader(name, definition)
148
- ivar = definition[:ivar]
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
- define_method(name) do
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
- value = definition[:default].call
154
- instance_variable_set(ivar, value)
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
- # Defines the writer method for an instance attribute.
207
+ # Internal registry of instance attributes defined on the class.
159
208
  #
160
- # @param name [Symbol] attribute name
161
- # @param definition [Hash] full attribute definition
162
- def define_writer(name, definition)
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
- # Resets all defined attributes to their default values.
171
- #
172
- # @return [void]
173
- def reset_instance_attributes!
174
- reset_attributes!(self, self.class.__cattri_instance_attributes.values)
175
- end
176
-
177
- # Resets a specific attribute to its default value.
178
- #
179
- # @param name [Symbol, String]
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
@@ -39,7 +39,7 @@ module Cattri
39
39
 
40
40
  class_attributes.each_with_object({}) do |attribute, hash|
41
41
  hash[attribute] = send(attribute)
42
- end
42
+ end.freeze
43
43
  end
44
44
 
45
45
  # @!method snapshot_cattrs
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cattri
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
@@ -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 adds support for both class-level and instance-level
11
- # attribute declarations using `cattr` and `iattr` style methods.
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
- # This module does **not** include introspection helpers by default —
14
- # use `include Cattri::Introspection` explicitly if needed.
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 MyConfig
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
- # MyConfig.enabled # => true
25
- # MyConfig.new.name # => "anonymous"
33
+ # Config.enabled # => true
34
+ # Config.new.name # => "anonymous"
26
35
  module Cattri
27
36
  # Hook triggered when `include Cattri` is called.
28
- # Adds class and instance attribute support to the target.
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