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.
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,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