cattri 0.1.3 → 0.2.0

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +34 -0
  3. data/.gitignore +72 -0
  4. data/.rubocop.yml +6 -3
  5. data/CHANGELOG.md +41 -0
  6. data/Gemfile +12 -0
  7. data/README.md +163 -151
  8. data/Steepfile +6 -0
  9. data/bin/console +33 -0
  10. data/bin/setup +8 -0
  11. data/cattri.gemspec +5 -5
  12. data/lib/cattri/attribute.rb +119 -155
  13. data/lib/cattri/attribute_compiler.rb +104 -0
  14. data/lib/cattri/attribute_options.rb +183 -0
  15. data/lib/cattri/attribute_registry.rb +155 -0
  16. data/lib/cattri/context.rb +124 -106
  17. data/lib/cattri/context_registry.rb +36 -0
  18. data/lib/cattri/deferred_attributes.rb +73 -0
  19. data/lib/cattri/dsl.rb +54 -0
  20. data/lib/cattri/error.rb +17 -90
  21. data/lib/cattri/inheritance.rb +35 -0
  22. data/lib/cattri/initializer_patch.rb +37 -0
  23. data/lib/cattri/internal_store.rb +104 -0
  24. data/lib/cattri/introspection.rb +56 -49
  25. data/lib/cattri/version.rb +3 -1
  26. data/lib/cattri.rb +38 -99
  27. data/sig/lib/cattri/attribute.rbs +105 -0
  28. data/sig/lib/cattri/attribute_compiler.rbs +61 -0
  29. data/sig/lib/cattri/attribute_options.rbs +150 -0
  30. data/sig/lib/cattri/attribute_registry.rbs +95 -0
  31. data/sig/lib/cattri/context.rbs +130 -0
  32. data/sig/lib/cattri/context_registry.rbs +31 -0
  33. data/sig/lib/cattri/deferred_attributes.rbs +53 -0
  34. data/sig/lib/cattri/dsl.rbs +55 -0
  35. data/sig/lib/cattri/error.rbs +28 -0
  36. data/sig/lib/cattri/inheritance.rbs +21 -0
  37. data/sig/lib/cattri/initializer_patch.rbs +26 -0
  38. data/sig/lib/cattri/internal_store.rbs +75 -0
  39. data/sig/lib/cattri/introspection.rbs +61 -0
  40. data/sig/lib/cattri/types.rbs +19 -0
  41. data/sig/lib/cattri/visibility.rbs +55 -0
  42. data/sig/lib/cattri.rbs +37 -0
  43. data/spec/cattri/attribute_compiler_spec.rb +179 -0
  44. data/spec/cattri/attribute_options_spec.rb +267 -0
  45. data/spec/cattri/attribute_registry_spec.rb +257 -0
  46. data/spec/cattri/attribute_spec.rb +297 -0
  47. data/spec/cattri/context_registry_spec.rb +45 -0
  48. data/spec/cattri/context_spec.rb +346 -0
  49. data/spec/cattri/deferred_attrributes_spec.rb +117 -0
  50. data/spec/cattri/dsl_spec.rb +69 -0
  51. data/spec/cattri/error_spec.rb +37 -0
  52. data/spec/cattri/inheritance_spec.rb +60 -0
  53. data/spec/cattri/initializer_patch_spec.rb +35 -0
  54. data/spec/cattri/internal_store_spec.rb +139 -0
  55. data/spec/cattri/introspection_spec.rb +90 -0
  56. data/spec/cattri/visibility_spec.rb +68 -0
  57. data/spec/cattri_spec.rb +54 -0
  58. data/spec/simplecov_helper.rb +21 -0
  59. data/spec/spec_helper.rb +16 -0
  60. metadata +79 -6
  61. data/lib/cattri/attribute_definer.rb +0 -143
  62. data/lib/cattri/class_attributes.rb +0 -277
  63. data/lib/cattri/instance_attributes.rb +0 -276
  64. data/sig/cattri.rbs +0 -4
data/lib/cattri/error.rb CHANGED
@@ -3,103 +3,30 @@
3
3
  module Cattri
4
4
  # Base error class for all exceptions raised by Cattri.
5
5
  #
