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
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "attribute_compiler"
|
4
|
+
|
5
|
+
module Cattri
|
6
|
+
# Cattri::AttributeRegistry is responsible for managing attribute definitions
|
7
|
+
# for a given context (class or module). It validates uniqueness, applies
|
8
|
+
# definition logic, and supports inheritance and introspection.
|
9
|
+
#
|
10
|
+
# It handles both eager and deferred attribute compilation and ensures correct
|
11
|
+
# behavior for `scope: :class`, `final: true`, and other attribute options.
|
12
|
+
class AttributeRegistry
|
13
|
+
# @return [Cattri::Context] the context this registry operates within
|
14
|
+
attr_reader :context
|
15
|
+
|
16
|
+
# Initializes a new registry for the provided context.
|
17
|
+
#
|
18
|
+
# @param context [Cattri::Context]
|
19
|
+
def initialize(context)
|
20
|
+
@context = context
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the attributes registered directly on this context.
|
24
|
+
#
|
25
|
+
# @return [Hash{Symbol => Cattri::Attribute}]
|
26
|
+
def registered_attributes
|
27
|
+
(@__cattri_registered_attributes ||= {}).dup.freeze # steep:ignore
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns all known attributes, optionally including inherited definitions.
|
31
|
+
#
|
32
|
+
# @param with_ancestors [Boolean] whether to include ancestors
|
33
|
+
# @return [Hash{Symbol => Cattri::Attribute}]
|
34
|
+
def defined_attributes(with_ancestors: false)
|
35
|
+
return registered_attributes unless with_ancestors
|
36
|
+
|
37
|
+
context.attribute_lookup_sources
|
38
|
+
.select { |mod| mod.respond_to?(:attribute_registry, true) }
|
39
|
+
.flat_map { |mod| mod.send(:attribute_registry).registered_attributes.to_a }
|
40
|
+
.to_h
|
41
|
+
.merge(registered_attributes)
|
42
|
+
.freeze
|
43
|
+
end
|
44
|
+
|
45
|
+
# Fetches an attribute by name, or returns nil.
|
46
|
+
#
|
47
|
+
# @param name [String, Symbol]
|
48
|
+
# @param with_ancestors [Boolean]
|
49
|
+
# @return [Cattri::Attribute, nil]
|
50
|
+
def fetch_attribute(name, with_ancestors: false)
|
51
|
+
defined_attributes(with_ancestors: with_ancestors)[name.to_sym]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Fetches an attribute by name, or raises if not found.
|
55
|
+
#
|
56
|
+
# @param name [String, Symbol]
|
57
|
+
# @param with_ancestors [Boolean]
|
58
|
+
# @return [Cattri::Attribute]
|
59
|
+
# @raise [Cattri::AttributeError] if the attribute is not defined
|
60
|
+
def fetch_attribute!(name, with_ancestors: false)
|
61
|
+
defined_attributes(with_ancestors: with_ancestors).fetch(name.to_sym) do
|
62
|
+
raise Cattri::AttributeError, "Attribute :#{name} has not been defined"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Defines a new attribute and registers it on the current context.
|
67
|
+
#
|
68
|
+
# @param name [String, Symbol] the attribute name
|
69
|
+
# @param value [Object, Proc, nil] default value or initializer
|
70
|
+
# @param options [Hash] attribute options (`:class`, `:final`, etc.)
|
71
|
+
# @yield [*args] optional transformation block used as setter
|
72
|
+
# @return [Array<Symbol>] list of methods defined by this attribute
|
73
|
+
# @raise [Cattri::AttributeError] if the name is already defined
|
74
|
+
def define_attribute(name, value, **options, &block)
|
75
|
+
name = name.to_sym
|
76
|
+
validate_unique!(name)
|
77
|
+
|
78
|
+
options_with_default = options.merge(default: value)
|
79
|
+
attribute = Cattri::Attribute.new(
|
80
|
+
name,
|
81
|
+
defined_in: context.target,
|
82
|
+
**options_with_default,
|
83
|
+
&block
|
84
|
+
)
|
85
|
+
|
86
|
+
register_attribute(attribute)
|
87
|
+
attribute.allowed_methods
|
88
|
+
end
|
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
|
+
private
|
108
|
+
|
109
|
+
# Validates that no attribute with the same name is already registered.
|
110
|
+
#
|
111
|
+
# @param name [Symbol]
|
112
|
+
# @raise [Cattri::AttributeError]
|
113
|
+
def validate_unique!(name)
|
114
|
+
return unless instance_variable_defined?(:@__cattri_registered_attributes)
|
115
|
+
return unless @__cattri_registered_attributes.key?(name)
|
116
|
+
|
117
|
+
raise Cattri::AttributeError, "Attribute :#{name} has already been defined"
|
118
|
+
end
|
119
|
+
|
120
|
+
# Registers an attribute and applies or defers its definition.
|
121
|
+
#
|
122
|
+
# @param attribute [Cattri::Attribute]
|
123
|
+
# @return [void]
|
124
|
+
def register_attribute(attribute)
|
125
|
+
unless instance_variable_defined?(:@__cattri_registered_attributes)
|
126
|
+
(@__cattri_registered_attributes ||= {}) # steep:ignore
|
127
|
+
end
|
128
|
+
|
129
|
+
@__cattri_registered_attributes[attribute.name] = attribute
|
130
|
+
return defer_definition(attribute) if context.defer_definitions?
|
131
|
+
|
132
|
+
apply_definition!(attribute)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Defers the attribute definition if in a module context.
|
136
|
+
#
|
137
|
+
# @param attribute [Cattri::Attribute]
|
138
|
+
# @return [void]
|
139
|
+
def defer_definition(attribute)
|
140
|
+
context.ensure_deferred_support!
|
141
|
+
context.target.defer_attribute(attribute) # steep:ignore
|
142
|
+
end
|
143
|
+
|
144
|
+
# Applies the attribute definition using the compiler.
|
145
|
+
#
|
146
|
+
# @param attribute [Cattri::Attribute]
|
147
|
+
# @return [void]
|
148
|
+
# @raise [Cattri::AttributeError]
|
149
|
+
def apply_definition!(attribute)
|
150
|
+
Cattri::AttributeCompiler.define_accessor(attribute, context)
|
151
|
+
rescue StandardError => e
|
152
|
+
raise Cattri::AttributeError, "Attribute #{attribute.name} could not be defined. Error: #{e.message}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/cattri/context.rb
CHANGED
@@ -1,171 +1,189 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "set"
|
4
|
+
require_relative "deferred_attributes"
|
4
5
|
|
5
6
|
module Cattri
|
6
|
-
#
|
7
|
-
# on a target class or module. Used internally by Cattri to define attribute
|
8
|
-
# readers and writers while preserving access visibility and tracking which
|
9
|
-
# methods were explicitly created.
|
7
|
+
# Cattri::Context encapsulates the class or module that attributes are being defined on.
|
10
8
|
#
|
11
|
-
#
|
12
|
-
#
|
9
|
+
# It provides a safe interface for dynamically defining methods and tracking metadata,
|
10
|
+
# such as declared accessors, access visibility, and deferred attribute declarations.
|
11
|
+
#
|
12
|
+
# It handles:
|
13
|
+
# - Attribute method definitions (reader/writer/predicate)
|
14
|
+
# - Visibility enforcement
|
15
|
+
# - Target resolution (instance vs. class-level)
|
16
|
+
# - Method deduplication and tracking
|
17
|
+
#
|
18
|
+
# All method definitions occur directly on the resolved target (e.g., the class or its singleton).
|
13
19
|
class Context
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
# @return [Module, Class] the receiver to which accessors will be added
|
20
|
+
# The class or module that owns the attributes.
|
21
|
+
#
|
22
|
+
# @return [Module]
|
19
23
|
attr_reader :target
|
20
24
|
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# @param target [Module, Class] the object to define methods or ivars on
|
25
|
+
# @param target [Module, Class]
|
24
26
|
def initialize(target)
|
25
27
|
@target = target
|
26
|
-
@defined_methods = Set.new
|
27
28
|
end
|
28
29
|
|
29
|
-
# Returns
|
30
|
+
# Returns a frozen copy of all attribute methods explicitly defined by this context.
|
30
31
|
#
|
31
|
-
# This
|
32
|
+
# This does not include inherited or module-defined methods.
|
32
33
|
#
|
33
|
-
# @return [
|
34
|
-
def
|
35
|
-
@
|
34
|
+
# @return [Hash{Symbol => Set<Symbol>}] map of attribute name to defined method names
|
35
|
+
def defined_methods
|
36
|
+
(@__cattri_defined_methods ||= {}).dup.freeze # steep:ignore
|
36
37
|
end
|
37
38
|
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# This includes public, protected, private, and previously defined methods
|
41
|
-
# via this context (tracked in `@defined_methods`).
|
39
|
+
# Whether this target should defer method definitions (e.g., if it's a module).
|
42
40
|
#
|
43
|
-
# @param method [String, Symbol]
|
44
41
|
# @return [Boolean]
|
45
|
-
def
|
46
|
-
@target.
|
47
|
-
@target.private_method_defined?(method) ||
|
48
|
-
@target.protected_method_defined?(method) ||
|
49
|
-
@defined_methods.include?(method.to_sym)
|
42
|
+
def defer_definitions?
|
43
|
+
@target.is_a?(Module) && !@target.is_a?(Class)
|
50
44
|
end
|
51
45
|
|
52
|
-
#
|
46
|
+
# Ensures the target includes Cattri::DeferredAttributes if needed.
|
53
47
|
#
|
54
|
-
#
|
55
|
-
# according to the attribute's `:access` setting (defaulting to `:public`).
|
48
|
+
# Used to prepare modules for later application of attributes when included elsewhere.
|
56
49
|
#
|
57
|
-
# @param attribute [Cattri::Attribute]
|
58
|
-
# @param name [Symbol, nil] optional method name override
|
59
|
-
# @yield the method implementation
|
60
|
-
# @raise [Cattri::AttributeDefinitionError] if method definition fails
|
61
50
|
# @return [void]
|
62
|
-
def
|
63
|
-
|
64
|
-
return if method_defined?(name)
|
51
|
+
def ensure_deferred_support!
|
52
|
+
return if @target < Cattri::DeferredAttributes
|
65
53
|
|
66
|
-
|
54
|
+
@target.extend(Cattri::DeferredAttributes)
|
67
55
|
end
|
68
56
|
|
69
|
-
#
|
57
|
+
# All ancestors and included modules used for attribute lookup and inheritance.
|
70
58
|
#
|
71
|
-
#
|
72
|
-
|
59
|
+
# @return [Array<Module>]
|
60
|
+
def attribute_lookup_sources
|
61
|
+
([@target] + @target.ancestors + @target.singleton_class.included_modules).uniq
|
62
|
+
end
|
63
|
+
|
64
|
+
# Defines a method for the given attribute unless already defined locally.
|
73
65
|
#
|
74
|
-
#
|
66
|
+
# Respects attribute-level force overwrite and enforces visibility rules.
|
75
67
|
#
|
76
|
-
# @param attribute [Cattri::Attribute]
|
77
|
-
# @param name [Symbol, nil]
|
78
|
-
# @yield
|
79
|
-
# @raise [Cattri::
|
68
|
+
# @param attribute [Cattri::Attribute]
|
69
|
+
# @param name [Symbol, nil] optional method name override
|
70
|
+
# @yield method implementation block
|
71
|
+
# @raise [Cattri::AttributeError] if method is already defined and not forced
|
80
72
|
# @return [void]
|
81
|
-
def define_method
|
73
|
+
def define_method(attribute, name: nil, &block)
|
74
|
+
name = (name || attribute.name).to_sym
|
82
75
|
target = target_for(attribute)
|
83
76
|
|
84
|
-
|
85
|
-
|
86
|
-
@defined_methods << name unless method_defined?(name)
|
87
|
-
apply_access(name, attribute)
|
88
|
-
rescue StandardError => e
|
89
|
-
raise Cattri::AttributeDefinitionError.new(target, attribute, e)
|
77
|
+
if method_defined?(attribute, name: name)
|
78
|
+
raise Cattri::AttributeError, "Method `:#{name}` already defined on #{target}"
|
90
79
|
end
|
91
|
-
end
|
92
80
|
|
93
|
-
|
94
|
-
#
|
95
|
-
# @param name [Symbol, String]
|
96
|
-
# @return [Boolean]
|
97
|
-
def ivar_defined?(name)
|
98
|
-
@target.instance_variable_defined?(sanitize_ivar(name))
|
81
|
+
define_method!(target, attribute, name, &block)
|
99
82
|
end
|
100
83
|
|
101
|
-
#
|
84
|
+
# Checks if the given method is already defined on the resolved target.
|
102
85
|
#
|
103
|
-
#
|
104
|
-
# @return [Object]
|
105
|
-
def ivar_get(name)
|
106
|
-
@target.instance_variable_get(sanitize_ivar(name))
|
107
|
-
end
|
108
|
-
|
109
|
-
# Assigns a value to the specified instance variable on the target.
|
86
|
+
# Only checks methods directly defined on the class or singleton—not ancestors.
|
110
87
|
#
|
111
|
-
# @param
|
112
|
-
# @param
|
113
|
-
# @return [
|
114
|
-
def
|
115
|
-
|
116
|
-
|
88
|
+
# @param attribute [Cattri::Attribute]
|
89
|
+
# @param name [Symbol, nil]
|
90
|
+
# @return [Boolean]
|
91
|
+
def method_defined?(attribute, name: nil)
|
92
|
+
normalized_name = (name || attribute.name).to_sym
|
93
|
+
target = target_for(attribute)
|
117
94
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
def ivar_memoize(name, value)
|
124
|
-
return ivar_get(name) if ivar_defined?(name)
|
95
|
+
defined_locally = (
|
96
|
+
target.public_instance_methods(false) +
|
97
|
+
target.protected_instance_methods(false) +
|
98
|
+
target.private_instance_methods(false)
|
99
|
+
)
|
125
100
|
|
126
|
-
|
101
|
+
defined_locally.include?(normalized_name) ||
|
102
|
+
__cattri_defined_methods[attribute.name].include?(normalized_name)
|
127
103
|
end
|
128
104
|
|
129
105
|
private
|
130
106
|
|
131
|
-
#
|
107
|
+
# Internal tracking of explicitly defined methods per attribute.
|
108
|
+
#
|
109
|
+
# @return [Hash{Symbol => Set<Symbol>}]
|
110
|
+
def __cattri_defined_methods
|
111
|
+
@__cattri_defined_methods ||= Hash.new { |h, k| h[k] = Set.new }
|
112
|
+
end
|
113
|
+
|
114
|
+
# Determines whether to define the method on the instance or singleton.
|
132
115
|
#
|
133
116
|
# @param attribute [Cattri::Attribute]
|
134
|
-
# @return [Module]
|
117
|
+
# @return [Module]
|
135
118
|
def target_for(attribute)
|
136
|
-
attribute.
|
119
|
+
attribute.class_attribute? ? @target.singleton_class : @target
|
137
120
|
end
|
138
121
|
|
139
|
-
#
|
122
|
+
# Defines the method and applies its access visibility.
|
140
123
|
#
|
141
|
-
# @param
|
142
|
-
# @
|
143
|
-
|
144
|
-
|
145
|
-
|
124
|
+
# @param target [Module]
|
125
|
+
# @param attribute [Cattri::Attribute]
|
126
|
+
# @param name [Symbol]
|
127
|
+
# @yield method implementation
|
128
|
+
# @return [void]
|
129
|
+
def define_method!(target, attribute, name, &block)
|
130
|
+
target.class_eval { define_method(name, &block) } # steep:ignore
|
131
|
+
__cattri_defined_methods[attribute.name] << name
|
146
132
|
|
147
|
-
|
148
|
-
|
133
|
+
apply_visibility!(target, name, attribute)
|
134
|
+
rescue StandardError => e
|
135
|
+
raise Cattri::AttributeError, "Failed to define accessor methods for `:#{name}` on #{target}. Error: #{e.message}"
|
149
136
|
end
|
150
137
|
|
151
|
-
# Applies
|
138
|
+
# Applies visibility (`public`, `protected`, `private`) to a method.
|
139
|
+
#
|
140
|
+
# Skips application for `:public` (default in Ruby).
|
152
141
|
#
|
153
|
-
# @param
|
142
|
+
# @param target [Module]
|
143
|
+
# @param name [Symbol]
|
154
144
|
# @param attribute [Cattri::Attribute]
|
155
145
|
# @return [void]
|
156
|
-
def
|
157
|
-
|
146
|
+
def apply_visibility!(target, name, attribute)
|
147
|
+
visibility = effective_visibility(attribute, name)
|
148
|
+
return if visibility == :public
|
158
149
|
|
159
|
-
|
160
|
-
Module.instance_method(access).bind(@target).call(method_name)
|
150
|
+
Module.instance_method(visibility).bind(target).call(name)
|
161
151
|
end
|
162
152
|
|
163
|
-
#
|
153
|
+
# Determines the effective visibility of the attribute.
|
164
154
|
#
|
165
|
-
#
|
155
|
+
# - If the attribute has no public writer or reader (i.e., `expose: :write` or `:none`)
|
156
|
+
# - Returns `:protected` for class-level attributes
|
157
|
+
# - Returns `:private` for instance-level attributes
|
158
|
+
# - Otherwise, returns the explicitly declared visibility (`attribute.visibility`)
|
159
|
+
#
|
160
|
+
# This ensures that internal-only attributes remain inaccessible outside their scope,
|
161
|
+
# while still being usable by subclasses if class-level.
|
162
|
+
#
|
163
|
+
# @param attribute [Cattri::Attribute]
|
166
164
|
# @return [Symbol]
|
167
|
-
def
|
168
|
-
:
|
165
|
+
def effective_visibility(attribute, name)
|
166
|
+
return :protected if attribute.class_attribute? && internal_method?(attribute, name)
|
167
|
+
return :private if !attribute.class_attribute? && internal_method?(attribute, name)
|
168
|
+
|
169
|
+
Cattri::AttributeOptions.validate_visibility!(attribute.visibility)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Determines whether the given method name (accessor or writer)
|
173
|
+
# should be treated as internal-only based on the attribute's `expose` configuration.
|
174
|
+
#
|
175
|
+
# This is used when resolving method visibility (e.g., private vs protected).
|
176
|
+
#
|
177
|
+
# - Writer methods (`:attr=`) are considered internal if the attribute lacks public read access.
|
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
|
183
|
+
def internal_method?(attribute, name)
|
184
|
+
return attribute.internal_writer? if name.to_s.end_with?("=")
|
185
|
+
|
186
|
+
attribute.internal_reader?
|
169
187
|
end
|
170
188
|
end
|
171
189
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "attribute_registry"
|
4
|
+
require_relative "context"
|
5
|
+
|
6
|
+
module Cattri
|
7
|
+
# Provides per-class or per-module access to the attribute registry and method definition context.
|
8
|
+
#
|
9
|
+
# This module is included into both the base and singleton class of any class using Cattri.
|
10
|
+
# It initializes and exposes a lazily-evaluated `attribute_registry` and `context` specific
|
11
|
+
# to the current scope, enabling safe and isolated attribute handling.
|
12
|
+
module ContextRegistry
|
13
|
+
private
|
14
|
+
|
15
|
+
# Returns the attribute definition registry for this class or module.
|
16
|
+
#
|
17
|
+
# The registry is responsible for tracking all defined attributes, both class-level and
|
18
|
+
# instance-level, handling application logic, and copying across subclasses where needed.
|
19
|
+
#
|
20
|
+
# @return [Cattri::AttributeRegistry] the registry used to define and apply attributes
|
21
|
+
def attribute_registry
|
22
|
+
@attribute_registry ||= Cattri::AttributeRegistry.new(context)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the method definition context for this class or module.
|
26
|
+
#
|
27
|
+
# The context wraps the current target (class or module) and provides utilities
|
28
|
+
# for defining attribute methods (readers, writers, predicates), managing visibility,
|
29
|
+
# and recording declared methods to avoid duplication.
|
30
|
+
#
|
31
|
+
# @return [Cattri::Context] the context used for method definition and visibility tracking
|
32
|
+
def context
|
33
|
+
@context ||= Context.new(self) # steep:ignore
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module Cattri
|
6
|
+
# Provides support for defining attributes within a module that should be
|
7
|
+
# applied later to any class or module that includes or extends it.
|
8
|
+
#
|
9
|
+
# This allows DSL modules to define Cattri attributes without prematurely
|
10
|
+
# applying them to themselves, deferring application to the including/extending context.
|
11
|
+
module DeferredAttributes
|
12
|
+
# Hook into the module extension lifecycle to ensure deferred attributes are
|
13
|
+
# applied when the module is included or extended.
|
14
|
+
#
|
15
|
+
# @param base [Module] the module that extended this module
|
16
|
+
# @return [void]
|
17
|
+
def self.extended(base)
|
18
|
+
return if base.singleton_class.ancestors.include?(Hook)
|
19
|
+
|
20
|
+
base.singleton_class.prepend(Hook)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Hook methods for inclusion/extension that trigger deferred application.
|
24
|
+
module Hook
|
25
|
+
# Called when a module including `DeferredAttributes` is included into another module/class.
|
26
|
+
#
|
27
|
+
# @param target [Module] the including class or module
|
28
|
+
# @return [void]
|
29
|
+
def included(target)
|
30
|
+
apply_deferred_attributes(target) if respond_to?(:apply_deferred_attributes) # steep:ignore
|
31
|
+
end
|
32
|
+
|
33
|
+
# Called when a module including `DeferredAttributes` is extended into another module/class.
|
34
|
+
#
|
35
|
+
# @param target [Module] the extending class or module
|
36
|
+
# @return [void]
|
37
|
+
def extended(target)
|
38
|
+
apply_deferred_attributes(target) if respond_to?(:apply_deferred_attributes) # steep:ignore
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Registers an attribute to be applied later when this module is included or extended.
|
43
|
+
#
|
44
|
+
# @param attribute [Cattri::Attribute] the attribute to defer
|
45
|
+
# @return [void]
|
46
|
+
def defer_attribute(attribute)
|
47
|
+
deferred_attributes[attribute.name] = attribute
|
48
|
+
end
|
49
|
+
|
50
|
+
# Applies all deferred attributes to the target class or module.
|
51
|
+
#
|
52
|
+
# This is triggered automatically by the {Hook} on `included` or `extended`.
|
53
|
+
#
|
54
|
+
# @param target [Module] the class or module to apply the attributes to
|
55
|
+
# @return [void]
|
56
|
+
def apply_deferred_attributes(target)
|
57
|
+
context = Cattri::Context.new(target)
|
58
|
+
|
59
|
+
deferred_attributes.each_value do |attribute|
|
60
|
+
Cattri::AttributeCompiler.define_accessor(attribute, context)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# Internal storage of deferred attributes for this module.
|
67
|
+
#
|
68
|
+
# @return [Hash{Symbol => Cattri::Attribute}]
|
69
|
+
def deferred_attributes
|
70
|
+
@deferred_attributes ||= {}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/cattri/dsl.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cattri
|
4
|
+
# Provides the primary DSL for defining class-level and instance-level attributes.
|
5
|
+
#
|
6
|
+
# This module is extended into any class or module that includes Cattri,
|
7
|
+
# and exposes methods like `cattri` and `final_cattri` for concise attribute declaration.
|
8
|
+
#
|
9
|
+
# All attributes are defined through the underlying attribute registry and
|
10
|
+
# are associated with method accessors (reader, writer, predicate) based on options.
|
11
|
+
module Dsl
|
12
|
+
# Defines a new attribute with optional default, coercion, and visibility options.
|
13
|
+
#
|
14
|
+
# The attribute can be defined with a static value, a lazy-evaluated block,
|
15
|
+
# or with additional options like `final`, `predicate`, or `expose`.
|
16
|
+
#
|
17
|
+
# The attribute will be defined as either class-level or instance-level
|
18
|
+
# depending on the `scope:` option.
|
19
|
+
#
|
20
|
+
# @param name [Symbol, String] the attribute name
|
21
|
+
# @param value [Object, nil] optional static value or default
|
22
|
+
# @param options [Hash] additional attribute configuration
|
23
|
+
# @option options [Boolean] :scope whether this is a class-level (:class) or instance-level (:instance) attribute
|
24
|
+
# @option options [Boolean] :final whether the attribute is write-once
|
25
|
+
# @option options [Boolean] :predicate whether to define a predicate method
|
26
|
+
# @option options [Symbol] :expose whether to expose `:read`, `:write`, `:read_write`, or `:none`
|
27
|
+
# @option options [Symbol] :visibility the visibility for generated methods (`:public`, `:protected`, `:private`)
|
28
|
+
# @yield optional block to lazily evaluate the attribute’s default value
|
29
|
+
# @return [Array<Symbol>] the defined methods
|
30
|
+
def cattri(name, value = nil, **options, &block)
|
31
|
+
options = { visibility: __cattri_visibility }.merge(options) # steep:ignore
|
32
|
+
attribute_registry.define_attribute(name, value, **options, &block) # steep:ignore
|
33
|
+
end
|
34
|
+
|
35
|
+
# Defines a write-once (final) attribute.
|
36
|
+
#
|
37
|
+
# Final attributes can be written only once and raise on re-assignment.
|
38
|
+
# This is equivalent to `cattri(..., final: true)`.
|
39
|
+
#
|
40
|
+
# @param name [Symbol, String] the attribute name
|
41
|
+
# @param value [Object, nil] static or lazy default value
|
42
|
+
# @param options [Hash] additional attribute configuration
|
43
|
+
# @option options [Boolean] :scope whether this is a class-level (:class) or instance-level (:instance) attribute
|
44
|
+
# @option options [Boolean] :final whether the attribute is write-once
|
45
|
+
# @option options [Boolean] :predicate whether to define a predicate method
|
46
|
+
# @option options [Symbol] :expose whether to expose `:read`, `:write`, `:read_write`, or `:none`
|
47
|
+
# @option options [Symbol] :visibility the visibility for generated methods (`:public`, `:protected`, `:private`)
|
48
|
+
# @yield optional block to lazily evaluate the default
|
49
|
+
# @return [Array<Symbol>] the defined methods
|
50
|
+
def final_cattri(name, value, **options, &block)
|
51
|
+
cattri(name, value, **options.merge(final: true), &block) # steep:ignore
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|