domainic-attributer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE +21 -0
  4. data/README.md +396 -0
  5. data/lib/domainic/attributer/attribute/callback.rb +68 -0
  6. data/lib/domainic/attributer/attribute/coercer.rb +93 -0
  7. data/lib/domainic/attributer/attribute/mixin/belongs_to_attribute.rb +68 -0
  8. data/lib/domainic/attributer/attribute/signature.rb +338 -0
  9. data/lib/domainic/attributer/attribute/validator.rb +128 -0
  10. data/lib/domainic/attributer/attribute.rb +256 -0
  11. data/lib/domainic/attributer/attribute_set.rb +208 -0
  12. data/lib/domainic/attributer/class_methods.rb +247 -0
  13. data/lib/domainic/attributer/dsl/attribute_builder/option_parser.rb +247 -0
  14. data/lib/domainic/attributer/dsl/attribute_builder.rb +233 -0
  15. data/lib/domainic/attributer/dsl/initializer.rb +130 -0
  16. data/lib/domainic/attributer/dsl/method_injector.rb +97 -0
  17. data/lib/domainic/attributer/dsl.rb +5 -0
  18. data/lib/domainic/attributer/instance_methods.rb +65 -0
  19. data/lib/domainic/attributer/undefined.rb +44 -0
  20. data/lib/domainic/attributer.rb +114 -0
  21. data/lib/domainic-attributer.rb +3 -0
  22. data/sig/domainic/attributer/attribute/callback.rbs +48 -0
  23. data/sig/domainic/attributer/attribute/coercer.rbs +59 -0
  24. data/sig/domainic/attributer/attribute/mixin/belongs_to_attribute.rbs +46 -0
  25. data/sig/domainic/attributer/attribute/signature.rbs +223 -0
  26. data/sig/domainic/attributer/attribute/validator.rbs +83 -0
  27. data/sig/domainic/attributer/attribute.rbs +150 -0
  28. data/sig/domainic/attributer/attribute_set.rbs +134 -0
  29. data/sig/domainic/attributer/class_methods.rbs +151 -0
  30. data/sig/domainic/attributer/dsl/attribute_builder/option_parser.rbs +130 -0
  31. data/sig/domainic/attributer/dsl/attribute_builder.rbs +156 -0
  32. data/sig/domainic/attributer/dsl/initializer.rbs +91 -0
  33. data/sig/domainic/attributer/dsl/method_injector.rbs +66 -0
  34. data/sig/domainic/attributer/dsl.rbs +1 -0
  35. data/sig/domainic/attributer/instance_methods.rbs +53 -0
  36. data/sig/domainic/attributer/undefined.rbs +14 -0
  37. data/sig/domainic/attributer.rbs +69 -0
  38. data/sig/domainic-attributer.rbs +1 -0
  39. data/sig/manifest.yaml +2 -0
  40. metadata +89 -0
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/attributer/attribute'
4
+ require 'domainic/attributer/dsl/attribute_builder/option_parser'
5
+ require 'domainic/attributer/undefined'
6
+
7
+ module Domainic
8
+ module Attributer
9
+ module DSL
10
+ # A class responsible for configuring attributes through a fluent interface.
11
+ #
12
+ # This class provides a rich DSL for configuring attributes with support for
13
+ # default values, coercion, validation, visibility controls, and change tracking.
14
+ # It uses method chaining to allow natural, declarative attribute definitions.
15
+ #
16
+ # @author {https://aaronmallen.me Aaron Allen}
17
+ # @since 0.1.0
18
+ class AttributeBuilder
19
+ # @rbs @base: __todo__
20
+ # @rbs @options: OptionParser::result
21
+
22
+ # Initialize a new AttributeBuilder.
23
+ #
24
+ # @param base [Class, Module] the class or module to build the attribute in
25
+ # @param attribute_name [String, Symbol] the name of the attribute
26
+ # @param attribute_type [String, Symbol] the type of attribute
27
+ # @param type_validator [Proc, Object, nil] optional type validator
28
+ # @param options [Hash] additional options for attribute configuration
29
+ # @yield configuration block for additional attribute settings
30
+ #
31
+ # @return [void]
32
+ # @rbs (
33
+ # __todo__ base,
34
+ # String | Symbol attribute_name,
35
+ # String | Symbol attribute_type,
36
+ # ?Attribute::Validator::handler? type_validator,
37
+ # OptionParser::options options,
38
+ # ) ? { (?) [self: AttributeBuilder] -> void } -> void
39
+ def initialize(base, attribute_name, attribute_type, type_validator = Undefined, **options, &block)
40
+ @base = base
41
+ # @type var options: OptionParser::options
42
+ @options = OptionParser.parse!(attribute_name, attribute_type, options)
43
+ @options[:validators] << type_validator if type_validator != Undefined
44
+ instance_exec(&block) if block
45
+ end
46
+
47
+ # Builds and finalizes the attribute.
48
+ #
49
+ # @return [Attribute] the configured attribute
50
+ # @rbs () -> Attribute
51
+ def build!
52
+ options = @options.compact
53
+ .reject { |_, value| value == Undefined || (value.respond_to?(:empty?) && value.empty?) }
54
+ Attribute.new(@base, **options) # steep:ignore InsufficientKeywordArguments
55
+ end
56
+
57
+ # Configure value coercion.
58
+ #
59
+ # @param proc_symbol [Proc, Symbol, nil] optional coercion handler
60
+ # @yield optional coercion block
61
+ #
62
+ # @return [self] the builder for method chaining
63
+ # @rbs (?(Attribute::Coercer::proc | Object)? proc_symbol) ?{ (untyped value) -> untyped } -> self
64
+ def coerce_with(proc_symbol = Undefined, &block)
65
+ handler = proc_symbol == Undefined ? block : proc_symbol #: Attribute::Coercer::handler
66
+ @options[:coercers] << handler
67
+ self
68
+ end
69
+ alias coerce coerce_with
70
+
71
+ # Configure default value.
72
+ #
73
+ # @param value_or_proc [Object, Proc, nil] optional default value or generator
74
+ # @yield optional default value generator block
75
+ #
76
+ # @return [self] the builder for method chaining
77
+ # @rbs (?untyped? value_or_proc) ?{ (?) -> untyped } -> self
78
+ def default(value_or_proc = Undefined, &block)
79
+ @options[:default] = value_or_proc == Undefined ? block : value_or_proc
80
+ self
81
+ end
82
+ alias default_generator default
83
+ alias default_value default
84
+
85
+ # Set attribute description.
86
+ #
87
+ # @param text [String] the description text
88
+ #
89
+ # @return [self] the builder for method chaining
90
+ # @rbs (String? text) -> self
91
+ def description(text)
92
+ @options[:description] = text
93
+ self
94
+ end
95
+ alias desc description
96
+
97
+ # Mark attribute as non-nilable.
98
+ #
99
+ # @return [self] the builder for method chaining
100
+ # @rbs () -> self
101
+ def non_nilable
102
+ @options[:nilable] = false
103
+ self
104
+ end
105
+ alias non_nil non_nilable
106
+ alias non_null non_nilable
107
+ alias non_nullable non_nilable
108
+ alias not_nil non_nilable
109
+ alias not_nilable non_nilable
110
+ alias not_null non_nilable
111
+ alias not_nullable non_nilable
112
+
113
+ # Configure change callback.
114
+ #
115
+ # @param proc [Proc, nil] optional callback handler
116
+ # @yield optional callback block
117
+ #
118
+ # @return [self] the builder for method chaining
119
+ # @rbs (?Attribute::Callback::handler? proc) ?{ (untyped old_value, untyped new_value) -> void } -> self
120
+ def on_change(proc = Undefined, &block)
121
+ handler = proc == Undefined ? block : proc #: Attribute::Callback::handler
122
+ @options[:callbacks] << handler
123
+ self
124
+ end
125
+
126
+ # Set private visibility for both read and write.
127
+ #
128
+ # @return [self] the builder for method chaining
129
+ # @rbs () -> self
130
+ def private
131
+ private_read
132
+ private_write
133
+ end
134
+
135
+ # Set private visibility for read.
136
+ #
137
+ # @return [self] the builder for method chaining
138
+ # @rbs () -> self
139
+ def private_read
140
+ @options[:read] = :private
141
+ self
142
+ end
143
+
144
+ # Set private visibility for write.
145
+ #
146
+ # @return [self] the builder for method chaining
147
+ # @rbs () -> self
148
+ def private_write
149
+ @options[:write] = :private
150
+ self
151
+ end
152
+
153
+ # Set protected visibility for both read and write.
154
+ #
155
+ # @return [self] the builder for method chaining
156
+ # @rbs () -> self
157
+ def protected
158
+ protected_read
159
+ protected_write
160
+ end
161
+
162
+ # Set protected visibility for read.
163
+ #
164
+ # @return [self] the builder for method chaining
165
+ # @rbs () -> self
166
+ def protected_read
167
+ @options[:read] = :protected
168
+ self
169
+ end
170
+
171
+ # Set protected visibility for write.
172
+ #
173
+ # @return [self] the builder for method chaining
174
+ # @rbs () -> self
175
+ def protected_write
176
+ @options[:write] = :protected
177
+ self
178
+ end
179
+
180
+ # Set public visibility for both read and write.
181
+ #
182
+ # @return [self] the builder for method chaining
183
+ # @rbs () -> self
184
+ def public
185
+ public_read
186
+ public_write
187
+ end
188
+
189
+ # Set public visibility for read.
190
+ #
191
+ # @return [self] the builder for method chaining
192
+ # @rbs () -> self
193
+ def public_read
194
+ @options[:read] = :public
195
+ self
196
+ end
197
+
198
+ # Set public visibility for write.
199
+ #
200
+ # @return [self] the builder for method chaining
201
+ # @rbs () -> self
202
+ def public_write
203
+ @options[:write] = :public
204
+ self
205
+ end
206
+
207
+ # Mark attribute as required.
208
+ #
209
+ # @return [self] the builder for method chaining
210
+ # @rbs () -> self
211
+ def required
212
+ @options[:required] = true
213
+ self
214
+ end
215
+
216
+ # Configure value validation.
217
+ #
218
+ # @param object_or_proc [Object, Proc, nil] optional validation handler
219
+ # @yield optional validation block
220
+ #
221
+ # @return [self] the builder for method chaining
222
+ # @rbs (?Attribute::Validator::handler? object_or_proc) ?{ (untyped value) -> boolish } -> self
223
+ def validate_with(object_or_proc = Undefined, &block)
224
+ handler = object_or_proc == Undefined ? block : object_or_proc #: Attribute::Validator::handler
225
+ @options[:validators] << handler
226
+ self
227
+ end
228
+ alias validate validate_with
229
+ alias validates validate_with
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domainic
4
+ module Attributer
5
+ module DSL
6
+ # A class responsible for handling object initialization with attributes.
7
+ #
8
+ # This class manages the process of setting attribute values during object
9
+ # initialization. It handles both positional arguments and keyword options,
10
+ # applying them to their corresponding attributes while respecting default
11
+ # values and required attributes.
12
+ #
13
+ # @author {https://aaronmallen.me Aaron Allen}
14
+ # @since 0.1.0
15
+ class Initializer
16
+ # @rbs @argument_attributes: Array[Attribute]
17
+ # @rbs @attributes: AttributeSet
18
+ # @rbs @base: Object
19
+ # @rbs @option_attributes: Array[Attribute]
20
+
21
+ # Initialize a new Initializer.
22
+ #
23
+ # @param base [Object] the instance being initialized
24
+ #
25
+ # @return [void]
26
+ # @rbs (Object base) -> void
27
+ def initialize(base)
28
+ @base = base
29
+ @attributes ||= @base.class.send(:__attributes__)
30
+ end
31
+
32
+ # Assign values to attributes.
33
+ #
34
+ # Validates and applies both positional arguments and keyword options to
35
+ # their corresponding attributes. Raises an error if required arguments
36
+ # are missing.
37
+ #
38
+ # @param arguments [Array] positional arguments to assign
39
+ # @param keyword_arguments [Hash] keyword arguments to assign
40
+ #
41
+ # @raise [ArgumentError] if required arguments are missing
42
+ # @return [void]
43
+ # @rbs (*untyped arguments, **untyped keyword_arguments) -> void
44
+ def assign!(*arguments, **keyword_arguments)
45
+ validate_positional_arguments!(arguments)
46
+ apply_arguments(arguments)
47
+ apply_options!(keyword_arguments)
48
+ end
49
+
50
+ private
51
+
52
+ # Access to the current attribute set.
53
+ #
54
+ # @return [AttributeSet] the attribute set for this instance
55
+ attr_reader :attributes #: AttributeSet
56
+
57
+ # Apply positional arguments to their attributes.
58
+ #
59
+ # @param arguments [Array] the positional arguments to apply
60
+ #
61
+ # @return [void]
62
+ # @rbs (Array[untyped]) -> void
63
+ def apply_arguments(arguments)
64
+ argument_attributes.each_with_index do |attribute, index|
65
+ value = arguments.length > index ? arguments[index] : Undefined
66
+ assign_value(attribute.name, value)
67
+ end
68
+ end
69
+
70
+ # Apply keyword arguments to their attributes.
71
+ #
72
+ # @param options [Hash] the keyword options to apply
73
+ #
74
+ # @return [void]
75
+ # @rbs (Hash[String | Symbol, untyped]) -> void
76
+ def apply_options!(options)
77
+ options = options.transform_keys(&:to_sym)
78
+
79
+ option_attributes.each do |attribute|
80
+ if options.key?(attribute.name)
81
+ assign_value(attribute.name, options[attribute.name])
82
+ else
83
+ assign_value(attribute.name, Undefined)
84
+ end
85
+ end
86
+ end
87
+
88
+ # Get all argument attributes.
89
+ #
90
+ # @return [Array<Attribute>] the argument attributes
91
+ # @rbs () -> Array[Attribute]
92
+ def argument_attributes
93
+ @argument_attributes ||= attributes.select { |_, attribute| attribute.signature.argument? }.attributes
94
+ end
95
+
96
+ # Assign a value to an attribute.
97
+ #
98
+ # @param attribute_name [Symbol] the name of the attribute
99
+ # @param value [Object] the value to assign
100
+ #
101
+ # @return [void]
102
+ def assign_value(attribute_name, value)
103
+ @base.send(:"#{attribute_name}=", value)
104
+ end
105
+
106
+ # Get all option attributes.
107
+ #
108
+ # @return [Array<Attribute>] the option attributes
109
+ # @rbs () -> Array[Attribute]
110
+ def option_attributes
111
+ @option_attributes ||= attributes.select { |_, attribute| attribute.signature.option? }.attributes
112
+ end
113
+
114
+ # Validate that all required positional arguments are provided.
115
+ #
116
+ # @param arguments [Array] the arguments to validate
117
+ #
118
+ # @raise [ArgumentError] if required arguments are missing
119
+ # @return [void]
120
+ # @rbs (Array[untyped]) -> void
121
+ def validate_positional_arguments!(arguments)
122
+ required = argument_attributes.reject(&:default?)
123
+ return unless arguments.length < required.length
124
+
125
+ raise ArgumentError, "wrong number of arguments (given #{arguments.length}, expected #{required.length}+)"
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domainic
4
+ module Attributer
5
+ module DSL
6
+ # A class responsible for injecting attribute methods into classes.
7
+ #
8
+ # This class handles the creation of reader and writer methods for attributes,
9
+ # ensuring they are injected safely without overwriting existing methods. It
10
+ # respects visibility settings and properly handles value assignment through
11
+ # the attribute system.
12
+ #
13
+ # @author {https://aaronmallen.me Aaron Allen}
14
+ # @since 0.1.0
15
+ class MethodInjector
16
+ # @rbs @attribute: Attribute
17
+ # @rbs @base: __todo__
18
+
19
+ # Inject methods for an attribute into a class.
20
+ #
21
+ # @param base [Class, Module] the class to inject methods into
22
+ # @param attribute [Attribute] the attribute to create methods for
23
+ #
24
+ # @return [void]
25
+ # @rbs (__todo__ base, Attribute attribute) -> void
26
+ def self.inject!(base, attribute)
27
+ new(base, attribute).inject!
28
+ end
29
+
30
+ # Initialize a new MethodInjector.
31
+ #
32
+ # @param base [Class, Module] the class to inject methods into
33
+ # @param attribute [Attribute] the attribute to create methods for
34
+ #
35
+ # @return [void]
36
+ # @rbs (__todo__ base, Attribute attribute) -> void
37
+ def initialize(base, attribute)
38
+ @attribute = attribute
39
+ @base = base
40
+ end
41
+
42
+ # Inject reader and writer methods.
43
+ #
44
+ # @return [void]
45
+ # @rbs () -> void
46
+ def inject!
47
+ inject_reader!
48
+ inject_writer!
49
+ end
50
+
51
+ private
52
+
53
+ # Define a method if it doesn't already exist.
54
+ #
55
+ # @param method_name [Symbol] the name of the method to define
56
+ # @yield the method body to define
57
+ #
58
+ # @return [void]
59
+ # @rbs (Symbol method_name) { (?) [self: untyped] -> void } -> void
60
+ def define_safe_method(method_name, &)
61
+ return if @base.method_defined?(method_name) || @base.private_method_defined?(method_name)
62
+
63
+ @base.define_method(method_name, &)
64
+ end
65
+
66
+ # Inject the attribute reader method.
67
+ #
68
+ # Creates a reader method with the configured visibility.
69
+ #
70
+ # @return [void]
71
+ # @rbs () -> void
72
+ def inject_reader!
73
+ @base.attr_reader @attribute.name
74
+ @base.send(@attribute.signature.read_visibility, @attribute.name)
75
+ end
76
+
77
+ # Inject the attribute writer method.
78
+ #
79
+ # Creates a writer method that processes values through the attribute
80
+ # system before assignment. Sets the configured visibility.
81
+ #
82
+ # @return [void]
83
+ # @rbs () -> void
84
+ def inject_writer!
85
+ attribute_name = @attribute.name
86
+
87
+ define_safe_method(:"#{attribute_name}=") do |value|
88
+ attribute = self.class.send(:__attributes__)[attribute_name] # steep:ignore NoMethod
89
+ attribute.apply!(self, value)
90
+ end
91
+
92
+ @base.send(@attribute.signature.write_visibility, :"#{attribute_name}=")
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/attributer/dsl/attribute_builder'
4
+ require 'domainic/attributer/dsl/initializer'
5
+ require 'domainic/attributer/dsl/method_injector'
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/attributer/dsl/initializer'
4
+
5
+ module Domainic
6
+ module Attributer
7
+ # A module providing instance-level attribute functionality.
8
+ #
9
+ # This module defines instance methods for objects that include Domainic::Attributer.
10
+ # It provides initialization handling and attribute serialization capabilities, making
11
+ # it easy to work with attribute values in a consistent way.
12
+ #
13
+ # @example Basic usage
14
+ # class Person
15
+ # include Domainic::Attributer
16
+ #
17
+ # argument :name
18
+ # option :age, default: nil
19
+ # option :role, default: 'user', private_read: true
20
+ # end
21
+ #
22
+ # person = Person.new('Alice', age: 30)
23
+ # person.to_h # => { name: 'Alice', age: 30 } # role is private, not included
24
+ #
25
+ # @author {https://aaronmallen.me Aaron Allen}
26
+ # @since 0.1.0
27
+ module InstanceMethods
28
+ # Initialize a new instance with attribute values.
29
+ #
30
+ # Handles both positional arguments and keyword options, applying them to their
31
+ # corresponding attributes. This process includes:
32
+ # 1. Validating required arguments
33
+ # 2. Applying default values
34
+ # 3. Type validation and coercion
35
+ # 4. Change notifications
36
+ #
37
+ # @raise [ArgumentError] if required arguments are missing
38
+ # @return [void]
39
+ # @rbs (*untyped arguments, **untyped keyword_arguments) -> void
40
+ def initialize(...)
41
+ DSL::Initializer.new(self).assign!(...)
42
+ end
43
+
44
+ # Convert public attribute values to a hash.
45
+ #
46
+ # Creates a hash containing all public readable attributes and their current values.
47
+ # Any attributes marked as private or protected for reading are excluded from
48
+ # the result.
49
+ #
50
+ # @example Basic conversion
51
+ # person = Person.new('Alice', age: 30)
52
+ # person.to_h # => { name: 'Alice', age: 30 }
53
+ #
54
+ # @return [Hash{Symbol => Object}] hash of attribute names to values
55
+ # @rbs () -> Hash[Symbol, untyped]
56
+ def to_hash
57
+ public = self.class.send(:__attributes__).select { |_, attribute| attribute.signature.public_read? }
58
+ public.attributes.each_with_object({}) do |attribute, result|
59
+ result[attribute.name] = public_send(attribute.name)
60
+ end
61
+ end
62
+ alias to_h to_hash
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domainic
4
+ module Attributer
5
+ # A singleton object representing an undefined value.
6
+ #
7
+ # This object is used throughout Domainic::Attributer to represent values that
8
+ # are explicitly undefined, as opposed to nil which represents the absence of
9
+ # a value. It is immutable and implements custom string representations for
10
+ # debugging purposes.
11
+ #
12
+ # @author {https://aaronmallen.me Aaron Allen}
13
+ # @since 0.1.0
14
+ Undefined = Object.new.tap do |undefined|
15
+ # Returns self to prevent cloning.
16
+ #
17
+ # @return [Undefined] self
18
+ def undefined.clone(...)
19
+ self
20
+ end
21
+
22
+ # Returns self to prevent duplication.
23
+ #
24
+ # @return [Undefined] self
25
+ def undefined.dup
26
+ self
27
+ end
28
+
29
+ # Returns a string representation of the object.
30
+ #
31
+ # @return [String] the string 'Undefined'
32
+ def undefined.inspect
33
+ to_s
34
+ end
35
+
36
+ # Converts the object to a string.
37
+ #
38
+ # @return [String] the string 'Undefined'
39
+ def undefined.to_s
40
+ 'Undefined'
41
+ end
42
+ end.freeze #: Object
43
+ end
44
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/attributer/attribute'
4
+ require 'domainic/attributer/attribute_set'
5
+ require 'domainic/attributer/class_methods'
6
+ require 'domainic/attributer/dsl'
7
+ require 'domainic/attributer/instance_methods'
8
+ require 'domainic/attributer/undefined'
9
+
10
+ module Domainic
11
+ # Core functionality for defining and managing Ruby class attributes.
12
+ #
13
+ # This module provides a flexible attribute system for Ruby classes that supports
14
+ # positional arguments and keyword options with features like type validation,
15
+ # coercion, and visibility control.
16
+ #
17
+ # Can be included directly with default method names or customized via {Domainic.Attributer}.
18
+ #
19
+ # @example Basic usage with default method names
20
+ # class Person
21
+ # include Domainic::Attributer
22
+ #
23
+ # argument :name
24
+ # option :age
25
+ # end
26
+ #
27
+ # @example Custom method names
28
+ # class Person
29
+ # include Domainic.Attributer(argument: :param, option: :opt)
30
+ #
31
+ # param :name
32
+ # opt :age
33
+ # end
34
+ #
35
+ # @author {https://aaronmallen.me Aaron Allen}
36
+ # @since 0.1.0
37
+ module Attributer
38
+ class << self
39
+ # Create a customized Attributer module.
40
+ #
41
+ # @param argument [Symbol, String] custom name for the argument method
42
+ # @param option [Symbol, String] custom name for the option method
43
+ # @return [Module] configured Attributer module
44
+ # @rbs (?argument: (String | Symbol)?, ?option: (String | Symbol)?) -> Module
45
+ def call(argument: :argument, option: :option)
46
+ Module.new do
47
+ @argument = argument
48
+ @option = option
49
+
50
+ # @rbs (untyped base) -> void
51
+ def self.included(base)
52
+ super
53
+ Domainic::Attributer.send(:include_attributer, base, argument: @argument, option: @option)
54
+ end
55
+ end
56
+ end
57
+
58
+ # Handle direct module inclusion.
59
+ #
60
+ # @param base [Class, Module] the including class/module
61
+ # @return [void]
62
+ # @rbs (untyped base) -> void
63
+ def included(base)
64
+ super
65
+ base.include(call)
66
+ end
67
+
68
+ private
69
+
70
+ # Configure base class with Attributer functionality.
71
+ #
72
+ # @param base [Class, Module] the target class/module
73
+ # @param options [Hash] method name customization options
74
+ # @return [void]
75
+ # @rbs (untyped base, ?argument: (String | Symbol)?, ?option: (String | Symbol)?) -> void
76
+ def include_attributer(base, **options)
77
+ base.extend(ClassMethods)
78
+ base.include(InstanceMethods)
79
+ inject_custom_methods!(base, **options)
80
+ end
81
+
82
+ # Set up custom method names.
83
+ #
84
+ # @param base [Class, Module] the target class/module
85
+ # @param options [Hash] method name customization options
86
+ # @return [void]
87
+ # @rbs (untyped base, ?argument: (String | Symbol)?, ?option: (String | Symbol)?) -> void
88
+ def inject_custom_methods!(base, **options)
89
+ options.each do |original, custom|
90
+ base.singleton_class.alias_method(custom, original) unless custom.nil?
91
+ if (custom.nil? || custom != original) && base.respond_to?(original)
92
+ base.singleton_class.undef_method(original)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ # Create a customized Attributer module.
100
+ #
101
+ # Provides a convenient way to include Attributer with customized method names.
102
+ #
103
+ # @example
104
+ # class Person
105
+ # include Domainic.Attributer(argument: :param, option: :opt)
106
+ # end
107
+ #
108
+ # @param options [Hash] method name customization options
109
+ # @return [Module] configured Attributer module
110
+ # @rbs (?argument: (String | Symbol)?, ?option: (String | Symbol)?) -> Module
111
+ def self.Attributer(**options) # rubocop:disable Naming/MethodName
112
+ Domainic::Attributer.call(**options)
113
+ end
114
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/attributer'