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,338 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'domainic/attributer/attribute/mixin/belongs_to_attribute'
|
4
|
+
|
5
|
+
module Domainic
|
6
|
+
module Attributer
|
7
|
+
class Attribute
|
8
|
+
# A class responsible for managing attribute signature information.
|
9
|
+
#
|
10
|
+
# This class encapsulates the type and visibility configuration for an attribute.
|
11
|
+
# It validates and manages whether an attribute is an argument or option, as well
|
12
|
+
# as controlling read and write visibility (public, protected, or private).
|
13
|
+
#
|
14
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
15
|
+
# @since 0.1.0
|
16
|
+
class Signature
|
17
|
+
# @rbs!
|
18
|
+
# type default_options = {
|
19
|
+
# nilable: bool,
|
20
|
+
# read: visibility_symbol,
|
21
|
+
# required: bool,
|
22
|
+
# write: visibility_symbol
|
23
|
+
# }
|
24
|
+
#
|
25
|
+
# type initialize_options = {
|
26
|
+
# ?nilable: bool,
|
27
|
+
# ?position: Integer?,
|
28
|
+
# ?read: visibility_symbol,
|
29
|
+
# ?required: bool,
|
30
|
+
# type: type_symbol,
|
31
|
+
# ?write: visibility_symbol
|
32
|
+
# }
|
33
|
+
#
|
34
|
+
# type type_symbol = :argument | :option
|
35
|
+
#
|
36
|
+
# type visibility_symbol = :private | :protected | :public
|
37
|
+
|
38
|
+
include BelongsToAttribute
|
39
|
+
|
40
|
+
# @return [Hash{Symbol => Object}] Default options for a new Signature instance.
|
41
|
+
DEFAULT_OPTIONS = { nilable: true, read: :public, required: false, write: :public }.freeze #: default_options
|
42
|
+
|
43
|
+
# Constants defining valid attribute types.
|
44
|
+
#
|
45
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
46
|
+
# @since 0.1.0
|
47
|
+
module TYPE
|
48
|
+
# @return [Symbol] argument type designation
|
49
|
+
ARGUMENT = :argument #: type_symbol
|
50
|
+
|
51
|
+
# @return [Symbol] option type designation
|
52
|
+
OPTION = :option #: type_symbol
|
53
|
+
|
54
|
+
# @return [Array<Symbol>] all valid type values
|
55
|
+
ALL = [ARGUMENT, OPTION].freeze #: Array[type_symbol]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Constants defining valid visibility levels.
|
59
|
+
#
|
60
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
61
|
+
# @since 0.1.0
|
62
|
+
module VISIBILITY
|
63
|
+
# @return [Symbol] private visibility level
|
64
|
+
PRIVATE = :private #: visibility_symbol
|
65
|
+
|
66
|
+
# @return [Symbol] protected visibility level
|
67
|
+
PROTECTED = :protected #: visibility_symbol
|
68
|
+
|
69
|
+
# @return [Symbol] public visibility level
|
70
|
+
PUBLIC = :public #: visibility_symbol
|
71
|
+
|
72
|
+
# @return [Array<Symbol>] all valid visibility levels
|
73
|
+
ALL = [PRIVATE, PROTECTED, PUBLIC].freeze #: Array[visibility_symbol]
|
74
|
+
end
|
75
|
+
|
76
|
+
# @rbs @nilable: bool
|
77
|
+
# @rbs @position: Integer?
|
78
|
+
# @rbs @read_visibility: visibility_symbol
|
79
|
+
# @rbs @required: bool
|
80
|
+
# @rbs @type: type_symbol
|
81
|
+
# @rbs @write_visibility: visibility_symbol
|
82
|
+
|
83
|
+
# @return [Integer, nil] the position of the attribute
|
84
|
+
attr_reader :position #: Integer?
|
85
|
+
|
86
|
+
# @return [Symbol] the visibility level for reading the attribute
|
87
|
+
attr_reader :read_visibility #: visibility_symbol
|
88
|
+
|
89
|
+
# @return [Symbol] the type of the attribute
|
90
|
+
attr_reader :type #: type_symbol
|
91
|
+
|
92
|
+
# @return [Symbol] the visibility level for writing the attribute
|
93
|
+
attr_reader :write_visibility #: visibility_symbol
|
94
|
+
|
95
|
+
# Initialize a new Signature instance.
|
96
|
+
#
|
97
|
+
# @param attribute [Attribute] the attribute this signature belongs to
|
98
|
+
# @param options [Hash{Symbol => Object}] the signature options
|
99
|
+
# @option options [Boolean] nilable (true) whether the attribute is allowed to be nil.
|
100
|
+
# @option options [Integer, nil] position (nil) optional position for ordered attributes
|
101
|
+
# @option options [Symbol] read (:public) the read visibility
|
102
|
+
# @option options [Boolean] required (false) whether the attribute is required
|
103
|
+
# @option options [Symbol] type the type of attribute
|
104
|
+
# @option options [Symbol] write (:public) the write visibility
|
105
|
+
#
|
106
|
+
# @return [void]
|
107
|
+
# @rbs (
|
108
|
+
# Attribute attribute,
|
109
|
+
# ?nilable: bool,
|
110
|
+
# ?position: Integer?,
|
111
|
+
# ?read: visibility_symbol,
|
112
|
+
# ?required: bool,
|
113
|
+
# type: type_symbol,
|
114
|
+
# ?write: visibility_symbol
|
115
|
+
# ) -> void
|
116
|
+
def initialize(attribute, **options)
|
117
|
+
super
|
118
|
+
options = DEFAULT_OPTIONS.merge(options.transform_keys(&:to_sym))
|
119
|
+
validate_initialize_options!(options)
|
120
|
+
|
121
|
+
# @type var options: initialize_options
|
122
|
+
@nilable = options.fetch(:nilable)
|
123
|
+
@position = options[:position]
|
124
|
+
@read_visibility = options.fetch(:read).to_sym
|
125
|
+
@required = options.fetch(:required)
|
126
|
+
@type = options.fetch(:type).to_sym
|
127
|
+
@write_visibility = options.fetch(:write).to_sym
|
128
|
+
end
|
129
|
+
|
130
|
+
# Check if this signature is for an argument attribute.
|
131
|
+
#
|
132
|
+
# @return [Boolean] true if this is an argument attribute
|
133
|
+
# @rbs () -> bool
|
134
|
+
def argument?
|
135
|
+
@type == TYPE::ARGUMENT
|
136
|
+
end
|
137
|
+
|
138
|
+
# Check if the attribute is allowed to be nil.
|
139
|
+
#
|
140
|
+
# @return [Boolean] true if the attribute is allowed to be nil
|
141
|
+
# @rbs () -> bool
|
142
|
+
def nilable?
|
143
|
+
@nilable
|
144
|
+
end
|
145
|
+
|
146
|
+
# Check if this signature is for an option attribute.
|
147
|
+
#
|
148
|
+
# @return [Boolean] true if this is an option attribute
|
149
|
+
# @rbs () -> bool
|
150
|
+
def option?
|
151
|
+
@type == TYPE::OPTION
|
152
|
+
end
|
153
|
+
|
154
|
+
# Check if this signature is for an optional attribute.
|
155
|
+
#
|
156
|
+
# @return [Boolean] true if this is an optional attribute
|
157
|
+
# @rbs () -> bool
|
158
|
+
def optional?
|
159
|
+
!required?
|
160
|
+
end
|
161
|
+
|
162
|
+
# Check if both read and write operations are private.
|
163
|
+
#
|
164
|
+
# @return [Boolean] true if both read and write are private
|
165
|
+
# @rbs () -> bool
|
166
|
+
def private?
|
167
|
+
private_read? && private_write?
|
168
|
+
end
|
169
|
+
|
170
|
+
# Check if read operations are private.
|
171
|
+
#
|
172
|
+
# @return [Boolean] true if read operations are private
|
173
|
+
# @rbs () -> bool
|
174
|
+
def private_read?
|
175
|
+
[VISIBILITY::PRIVATE, VISIBILITY::PROTECTED].include?(@read_visibility)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Check if write operations are private.
|
179
|
+
#
|
180
|
+
# @return [Boolean] true if write operations are private
|
181
|
+
# @rbs () -> bool
|
182
|
+
def private_write?
|
183
|
+
[VISIBILITY::PRIVATE, VISIBILITY::PROTECTED].include?(@write_visibility)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Check if both read and write operations are protected.
|
187
|
+
#
|
188
|
+
# @return [Boolean] true if both read and write are protected
|
189
|
+
# @rbs () -> bool
|
190
|
+
def protected?
|
191
|
+
protected_read? && protected_write?
|
192
|
+
end
|
193
|
+
|
194
|
+
# Check if read operations are protected.
|
195
|
+
#
|
196
|
+
# @return [Boolean] true if read operations are protected
|
197
|
+
# @rbs () -> bool
|
198
|
+
def protected_read?
|
199
|
+
@read_visibility == VISIBILITY::PROTECTED
|
200
|
+
end
|
201
|
+
|
202
|
+
# Check if write operations are protected.
|
203
|
+
#
|
204
|
+
# @return [Boolean] true if write operations are protected
|
205
|
+
# @rbs () -> bool
|
206
|
+
def protected_write?
|
207
|
+
@write_visibility == VISIBILITY::PROTECTED
|
208
|
+
end
|
209
|
+
|
210
|
+
# Check if both read and write operations are public.
|
211
|
+
#
|
212
|
+
# @return [Boolean] true if both read and write are public
|
213
|
+
# @rbs () -> bool
|
214
|
+
def public?
|
215
|
+
public_read? && public_write?
|
216
|
+
end
|
217
|
+
|
218
|
+
# Check if read operations are public.
|
219
|
+
#
|
220
|
+
# @return [Boolean] true if read operations are public
|
221
|
+
# @rbs () -> bool
|
222
|
+
def public_read?
|
223
|
+
@read_visibility == VISIBILITY::PUBLIC
|
224
|
+
end
|
225
|
+
|
226
|
+
# Check if write operations are public.
|
227
|
+
#
|
228
|
+
# @return [Boolean] true if write operations are public
|
229
|
+
# @rbs () -> bool
|
230
|
+
def public_write?
|
231
|
+
@write_visibility == VISIBILITY::PUBLIC
|
232
|
+
end
|
233
|
+
|
234
|
+
# Check if the attribute is required.
|
235
|
+
#
|
236
|
+
# @return [Boolean] true if the attribute is required
|
237
|
+
# @rbs () -> bool
|
238
|
+
def required?
|
239
|
+
@required
|
240
|
+
end
|
241
|
+
|
242
|
+
private
|
243
|
+
|
244
|
+
# Get signature options as a hash.
|
245
|
+
#
|
246
|
+
# @return [Hash] the signature options
|
247
|
+
# @rbs () -> initialize_options
|
248
|
+
def to_options
|
249
|
+
{
|
250
|
+
nilable: @nilable,
|
251
|
+
position: @position,
|
252
|
+
read: @read_visibility,
|
253
|
+
required: @required,
|
254
|
+
type: @type,
|
255
|
+
write: @write_visibility
|
256
|
+
}
|
257
|
+
end
|
258
|
+
|
259
|
+
# Validate that a value is a Boolean.
|
260
|
+
#
|
261
|
+
# @param name [String, Symbol] the name of the attribute being validated
|
262
|
+
# @param value [Boolean] the value to validate
|
263
|
+
#
|
264
|
+
# @raise [ArgumentError] if the value is invalid
|
265
|
+
# @return [void]
|
266
|
+
# @rbs (String | Symbol name, bool value) -> void
|
267
|
+
def validate_boolean!(name, value)
|
268
|
+
return if [true, false].include?(value)
|
269
|
+
|
270
|
+
raise ArgumentError, "`#{attribute_method_name}`: invalid #{name}: #{value}. Must be `true` or `false`."
|
271
|
+
end
|
272
|
+
|
273
|
+
# Validate all initialization options.
|
274
|
+
#
|
275
|
+
# @param options [Hash{Symbol => Object}] the options to validate
|
276
|
+
# @option options [Boolean] nilable the nilable flag to validate
|
277
|
+
# @option options [Integer, nil] position the position value to validate
|
278
|
+
# @option options [Symbol] read the read visibility to validate
|
279
|
+
# @option options [Boolean] required the required flag to validate
|
280
|
+
# @option options [Symbol] type the type to validate
|
281
|
+
# @option options [Symbol] write the write visibility to validate
|
282
|
+
#
|
283
|
+
# @return [void]
|
284
|
+
# @rbs (Hash[Symbol, untyped] options) -> void
|
285
|
+
def validate_initialize_options!(options)
|
286
|
+
validate_position!(options[:position])
|
287
|
+
validate_visibility!(:read, options[:read])
|
288
|
+
validate_visibility!(:write, options[:write])
|
289
|
+
validate_boolean!(:nilable, options[:nilable])
|
290
|
+
validate_boolean!(:required, options[:required])
|
291
|
+
validate_type!(options[:type])
|
292
|
+
end
|
293
|
+
|
294
|
+
# Validate that a position value is valid.
|
295
|
+
#
|
296
|
+
# @param position [Integer, nil] the position to validate
|
297
|
+
#
|
298
|
+
# @raise [ArgumentError] if the position is invalid
|
299
|
+
# @return [void]
|
300
|
+
# @rbs (Integer? position) -> void
|
301
|
+
def validate_position!(position)
|
302
|
+
return if position.nil? || position.is_a?(Integer)
|
303
|
+
|
304
|
+
raise ArgumentError, "`#{attribute_method_name}`: invalid position: #{position}. Must be Integer or nil."
|
305
|
+
end
|
306
|
+
|
307
|
+
# Validate that a type value is valid.
|
308
|
+
#
|
309
|
+
# @param type [Symbol] the type to validate
|
310
|
+
#
|
311
|
+
# @raise [ArgumentError] if the type is invalid
|
312
|
+
# @return [void]
|
313
|
+
# @rbs (type_symbol type) -> void
|
314
|
+
def validate_type!(type)
|
315
|
+
return if TYPE::ALL.include?(type.to_sym)
|
316
|
+
|
317
|
+
raise ArgumentError,
|
318
|
+
"`#{attribute_method_name}`: invalid type: #{type}. Must be one of #{TYPE::ALL.join(', ')}"
|
319
|
+
end
|
320
|
+
|
321
|
+
# Validate that visibility values are valid.
|
322
|
+
#
|
323
|
+
# @param type [Symbol] which visibility setting to validate
|
324
|
+
# @param value [Symbol] the visibility value to validate
|
325
|
+
#
|
326
|
+
# @raise [ArgumentError] if the visibility is invalid
|
327
|
+
# @return [void]
|
328
|
+
# @rbs (Symbol type, visibility_symbol value) -> void
|
329
|
+
def validate_visibility!(type, value)
|
330
|
+
return if VISIBILITY::ALL.include?(value.to_sym)
|
331
|
+
|
332
|
+
raise ArgumentError, "`#{attribute_method_name}`: invalid #{type} visibility: #{value}. " \
|
333
|
+
"Must be one of #{VISIBILITY::ALL.join(', ')}"
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'domainic/attributer/attribute/mixin/belongs_to_attribute'
|
4
|
+
|
5
|
+
module Domainic
|
6
|
+
module Attributer
|
7
|
+
class Attribute
|
8
|
+
# A class responsible for validating attribute values.
|
9
|
+
#
|
10
|
+
# This class manages the validation of values assigned to an attribute. Validation
|
11
|
+
# can be performed either by a Proc that accepts a single value argument and returns
|
12
|
+
# a boolean, or by any object that responds to the `===` operator.
|
13
|
+
#
|
14
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
15
|
+
# @since 0.1.0
|
16
|
+
class Validator
|
17
|
+
# @rbs!
|
18
|
+
# type handler = proc | Proc | _ValidHandler
|
19
|
+
#
|
20
|
+
# type proc = ^(untyped value) -> bool
|
21
|
+
#
|
22
|
+
# interface _ValidHandler
|
23
|
+
# def !=: (untyped value) -> bool
|
24
|
+
#
|
25
|
+
# def ==: (untyped value) -> bool
|
26
|
+
#
|
27
|
+
# def ===: (untyped value) -> bool
|
28
|
+
#
|
29
|
+
# def inspect: () -> untyped
|
30
|
+
#
|
31
|
+
# def is_a?: (Class | Module) -> bool
|
32
|
+
#
|
33
|
+
# def respond_to?: (Symbol) -> bool
|
34
|
+
# end
|
35
|
+
|
36
|
+
include BelongsToAttribute
|
37
|
+
|
38
|
+
# @rbs @handlers: Array[handler]
|
39
|
+
|
40
|
+
# Initialize a new Validator instance.
|
41
|
+
#
|
42
|
+
# @param attribute [Attribute] the attribute this Validator belongs to
|
43
|
+
# @param handlers [Array<Class, Module, Object, Proc>] the handlers to use for processing
|
44
|
+
#
|
45
|
+
# @return [Validator] the new instance of Validator
|
46
|
+
# @rbs (Attribute attribute, Array[handler] | handler handlers) -> void
|
47
|
+
def initialize(attribute, handlers = [])
|
48
|
+
super
|
49
|
+
@handlers = [*handlers].map do |handler|
|
50
|
+
validate_handler!(handler)
|
51
|
+
handler
|
52
|
+
end.uniq
|
53
|
+
end
|
54
|
+
|
55
|
+
# Validate a value using all configured validators.
|
56
|
+
#
|
57
|
+
# @param instance [Object] the instance on which to perform validation
|
58
|
+
# @param value [Object] the value to validate
|
59
|
+
#
|
60
|
+
# @raise [ArgumentError] if the value fails validation
|
61
|
+
# @return [void]
|
62
|
+
# @rbs (untyped instance, untyped value) -> void
|
63
|
+
def call(instance, value)
|
64
|
+
return if value == Undefined && handle_undefined!
|
65
|
+
return if value.nil? && handle_nil!
|
66
|
+
return if @handlers.all? { |handler| validate_value!(handler, instance, value) }
|
67
|
+
|
68
|
+
raise ArgumentError, "`#{attribute_method_name}`: has invalid value: #{value.inspect}"
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Handle a `nil` value.
|
74
|
+
#
|
75
|
+
# @raise [ArgumentError] if the attribute is not nilable
|
76
|
+
# @return [true] if the attribute is nilable
|
77
|
+
# @rbs () -> bool
|
78
|
+
def handle_nil!
|
79
|
+
return true if @attribute.signature.nilable?
|
80
|
+
|
81
|
+
raise ArgumentError, "`#{attribute_method_name}`: cannot be nil"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Handle an {Undefined} value.
|
85
|
+
#
|
86
|
+
# @raise [ArgumentError] if the attribute is required
|
87
|
+
# @return [true] if the attribute is optional
|
88
|
+
# @rbs () -> bool
|
89
|
+
def handle_undefined!
|
90
|
+
return true if @attribute.signature.optional?
|
91
|
+
|
92
|
+
raise ArgumentError, "`#{attribute_method_name}`: is required"
|
93
|
+
end
|
94
|
+
|
95
|
+
# Validate that a validation handler is valid.
|
96
|
+
#
|
97
|
+
# @param handler [Object] the handler to validate
|
98
|
+
#
|
99
|
+
# @raise [TypeError] if the handler is not valid
|
100
|
+
# @return [void]
|
101
|
+
# @rbs (handler handler) -> void
|
102
|
+
def validate_handler!(handler)
|
103
|
+
return if handler.is_a?(Proc) || (!handler.is_a?(Proc) && handler.respond_to?(:===))
|
104
|
+
|
105
|
+
raise TypeError, "`#{attribute_method_name}`: invalid validator: #{handler.inspect}. Must be a Proc " \
|
106
|
+
'or an object responding to `#===`.'
|
107
|
+
end
|
108
|
+
|
109
|
+
# Validate a value using a single handler.
|
110
|
+
#
|
111
|
+
# @param handler [Object] the handler to use for validation
|
112
|
+
# @param instance [Object] the instance on which to perform validation
|
113
|
+
# @param value [Object] the value to validate
|
114
|
+
# @rbs (handler handler, untyped instance, untyped value) -> bool
|
115
|
+
def validate_value!(handler, instance, value)
|
116
|
+
if handler.is_a?(Proc)
|
117
|
+
instance.instance_exec(value, &handler)
|
118
|
+
elsif handler.respond_to?(:===)
|
119
|
+
handler === value # rubocop:disable Style/CaseEquality
|
120
|
+
else
|
121
|
+
# We should never get here because we validate the handlers in the initializer.
|
122
|
+
raise TypeError, "`#{attribute_method_name}`: invalid validator: #{handler.inspect}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|