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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +34 -0
- data/.gitignore +72 -0
- data/.rubocop.yml +6 -3
- data/CHANGELOG.md +41 -0
- data/Gemfile +12 -0
- data/README.md +163 -151
- data/Steepfile +6 -0
- data/bin/console +33 -0
- data/bin/setup +8 -0
- data/cattri.gemspec +5 -5
- data/lib/cattri/attribute.rb +119 -155
- data/lib/cattri/attribute_compiler.rb +104 -0
- data/lib/cattri/attribute_options.rb +183 -0
- data/lib/cattri/attribute_registry.rb +155 -0
- data/lib/cattri/context.rb +124 -106
- data/lib/cattri/context_registry.rb +36 -0
- data/lib/cattri/deferred_attributes.rb +73 -0
- data/lib/cattri/dsl.rb +54 -0
- data/lib/cattri/error.rb +17 -90
- data/lib/cattri/inheritance.rb +35 -0
- data/lib/cattri/initializer_patch.rb +37 -0
- data/lib/cattri/internal_store.rb +104 -0
- data/lib/cattri/introspection.rb +56 -49
- data/lib/cattri/version.rb +3 -1
- data/lib/cattri.rb +38 -99
- data/sig/lib/cattri/attribute.rbs +105 -0
- data/sig/lib/cattri/attribute_compiler.rbs +61 -0
- data/sig/lib/cattri/attribute_options.rbs +150 -0
- data/sig/lib/cattri/attribute_registry.rbs +95 -0
- data/sig/lib/cattri/context.rbs +130 -0
- data/sig/lib/cattri/context_registry.rbs +31 -0
- data/sig/lib/cattri/deferred_attributes.rbs +53 -0
- data/sig/lib/cattri/dsl.rbs +55 -0
- data/sig/lib/cattri/error.rbs +28 -0
- data/sig/lib/cattri/inheritance.rbs +21 -0
- data/sig/lib/cattri/initializer_patch.rbs +26 -0
- data/sig/lib/cattri/internal_store.rbs +75 -0
- data/sig/lib/cattri/introspection.rbs +61 -0
- data/sig/lib/cattri/types.rbs +19 -0
- data/sig/lib/cattri/visibility.rbs +55 -0
- data/sig/lib/cattri.rbs +37 -0
- data/spec/cattri/attribute_compiler_spec.rb +179 -0
- data/spec/cattri/attribute_options_spec.rb +267 -0
- data/spec/cattri/attribute_registry_spec.rb +257 -0
- data/spec/cattri/attribute_spec.rb +297 -0
- data/spec/cattri/context_registry_spec.rb +45 -0
- data/spec/cattri/context_spec.rb +346 -0
- data/spec/cattri/deferred_attrributes_spec.rb +117 -0
- data/spec/cattri/dsl_spec.rb +69 -0
- data/spec/cattri/error_spec.rb +37 -0
- data/spec/cattri/inheritance_spec.rb +60 -0
- data/spec/cattri/initializer_patch_spec.rb +35 -0
- data/spec/cattri/internal_store_spec.rb +139 -0
- data/spec/cattri/introspection_spec.rb +90 -0
- data/spec/cattri/visibility_spec.rb +68 -0
- data/spec/cattri_spec.rb +54 -0
- data/spec/simplecov_helper.rb +21 -0
- data/spec/spec_helper.rb +16 -0
- metadata +79 -6
- data/lib/cattri/attribute_definer.rb +0 -143
- data/lib/cattri/class_attributes.rb +0 -277
- data/lib/cattri/instance_attributes.rb +0 -276
- 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
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
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
|
-
#
|
51
|
-
#
|
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
|
-
#
|
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
|
data/lib/cattri/introspection.rb
CHANGED
@@ -1,70 +1,77 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Cattri
|
4
|
-
#
|
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
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
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
|
-
#
|
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
|
-
#
|
18
|
+
# @api public
|
19
|
+
# Class-level introspection methods exposed via `.attribute_defined?`, `.attribute`, etc.
|
33
20
|
module ClassMethods
|
34
|
-
# Returns
|
21
|
+
# Returns true if the given attribute has been defined on this class or any ancestor.
|
35
22
|
#
|
36
|
-
# @
|
37
|
-
|
38
|
-
|
23
|
+
# @param name [Symbol, String] the attribute name
|
24
|
+
# @return [Boolean]
|
25
|
+
def attribute_defined?(name)
|
26
|
+
!!attribute(name)
|
27
|
+
end
|
39
28
|
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
|
49
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
return {
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
data/lib/cattri/version.rb
CHANGED
data/lib/cattri.rb
CHANGED
@@ -1,119 +1,58 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "cattri/
|
4
|
-
require_relative "cattri/
|
5
|
-
require_relative "cattri/
|
6
|
-
require_relative "cattri/
|
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
|
-
#
|
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
|
-
#
|
30
|
-
#
|
31
|
-
#
|
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
|
-
#
|
34
|
-
#
|
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
|
-
#
|
24
|
+
# Sets up the Cattri DSL on the including class or module.
|
37
25
|
#
|
38
|
-
#
|
39
|
-
#
|
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
|
30
|
+
# @param base [Class, Module] the target that includes `Cattri`
|
42
31
|
# @return [void]
|
43
32
|
def self.included(base)
|
44
|
-
base.
|
45
|
-
|
46
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
38
|
+
base.prepend(Cattri::InitializerPatch)
|
39
|
+
base.extend(Cattri::Visibility)
|
40
|
+
base.extend(Cattri::Dsl)
|
41
|
+
base.extend(ClassMethods)
|
78
42
|
|
79
|
-
|
80
|
-
|
43
|
+
Cattri::Inheritance.install(base)
|
44
|
+
end
|
81
45
|
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
91
|
-
|
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
|