cattri 0.2.1 → 0.2.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/CHANGELOG.md +37 -1
- data/lib/cattri/attribute_compiler.rb +8 -13
- data/lib/cattri/attribute_registry.rb +0 -17
- data/lib/cattri/context.rb +80 -17
- data/lib/cattri/context_registry.rb +2 -1
- data/lib/cattri/initializer_patch.rb +1 -1
- data/lib/cattri/internal_store.rb +11 -0
- data/lib/cattri/version.rb +1 -1
- data/lib/cattri/visibility.rb +3 -3
- data/lib/cattri.rb +4 -3
- data/sig/lib/cattri/context.rbs +34 -3
- data/sig/lib/cattri/internal_store.rbs +9 -0
- data/spec/cattri/attribute_compiler_spec.rb +10 -40
- data/spec/cattri/attribute_registry_spec.rb +0 -37
- data/spec/cattri/context_spec.rb +127 -11
- data/spec/cattri/internal_store_spec.rb +10 -0
- data/spec/cattri_spec.rb +144 -0
- metadata +2 -5
- data/lib/cattri/inheritance.rb +0 -35
- data/sig/lib/cattri/inheritance.rbs +0 -21
- data/spec/cattri/inheritance_spec.rb +0 -60
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f432670c63ac3fc653b105988804c219c7f73e0023c7fc1e11b2f56a16a9ac66
|
4
|
+
data.tar.gz: 4302d8ffafd8c3f0e321aea61ec5c06e42ec7c5178130768c967b25ed0bcdf43
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '08c0a0719c6699e42a40c92081700a0466ae78f3eda2ff08e34c75f76006c9a9bf915b65235893ee72ff28a0402c553b2dbca4ea596591a33cf1acd28a6fa975'
|
7
|
+
data.tar.gz: 99c6caeb8fd09b5c0a3de773efec82f0f63648d73ba1f2e7597351a17bea6450b7b05ec8ca952cf0f2e0e9abc7a974738807d4b67048b8ae5190cc85ef043427
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,44 @@
|
|
1
|
+
## [0.2.2] - 2025-05-04
|
2
|
+
|
3
|
+
No breaking changes – the public DSL (cattri, final_cattri) remains identical to v0.2.0.
|
4
|
+
|
5
|
+
### Removed
|
6
|
+
- Removed `Cattri::AttributeRegistry#copy_attributes_to` and all associated logic. Subclass attribute propagation
|
7
|
+
is now handled entirely via runtime resolution in `Context#storage_receiver_for`, eliminating the need for eager
|
8
|
+
copying of attribute values or metadata.
|
9
|
+
- Removed `Cattri::Inheritance` and its use of `.inherited` hooks for subclass propagation, since all relevant state
|
10
|
+
is now derived dynamically.
|
11
|
+
|
12
|
+
### Changed
|
13
|
+
- Replaced static subclass propagation with dynamic storage resolution using `Context#storage_receiver_for`,
|
14
|
+
which now determines the correct backing object (instance, class, or singleton class) for all reads and writes.
|
15
|
+
- `AttributeCompiler.define_accessor` now uses `storage_receiver_for` to set final class-level attribute values
|
16
|
+
during compilation.
|
17
|
+
- Final class-level attributes (`final: true, scope: :class`) are now evaluated and written directly to the correct
|
18
|
+
storage receiver without relying on prior duplication logic.
|
19
|
+
|
20
|
+
### Added
|
21
|
+
- Proper support for defining attributes directly on a module’s singleton_class, ensuring values are stored and
|
22
|
+
accessed consistently across runtime and functional tests.
|
23
|
+
```ruby
|
24
|
+
module MyModule
|
25
|
+
class << self
|
26
|
+
include Cattri
|
27
|
+
|
28
|
+
cattri :version, "0.1.0", final: true, scope: :class
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
- Regression tests ensuring:
|
33
|
+
- Subclass attribute shadowing works for `final: true, scope: :class`.
|
34
|
+
- Instance and class attribute isolation behaves as expected across inheritance chains.
|
35
|
+
- No mutation of final attributes once defined.
|
36
|
+
- Test coverage for `Context#storage_receiver` for and `Context#resolve_class_storage_for` across all branching logic.
|
37
|
+
|
1
38
|
## [0.2.1] - 2025-05-01
|
2
39
|
|
3
40
|
- Fixed an issue where only `final: true` instance variables defined on the current/class had their values applied.
|
4
41
|
- Now walks the ancestor tree to ensure all attributes get set.
|
5
|
-
|
6
42
|
```ruby
|
7
43
|
module Options
|
8
44
|
include Cattri
|
@@ -26,7 +26,8 @@ module Cattri
|
|
26
26
|
def define_accessor(attribute, context)
|
27
27
|
if attribute.class_attribute? && attribute.final?
|
28
28
|
value = attribute.evaluate_default
|
29
|
-
context.
|
29
|
+
context.storage_receiver_for(attribute) # steep:ignore
|
30
|
+
.cattri_variable_set(attribute.ivar, value, final: attribute.final?) # steep:ignore
|
30
31
|
end
|
31
32
|
|
32
33
|
return if attribute.expose == :none
|
@@ -48,12 +49,13 @@ module Cattri
|
|
48
49
|
# @return [void]
|
49
50
|
def define_accessor!(attribute, context)
|
50
51
|
context.define_method(attribute) do |*args, **kwargs|
|
52
|
+
receiver = context.storage_receiver_for(attribute, self)
|
51
53
|
readonly_call = args.empty? && kwargs.empty?
|
52
|
-
return AttributeCompiler.send(:memoize_default_value,
|
54
|
+
return AttributeCompiler.send(:memoize_default_value, receiver, attribute) if readonly_call
|
53
55
|
|
54
56
|
attribute.validate_assignment!
|
55
57
|
value = attribute.process_assignment(*args, **kwargs)
|
56
|
-
cattri_variable_set(attribute.ivar, value) # steep:ignore
|
58
|
+
receiver.cattri_variable_set(attribute.ivar, value) # steep:ignore
|
57
59
|
end
|
58
60
|
end
|
59
61
|
|
@@ -64,8 +66,10 @@ module Cattri
|
|
64
66
|
# @return [void]
|
65
67
|
def define_writer!(attribute, context)
|
66
68
|
context.define_method(attribute, name: :"#{attribute.name}=") do |value|
|
69
|
+
receiver = context.storage_receiver_for(attribute, self)
|
70
|
+
|
67
71
|
coerced_value = attribute.process_assignment(value)
|
68
|
-
cattri_variable_set(attribute.ivar, coerced_value, final: attribute.final?) # steep:ignore
|
72
|
+
receiver.cattri_variable_set(attribute.ivar, coerced_value, final: attribute.final?) # steep:ignore
|
69
73
|
end
|
70
74
|
end
|
71
75
|
|
@@ -82,19 +86,10 @@ module Cattri
|
|
82
86
|
|
83
87
|
# Returns the default value for the attribute, memoizing it in the backing store.
|
84
88
|
#
|
85
|
-
# For `final` attributes, raises unless explicitly initialized.
|
86
|
-
#
|
87
89
|
# @param receiver [Object] the instance or class receiving the value
|
88
90
|
# @param attribute [Cattri::Attribute]
|
89
91
|
# @return [Object] the stored or evaluated default
|
90
|
-
# @raise [Cattri::AttributeError] if final attribute is unset or evaluation fails
|
91
92
|
def memoize_default_value(receiver, attribute)
|
92
|
-
if attribute.final?
|
93
|
-
return receiver.cattri_variable_get(attribute.ivar) if receiver.cattri_variable_defined?(attribute.ivar)
|
94
|
-
|
95
|
-
raise Cattri::AttributeError, "Final attribute :#{attribute.name} cannot be written to"
|
96
|
-
end
|
97
|
-
|
98
93
|
receiver.cattri_variable_memoize(attribute.ivar, final: attribute.final?) do
|
99
94
|
attribute.evaluate_default
|
100
95
|
end
|
@@ -87,23 +87,6 @@ module Cattri
|
|
87
87
|
attribute.allowed_methods
|
88
88
|
end
|
89
89
|
|
90
|
-
# Copies registered attributes from this context to another,
|
91
|
-
# preserving definitions and assigning values for `final: true, scope: :class`.
|
92
|
-
#
|
93
|
-
# @param target_context [Cattri::Context]
|
94
|
-
# @return [void]
|
95
|
-
def copy_attributes_to(target_context)
|
96
|
-
registered_attributes.each_value do |attribute|
|
97
|
-
next unless attribute.class_attribute? && attribute.final?
|
98
|
-
|
99
|
-
target_registry = target_context.target.send(:attribute_registry)
|
100
|
-
target_registry.send(:register_attribute, attribute)
|
101
|
-
|
102
|
-
value = context.target.cattri_variable_get(attribute.ivar) # steep:ignore
|
103
|
-
target_context.target.cattri_variable_set(attribute.ivar, value) # steep:ignore
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
90
|
private
|
108
91
|
|
109
92
|
# Validates that no attribute with the same name is already registered.
|
data/lib/cattri/context.rb
CHANGED
@@ -22,7 +22,9 @@ module Cattri
|
|
22
22
|
# @return [Module]
|
23
23
|
attr_reader :target
|
24
24
|
|
25
|
-
#
|
25
|
+
# Initializes the context wrapper.
|
26
|
+
#
|
27
|
+
# @param target [Module, Class] the receiver where attributes are defined
|
26
28
|
def initialize(target)
|
27
29
|
@target = target
|
28
30
|
end
|
@@ -102,6 +104,44 @@ module Cattri
|
|
102
104
|
__cattri_defined_methods[attribute.name].include?(normalized_name)
|
103
105
|
end
|
104
106
|
|
107
|
+
# Resolves and returns the module or class where methods and storage should be defined
|
108
|
+
# for the given attribute. Ensures the internal store is included on the resolved target.
|
109
|
+
#
|
110
|
+
# - For class-level attributes, this returns the singleton class unless it's already a singleton.
|
111
|
+
# - For instance-level attributes, this returns the class/module directly.
|
112
|
+
#
|
113
|
+
# @param attribute [Cattri::Attribute]
|
114
|
+
# @return [Module]
|
115
|
+
def target_for(attribute)
|
116
|
+
return @target if attribute.class_attribute? && singleton_class?(@target)
|
117
|
+
|
118
|
+
attribute.class_attribute? ? @target.singleton_class : @target
|
119
|
+
end
|
120
|
+
|
121
|
+
# Determines the object (class/module or runtime instance) that should hold
|
122
|
+
# the backing storage for the given attribute.
|
123
|
+
#
|
124
|
+
# - For class-level attributes, uses the singleton class of `defined_in` or the module itself
|
125
|
+
# - For instance-level attributes, uses the provided instance
|
126
|
+
#
|
127
|
+
# @param attribute [Cattri::Attribute]
|
128
|
+
# @param instance [Object, nil] the runtime instance, if needed for instance-level access
|
129
|
+
# @return [Object] the receiver for attribute value storage
|
130
|
+
# @raise [Cattri::Error] if instance is required but missing
|
131
|
+
def storage_receiver_for(attribute, instance = nil)
|
132
|
+
receiver =
|
133
|
+
if attribute.class_attribute?
|
134
|
+
resolve_class_storage_for(attribute, instance)
|
135
|
+
elsif instance
|
136
|
+
instance
|
137
|
+
else
|
138
|
+
raise Cattri::Error, "Missing runtime instance for instance-level attribute :#{attribute.name}"
|
139
|
+
end
|
140
|
+
|
141
|
+
install_internal_store!(receiver)
|
142
|
+
receiver
|
143
|
+
end
|
144
|
+
|
105
145
|
private
|
106
146
|
|
107
147
|
# Internal tracking of explicitly defined methods per attribute.
|
@@ -111,12 +151,42 @@ module Cattri
|
|
111
151
|
@__cattri_defined_methods ||= Hash.new { |h, k| h[k] = Set.new }
|
112
152
|
end
|
113
153
|
|
114
|
-
# Determines whether
|
154
|
+
# Determines whether the object is a singleton_class or not.
|
155
|
+
#
|
156
|
+
# @param obj [Object]
|
157
|
+
# @return [Boolean]
|
158
|
+
def singleton_class?(obj)
|
159
|
+
obj.singleton_class? # Ruby 3.2+
|
160
|
+
rescue NoMethodError
|
161
|
+
obj.inspect.start_with?("#<Class:")
|
162
|
+
end
|
163
|
+
|
164
|
+
# Resolves the proper class-level storage receiver for the attribute.
|
115
165
|
#
|
116
166
|
# @param attribute [Cattri::Attribute]
|
117
|
-
# @
|
118
|
-
|
119
|
-
|
167
|
+
# @param instance [Object, nil]
|
168
|
+
# @return [Object]
|
169
|
+
def resolve_class_storage_for(attribute, instance)
|
170
|
+
if attribute.final?
|
171
|
+
singleton_class?(attribute.defined_in) ? attribute.defined_in : attribute.defined_in.singleton_class
|
172
|
+
else
|
173
|
+
attribute_scope = instance || attribute.defined_in
|
174
|
+
attribute_scope.singleton_class
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Installs the internal store on the receiver if not present.
|
179
|
+
#
|
180
|
+
# @param receiver [Object]
|
181
|
+
# @return [void]
|
182
|
+
def install_internal_store!(receiver)
|
183
|
+
return if receiver.respond_to?(:cattri_variables)
|
184
|
+
|
185
|
+
if singleton_class?(receiver)
|
186
|
+
receiver.extend(Cattri::InternalStore)
|
187
|
+
else
|
188
|
+
receiver.include(Cattri::InternalStore) # steep:ignore
|
189
|
+
end
|
120
190
|
end
|
121
191
|
|
122
192
|
# Defines the method and applies its access visibility.
|
@@ -157,11 +227,9 @@ module Cattri
|
|
157
227
|
# - Returns `:private` for instance-level attributes
|
158
228
|
# - Otherwise, returns the explicitly declared visibility (`attribute.visibility`)
|
159
229
|
#
|
160
|
-
# This ensures that internal-only attributes remain inaccessible outside their scope,
|
161
|
-
# while still being usable by subclasses if class-level.
|
162
|
-
#
|
163
230
|
# @param attribute [Cattri::Attribute]
|
164
|
-
# @
|
231
|
+
# @param name [Symbol]
|
232
|
+
# @return [Symbol] one of `:public`, `:protected`, or `:private`
|
165
233
|
def effective_visibility(attribute, name)
|
166
234
|
return :protected if attribute.class_attribute? && internal_method?(attribute, name)
|
167
235
|
return :private if !attribute.class_attribute? && internal_method?(attribute, name)
|
@@ -172,14 +240,9 @@ module Cattri
|
|
172
240
|
# Determines whether the given method name (accessor or writer)
|
173
241
|
# should be treated as internal-only based on the attribute's `expose` configuration.
|
174
242
|
#
|
175
|
-
#
|
176
|
-
#
|
177
|
-
#
|
178
|
-
# - Reader methods (`:attr`) are considered internal if the attribute lacks public write access.
|
179
|
-
#
|
180
|
-
# @param attribute [Cattri::Attribute] the attribute definition
|
181
|
-
# @param name [Symbol, String] the method name being defined
|
182
|
-
# @return [Boolean] true if the method should be scoped for internal use only
|
243
|
+
# @param attribute [Cattri::Attribute]
|
244
|
+
# @param name [Symbol, String]
|
245
|
+
# @return [Boolean]
|
183
246
|
def internal_method?(attribute, name)
|
184
247
|
return attribute.internal_writer? if name.to_s.end_with?("=")
|
185
248
|
|
@@ -30,7 +30,8 @@ module Cattri
|
|
30
30
|
#
|
31
31
|
# @return [Cattri::Context] the context used for method definition and visibility tracking
|
32
32
|
def context
|
33
|
-
|
33
|
+
base = instance_variable_get(:@__cattri_base_target) || self
|
34
|
+
@context ||= Context.new(base) # steep:ignore
|
34
35
|
end
|
35
36
|
end
|
36
37
|
end
|
@@ -31,7 +31,7 @@ module Cattri
|
|
31
31
|
next if cattri_variable_defined?(attribute.ivar) # steep:ignore
|
32
32
|
next unless attribute.final?
|
33
33
|
|
34
|
-
cattri_variable_set(attribute.ivar, attribute.evaluate_default) # steep:ignore
|
34
|
+
cattri_variable_set(attribute.ivar, attribute.evaluate_default, final: true) # steep:ignore
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
@@ -13,6 +13,17 @@ module Cattri
|
|
13
13
|
#
|
14
14
|
# It supports enforcement of `final` semantics and tracks explicit assignments.
|
15
15
|
module InternalStore
|
16
|
+
# Returns the list of attribute keys stored by Cattri on this object.
|
17
|
+
#
|
18
|
+
# Mimics Ruby's `#instance_variables`, but only includes attributes defined
|
19
|
+
# via `cattri` and omits the leading `@` from names. All keys are returned as
|
20
|
+
# frozen symbols (e.g., `:enabled` instead of `:@enabled`).
|
21
|
+
#
|
22
|
+
# @return [Array<Symbol>] the list of internally tracked attribute keys
|
23
|
+
def cattri_variables
|
24
|
+
__cattri_store.keys.freeze
|
25
|
+
end
|
26
|
+
|
16
27
|
# Checks whether the internal store contains a value for the given key.
|
17
28
|
#
|
18
29
|
# @param key [String, Symbol] the attribute name or instance variable
|
data/lib/cattri/version.rb
CHANGED
data/lib/cattri/visibility.rb
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
module Cattri
|
4
4
|
# Cattri::Visibility tracks the current method visibility context (`public`, `protected`, `private`)
|
5
5
|
# when defining methods dynamically. It mimics Ruby's native visibility behavior so that
|
6
|
-
# `
|
7
|
-
#
|
6
|
+
# `cattri` definitions can automatically infer the intended access level based on the current context
|
7
|
+
# in the source file.
|
8
8
|
#
|
9
9
|
# This module is intended to be extended by classes that include or extend Cattri.
|
10
10
|
#
|
@@ -13,7 +13,7 @@ module Cattri
|
|
13
13
|
# include Cattri
|
14
14
|
#
|
15
15
|
# private
|
16
|
-
#
|
16
|
+
# cattri :sensitive_data
|
17
17
|
# end
|
18
18
|
#
|
19
19
|
# # => :sensitive_data will be defined as a private method
|
data/lib/cattri.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
require_relative "cattri/attribute"
|
4
4
|
require_relative "cattri/context_registry"
|
5
5
|
require_relative "cattri/dsl"
|
6
|
-
require_relative "cattri/inheritance"
|
7
6
|
require_relative "cattri/initializer_patch"
|
8
7
|
require_relative "cattri/internal_store"
|
9
8
|
require_relative "cattri/introspection"
|
@@ -33,14 +32,16 @@ module Cattri
|
|
33
32
|
[base, base.singleton_class].each do |mod|
|
34
33
|
mod.include(Cattri::InternalStore)
|
35
34
|
mod.include(Cattri::ContextRegistry)
|
35
|
+
mod.instance_variable_set(:@__cattri_base_target, base)
|
36
36
|
end
|
37
37
|
|
38
|
+
base.extend(Cattri::InternalStore)
|
39
|
+
base.singleton_class.extend(Cattri::InternalStore)
|
40
|
+
|
38
41
|
base.prepend(Cattri::InitializerPatch)
|
39
42
|
base.extend(Cattri::Visibility)
|
40
43
|
base.extend(Cattri::Dsl)
|
41
44
|
base.extend(ClassMethods)
|
42
|
-
|
43
|
-
Cattri::Inheritance.install(base)
|
44
45
|
end
|
45
46
|
|
46
47
|
# Provides opt-in class-level introspection support.
|
data/sig/lib/cattri/context.rbs
CHANGED
@@ -68,6 +68,24 @@ module Cattri
|
|
68
68
|
# @return [Boolean]
|
69
69
|
def method_defined?: (Attribute attribute, ?name: identifier?) -> bool
|
70
70
|
|
71
|
+
# Determines whether to define the method on the instance or singleton.
|
72
|
+
#
|
73
|
+
# @param attribute [Cattri::Attribute]
|
74
|
+
# @return [Module]
|
75
|
+
def target_for: (Attribute attribute) -> ::Module
|
76
|
+
|
77
|
+
# Determines the object (class/module or runtime instance) that should hold
|
78
|
+
# the backing storage for the given attribute.
|
79
|
+
#
|
80
|
+
# - For class-level attributes, uses the singleton class of `defined_in` or the module itself
|
81
|
+
# - For instance-level attributes, uses the provided instance
|
82
|
+
#
|
83
|
+
# @param attribute [Cattri::Attribute]
|
84
|
+
# @param instance [Object, nil] the runtime instance, if needed for instance-level access
|
85
|
+
# @return [Object] the receiver for attribute value storage
|
86
|
+
# @raise [Cattri::Error] if instance is required but missing
|
87
|
+
def storage_receiver_for: (Attribute attribute, ::Object? instance) -> ::Object
|
88
|
+
|
71
89
|
private
|
72
90
|
|
73
91
|
# Internal tracking of explicitly defined methods per attribute.
|
@@ -75,11 +93,24 @@ module Cattri
|
|
75
93
|
# @return [Hash{Symbol => Set<Symbol>}]
|
76
94
|
def __cattri_defined_methods: () -> ::Hash[::Symbol, ::Set[::Symbol]]
|
77
95
|
|
78
|
-
# Determines whether
|
96
|
+
# Determines whether the object is a singleton_class or not.
|
97
|
+
#
|
98
|
+
# @param obj [Object]
|
99
|
+
# @return [Boolean]
|
100
|
+
def singleton_class?: (untyped obj) -> bool
|
101
|
+
|
102
|
+
# Resolves the proper class-level storage receiver for the attribute.
|
79
103
|
#
|
80
104
|
# @param attribute [Cattri::Attribute]
|
81
|
-
# @
|
82
|
-
|
105
|
+
# @param instance [Object, nil]
|
106
|
+
# @return [Object]
|
107
|
+
def resolve_class_storage_for: (Attribute attribute, ::Object? instance) -> ::Object
|
108
|
+
|
109
|
+
# Installs the internal store on the receiver if not present.
|
110
|
+
#
|
111
|
+
# @param receiver [Object]
|
112
|
+
# @return [void]
|
113
|
+
def install_internal_store!: (::Object receiver) -> void
|
83
114
|
|
84
115
|
# Defines the method and applies its access visibility.
|
85
116
|
#
|
@@ -13,6 +13,15 @@ module Cattri
|
|
13
13
|
|
14
14
|
@__cattri_set_variables: ::Set[::Symbol]
|
15
15
|
|
16
|
+
# Returns the list of attribute keys stored by Cattri on this object.
|
17
|
+
#
|
18
|
+
# Mimics Ruby's `#instance_variables`, but only includes attributes defined
|
19
|
+
# via `cattri` and omits the leading `@` from names. All keys are returned as
|
20
|
+
# frozen symbols (e.g., `:enabled` instead of `:@enabled`).
|
21
|
+
#
|
22
|
+
# @return [Array<Symbol>] the list of internally tracked attribute keys
|
23
|
+
def cattri_variables: () -> ::Array[::Symbol]
|
24
|
+
|
16
25
|
# Checks whether the internal store contains a value for the given key.
|
17
26
|
#
|
18
27
|
# @param key [String, Symbol] the attribute name or instance variable
|
@@ -26,7 +26,7 @@ RSpec.describe Cattri::AttributeCompiler do
|
|
26
26
|
|
27
27
|
it "eagerly sets the default value on the target class" do
|
28
28
|
described_class.define_accessor(attribute, context)
|
29
|
-
expect(dummy_class.
|
29
|
+
expect(dummy_class.count).to eq(100)
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
@@ -134,46 +134,16 @@ RSpec.describe Cattri::AttributeCompiler do
|
|
134
134
|
describe ".memoize_default_value" do
|
135
135
|
let(:instance) { dummy_class.new }
|
136
136
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
)
|
145
|
-
end
|
146
|
-
|
147
|
-
it "stores and returns the evaluated default" do
|
148
|
-
result = described_class.send(:memoize_default_value, instance, attribute)
|
149
|
-
expect(result).to eq("bar")
|
150
|
-
expect(instance.cattri_variable_get(:foo)).to eq("bar")
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
context "final attribute" do
|
155
|
-
let(:attribute) do
|
156
|
-
Cattri::Attribute.new(
|
157
|
-
:immutable,
|
158
|
-
defined_in: dummy_class,
|
159
|
-
final: true,
|
160
|
-
default: -> { "locked" },
|
161
|
-
expose: :read
|
162
|
-
)
|
163
|
-
end
|
164
|
-
|
165
|
-
it "raises if value is not already set" do
|
166
|
-
expect do
|
167
|
-
described_class.send(:memoize_default_value, instance, attribute)
|
168
|
-
end.to raise_error(Cattri::AttributeError, /Final attribute :immutable cannot be written to/)
|
169
|
-
end
|
170
|
-
|
171
|
-
it "returns value if already set" do
|
172
|
-
instance.cattri_variable_set(:immutable, "preset", final: true)
|
173
|
-
result = described_class.send(:memoize_default_value, instance, attribute)
|
137
|
+
it "memoizes the evaluated default" do
|
138
|
+
attribute = Cattri::Attribute.new(
|
139
|
+
:foo,
|
140
|
+
defined_in: dummy_class,
|
141
|
+
default: -> { "bar" },
|
142
|
+
expose: :read_write
|
143
|
+
)
|
174
144
|
|
175
|
-
|
176
|
-
|
145
|
+
expect(described_class.send(:memoize_default_value, instance, attribute)).to eq("bar")
|
146
|
+
expect(instance.cattri_variable_get(:foo)).to eq("bar")
|
177
147
|
end
|
178
148
|
end
|
179
149
|
end
|
@@ -117,43 +117,6 @@ RSpec.describe Cattri::AttributeRegistry do
|
|
117
117
|
end
|
118
118
|
end
|
119
119
|
|
120
|
-
describe "#copy_attributes_to" do
|
121
|
-
let(:target_klass) do
|
122
|
-
Class.new do
|
123
|
-
include Cattri
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
let!(:target_registry) { target_klass.send(:attribute_registry) }
|
128
|
-
let(:target_context) { Cattri::Context.new(target_klass) }
|
129
|
-
|
130
|
-
it "copies final class attributes and sets their values" do
|
131
|
-
registry.define_attribute(:enabled, "yes", scope: :class, final: true)
|
132
|
-
registry.context.target.cattri_variable_set(:@enabled, "yes")
|
133
|
-
|
134
|
-
registry.copy_attributes_to(target_context)
|
135
|
-
|
136
|
-
expect(target_klass.cattri_variable_get(:@enabled)).to eq("yes")
|
137
|
-
expect(target_registry.fetch_attribute(:enabled)).to be_a(Cattri::Attribute)
|
138
|
-
end
|
139
|
-
|
140
|
-
it "skips non-final or instance-level attributes" do
|
141
|
-
registry.define_attribute(:skipped1, "val", scope: :instance, final: true)
|
142
|
-
registry.define_attribute(:skipped2, "val", scope: :class, final: false)
|
143
|
-
|
144
|
-
expect do
|
145
|
-
registry.copy_attributes_to(target_context)
|
146
|
-
end.not_to(change { target_klass.send(:attribute_registry).defined_attributes })
|
147
|
-
end
|
148
|
-
|
149
|
-
it "restores original context after copying" do
|
150
|
-
original_context = registry.context
|
151
|
-
registry.copy_attributes_to(target_context)
|
152
|
-
|
153
|
-
expect(registry.context).to equal(original_context)
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
120
|
describe "#validate_unique!" do
|
158
121
|
context "when no attributes are registered" do
|
159
122
|
it "returns without raising" do
|
data/spec/cattri/context_spec.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require "spec_helper"
|
4
4
|
|
5
5
|
RSpec.describe Cattri::Context do
|
6
|
-
let(:context_target) { Class.new }
|
6
|
+
let!(:context_target) { Class.new }
|
7
7
|
|
8
8
|
def define_attribute(name, value, scope: :instance, visibility: :public)
|
9
9
|
Cattri::Attribute.new(
|
@@ -181,6 +181,63 @@ RSpec.describe Cattri::Context do
|
|
181
181
|
end
|
182
182
|
end
|
183
183
|
|
184
|
+
describe "#target_for" do
|
185
|
+
subject(:result) { context.target_for(attribute) }
|
186
|
+
|
187
|
+
context "when a class-level attribute is provided" do
|
188
|
+
let(:attribute) { class_attribute }
|
189
|
+
|
190
|
+
it "returns the singleton_class" do
|
191
|
+
expect(result).to eq(context_target.singleton_class)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context "when a instance-level attribute is provided" do
|
196
|
+
let(:attribute) { instance_attribute }
|
197
|
+
|
198
|
+
it "returns the context's target" do
|
199
|
+
expect(result).to eq(context_target)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
context "when a class-level attribute is provided and the context is a singleton_class" do
|
204
|
+
let(:context_target) { singleton_class }
|
205
|
+
let(:attribute) { class_attribute }
|
206
|
+
|
207
|
+
it "returns the singleton_class" do
|
208
|
+
expect(result).to eq(singleton_class)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
describe "#storage_receiver_for" do
|
214
|
+
context "when a class-level attribute is provided" do
|
215
|
+
it "resolves to the instance's singleton_class, if instance is provided" do
|
216
|
+
receiver = context.storage_receiver_for(class_attribute, self)
|
217
|
+
expect(receiver).to eq(singleton_class)
|
218
|
+
end
|
219
|
+
|
220
|
+
it "resolves to the attribute's defined_in.singleton_class when no instance is provided" do
|
221
|
+
receiver = context.storage_receiver_for(class_attribute) # no instance passed
|
222
|
+
expect(receiver).to eq(context_target.singleton_class)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
context "when a instance-level attribute is provided" do
|
227
|
+
it "returns the instance" do
|
228
|
+
receiver = context.storage_receiver_for(instance_attribute, self)
|
229
|
+
expect(receiver).to eq(self)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
context "when a received cannot be resolved" do
|
234
|
+
it "raises an error" do
|
235
|
+
expect { context.storage_receiver_for(instance_attribute, nil) }
|
236
|
+
.to raise_error
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
184
241
|
describe "#__cattri_defined_methods" do
|
185
242
|
it "returns a Hash" do
|
186
243
|
expect(context.send(:__cattri_defined_methods)).to be_a(Hash)
|
@@ -201,26 +258,85 @@ RSpec.describe Cattri::Context do
|
|
201
258
|
end
|
202
259
|
end
|
203
260
|
|
204
|
-
describe "#
|
205
|
-
|
261
|
+
describe "#resolve_class_storage_for" do
|
262
|
+
def define_attribute(name, final:, defined_in:, scope: :class)
|
263
|
+
Cattri::Attribute.new(
|
264
|
+
name,
|
265
|
+
final: final,
|
266
|
+
defined_in: defined_in,
|
267
|
+
scope: scope,
|
268
|
+
default: -> { "value" },
|
269
|
+
visibility: :public
|
270
|
+
)
|
271
|
+
end
|
272
|
+
|
273
|
+
context "when attribute is final" do
|
274
|
+
it "returns defined_in if already a singleton_class" do
|
275
|
+
mod = Module.new
|
276
|
+
singleton = mod.singleton_class
|
277
|
+
attribute = define_attribute(:final_attr, final: true, defined_in: singleton)
|
278
|
+
|
279
|
+
expect(context.send(:resolve_class_storage_for, attribute, nil)).to eq(singleton)
|
280
|
+
end
|
206
281
|
|
207
|
-
|
208
|
-
|
282
|
+
it "returns singleton_class of defined_in if not already a singleton" do
|
283
|
+
klass = Class.new
|
284
|
+
attribute = define_attribute(:final_attr, final: true, defined_in: klass)
|
209
285
|
|
210
|
-
|
211
|
-
expect(result).to eq(context_target.singleton_class)
|
286
|
+
expect(context.send(:resolve_class_storage_for, attribute, nil)).to eq(klass.singleton_class)
|
212
287
|
end
|
213
288
|
end
|
214
289
|
|
215
|
-
context "when
|
216
|
-
|
290
|
+
context "when attribute is not final" do
|
291
|
+
it "returns singleton_class of instance if instance is provided" do
|
292
|
+
instance = Object.new
|
293
|
+
attribute = define_attribute(:nonfinal_attr, final: false, defined_in: context_target)
|
217
294
|
|
218
|
-
|
219
|
-
|
295
|
+
expect(context.send(:resolve_class_storage_for, attribute, instance)).to eq(instance.singleton_class)
|
296
|
+
end
|
297
|
+
|
298
|
+
it "returns singleton_class of defined_in if no instance is provided" do
|
299
|
+
attribute = define_attribute(:nonfinal_attr, final: false, defined_in: context_target)
|
300
|
+
|
301
|
+
expect(context.send(:resolve_class_storage_for, attribute, nil)).to eq(context_target.singleton_class)
|
220
302
|
end
|
221
303
|
end
|
222
304
|
end
|
223
305
|
|
306
|
+
describe "#install_internal_store!" do
|
307
|
+
let(:context) { Cattri::Context.new(Object.new) }
|
308
|
+
|
309
|
+
it "does nothing if receiver already responds to cattri_variables" do
|
310
|
+
receiver = Class.new do
|
311
|
+
def self.cattri_variables; end
|
312
|
+
end
|
313
|
+
|
314
|
+
expect { context.send(:install_internal_store!, receiver) }
|
315
|
+
.not_to(change { receiver.ancestors })
|
316
|
+
end
|
317
|
+
|
318
|
+
it "extends singleton class if receiver is a singleton class" do
|
319
|
+
base = Class.new
|
320
|
+
singleton = base.singleton_class
|
321
|
+
|
322
|
+
expect(singleton.ancestors).not_to include(Cattri::InternalStore)
|
323
|
+
|
324
|
+
context.send(:install_internal_store!, singleton)
|
325
|
+
|
326
|
+
expect(singleton.ancestors).to include(Cattri::InternalStore)
|
327
|
+
end
|
328
|
+
|
329
|
+
it "includes module if receiver is a regular class or module" do
|
330
|
+
klass = Class.new
|
331
|
+
|
332
|
+
expect(klass.ancestors).not_to include(Cattri::InternalStore)
|
333
|
+
|
334
|
+
context.send(:install_internal_store!, klass)
|
335
|
+
|
336
|
+
expect(klass.ancestors).to include(Cattri::InternalStore)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
224
340
|
describe "#define_method!" do
|
225
341
|
let(:target) { context.send(:target_for, class_attribute) }
|
226
342
|
let(:attribute_name) { class_attribute.name }
|
@@ -13,6 +13,16 @@ RSpec.describe Cattri::InternalStore do
|
|
13
13
|
|
14
14
|
subject(:instance) { klass.new }
|
15
15
|
|
16
|
+
describe "#cattri_variables" do
|
17
|
+
it "returns a list of all cattri variables" do
|
18
|
+
instance.cattri_variable_set(:foo, 123)
|
19
|
+
instance.cattri_variable_set(:bar, 123)
|
20
|
+
instance.cattri_variable_set(:@baz, 123)
|
21
|
+
|
22
|
+
expect(instance.cattri_variables).to match_array(%i[foo bar baz])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
16
26
|
describe "#cattri_variable_defined?" do
|
17
27
|
it "returns false when variable is not set" do
|
18
28
|
expect(instance.cattri_variable_defined?(:foo)).to be false
|
data/spec/cattri_spec.rb
CHANGED
@@ -51,4 +51,148 @@ RSpec.describe Cattri do
|
|
51
51
|
expect(introspective_class.attribute_methods).to include(:id)
|
52
52
|
end
|
53
53
|
end
|
54
|
+
|
55
|
+
describe "regression tests" do
|
56
|
+
it "allows one-time assignment to final instance attribute" do
|
57
|
+
klass = Class.new do
|
58
|
+
include Cattri
|
59
|
+
cattri :value, final: true
|
60
|
+
|
61
|
+
def initialize(value)
|
62
|
+
self.value = value
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
instance = klass.new("first")
|
67
|
+
expect(instance.value).to eq("first")
|
68
|
+
|
69
|
+
expect { instance.value = "second" }.to raise_error(Cattri::AttributeError)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "prevents any assignment to final class attribute" do
|
73
|
+
klass = Class.new do
|
74
|
+
include Cattri
|
75
|
+
cattri :value, -> { "init" }, final: true, scope: :class
|
76
|
+
end
|
77
|
+
|
78
|
+
expect(klass.value).to eq("init")
|
79
|
+
expect { klass.value = "fail" }.to raise_error(Cattri::AttributeError)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "allows shadowing parent class attributes" do
|
83
|
+
parent = Class.new do
|
84
|
+
include Cattri
|
85
|
+
cattri :enabled, true, final: true, scope: :class
|
86
|
+
end
|
87
|
+
|
88
|
+
child = Class.new do
|
89
|
+
include Cattri
|
90
|
+
cattri :enabled, false, final: true, scope: :class
|
91
|
+
end
|
92
|
+
|
93
|
+
expect(parent.enabled).to be(true)
|
94
|
+
expect(child.enabled).to be(false)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "defines predicate method for instance attribute" do
|
98
|
+
klass = Class.new do
|
99
|
+
include Cattri
|
100
|
+
cattri :flag, false, predicate: true
|
101
|
+
end
|
102
|
+
|
103
|
+
instance = klass.new
|
104
|
+
expect(instance.flag?).to eq(false)
|
105
|
+
|
106
|
+
instance.flag = true
|
107
|
+
expect(instance.flag?).to eq(true)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "evaluates and stores default value lazily" do
|
111
|
+
klass = Class.new do
|
112
|
+
include Cattri
|
113
|
+
cattri :computed, -> { "value" }
|
114
|
+
end
|
115
|
+
|
116
|
+
instance = klass.new
|
117
|
+
|
118
|
+
expect(instance.cattri_variable_defined?(:computed)).to eq(false)
|
119
|
+
expect(instance.computed).to eq("value")
|
120
|
+
expect(instance.cattri_variable_defined?(:computed)).to eq(true)
|
121
|
+
end
|
122
|
+
|
123
|
+
it "isolates instance and class attributes correctly" do
|
124
|
+
klass = Class.new do
|
125
|
+
include Cattri
|
126
|
+
cattri :config, scope: :class
|
127
|
+
cattri :state, scope: :instance
|
128
|
+
end
|
129
|
+
|
130
|
+
klass.config = "shared"
|
131
|
+
a = klass.new
|
132
|
+
b = klass.new
|
133
|
+
a.state = "one"
|
134
|
+
b.state = "two"
|
135
|
+
|
136
|
+
expect(klass.config).to eq("shared")
|
137
|
+
expect(a.state).to eq("one")
|
138
|
+
expect(b.state).to eq("two")
|
139
|
+
end
|
140
|
+
|
141
|
+
it "allows for class attributes set on a module's singleton_class" do
|
142
|
+
mod = Module.new do
|
143
|
+
class << self
|
144
|
+
include Cattri
|
145
|
+
cattri :version, "0.1.0", final: true, scope: :class
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
expect(mod.version).to eq("0.1.0"), "Expected module singleton_class to retain class-level attribute value"
|
150
|
+
end
|
151
|
+
|
152
|
+
it "isolates class attributes across subclasses" do
|
153
|
+
parent = Class.new do
|
154
|
+
include Cattri
|
155
|
+
cattri :level, "parent", scope: :class
|
156
|
+
end
|
157
|
+
|
158
|
+
child = Class.new(parent)
|
159
|
+
expect(child.level).to eq("parent")
|
160
|
+
|
161
|
+
child.level = "child"
|
162
|
+
|
163
|
+
expect(parent.level).to eq("parent")
|
164
|
+
expect(child.level).to eq("child")
|
165
|
+
end
|
166
|
+
|
167
|
+
it "applies custom coercion via block during assignment" do
|
168
|
+
klass = Class.new do
|
169
|
+
include Cattri
|
170
|
+
cattri :age do |value|
|
171
|
+
Integer(value)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
instance = klass.new
|
176
|
+
instance.age = "42"
|
177
|
+
|
178
|
+
expect(instance.age).to eq(42)
|
179
|
+
end
|
180
|
+
|
181
|
+
it "allows inherited initialize to set final attribute once" do
|
182
|
+
base = Class.new do
|
183
|
+
include Cattri
|
184
|
+
cattri :token, final: true
|
185
|
+
|
186
|
+
def initialize(token)
|
187
|
+
self.token = token
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
subclass = Class.new(base)
|
192
|
+
obj = subclass.new("abc123")
|
193
|
+
|
194
|
+
expect(obj.token).to eq("abc123")
|
195
|
+
expect { obj.token = "fail" }.to raise_error(Cattri::AttributeError)
|
196
|
+
end
|
197
|
+
end
|
54
198
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cattri
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Lucas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-05-
|
11
|
+
date: 2025-05-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: debride
|
@@ -154,7 +154,6 @@ files:
|
|
154
154
|
- lib/cattri/deferred_attributes.rb
|
155
155
|
- lib/cattri/dsl.rb
|
156
156
|
- lib/cattri/error.rb
|
157
|
-
- lib/cattri/inheritance.rb
|
158
157
|
- lib/cattri/initializer_patch.rb
|
159
158
|
- lib/cattri/internal_store.rb
|
160
159
|
- lib/cattri/introspection.rb
|
@@ -170,7 +169,6 @@ files:
|
|
170
169
|
- sig/lib/cattri/deferred_attributes.rbs
|
171
170
|
- sig/lib/cattri/dsl.rbs
|
172
171
|
- sig/lib/cattri/error.rbs
|
173
|
-
- sig/lib/cattri/inheritance.rbs
|
174
172
|
- sig/lib/cattri/initializer_patch.rbs
|
175
173
|
- sig/lib/cattri/internal_store.rbs
|
176
174
|
- sig/lib/cattri/introspection.rbs
|
@@ -185,7 +183,6 @@ files:
|
|
185
183
|
- spec/cattri/deferred_attrributes_spec.rb
|
186
184
|
- spec/cattri/dsl_spec.rb
|
187
185
|
- spec/cattri/error_spec.rb
|
188
|
-
- spec/cattri/inheritance_spec.rb
|
189
186
|
- spec/cattri/initializer_patch_spec.rb
|
190
187
|
- spec/cattri/internal_store_spec.rb
|
191
188
|
- spec/cattri/introspection_spec.rb
|
data/lib/cattri/inheritance.rb
DELETED
@@ -1,35 +0,0 @@
|
|
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
|
@@ -1,21 +0,0 @@
|
|
1
|
-
module Cattri
|
2
|
-
# Handles subclassing behavior for classes that use Cattri.
|
3
|
-
#
|
4
|
-
# This module installs a custom `.inherited` hook on the target class's singleton,
|
5
|
-
# ensuring that attribute definitions and values are deep-copied to the subclass.
|
6
|
-
#
|
7
|
-
# The hook preserves any existing `.inherited` behavior defined on the class,
|
8
|
-
# calling it before applying attribute propagation.
|
9
|
-
module Inheritance
|
10
|
-
# Installs an `inherited` hook on the given class.
|
11
|
-
#
|
12
|
-
# When the class is subclassed, Cattri will copy over attribute metadata and values
|
13
|
-
# using the subclass’s context. This ensures subclass safety and definition isolation.
|
14
|
-
#
|
15
|
-
# Any pre-existing `.inherited` method is preserved and invoked first.
|
16
|
-
#
|
17
|
-
# @param base [Class] the class to install the hook on
|
18
|
-
# @return [void]
|
19
|
-
def self.install: (::Module base) -> void
|
20
|
-
end
|
21
|
-
end
|
@@ -1,60 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "spec_helper"
|
4
|
-
|
5
|
-
RSpec.describe Cattri::Inheritance do
|
6
|
-
let(:base_class) do
|
7
|
-
Class.new.tap do |klass|
|
8
|
-
klass.include(Cattri::ContextRegistry)
|
9
|
-
klass.singleton_class.include(Cattri::ContextRegistry)
|
10
|
-
|
11
|
-
klass.attr_reader :inherited_called_with
|
12
|
-
|
13
|
-
def klass.inherited(subclass)
|
14
|
-
@inherited_called_with = subclass
|
15
|
-
end
|
16
|
-
|
17
|
-
def klass.inherited_called_with # rubocop:disable Style/TrivialAccessors
|
18
|
-
@inherited_called_with
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
before do
|
24
|
-
allow(Cattri::Context).to receive(:new).and_call_original
|
25
|
-
allow_any_instance_of(Cattri::AttributeRegistry).to receive(:copy_attributes_to)
|
26
|
-
end
|
27
|
-
|
28
|
-
describe ".install" do
|
29
|
-
it "preserves and calls existing inherited method" do
|
30
|
-
Cattri::Inheritance.install(base_class)
|
31
|
-
|
32
|
-
subclass = Class.new(base_class) # triggers inherited hook
|
33
|
-
|
34
|
-
expect(base_class.inherited_called_with).to eq(subclass)
|
35
|
-
end
|
36
|
-
|
37
|
-
it "initializes a Cattri::Context for the subclass" do
|
38
|
-
Cattri::Inheritance.install(base_class)
|
39
|
-
|
40
|
-
expect(Cattri::Context).to receive(:new) do |actual_subclass|
|
41
|
-
expect(actual_subclass.superclass).to eq(base_class)
|
42
|
-
end
|
43
|
-
|
44
|
-
Class.new(base_class)
|
45
|
-
end
|
46
|
-
|
47
|
-
it "invokes copy_attributes_to with the subclass context" do
|
48
|
-
context = instance_double(Cattri::Context)
|
49
|
-
allow(Cattri::Context).to receive(:new).and_return(context)
|
50
|
-
|
51
|
-
registry = instance_double(Cattri::AttributeRegistry)
|
52
|
-
allow(base_class).to receive(:attribute_registry).and_return(registry)
|
53
|
-
|
54
|
-
expect(registry).to receive(:copy_attributes_to).with(context)
|
55
|
-
|
56
|
-
Cattri::Inheritance.install(base_class)
|
57
|
-
Class.new(base_class)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|