domainic-attributer 0.1.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 +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE +21 -0
- data/README.md +396 -0
- data/lib/domainic/attributer/attribute/callback.rb +68 -0
- data/lib/domainic/attributer/attribute/coercer.rb +93 -0
- data/lib/domainic/attributer/attribute/mixin/belongs_to_attribute.rb +68 -0
- data/lib/domainic/attributer/attribute/signature.rb +338 -0
- data/lib/domainic/attributer/attribute/validator.rb +128 -0
- data/lib/domainic/attributer/attribute.rb +256 -0
- data/lib/domainic/attributer/attribute_set.rb +208 -0
- data/lib/domainic/attributer/class_methods.rb +247 -0
- data/lib/domainic/attributer/dsl/attribute_builder/option_parser.rb +247 -0
- data/lib/domainic/attributer/dsl/attribute_builder.rb +233 -0
- data/lib/domainic/attributer/dsl/initializer.rb +130 -0
- data/lib/domainic/attributer/dsl/method_injector.rb +97 -0
- data/lib/domainic/attributer/dsl.rb +5 -0
- data/lib/domainic/attributer/instance_methods.rb +65 -0
- data/lib/domainic/attributer/undefined.rb +44 -0
- data/lib/domainic/attributer.rb +114 -0
- data/lib/domainic-attributer.rb +3 -0
- data/sig/domainic/attributer/attribute/callback.rbs +48 -0
- data/sig/domainic/attributer/attribute/coercer.rbs +59 -0
- data/sig/domainic/attributer/attribute/mixin/belongs_to_attribute.rbs +46 -0
- data/sig/domainic/attributer/attribute/signature.rbs +223 -0
- data/sig/domainic/attributer/attribute/validator.rbs +83 -0
- data/sig/domainic/attributer/attribute.rbs +150 -0
- data/sig/domainic/attributer/attribute_set.rbs +134 -0
- data/sig/domainic/attributer/class_methods.rbs +151 -0
- data/sig/domainic/attributer/dsl/attribute_builder/option_parser.rbs +130 -0
- data/sig/domainic/attributer/dsl/attribute_builder.rbs +156 -0
- data/sig/domainic/attributer/dsl/initializer.rbs +91 -0
- data/sig/domainic/attributer/dsl/method_injector.rbs +66 -0
- data/sig/domainic/attributer/dsl.rbs +1 -0
- data/sig/domainic/attributer/instance_methods.rbs +53 -0
- data/sig/domainic/attributer/undefined.rbs +14 -0
- data/sig/domainic/attributer.rbs +69 -0
- data/sig/domainic-attributer.rbs +1 -0
- data/sig/manifest.yaml +2 -0
- 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,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
|