cattri 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +34 -0
  3. data/.gitignore +72 -0
  4. data/.rubocop.yml +6 -3
  5. data/CHANGELOG.md +41 -0
  6. data/Gemfile +12 -0
  7. data/README.md +163 -151
  8. data/Steepfile +6 -0
  9. data/bin/console +33 -0
  10. data/bin/setup +8 -0
  11. data/cattri.gemspec +5 -5
  12. data/lib/cattri/attribute.rb +119 -155
  13. data/lib/cattri/attribute_compiler.rb +104 -0
  14. data/lib/cattri/attribute_options.rb +183 -0
  15. data/lib/cattri/attribute_registry.rb +155 -0
  16. data/lib/cattri/context.rb +124 -106
  17. data/lib/cattri/context_registry.rb +36 -0
  18. data/lib/cattri/deferred_attributes.rb +73 -0
  19. data/lib/cattri/dsl.rb +54 -0
  20. data/lib/cattri/error.rb +17 -90
  21. data/lib/cattri/inheritance.rb +35 -0
  22. data/lib/cattri/initializer_patch.rb +37 -0
  23. data/lib/cattri/internal_store.rb +104 -0
  24. data/lib/cattri/introspection.rb +56 -49
  25. data/lib/cattri/version.rb +3 -1
  26. data/lib/cattri.rb +38 -99
  27. data/sig/lib/cattri/attribute.rbs +105 -0
  28. data/sig/lib/cattri/attribute_compiler.rbs +61 -0
  29. data/sig/lib/cattri/attribute_options.rbs +150 -0
  30. data/sig/lib/cattri/attribute_registry.rbs +95 -0
  31. data/sig/lib/cattri/context.rbs +130 -0
  32. data/sig/lib/cattri/context_registry.rbs +31 -0
  33. data/sig/lib/cattri/deferred_attributes.rbs +53 -0
  34. data/sig/lib/cattri/dsl.rbs +55 -0
  35. data/sig/lib/cattri/error.rbs +28 -0
  36. data/sig/lib/cattri/inheritance.rbs +21 -0
  37. data/sig/lib/cattri/initializer_patch.rbs +26 -0
  38. data/sig/lib/cattri/internal_store.rbs +75 -0
  39. data/sig/lib/cattri/introspection.rbs +61 -0
  40. data/sig/lib/cattri/types.rbs +19 -0
  41. data/sig/lib/cattri/visibility.rbs +55 -0
  42. data/sig/lib/cattri.rbs +37 -0
  43. data/spec/cattri/attribute_compiler_spec.rb +179 -0
  44. data/spec/cattri/attribute_options_spec.rb +267 -0
  45. data/spec/cattri/attribute_registry_spec.rb +257 -0
  46. data/spec/cattri/attribute_spec.rb +297 -0
  47. data/spec/cattri/context_registry_spec.rb +45 -0
  48. data/spec/cattri/context_spec.rb +346 -0
  49. data/spec/cattri/deferred_attrributes_spec.rb +117 -0
  50. data/spec/cattri/dsl_spec.rb +69 -0
  51. data/spec/cattri/error_spec.rb +37 -0
  52. data/spec/cattri/inheritance_spec.rb +60 -0
  53. data/spec/cattri/initializer_patch_spec.rb +35 -0
  54. data/spec/cattri/internal_store_spec.rb +139 -0
  55. data/spec/cattri/introspection_spec.rb +90 -0
  56. data/spec/cattri/visibility_spec.rb +68 -0
  57. data/spec/cattri_spec.rb +54 -0
  58. data/spec/simplecov_helper.rb +21 -0
  59. data/spec/spec_helper.rb +16 -0
  60. metadata +79 -6
  61. data/lib/cattri/attribute_definer.rb +0 -143
  62. data/lib/cattri/class_attributes.rb +0 -277
  63. data/lib/cattri/instance_attributes.rb +0 -276
  64. data/sig/cattri.rbs +0 -4
@@ -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
@@ -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
- # Provides a controlled interface for defining methods and instance variables
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
- # This abstraction allows class and instance attribute logic to be composed
12
- # consistently and safely across both standard and singleton contexts.
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
- # Allowed Ruby visibility levels for methods.
15
- ACCESS_LEVELS = %i[public protected private].freeze
16
- private_constant :ACCESS_LEVELS
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
- # Initializes a new context wrapper around the given target.
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 the singleton class of the target.
30
+ # Returns a frozen copy of all attribute methods explicitly defined by this context.
30
31
  #