6
- # All Cattri-specific errors inherit from this class.
6
+ # All Cattri-specific errors inherit from this class, allowing unified
7
+ # rescue handling at the framework level.
8
+ #
9
+ # The backtrace is preserved and may be filtered before raising.
7
10
  #
8
11
  # @example
9
12
  # rescue Cattri::Error => e
10
13
  # puts "Something went wrong with Cattri: #{e.message}"
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")
14
+ class Error < StandardError
15
+ # Initializes the error with an optional message and caller backtrace.
16
+ #
17
+ # @param msg [String, nil] the error message
18
+ # @param backtrace [Array<String>] optional backtrace (defaults to `caller`)
19
+ def initialize(msg = nil, backtrace = caller)
20
+ super(msg)
21
+ set_backtrace(backtrace)
37
22
  end
38
23
  end
39
24
 
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
25
+ # Raised for any attribute-related definition or usage failure.
49
26
  #
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.
27
+ # This includes method conflicts, invalid configuration, and
28
+ # write attempts on `final` attributes.
62
29
  #
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
30
+ # @see Cattri::Error
31
+ class AttributeError < Error; end
105
32
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cattri
4
+ # Handles subclassing behavior for classes that use Cattri.
5
+ #
6
+ # This module installs a custom `.inherited` hook on the target class's singleton,
7
+ # ensuring that attribute definitions and values are deep-copied to the subclass.
8
+ #
9
+ # The hook preserves any existing `.inherited` behavior defined on the class,
10
+ # calling it before applying attribute propagation.
11
+ module Inheritance
12
+ # Installs an `inherited` hook on the given class.
13
+ #
14
+ # When the class is subclassed, Cattri will copy over attribute metadata and values
15
+ # using the subclass’s context. This ensures subclass safety and definition isolation.
16
+ #
17
+ # Any pre-existing `.inherited` method is preserved and invoked first.
18
+ #
19
+ # @param base [Class] the class to install the hook on
20
+ # @return [void]
21
+ def self.install(base)
22
+ singleton = base.singleton_class
23
+ existing = singleton.instance_method(:inherited) rescue nil # rubocop:disable Style/RescueModifier
24
+
25
+ singleton.define_method(:inherited) do |subclass|
26
+ # :nocov:
27
+ existing.bind(self).call(subclass) # steep:ignore
28
+ # :nocov:
29
+
30
+ context = Cattri::Context.new(subclass)
31
+ attribute_registry.send(:copy_attributes_to, context) # steep:ignore
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cattri
4
+ # Provides a patch to `#initialize` that ensures all final attributes
5
+ # are initialized with their default values if not already set.
6
+ #
7
+ # This module is prepended into Cattri-including classes to enforce
8
+ # write-once semantics for instance-level `final: true` attributes.
9
+ #
10
+ # @example
11
+ # class MyClass
12
+ # include Cattri
13
+ #
14
+ # cattri :id, -> { SecureRandom.uuid }, final: true
15
+ # end
16
+ #
17
+ # MyClass.new # => will have a UUID assigned to @id unless explicitly set
18
+ module InitializerPatch
19
+ # Hooked constructor that initializes final attributes using their defaults
20
+ # if no value has been set by the user.
21
+ #
22
+ # @param args [Array] any positional arguments passed to initialize
23
+ # @param kwargs [Hash] any keyword arguments passed to initialize
24
+ # @yield an optional block to pass to `super`
25
+ # @return [void]
26
+ def initialize(*args, **kwargs, &block)
27
+ super
28
+
29
+ self.class.send(:attribute_registry).registered_attributes.each_value do |attribute| # steep:ignore
30
+ next if cattri_variable_defined?(attribute.ivar) # steep:ignore
31
+ next unless attribute.final?
32
+
33
+ cattri_variable_set(attribute.ivar, attribute.evaluate_default) # steep:ignore
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Cattri
6
+ # Internal representation of a stored attribute value.
7
+ AttributeValue = Struct.new(:value, :final)
8
+
9
+ # Provides an internal storage mechanism for attribute values defined via `cattri`.
10
+ #
11
+ # This module is included into any class or module using Cattri and replaces
12
+ # direct instance variable access with a namespaced store.
13
+ #
14
+ # It supports enforcement of `final` semantics and tracks explicit assignments.
15
+ module InternalStore
16
+ # Checks whether the internal store contains a value for the given key.
17
+ #
18
+ # @param key [String, Symbol] the attribute name or instance variable
19
+ # @return [Boolean] true if a value is present
20
+ def cattri_variable_defined?(key)
21
+ __cattri_store.key?(normalize_ivar(key))
22
+ end
23
+
24
+ # Fetches the value for a given attribute key from the internal store.
25
+ #
26
+ # @param key [String, Symbol] the attribute name or instance variable
27
+ # @return [Object, nil] the stored value, or nil if not present
28
+ def cattri_variable_get(key)
29
+ __cattri_store[normalize_ivar(key)]&.value
30
+ end
31
+
32
+ # Sets a value in the internal store for the given attribute key.
33
+ #
34
+ # Enforces final semantics if a final value was already set.
35
+ #
36
+ # @param key [String, Symbol] the attribute name or instance variable
37
+ # @param value [Object] the value to store
38
+ # @param final [Boolean] whether the value should be locked as final
39
+ # @return [Object] the stored value
40
+ def cattri_variable_set(key, value, final: false)
41
+ key = normalize_ivar(key)
42
+ guard_final!(key)
43
+
44
+ __cattri_store[key] = AttributeValue.new(value, final)
45
+ __cattri_set_variables << key
46
+
47
+ value
48
+ end
49
+
50
+ # Evaluates and sets a value for the given key only if it hasn't already been set.
51
+ #
52
+ # If a value is already present, it is returned as-is. Otherwise, the provided block
53
+ # is called to compute the value, which is then stored. If `final: true` is passed,
54
+ # the value is marked as final and cannot be reassigned.
55
+ #
56
+ # @param key [String, Symbol] the attribute name or instance variable
57
+ # @param final [Boolean] whether to mark the value as final (immutable once set)
58
+ # @yieldreturn [Object] the value to memoize if not already present
59
+ # @return [Object] the existing or newly memoized value
60
+ # @raise [Cattri::AttributeError] if attempting to overwrite a final value
61
+ def cattri_variable_memoize(key, final: false)
62
+ key = normalize_ivar(key)
63
+ return cattri_variable_get(key) if cattri_variable_defined?(key)
64
+
65
+ value = yield
66
+ cattri_variable_set(key, value, final: final)
67
+ end
68
+
69
+ private
70
+
71
+ # Returns the internal storage hash used for attribute values.
72
+ #
73
+ # @return [Hash<Symbol, Cattri::AttributeValue>]
74
+ def __cattri_store
75
+ @__cattri_store ||= {}
76
+ end
77
+
78
+ # Returns the set of attribute keys that have been explicitly assigned.
79
+ #
80
+ # @return [Set<Symbol>]
81
+ def __cattri_set_variables
82
+ @__cattri_set_variables ||= Set.new
83
+ end
84
+
85
+ # Normalizes the attribute key to a symbol without `@` prefix.
86
+ #
87
+ # @param key [String, Symbol]
88
+ # @return [Symbol]
89
+ def normalize_ivar(key)
90
+ key.to_s.delete_prefix("@").to_sym.freeze
91
+ end
92
+
93
+ # Raises if attempting to modify a value that was marked as final.
94
+ #
95
+ # @param key [Symbol]
96
+ # @raise [Cattri::AttributeError] if the key is final and already set
97
+ def guard_final!(key)
98
+ return unless cattri_variable_defined?(key)
99
+
100
+ existing = __cattri_store[key]
101
+ raise Cattri::AttributeError, "Cannot modify final attribute :#{key}" if existing.final
102
+ end
103
+ end
104
+ end
@@ -1,70 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cattri
4
- # Adds debugging and inspection helpers for reading the current values of
5
- # class- and instance-level attributes defined via Cattri.
4
+ # Provides a read-only interface for inspecting attributes defined via the Cattri DSL.
6
5
  #
