domainic-type 0.1.0.alpha.3.0.2 → 0.1.0.alpha.3.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 (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