31
- # This is used to define methods on the class itself (not its instances).
32
+ # This does not include inherited or module-defined methods.
32
33
  #
33
- # @return [Class]
34
- def singleton
35
- @target.singleton_class
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
- # Checks whether a method is already defined on the target.
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 method_defined?(method)
46
- @target.method_defined?(method) ||
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
- # Defines a method on the appropriate context (class or singleton).
46
+ # Ensures the target includes Cattri::DeferredAttributes if needed.
53
47
  #
54
- # If the method already exists, it is not redefined. Visibility is applied
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 define_method(attribute, name: nil, &block)
63
- name = (name || attribute.name).to_sym
64
- return if method_defined?(name)
51
+ def ensure_deferred_support!
52
+ return if @target < Cattri::DeferredAttributes
65
53
 
66
- define_method!(attribute, name: name, &block)
54
+ @target.extend(Cattri::DeferredAttributes)
67
55
  end
68
56
 
69
- # Defines a method on the target class or singleton class, regardless of whether it already exists.
57
+ # All ancestors and included modules used for attribute lookup and inheritance.
70
58
  #
71
- # This bypasses any checks for prior definition and forcibly installs the method using `define_method`.
72
- # The method will be assigned visibility based on the attribute's `:access` setting.
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
- # Used internally by attribute definers to (re)define writers, readers, or callables.
66
+ # Respects attribute-level force overwrite and enforces visibility rules.
75
67
  #
76
- # @param attribute [Cattri::Attribute] the attribute whose context and access rules apply
77
- # @param name [Symbol, nil] the method name to define (defaults to attribute name)
78
- # @yield the method body to define
79
- # @raise [Cattri::AttributeDefinitionError] if method definition fails
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!(attribute, name: nil, &block)
73
+ def define_method(attribute, name: nil, &block)
74
+ name = (name || attribute.name).to_sym
82
75
  target = target_for(attribute)
83
76
 
84
- begin
85
- target.define_method(name, &block)
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
- # Checks whether the target has the given instance variable.
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
- # Retrieves the value of the specified instance variable on the target.
84
+ # Checks if the given method is already defined on the resolved target.
102
85
  #
103
- # @param name [Symbol, String]
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 name [Symbol, String]
112
- # @param value [Object]
113
- # @return [void]
114
- def ivar_set(name, value)
115
- @target.instance_variable_set(sanitize_ivar(name), value)
116
- end
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
- # Memoizes a value in the instance variable only if not already defined.
119
- #
120
- # @param name [Symbol, String]
121
- # @param value [Object]
122
- # @return [Object] the existing or assigned value
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
- ivar_set(name, value)
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
- # Selects the correct definition target based on attribute type.
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] either the target or its singleton class
117
+ # @return [Module]
135
118
  def target_for(attribute)
136
- attribute.class_level? ? singleton : @target
119
+ attribute.class_attribute? ? @target.singleton_class : @target
137
120
  end
138
121
 
139
- # Validates and normalizes access level.
122
+ # Defines the method and applies its access visibility.
140
123
  #
141
- # @param access [Symbol, String, nil]
142
- # @return [Symbol] a valid visibility level
143
- def validate_access(access)
144
- access = (access || :public).to_sym
145
- return access if ACCESS_LEVELS.include?(access)
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
- warn "[Cattri] `#{access.inspect}` is not a supported access level, defaulting to :public"
148
- :public
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 method visibility to a newly defined method.
138
+ # Applies visibility (`public`, `protected`, `private`) to a method.
139
+ #
140
+ # Skips application for `:public` (default in Ruby).
152
141
  #
153
- # @param method_name [Symbol]
142
+ # @param target [Module]
143
+ # @param name [Symbol]
154
144
  # @param attribute [Cattri::Attribute]
155
145
  # @return [void]
156
- def apply_access(method_name, attribute)
157
- return if attribute.public?
146
+ def apply_visibility!(target, name, attribute)
147
+ visibility = effective_visibility(attribute, name)
148
+ return if visibility == :public
158
149
 
159
- access = validate_access(attribute[:access])
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
- # Ensures consistent formatting for ivar keys.
153
+ # Determines the effective visibility of the attribute.
164
154
  #
165
- # @param name [Symbol, String]
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 sanitize_ivar(name)
168
- :"@#{name.to_s.delete_prefix("@")}"
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