7
- # This module is intended for use in development and test environments to
8
- # help capture attribute state at a given moment.
9
- #
10
- # It can be included in any class or module that uses {Cattri::ClassAttributes}
11
- # or {Cattri::InstanceAttributes}.
12
- #
13
- # @example
14
- # class MyConfig
15
- # extend Cattri::ClassAttributes
16
- # include Cattri::Introspection
17
- #
18
- # cattr :items, default: []
19
- # end
20
- #
21
- # MyConfig.items << :a
22
- # MyConfig.snapshot_class_attributes #=> { items: [:a] }
6
+ # When included, adds class-level methods to:
7
+ # - Check if an attribute is defined
8
+ # - Retrieve attribute definitions
9
+ # - List defined attribute methods
10
+ # - Trace the origin of an attribute
23
11
  module Introspection
24
- # Hook called when the module is included. Extends the base with class methods.
25
- #
26
- # @param base [Class, Module]
12
+ # @param base [Class, Module] the target that includes `Cattri`
27
13
  # @return [void]
28
14
  def self.included(base)
29
15
  base.extend(ClassMethods)
30
16
  end
31
17
 
32
- # Class-level methods for introspection.
18
+ # @api public
19
+ # Class-level introspection methods exposed via `.attribute_defined?`, `.attribute`, etc.
33
20
  module ClassMethods
