domainic-attributer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|