domainic-type 0.1.0.alpha.3.0.2 → 0.1.0.alpha.3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/lib/domainic/type/behavior/string_behavior/matching_behavior.rb +115 -0
  3. data/lib/domainic/type/behavior/string_behavior.rb +2 -111
  4. data/lib/domainic/type/behavior/uri_behavior.rb +97 -0
  5. data/lib/domainic/type/behavior.rb +21 -1
  6. data/lib/domainic/type/config/registry.yml +15 -0
  7. data/lib/domainic/type/definitions.rb +182 -1
  8. data/lib/domainic/type/types/core/array_type.rb +1 -1
  9. data/lib/domainic/type/types/core/float_type.rb +1 -1
  10. data/lib/domainic/type/types/core/hash_type.rb +1 -1
  11. data/lib/domainic/type/types/core/integer_type.rb +1 -1
  12. data/lib/domainic/type/types/core/string_type.rb +1 -1
  13. data/lib/domainic/type/types/core/symbol_type.rb +1 -1
  14. data/lib/domainic/type/types/identifier/cuid_type.rb +140 -0
  15. data/lib/domainic/type/types/identifier/uuid_type.rb +513 -0
  16. data/lib/domainic/type/types/network/email_address_type.rb +149 -0
  17. data/lib/domainic/type/types/network/hostname_type.rb +107 -0
  18. data/lib/domainic/type/types/network/uri_type.rb +224 -0
  19. data/sig/domainic/type/behavior/string_behavior/matching_behavior.rbs +84 -0
  20. data/sig/domainic/type/behavior/string_behavior.rbs +2 -82
  21. data/sig/domainic/type/behavior/uri_behavior.rbs +95 -0
  22. data/sig/domainic/type/behavior.rbs +9 -0
  23. data/sig/domainic/type/definitions.rbs +149 -1
  24. data/sig/domainic/type/types/identifier/cuid_type.rbs +110 -0
  25. data/sig/domainic/type/types/identifier/uuid_type.rbs +469 -0
  26. data/sig/domainic/type/types/network/email_address_type.rbs +127 -0
  27. data/sig/domainic/type/types/network/hostname_type.rbs +76 -0
  28. data/sig/domainic/type/types/network/uri_type.rbs +159 -0
  29. metadata +20 -6