34
- # Returns a hash of current class attribute values.
21
+ # Returns true if the given attribute has been defined on this class or any ancestor.
35
22
  #
36
- # @return [Hash<Symbol, Object>] a snapshot of each defined class attribute
37
- def snapshot_class_attributes
38
- return {} unless respond_to?(:class_attributes)
23
+ # @param name [Symbol, String] the attribute name
24
+ # @return [Boolean]
25
+ def attribute_defined?(name)
26
+ !!attribute(name)
27
+ end
39
28
 
40
- class_attributes.each_with_object({}) do |attribute, hash|
41
- hash[attribute] = send(attribute)
42
- end.freeze
29
+ # Returns the attribute definition for the given name.
30
+ #
31
+ # Includes inherited definitions if available.
32
+ #
33
+ # @param name [Symbol, String] the attribute name
34
+ # @return [Cattri::Attribute, nil]
35
+ def attribute(name)
36
+ attribute_registry.defined_attributes(with_ancestors: true)[name.to_sym] # steep:ignore
43
37
  end
44
38
 
45
- # @!method snapshot_cattrs
46
- # Alias for {.snapshot_class_attributes}
47
- # @see .snapshot_class_attributes
48
- alias snapshot_cattrs snapshot_class_attributes
49
- end
39
+ # Returns a list of attribute names defined on this class.
40
+ #
41
+ # Includes inherited attributes if `with_ancestors` is true.
42
+ #
43
+ # @param with_ancestors [Boolean]
44
+ # @return [Array<Symbol>]
45
+ def attributes(with_ancestors: false)
46
+ attribute_registry.defined_attributes(with_ancestors: with_ancestors).keys # steep:ignore
47
+ end
48
+
49
+ # Returns a hash of attribute definitions defined on this class.
50
+ #
51
+ # Includes inherited attributes if `with_ancestors` is true.
52
+ #
53
+ # @param with_ancestors [Boolean]
54
+ # @return [Hash{Symbol => Cattri::Attribute}]
55
+ def attribute_definitions(with_ancestors: false)
56
+ attribute_registry.defined_attributes(with_ancestors: with_ancestors) # steep:ignore
57
+ end
50
58
 
51
- # Returns a hash of current instance attribute values for this object.
52
- #
53
- # @return [Hash<Symbol, Object>] a snapshot of each defined instance attribute
54
- def snapshot_instance_attributes
55
- return {} unless self.class.respond_to?(:instance_attributes)
59
+ # Returns a hash of all methods defined by Cattri attributes.
60
+ #
61
+ # This includes accessors, writers, and predicates where applicable.
62
+ #
63
+ # @return [Hash{Symbol => Set<Symbol>}]
64
+ def attribute_methods
65
+ context.defined_methods # steep:ignore
66
+ end
56
67
 
57
- self.class.instance_attributes.each_with_object({}) do |attribute, hash|
58
- hash[attribute] = send(attribute)
59
- rescue NoMethodError
60
- # Catch for write-only methods
61
- hash[attribute] = instance_variable_get(:"@#{attribute}")
68
+ # Returns the original class or module where the given attribute was defined.
69
+ #
70
+ # @param name [Symbol, String] the attribute name
71
+ # @return [Module, nil]
72
+ def attribute_source(name)
73
+ attribute(name)&.defined_in
62
74
  end
