domainic-attributer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE +21 -0
  4. data/README.md +396 -0
  5. data/lib/domainic/attributer/attribute/callback.rb +68 -0
  6. data/lib/domainic/attributer/attribute/coercer.rb +93 -0
  7. data/lib/domainic/attributer/attribute/mixin/belongs_to_attribute.rb +68 -0
  8. data/lib/domainic/attributer/attribute/signature.rb +338 -0
  9. data/lib/domainic/attributer/attribute/validator.rb +128 -0
  10. data/lib/domainic/attributer/attribute.rb +256 -0
  11. data/lib/domainic/attributer/attribute_set.rb +208 -0
  12. data/lib/domainic/attributer/class_methods.rb +247 -0
  13. data/lib/domainic/attributer/dsl/attribute_builder/option_parser.rb +247 -0
  14. data/lib/domainic/attributer/dsl/attribute_builder.rb +233 -0
  15. data/lib/domainic/attributer/dsl/initializer.rb +130 -0
  16. data/lib/domainic/attributer/dsl/method_injector.rb +97 -0
  17. data/lib/domainic/attributer/dsl.rb +5 -0
  18. data/lib/domainic/attributer/instance_methods.rb +65 -0
  19. data/lib/domainic/attributer/undefined.rb +44 -0
  20. data/lib/domainic/attributer.rb +114 -0
  21. data/lib/domainic-attributer.rb +3 -0
  22. data/sig/domainic/attributer/attribute/callback.rbs +48 -0
  23. data/sig/domainic/attributer/attribute/coercer.rbs +59 -0
  24. data/sig/domainic/attributer/attribute/mixin/belongs_to_attribute.rbs +46 -0
  25. data/sig/domainic/attributer/attribute/signature.rbs +223 -0
  26. data/sig/domainic/attributer/attribute/validator.rbs +83 -0
  27. data/sig/domainic/attributer/attribute.rbs +150 -0
  28. data/sig/domainic/attributer/attribute_set.rbs +134 -0
  29. data/sig/domainic/attributer/class_methods.rbs +151 -0
  30. data/sig/domainic/attributer/dsl/attribute_builder/option_parser.rbs +130 -0
  31. data/sig/domainic/attributer/dsl/attribute_builder.rbs +156 -0
  32. data/sig/domainic/attributer/dsl/initializer.rbs +91 -0
  33. data/sig/domainic/attributer/dsl/method_injector.rbs +66 -0
  34. data/sig/domainic/attributer/dsl.rbs +1 -0
  35. data/sig/domainic/attributer/instance_methods.rbs +53 -0
  36. data/sig/domainic/attributer/undefined.rbs +14 -0
  37. data/sig/domainic/attributer.rbs +69 -0
  38. data/sig/domainic-attributer.rbs +1 -0
  39. data/sig/manifest.yaml +2 -0
  40. metadata +89 -0
@@ -0,0 +1,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