@@ -0,0 +1,513 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/type/behavior'
4
+
5
+ module Domainic
6
+ # NOTE: This file is located at lib/domainic/type/types/identifier/uuid_type.rb
7
+ module Type
8
+ # A type for validating UUIDs according to RFC 4122 standards
9
+ #
10
+ # This type provides comprehensive UUID validation following RFC 4122 standards.
11
+ # It supports both standard (hyphenated) and compact formats for all UUID versions
12
+ # (1 through 7), while enforcing proper formatting and version-specific constraints.
13
+ #
14
+ # Key features:
15
+ # - RFC 4122 compliant UUID format validation
16
+ # - Support for versions 1-7
17
+ # - Standard (hyphenated) and compact format support
18
+ # - Length validation (36 chars for standard, 32 for compact)
19
+ # - Version-specific validation
20
+ #
21
+ # @example Basic usage
22
+ # type = UUIDType.new
23
+ # type.validate("123e4567-e89b-12d3-a456-426614174000") # => true
24
+ # type.validate("invalid") # => false
25
+ #
26
+ # @example With version constraints
27
+ # type = UUIDType.new.being_version(4)
28
+ # type.validate("123e4567-e89b-42d3-a456-426614174000") # => true
29
+ #
30
+ # @example With format constraints
31
+ # type = UUIDType.new.being_compact
32
+ # type.validate("123e4567e89b12d3a456426614174000") # => true
33
+ #
34
+ # @author {https://aaronmallen.me Aaron Allen}
35
+ # @since 0.1.0
36
+ class UUIDType
37
+ # @rbs! extend Behavior::ClassMethods
38
+
39
+ include Behavior
40
+
41
+ # Contains UUID validation regular expressions and version-specific modules
42
+ #
43
+ # This module provides both standard (hyphenated) and compact format regular
44
+ # expressions for UUID validation, along with version-specific submodules for
45
+ # more granular validation requirements.
46
+ #
47
+ # @since 0.1.0
48
+ module UUID
49
+ # Regular expression for standard (hyphenated) UUID format
50
+ #
51
+ # Validates UUIDs in the format: 8-4-4-4-12 hexadecimal digits with hyphens
52
+ # Example: "123e4567-e89b-12d3-a456-426614174000"
53
+ #
54
+ # @since 0.1.0
55
+ STANDARD_REGEXP = /\A[0-9a-f]{8}-[0-9a-f]{4}-[1-7][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i #: Regexp
56
+
57
+ # Regular expression for compact (non-hyphenated) UUID format
58
+ #
59
+ # Validates UUIDs in the format: 32 continuous hexadecimal digits
60
+ # Example: "123e4567e89b12d3a456426614174000"
61
+ #
62
+ # @since 0.1.0
63
+ COMPACT_REGEXP = /\A[0-9a-f]{8}[0-9a-f]{4}[1-7][0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}\z/i #: Regexp
64
+
65
+ # Version 1 UUID validation patterns
66
+ #
67
+ # Provides regular expressions for validating time-based Version 1 UUIDs.
68
+ # These UUIDs are generated using a timestamp and node ID.
69
+ #
70
+ # @since 0.1.0
71
+ module V1
72
+ # Standard format regex for Version 1 UUIDs
73
+ # @return [Regexp] Pattern matching hyphenated Version 1 UUIDs
74
+ STANDARD_REGEXP = /\A[a-f0-9]{8}-[a-f0-9]{4}-1[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}\z/ #: Regexp
75
+
76
+ # Compact format regex for Version 1 UUIDs
77
+ # @return [Regexp] Pattern matching non-hyphenated Version 1 UUIDs
78
+ COMPACT_REGEXP = /\A[a-f0-9]{8}[a-f0-9]{4}1[a-f0-9]{3}[89ab][a-f0-9]{3}[a-f0-9]{12}\z/ #: Regexp
79
+ end
80
+
81
+ # Version 2 UUID validation patterns
82
+ #
83
+ # Provides regular expressions for validating DCE Security Version 2 UUIDs.
84
+ # These UUIDs incorporate local domain identifiers.
85
+ #
86
+ # @since 0.1.0
87
+ module V2
88
+ # Standard format regex for Version 2 UUIDs
89
+ # @return [Regexp] Pattern matching hyphenated Version 2 UUIDs
90
+ STANDARD_REGEXP = /\A[a-f0-9]{8}-[a-f0-9]{4}-2[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}\z/ #:Regexp
91
+
92
+ # Compact format regex for Version 2 UUIDs
93
+ # @return [Regexp] Pattern matching non-hyphenated Version 2 UUIDs
94
+ COMPACT_REGEXP = /\A[a-f0-9]{8}[a-f0-9]{4}2[a-f0-9]{3}[89ab][a-f0-9]{3}[a-f0-9]{12}\z/ #:Regexp
95
+ end
96
+
97
+ # Version 3 UUID validation patterns
98
+ #
99
+ # Provides regular expressions for validating name-based Version 3 UUIDs
100
+ # using MD5 hashing.
101
+ #
102
+ # @since 0.1.0
103
+ module V3
104
+ # Standard format regex for Version 3 UUIDs
105
+ # @return [Regexp] Pattern matching hyphenated Version 3 UUIDs
106
+ STANDARD_REGEXP = /\A[a-f0-9]{8}-[a-f0-9]{4}-3[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}\z/ #:Regexp
107
+
108
+ # Compact format regex for Version 3 UUIDs
109
+ # @return [Regexp] Pattern matching non-hyphenated Version 3 UUIDs
110
+ COMPACT_REGEXP = /\A[a-f0-9]{8}[a-f0-9]{4}3[a-f0-9]{3}[89ab][a-f0-9]{3}[a-f0-9]{12}\z/ #:Regexp
111
+ end
112
+
113
+ # Version 4 UUID validation patterns
114
+ #
115
+ # Provides regular expressions for validating randomly generated Version 4
116
+ # UUIDs. These are the most commonly used UUID format.
117
+ #
118
+ # @since 0.1.0
119
+ module V4
120
+ # Standard format regex for Version 4 UUIDs
121
+ # @return [Regexp] Pattern matching hyphenated Version 4 UUIDs
122
+ STANDARD_REGEXP = /\A[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}\z/ #:Regexp
123
+
124
+ # Compact format regex for Version 4 UUIDs
125
+ # @return [Regexp] Pattern matching non-hyphenated Version 4 UUIDs
126
+ COMPACT_REGEXP = /\A[a-f0-9]{8}[a-f0-9]{4}4[a-f0-9]{3}[89ab][a-f0-9]{3}[a-f0-9]{12}\z/ #:Regexp
127
+ end
128
+
129
+ # Version 5 UUID validation patterns
130
+ #
131
+ # Provides regular expressions for validating name-based Version 5 UUIDs
132
+ # using SHA-1 hashing.
133
+ #
134
+ # @since 0.1.0
135
+ module V5
136
+ # Standard format regex for Version 5 UUIDs
137
+ # @return [Regexp] Pattern matching hyphenated Version 5 UUIDs
138
+ STANDARD_REGEXP = /\A[a-f0-9]{8}-[a-f0-9]{4}-5[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}\z/ #:Regexp
139
+
140
+ # Compact format regex for Version 5 UUIDs
141
+ # @return [Regexp] Pattern matching non-hyphenated Version 5 UUIDs
142
+ COMPACT_REGEXP = /\A[a-f0-9]{8}[a-f0-9]{4}5[a-f0-9]{3}[89ab][a-f0-9]{3}[a-f0-9]{12}\z/ #:Regexp
143
+ end
144
+
145
+ # Version 6 UUID validation patterns
146
+ #
147
+ # Provides regular expressions for validating ordered-time Version 6 UUIDs.
148
+ # These UUIDs are similar to Version 1 but with improved timestamp ordering.
149
+ #
150
+ # @since 0.1.0
151
+ module V6
152
+ # Standard format regex for Version 6 UUIDs
153
+ # @return [Regexp] Pattern matching hyphenated Version 6 UUIDs
154
+ STANDARD_REGEXP = /\A[a-f0-9]{8}-[a-f0-9]{4}-6[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}\z/ #:Regexp
155
+
156
+ # Compact format regex for Version 6 UUIDs
157
+ # @return [Regexp] Pattern matching non-hyphenated Version 6 UUIDs
158
+ COMPACT_REGEXP = /\A[a-f0-9]{8}[a-f0-9]{4}6[a-f0-9]{3}[89ab][a-f0-9]{3}[a-f0-9]{12}\z/ #:Regexp
159
+ end
160
+
161
+ # Version 7 UUID validation patterns
162
+ #
163
+ # Provides regular expressions for validating Unix Epoch time-based Version 7
164
+ # UUIDs. These UUIDs use millisecond precision timestamps.
165
+ #
166
+ # @since 0.1.0
167
+ module V7
168
+ # Standard format regex for Version 7 UUIDs
169
+ # @return [Regexp] Pattern matching hyphenated Version 7 UUIDs
170
+ STANDARD_REGEXP = /\A[a-f0-9]{8}-[a-f0-9]{4}-7[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}\z/ #:Regexp
171
+
172
+ # Compact format regex for Version 7 UUIDs
173
+ # @return [Regexp] Pattern matching non-hyphenated Version 7 UUIDs
174
+ COMPACT_REGEXP = /\A[a-f0-9]{8}[a-f0-9]{4}7[a-f0-9]{3}[89ab][a-f0-9]{3}[a-f0-9]{12}\z/ #:Regexp
175
+ end
176
+ end
177
+
178
+ # Core UUID constraints based on RFC 4122
179
+ intrinsically_constrain :self, :type, String, description: :not_described
180
+
181
+ # The base UUID type should accept either standard or compact format
182
+ intrinsically_constrain :self, :match_pattern,
183
+ Regexp.union(UUID::STANDARD_REGEXP, UUID::COMPACT_REGEXP),
184
+ description: 'matching UUID format', concerning: :format
185
+
186
+ # Length constraint is handled by the format-specific methods
187
+ intrinsically_constrain :length, :range,
188
+ { minimum: 32, maximum: 36 },
189
+ description: 'having valid length', concerning: :size
190
+
191
+ # Constrain UUID to compact format
192
+ #
193
+ # Creates a constraint ensuring the UUID is in compact format (32 characters,
194
+ # no hyphens). Useful when working with systems that prefer compact UUIDs.
195
+ #
196
+ # @example
197
+ # type.being_compact
198
+ # type.validate("123e4567e89b12d3a456426614174000") # => true
199
+ # type.validate("123e4567-e89b-12d3-a456-426614174000") # => false
200
+ #
201
+ # @return [self] self for method chaining
202
+ # @rbs () -> self
203
+ def being_compact
204
+ constrain :length, :range, { minimum: 32, maximum: 32 },
205
+ concerning: :size, description: 'having compact format length'
206
+ constrain :self, :match_pattern, UUID::COMPACT_REGEXP,
207
+ description: 'matching compact UUID format',
208
+ concerning: :format
209
+ end
210
+ alias compact being_compact
211
+
212
+ # Constrain UUID to standard format
213
+ #
214
+ # Creates a constraint ensuring the UUID is in standard format (36 characters,
215
+ # with hyphens). This is the default format per RFC 4122.
216
+ #
217
+ # @example
218
+ # type.being_standard
219
+ # type.validate("123e4567-e89b-12d3-a456-426614174000") # => true
220
+ # type.validate("123e4567e89b12d3a456426614174000") # => false
221
+ #
222
+ # @return [self] self for method chaining
223
+ # @rbs () -> self
224
+ def being_standard
225
+ constrain :length, :range, { minimum: 36, maximum: 36 },
226
+ concerning: :size, description: 'having standard format length'
227
+ constrain :self, :match_pattern, UUID::STANDARD_REGEXP,
228
+ description: 'matching standard UUID format',
229
+ concerning: :format
230
+ end
231
+ alias standard being_standard
232
+
233
+ # Constrain UUID to specific version
234
+ #
235
+ # Creates a constraint ensuring the UUID matches a specific version (1-7) in
236
+ # either standard or compact format.
237
+ #
238
+ # @example
239
+ # type.being_version(4)
240
+ # type.validate("123e4567-e89b-42d3-a456-426614174000") # => true
241
+ # type.validate("123e4567-e89b-12d3-a456-426614174000") # => false
242
+ #
243
+ # @param version [Integer] UUID version (1-7)
244
+ # @param format [Symbol] :standard or :compact
245
+ # @return [self] self for method chaining
246
+ # @raise [ArgumentError] if format is neither :standard nor :compact
247
+ # @rbs (Integer version, ?format: :compact | :standard) -> self
248
+ def being_version(version, format: :standard)
249
+ case format
250
+ when :compact
251
+ constrain :length, :range, { minimum: 32, maximum: 32 },
252
+ concerning: :size, description: 'having length'
253
+ when :standard
254
+ constrain :length, :range, { minimum: 36, maximum: 36 },
255
+ concerning: :size, description: 'having length'
256
+ else
257
+ raise ArgumentError, "Invalid format: #{format}. Must be :compact or :standard"
258
+ end
259
+
260
+ constrain :self, :match_pattern, UUID.const_get("V#{version}::#{format.upcase}_REGEXP"),
261
+ concerning: :version
262
+ end
263
+
264
+ # Constrain UUID to Version 5 compact format
265
+ #
266
+ # Creates a constraint ensuring the UUID is a Version 5 UUID in compact format.
267
+ # Version 5 UUIDs are name-based using SHA-1 hashing.
268
+ #
269
+ # @return [self] self for method chaining
270
+ # @rbs () -> self
271
+ def being_version_five_compact
272
+ being_version(5, format: :compact)
273
+ end
274
+ alias being_v5_compact being_version_five_compact
275
+ alias v5_compact being_version_five_compact
276
+
277
+ # Constrain UUID to Version 5 standard format
278
+ #
279
+ # Creates a constraint ensuring the UUID is a Version 5 UUID in standard format.
280
+ # Version 5 UUIDs are name-based using SHA-1 hashing.
281
+ #
282
+ # @example
283
+ # type.being_v5
284
+ # type.validate("123e4567-e89b-52d3-a456-426614174000") # => true
285
+ #
286
+ # @return [self] self for method chaining
287
+ # @rbs () -> self
288
+ def being_version_five_standard
289
+ being_version(5)
290
+ end
291
+ alias being_v5 being_version_five_standard
292
+ alias being_v5_standard being_version_five_standard
293
+ alias being_version_five being_version_five_standard
294
+ alias v5 being_version_five_standard
295
+
296
+ # Constrain UUID to Version 4 compact format
297
+ #
298
+ # Creates a constraint ensuring the UUID is a Version 4 UUID in compact format.
299
+ # Version 4 UUIDs are randomly generated and are the most commonly used format.
300
+ #
301
+ # @example
302
+ # type.being_v4_compact
303
+ # type.validate("123e4567e89b42d3a456426614174000") # => true
304
+ #
305
+ # @return [self] self for method chaining
306
+ # @rbs () -> self
307
+ def being_version_four_compact
308
+ being_version(4, format: :compact)
309
+ end
310
+ alias being_v4_compact being_version_four_compact
311
+ alias v4_compact being_version_four_compact
312
+
313
+ # Constrain UUID to Version 4 standard format
314
+ #
315
+ # Creates a constraint ensuring the UUID is a Version 4 UUID in standard format.
316
+ # Version 4 UUIDs are randomly generated and are the most commonly used format.
317
+ #
318
+ # @example
319
+ # type.being_v4
320
+ # type.validate("123e4567-e89b-42d3-a456-426614174000") # => true
321
+ #
322
+ # @return [self] self for method chaining
323
+ # @rbs () -> self
324
+ def being_version_four_standard
325
+ being_version(4)
326
+ end
327
+ alias being_v4 being_version_four_standard
328
+ alias being_v4_standard being_version_four_standard
329
+ alias being_version_four being_version_four_standard
330
+ alias v4 being_version_four_standard
331
+
332
+ # Constrain UUID to Version 1 compact format
333
+ #
334
+ # Creates a constraint ensuring the UUID is a Version 1 UUID in compact format.
335
+ # Version 1 UUIDs are time-based using a timestamp and node ID.
336
+ #
337
+ # @example
338
+ # type.being_v1_compact
339
+ # type.validate("123e4567e89b12d3a456426614174000") # => true
340
+ #
341
+ # @return [self] self for method chaining
342
+ # @rbs () -> self
343
+ def being_version_one_compact
344
+ being_version(1, format: :compact)
345
+ end
346
+ alias being_v1_compact being_version_one_compact
347
+ alias v1_compact being_version_one_compact
348
+
349
+ # Constrain UUID to Version 1 standard format
350
+ #
351
+ # Creates a constraint ensuring the UUID is a Version 1 UUID in standard format.
352
+ # Version 1 UUIDs are time-based using a timestamp and node ID.
353
+ #
354
+ # @example
355
+ # type.being_v1
356
+ # type.validate("123e4567-e89b-12d3-a456-426614174000") # => true
357
+ #
358
+ # @return [self] self for method chaining
359
+ # @rbs () -> self
360
+ def being_version_one_standard
361
+ being_version(1)
362
+ end
363
+ alias being_v1 being_version_one_standard
364
+ alias being_v1_standard being_version_one_standard
365
+ alias being_version_one being_version_one_standard
366
+ alias v1 being_version_one_standard
367
+
368
+ # Constrain UUID to Version 7 compact format
369
+ #
370
+ # Creates a constraint ensuring the UUID is a Version 7 UUID in compact format.
371
+ # Version 7 UUIDs use Unix Epoch timestamps with millisecond precision.
372
+ #
373
+ # @example
374
+ # type.being_v7_compact
375
+ # type.validate("123e4567e89b72d3a456426614174000") # => true
376
+ #
377
+ # @return [self] self for method chaining
378
+ # @rbs () -> self
379
+ def being_version_seven_compact
380
+ being_version(7, format: :compact)
381
+ end
382
+ alias being_v7_compact being_version_seven_compact
383
+ alias v7_compact being_version_seven_compact
384
+
385
+ # Constrain UUID to Version 7 standard format
386
+ #
387
+ # Creates a constraint ensuring the UUID is a Version 7 UUID in standard format.
388
+ # Version 7 UUIDs use Unix Epoch timestamps with millisecond precision.
389
+ #
390
+ # @example
391
+ # type.being_v7
392
+ # type.validate("123e4567-e89b-72d3-a456-426614174000") # => true
393
+ #
394
+ # @return [self] self for method chaining
395
+ # @rbs () -> self
396
+ def being_version_seven_standard
397
+ being_version(7)
398
+ end
399
+ alias being_v7 being_version_seven_standard
400
+ alias being_v7_standard being_version_seven_standard
401
+ alias being_version_seven being_version_seven_standard
402
+ alias v7 being_version_seven_standard
403
+
404
+ # Constrain UUID to Version 6 compact format
405
+ #
406
+ # Creates a constraint ensuring the UUID is a Version 6 UUID in compact format.
407
+ # Version 6 UUIDs are similar to Version 1 but with improved timestamp ordering.
408
+ #
409
+ # @example
410
+ # type.being_v6_compact
411
+ # type.validate("123e4567e89b62d3a456426614174000") # => true
412
+ #
413
+ # @return [self] self for method chaining
414
+ # @rbs () -> self
415
+ def being_version_six_compact
416
+ being_version(6, format: :compact)
417
+ end
418
+ alias being_v6_compact being_version_six_compact
419
+ alias v6_compact being_version_six_compact
420
+
421
+ # Constrain UUID to Version 6 standard format
422
+ #
423
+ # Creates a constraint ensuring the UUID is a Version 6 UUID in standard format.
424
+ # Version 6 UUIDs are similar to Version 1 but with improved timestamp ordering.
425
+ #
426
+ # @example
427
+ # type.being_v6
428
+ # type.validate("123e4567-e89b-62d3-a456-426614174000") # => true
429
+ #
430
+ # @return [self] self for method chaining
431
+ # @rbs () -> self
432
+ def being_version_six_standard
433
+ being_version(6)
434
+ end
435
+ alias being_v6 being_version_six_standard
436
+ alias being_v6_standard being_version_six_standard
437
+ alias being_version_six being_version_six_standard
438
+ alias v6 being_version_six_standard
439
+
440
+ # Constrain UUID to Version 3 compact format
441
+ #
442
+ # Creates a constraint ensuring the UUID is a Version 3 UUID in compact format.
443
+ # Version 3 UUIDs are name-based using MD5 hashing.
444
+ #
445
+ # @example
446
+ # type.being_v3_compact
447
+ # type.validate("123e4567e89b32d3a456426614174000") # => true
448
+ #
449
+ # @return [self] self for method chaining
450
+ # @rbs () -> self
451
+ def being_version_three_compact
452
+ being_version(3, format: :compact)
453
+ end
454
+ alias being_v3_compact being_version_three_compact
455
+ alias v3_compact being_version_three_compact
456
+
457
+ # Constrain UUID to Version 3 standard format
458
+ #
459
+ # Creates a constraint ensuring the UUID is a Version 3 UUID in standard format.
460
+ # Version 3 UUIDs are name-based using MD5 hashing.
461
+ #
462
+ # @example
463
+ # type.being_v3
464
+ # type.validate("123e4567-e89b-32d3-a456-426614174000") # => true
465
+ #
466
+ # @return [self] self for method chaining
467
+ # @rbs () -> self
468
+ def being_version_three_standard
469
+ being_version(3)
470
+ end
471
+ alias being_v3 being_version_three_standard
472
+ alias being_v3_standard being_version_three_standard
473
+ alias being_version_three being_version_three_standard
474
+ alias v3 being_version_three_standard
475
+
476
+ # Constrain UUID to Version 2 compact format
477
+ #
478
+ # Creates a constraint ensuring the UUID is a Version 2 UUID in compact format.
479
+ # Version 2 UUIDs are DCE Security version that incorporate local domain identifiers.
480
+ #
481
+ # @example
482
+ # type.being_v2_compact
483
+ # type.validate("123e4567e89b22d3a456426614174000") # => true
484
+ #
485
+ # @return [self] self for method chaining
486
+ # @rbs () -> self
487
+ def being_version_two_compact
488
+ being_version(2, format: :compact)
489
+ end
490
+ alias being_v2_compact being_version_two_compact
491
+ alias v2_compact being_version_two_compact
492
+
493
+ # Constrain UUID to Version 2 standard format
494
+ #
495
+ # Creates a constraint ensuring the UUID is a Version 2 UUID in standard format.
496
+ # Version 2 UUIDs are DCE Security version that incorporate local domain identifiers.
497
+ #
498
+ # @example
499
+ # type.being_v2
500
+ # type.validate("123e4567-e89b-22d3-a456-426614174000") # => true
501
+ #
502
+ # @return [self] self for method chaining
503
+ # @rbs () -> self
504
+ def being_version_two_standard
505
+ being_version(2)
506
+ end
507
+ alias being_v2 being_version_two_standard
508
+ alias being_v2_standard being_version_two_standard
509
+ alias being_version_two being_version_two_standard
510
+ alias v2 being_version_two_standard
511
+ end
512
+ end
513
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/type/behavior'
4
+ require 'domainic/type/behavior/string_behavior/matching_behavior'
5
+ require 'domainic/type/behavior/sizable_behavior'
6
+ require 'domainic/type/behavior/uri_behavior'
7
+ require 'uri'
8
+
9
+ module Domainic
10
+ module Type
11
+ # A type for validating email addresses according to RFC standards
12
+ #
13
+ # This type provides comprehensive email address validation following RFC 5321 and 5322
14
+ # standards. It supports constraints on all email components (local part, hostname, TLD)
15
+ # while enforcing basic email requirements like maximum length and character set rules.
16
+ #
17
+ # Key features:
18
+ # - RFC compliant email format validation
19
+ # - Maximum length enforcement (254 characters)
20
+ # - ASCII character set requirement
21
+ # - Hostname and TLD validation
22
+ # - Local part pattern matching
23
+ #
24
+ # @example Basic usage
25
+ # type = EmailAddressType.new
26
+ # type.validate("user@example.com") # => true
27
+ # type.validate("invalid") # => false
28
+ #
29
+ # @example With domain constraints
30
+ # type = EmailAddressType.new
31
+ # .having_hostname("example.com", "company.com")
32
+ # .having_top_level_domain("com", "org")
33
+ #
34
+ # @example With local part validation
35
+ # type = EmailAddressType.new
36
+ # .having_local_matching(/^[a-z]+$/)
37
+ # .not_having_local_matching(/^admin/)
38
+ #
39
+ # @author {https://aaronmallen.me Aaron Allen}
40
+ # @since 0.1.0
41
+ class EmailAddressType
42
+ # @rbs! extend Behavior::ClassMethods
43
+
44
+ include Behavior
45
+ include Behavior::StringBehavior::MatchingBehavior
46
+ include Behavior::SizableBehavior
47
+ include Behavior::URIBehavior
48
+
49
+ # Core email constraints based on RFCs 5321 and 5322
50
+ intrinsically_constrain :self, :type, String, description: :not_described
51
+ intrinsically_constrain :self, :match_pattern, URI::MailTo::EMAIL_REGEXP, description: :not_described
52
+ intrinsically_constrain :length, :range, { maximum: 254 }, description: :not_described, concerning: :size
53
+ intrinsically_constrain :self, :character_set, :ascii, description: :not_described
54
+ empty = intrinsic_constraints.prepare :self, :emptiness
55
+ intrinsically_constrain :self, :not, empty, description: :not_described
56
+
57
+ # Constrain email to allowed hostnames
58
+ #
59
+ # Creates a constraint ensuring the domain part of the email matches one of the
60
+ # specified hostnames. This is useful for restricting emails to specific domains.
61
+ #
62
+ # @example
63
+ # type.having_hostname("example.com", "company.com")
64
+ # type.validate("user@example.com") # => true
65
+ # type.validate("user@other.com") # => false
66
+ #
67
+ # @param hostnames [Array<String>] List of allowed hostnames
68
+ # @return [self] self for method chaining
69
+ # @rbs (*String hostnames) -> self
70
+ def having_hostname(*hostnames)
71
+ hostnames = hostnames.map { |h| Regexp.escape(h) }
72
+ pattern = /\A(?:#{hostnames.join('|')})(?:\.[a-z]+)?+\z/i
73
+ constrain :self, :match_pattern, pattern,
74
+ coerce_with: ->(value) { value.split('@').last }, concerning: :hostname_inclusion
75
+ end
76
+ alias allowing_host having_hostname
77
+ alias allowing_hostname having_hostname
78
+ alias host having_hostname
79
+ alias hostname having_hostname
80
+ alias with_host having_hostname
81
+ alias with_hostname having_hostname
82
+
83
+ # Constrain email local part to match pattern
84
+ #
85
+ # Creates a constraint requiring the local part (before @) to match the given pattern.
86
+ # Useful for enforcing username conventions or restrictions.
87
+ #
88
+ # @example
89
+ # type.having_local_matching(/^[a-z]+$/)
90
+ # type.validate("user@example.com") # => true
91
+ # type.validate("123@example.com") # => false
92
+ #
93
+ # @param pattern [Regexp] Pattern the local part must match
94
+ # @return [self] self for method chaining
95
+ # @rbs (String | Regexp pattern) -> self
96
+ def having_local_matching(pattern)
97
+ constrain :self, :match_pattern, pattern,
98
+ coerce_with: ->(value) { value.split('@').first }, concerning: :local_part_inclusion
99
+ end
100
+ alias matching_local having_local_matching
101
+ alias with_local_matching having_local_matching
102
+
103
+ # Constrain email to exclude specific hostnames
104
+ #
105
+ # Creates a constraint ensuring the domain part of the email does not match any
106
+ # of the specified hostnames. Useful for blacklisting certain domains.
107
+ #
108
+ # @example
109
+ # type.not_having_hostname("example.com")
110
+ # type.validate("user@company.com") # => true
111
+ # type.validate("user@example.com") # => false
112
+ #
113
+ # @param hostnames [Array<String>] List of forbidden hostnames
114
+ # @return [self] self for method chaining
115
+ # @rbs (*String hostnames) -> self
116
+ def not_having_hostname(*hostnames)
117
+ hostnames = hostnames.map { |h| Regexp.escape(h) }
118
+ pattern = /\A(?:#{hostnames.join('|')})(?:\.[a-z]+)?+\z/i
119
+ hostname_pattern = @constraints.prepare :self, :match_pattern, pattern,
120
+ coerce_with: ->(value) { value.split('@').last }
121
+ constrain :self, :not, hostname_pattern, concerning: :hostname_exclusion
122
+ end
123
+ alias forbidding_host not_having_hostname
124
+ alias forbidding_hostname not_having_hostname
125
+ alias not_host not_having_hostname
126
+ alias not_hostname not_having_hostname
127
+
128
+ # Constrain email local part to not match pattern
129
+ #
130
+ # Creates a constraint ensuring the local part (before @) does not match the given
131
+ # pattern. Useful for preventing certain username patterns.
132
+ #
133
+ # @example
134
+ # type.not_having_local_matching(/^admin/)
135
+ # type.validate("user@example.com") # => true
136
+ # type.validate("admin@example.com") # => false
137
+ #
138
+ # @param pattern [Regexp] Pattern the local part must not match
139
+ # @return [self] self for method chaining
140
+ # @rbs (String | Regexp pattern) -> self
141
+ def not_having_local_matching(pattern)
142
+ local_pattern = @constraints.prepare :self, :match_pattern, pattern,
143
+ coerce_with: ->(value) { value.split('@').first }
144
+ constrain :self, :not, local_pattern, concerning: :local_part_exclusion
145
+ end
146
+ alias not_matching_local not_having_local_matching
147
+ end
148
+ end
149
+ end