63
75
  end
64
-
65
- # @!method snapshot_iattrs
66
- # Alias for {#snapshot_instance_attributes}
67
- # @see #snapshot_instance_attributes
68
- alias snapshot_iattrs snapshot_instance_attributes
69
76
  end
70
77
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cattri
4
- VERSION = "0.1.3"
4
+ # :nocov:
5
+ VERSION = "0.2.0"
6
+ # :nocov:
5
7
  end
data/lib/cattri.rb CHANGED
@@ -1,119 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "cattri/version"
4
- require_relative "cattri/visibility"
5
- require_relative "cattri/class_attributes"
6
- require_relative "cattri/instance_attributes"
3
+ require_relative "cattri/attribute"
4
+ require_relative "cattri/context_registry"
5
+ require_relative "cattri/dsl"
6
+ require_relative "cattri/inheritance"
7
+ require_relative "cattri/initializer_patch"
8
+ require_relative "cattri/internal_store"
7
9
  require_relative "cattri/introspection"
10
+ require_relative "cattri/visibility"
8
11
 
9
- # The primary entry point for the Cattri gem.
10
- #
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.
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.
24
- #
25
- # @example Using both class and instance attributes
26
- # class Config
27
- # include Cattri
12
+ # Main entrypoint for the Cattri DSL.
28
13
  #
29
- # cattr :enabled, default: true
30
- # iattr :name, default: "anonymous"
31
- # end
14
+ # When included in a class or module, this installs both class-level and instance-level
15
+ # attribute handling logic, visibility tracking, subclass inheritance propagation,
16
+ # and default value enforcement.
32
17
  #
33
- # Config.enabled # => true
34
- # Config.new.name # => "anonymous"
18
+ # It supports:
19
+ # - Defining class or instance attributes using `cattri` or `final_cattri`
20
+ # - Visibility tracking and method scoping
21
+ # - Write-once semantics for `final: true` attributes
22
+ # - Safe method generation with introspection support
35
23
  module Cattri
36
- # Hook triggered when `include Cattri` is called.
24
+ # Sets up the Cattri DSL on the including class or module.
37
25
  #
38
- # Installs core attribute DSLs and visibility settings into the host class.
39
- # Also injects `.inherited` logic to propagate attribute metadata to subclasses.
26
+ # Includes internal storage and registry infrastructure into both the base and its singleton class,
27
+ # prepends initialization logic, extends visibility and DSL handling, and installs the
28
+ # subclassing hook to propagate attributes to descendants.
40
29
  #
41
- # @param base [Class, Module] the receiving class or module
30
+ # @param base [Class, Module] the target that includes `Cattri`
42
31
  # @return [void]
43
32
  def self.included(base)
44
- base.extend(Cattri::Visibility)
45
- base.extend(Cattri::ClassAttributes)
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
33
+ [base, base.singleton_class].each do |mod|
34
+ mod.include(Cattri::InternalStore)
35
+ mod.include(Cattri::ContextRegistry)
54
36
  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
37
 
74
- subclass_attributes = attributes.transform_values do |attribute|
75
- copy_ivar_to(origin, subclass, attribute)
76
- attribute.dup
77
- end
38
+ base.prepend(Cattri::InitializerPatch)
39
+ base.extend(Cattri::Visibility)
40
+ base.extend(Cattri::Dsl)
41
+ base.extend(ClassMethods)
78
42
 
79
- subclass.instance_variable_set(ivar, subclass_attributes)
80
- end
43
+ Cattri::Inheritance.install(base)
44
+ end
81
45
 
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.
46
+ # Provides opt-in class-level introspection support.
47
+ #
48
+ # This allows users to call methods like `.attribute_defined?`, `.attribute_methods`, etc.,
49
+ # to inspect which attributes have been defined.
50
+ module ClassMethods
51
+ # Enables Cattri's attribute introspection methods on the current class.
85
52
  #
86
- # @param origin [Class] the parent class
87
- # @param subclass [Class] the receiving subclass
88
- # @param attribute [Cattri::Attribute]
89
53
  # @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}"
54
+ def with_cattri_introspection
55
+ include(Cattri::Introspection) # steep:ignore
117
56
  end
118
57
  end
119
58
  end