File Coverage

blib/lib/Poz/Types/string.pm
Criterion Covered Total %
statement 294 295 99.6
branch 66 68 97.0
condition 113 122 92.6
subroutine 64 64 100.0
pod 26 27 96.3
total 563 576 97.7


line stmt bran cond sub pod time code
1             package Poz::Types::string;
2 11     11   184 use 5.032;
  11         36  
3 11     11   56 use strict;
  11         17  
  11         239  
4 11     11   48 use warnings;
  11         14  
  11         541  
5 11     11   1447 use utf8;
  11         826  
  11         111  
6 11     11   527 use parent 'Poz::Types::scalar';
  11         17  
  11         53  
7 11     11   6745 use Email::Address ();
  11         152436  
  11         648  
8 11     11   5511 use URI::URL ();
  11         125118  
  11         285  
9 11     11   6069 use Net::IPv6Addr ();
  11         966448  
  11         573  
10 11     11   6860 use Time::Piece ();
  11         132402  
  11         421  
11 11     11   6935 use DateTime::Format::Strptime ();
  11         7166217  
  11         542  
12 11     11   8032 use DateTime::Format::ISO8601 ();
  11         983928  
  11         783  
13 11     11   7909 use DateTime::Format::Duration::ISO8601 ();
  11         13873  
  11         45136  
14              
15             sub new {
16 81     81 1 192 my ($class, $opts) = @_;
17 81   50     187 $opts = $opts || {};
18 81   100     431 $opts->{required_error} //= "required";
19 81   100     378 $opts->{invalid_type_error} //= "Not a string";
20 81         335 my $self = $class->SUPER::new($opts);
21 81         606 return $self;
22             }
23              
24             sub rule {
25 271     271 1 489 my ($self, $value) = @_;
26 271 100       595 return $self->{required_error} unless defined $value;
27 267 100       590 return $self->{invalid_type_error} unless !ref $value;
28 260         571 return;
29             }
30              
31             sub coerce {
32 5     5 1 74 my ($self, $value) = @_;
33 5         19 return "$value";
34             }
35              
36             sub max {
37 4     4 1 10 my ($self, $max, $opts) = @_;
38 4   100     16 $opts = $opts || {};
39 4   100     17 $opts->{message} //= "Too long";
40 4         27 push @{$self->{rules}}, sub {
41 9     9   17 my ($self, $value) = @_;
42 9 100       38 return $opts->{message} if CORE::length($value) > $max;
43 7         13 return;
44 4         5 };
45 4         15 return $self;
46             }
47              
48             sub min {
49 2     2 1 7 my ($self, $min, $opts) = @_;
50 2   100     11 $opts = $opts || {};
51 2   100     11 $opts->{message} //= "Too short";
52 2         17 push @{$self->{rules}}, sub {
53 6     6   13 my ($self, $value) = @_;
54 6 100       21 return $opts->{message} if CORE::length($value) < $min;
55 3         7 return;
56 2         3 };
57 2         8 return $self;
58             }
59              
60             sub length {
61 2     2 0 6 my ($self, $length, $opts) = @_;
62 2   100     9 $opts = $opts || {};
63 2   100     11 $opts->{message} //= "Not the right length";
64 2         15 push @{$self->{rules}}, sub {
65 6     6   13 my ($self, $value) = @_;
66 6 100       44 return $opts->{message} if CORE::length($value) != $length;
67 2         5 return;
68 2         4 };
69 2         7 return $self;
70             }
71              
72             sub email {
73 2     2 1 7 my ($self, $opts) = @_;
74 2   100     10 $opts = $opts || {};
75 2   100     12 $opts->{message} //= "Not an email";
76 2         14 push @{$self->{rules}}, sub {
77 4     4   10 my ($self, $value) = @_;
78 4         26 my ($addr) = Email::Address->parse($value);
79 4 100       3536 return $opts->{message} unless defined $addr;
80 2         6 return;
81 2         3 };
82 2         9 return $self;
83             }
84              
85             sub url {
86 2     2 1 6 my ($self, $opts) = @_;
87 2   100     13 $opts = $opts || {};
88 2   100     11 $opts->{message} //= "Not an URL";
89 2         17 push @{$self->{rules}}, sub {
90 6     6   13 my ($self, $value) = @_;
91 6         36 my $url = URI::URL->new($value);
92 6 100 66     21761 return $opts->{message} if !defined $url || !defined $url->scheme;
93 3         125 return;
94 2         3 };
95 2         7 return $self;
96             }
97              
98             sub emoji {
99 2     2 1 6 my ($self, $opts) = @_;
100 2   100     10 $opts = $opts || {};
101 2   100     11 $opts->{message} //= "Not an emoji";
102 2         16 push @{$self->{rules}}, sub {
103 5     5   10 my ($self, $value) = @_;
104 5 100       37 return $opts->{message} unless $value =~ /\p{Emoji}/;
105 3         8 return;
106 2         4 };
107 2         8 return $self;
108             }
109              
110             sub uuid {
111 3     3 1 6 my ($self, $opts) = @_;
112 3   100     9 $opts = $opts || {};
113 3   100     11 $opts->{message} //= "Not an UUID";
114 3         15 push @{$self->{rules}}, sub {
115 8     8   15 my ($self, $value) = @_;
116 8 100       92 return $opts->{message} unless lc($value) =~ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
117 6         15 return;
118 3         4 };
119 3         15 return $self;
120             }
121              
122             sub nanoid {
123 2     2 1 6 my ($self, $opts) = @_;
124 2   100     8 $opts = $opts || {};
125 2   100     21 $opts->{message} //= "Not a nanoid";
126 2         13 push @{$self->{rules}}, sub {
127 4     4   7 my ($self, $value) = @_;
128 4 100       39 return $opts->{message} unless $value =~ /^[0-9a-zA-Z_-]{21}$/;
129 2         4 return;
130 2         5 };
131 2         6 return $self;
132             }
133              
134             sub cuid {
135 2     2 1 4 my ($self, $opts) = @_;
136 2   100     8 $opts = $opts || {};
137 2   100     7 $opts->{message} //= "Not a cuid";
138 2         9 push @{$self->{rules}}, sub {
139 4     4   5 my ($self, $value) = @_;
140 4 100       22 return $opts->{message} unless $value =~ /^c[a-z0-9]{24}$/;
141 2         3 return;
142 2         3 };
143 2         4 return $self;
144             }
145              
146             sub cuid2 {
147 2     2 1 8 my ($self, $opts) = @_;
148 2   100     8 $opts = $opts || {};
149 2   100     9 $opts->{message} //= "Not a cuid2";
150 2         14 push @{$self->{rules}}, sub {
151 4     4   6 my ($self, $value) = @_;
152 4 100       24 return $opts->{message} unless $value =~ /^[a-z0-9]{24,32}$/;
153 2         3 return;
154 2         5 };
155 2         6 return $self;
156             }
157              
158             sub ulid {
159 2     2 1 4 my ($self, $opts) = @_;
160 2   100     6 $opts = $opts || {};
161 2   100     8 $opts->{message} //= "Not an ulid";
162 2         9 push @{$self->{rules}}, sub {
163 4     4   7 my ($self, $value) = @_;
164 4 100       20 return $opts->{message} unless $value =~ /^[0-9A-HJKMNP-TV-Z]{26}$/;
165 2         3 return;
166 2         2 };
167 2         4 return $self;
168             }
169              
170             sub regex {
171 2     2 1 4 my ($self, $regex, $opts) = @_;
172 2   100     7 $opts = $opts || {};
173 2   100     5 $opts->{message} //= "Not match regex";
174 2         11 push @{$self->{rules}}, sub {
175 4     4   6 my ($self, $value) = @_;
176 4 100       31 return $opts->{message} unless $value =~ $regex;
177 2         2 return;
178 2         2 };
179 2         4 return $self;
180             }
181              
182             sub includes {
183 2     2 1 3 my ($self, $includes, $opts) = @_;
184 2   100     7 $opts = $opts || {};
185 2   66     7 $opts->{message} //= "Not includes $includes";
186 2         9 push @{$self->{rules}}, sub {
187 6     6   7 my ($self, $value) = @_;
188 6 100       15 return $opts->{message} unless index($value, $includes) != -1;
189 4         6 return;
190 2         2 };
191 2         5 return $self;
192             }
193              
194             sub startsWith {
195 2     2 1 4 my ($self, $startWith, $opts) = @_;
196 2   100     6 $opts = $opts || {};
197 2   66     8 $opts->{message} //= "Not starts with $startWith";
198 2         10 push @{$self->{rules}}, sub {
199 5     5   10 my ($self, $value) = @_;
200 5 100       17 return $opts->{message} unless index($value, $startWith) == 0;
201 3         5 return;
202 2         2 };
203 2         4 return $self;
204             }
205              
206             sub endsWith {
207 2     2 1 4 my ($self, $endsWith, $opts) = @_;
208 2   100     7 $opts = $opts || {};
209 2   66     7 $opts->{message} //= "Not ends with $endsWith";
210 2         11 push @{$self->{rules}}, sub {
211 5     5   8 my ($self, $value) = @_;
212 5 100       15 return $opts->{message} unless substr($value, -1 * CORE::length($endsWith)) eq $endsWith;
213 3         4 return;
214 2         2 };
215 2         5 return $self;
216             }
217              
218             # supports ipv4 / ipv6
219             sub ip {
220 4     4 1 7 my ($self, $opts) = @_;
221 4   100     11 $opts = $opts || {};
222 4   100     13 $opts->{message} //= "Not an IP address";
223 4   100     12 my $version = $opts->{version} || "any";
224 4         20 push @{$self->{rules}}, sub {
225 16     16   23 my ($self, $value) = @_;
226 16         24 my $pass = 0;
227 16 100 100     53 if ($version eq "v4" || $version eq "any") {
228 12         38 my @octets = split(/\./, $value);
229 12 100       28 if (scalar(@octets) == 4) {
230 6         9 foreach my $octet (@octets) {
231 24 100 33     150 return $opts->{message} unless $octet =~ /^\d+$/ && $octet >= 0 && $octet <= 255;
      66        
232             }
233 3         5 $pass = 1;
234             }
235 9 100 100     30 if ($version eq "v4" && !$pass) {
236 2         5 return $opts->{message};
237             }
238             }
239 11 50 66     44 if (!$pass && ($version eq "v6" || $version eq "any")) {
      100        
240 8 100       31 return $opts->{message} unless Net::IPv6Addr::is_ipv6($value);
241             }
242 6         343 return;
243 4         6 };
244 4         11 return $self;
245             }
246              
247             sub trim {
248 1     1 1 2 my ($self) = @_;
249 1         6 push @{$self->{transform}}, sub {
250 4     4   5 my ($self, $value) = @_;
251 4         23 $value =~ s/^\s+|\s+$//g;
252 4         8 return $value;
253 1         2 };
254 1         3 return $self;
255             }
256              
257             sub toLowerCase {
258 1     1 1 4 my ($self) = @_;
259 1         7 push @{$self->{transform}}, sub {
260 2     2   6 my ($self, $value) = @_;
261 2         9 return lc($value);
262 1         3 };
263 1         4 return $self;
264             }
265              
266             sub toUpperCase {
267 1     1 1 4 my ($self) = @_;
268 1         8 push @{$self->{transform}}, sub {
269 2     2   23 my ($self, $value) = @_;
270 2         9 return uc($value);
271 1         3 };
272 1         5 return $self;
273             }
274              
275             sub date {
276 6     6 1 14 my ($self, $opts) = @_;
277 6   100     23 $opts = $opts || {};
278 6   100     32 $opts->{message} //= "Not a date";
279 6         41 push @{$self->{rules}}, sub {
280 27     27   48 my ($self, $value) = @_;
281 27 100       346 return $opts->{message} unless $value =~ /^\d{4}-\d{2}-\d{2}$/;
282 23 100       47 return $opts->{message} unless eval { Time::Piece->strptime($value, "%Y-%m-%d") };
  23         121  
283 20         2629 return;
284 6         8 };
285 6         36 return $self;
286             }
287              
288             sub time {
289 3     3 1 8 my ($self, $opts) = @_;
290 3   100     14 $opts = $opts || {};
291 3   100     27 $opts->{message} //= "Not a time";
292 3   100     16 my $precision = $opts->{precision} || 6;
293 3         12 my $precision_regex = _build_precision_regex($precision);
294 3         134 my $format_check = qr/^\d{2}:\d{2}:\d{2}$precision_regex$/;
295 3         27 push @{$self->{rules}}, sub {
296 18     18   36 my ($self, $value) = @_;
297 18         33 my $format = "%H:%M:%S";
298 18 100       263 return $opts->{message} unless $value =~ $format_check;
299 15 100       70 if ($value =~ /\.[0-9]+/) {
300 3         8 $format = "%H:%M:%S.%N";
301             }
302 15         86 my $formatter = DateTime::Format::Strptime->new(pattern => $format);
303 15 100       22382 return $opts->{message} unless eval { $formatter->parse_datetime($value) };
  15         58  
304 6         6137 return;
305 3         10 };
306 3         14 return $self;
307             }
308              
309             # iso8601 format
310             sub datetime {
311 5     5 1 13 my ($self, $opts) = @_;
312 5   100     16 $opts = $opts || {};
313 5   100     26 $opts->{message} //= "Not a datetime";
314 5   100     18 my $precision = $opts->{precision} || 6;
315 5         16 my $precision_regex = _build_precision_regex($precision);
316 5   100     21 my $offset = $opts->{offset} || 0;
317 5         80 my $offset_regex = "(Z)?";
318 5 100       16 if ($offset) {
319 2         4 $offset_regex = "(Z|[+-][0-9]{4}|[+-][0-9]{2}(:[0-9]{2})?)?";
320             }
321              
322 5         315 my $format_check = qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$precision_regex$offset_regex$/;
323 5         41 push @{$self->{rules}}, sub {
324 46     46   107 my ($self, $value) = @_;
325 46 100       677 return $opts->{message} unless $value =~ $format_check;
326 34 100       76 return $opts->{message} unless eval { DateTime::Format::ISO8601->parse_datetime($value) };
  34         163  
327 15         9152 return;
328 5         17 };
329 5         27 return $self;
330             }
331              
332             sub _build_precision_regex {
333 8     8   19 my ($precision) = @_;
334 8         17 my $min_precision = 1;
335 8 50       25 if ($precision < $min_precision) {
336 0         0 $min_precision = $precision;
337             }
338 8         29 return "(\\.[0-9]{$min_precision,$precision})?";
339             }
340              
341             # iso8601 duration
342             sub duration {
343 2     2 1 5 my ($self, $opts) = @_;
344 2   100     12 $opts = $opts || {};
345 2   100     12 $opts->{message} //= "Not a duration";
346 2         14 push @{$self->{rules}}, sub {
347 5     5   11 my ($self, $value) = @_;
348 5         35 my $format = DateTime::Format::Duration::ISO8601->new;
349 5 100       80 return $opts->{message} unless eval { $format->parse_duration($value) };
  5         21  
350 2         654 return;
351 2         5 };
352 2         8 return $self;
353             }
354              
355             sub base64 {
356 2     2 1 6 my ($self, $opts) = @_;
357 2   100     10 $opts = $opts || {};
358 2   100     9 $opts->{message} //= "Not a base64";
359 2         13 push @{$self->{rules}}, sub {
360 4     4   10 my ($self, $value) = @_;
361 4 100       30 return $opts->{message} unless $value =~ /^[A-Za-z0-9+\/]+={0,2}$/;
362 2         5 return;
363 2         6 };
364 2         7 return $self;
365             }
366             1;
367              
368             =head1 NAME
369              
370             Poz::Types::string - A module for validating and transforming strings.
371              
372             =head1 SYNOPSIS
373              
374             use Poz qw/z/;
375              
376             my $string = z->string;
377              
378             # Validate a string
379             $string->rule($value);
380              
381             # Coerce a value to a string
382             my $coerced_value = $string->coerce($value);
383              
384             # Add validation rules
385             $string->max($max_length, \%opts);
386             $string->min($min_length, \%opts);
387             $string->email(\%opts);
388             $string->url(\%opts);
389             $string->emoji(\%opts);
390             $string->uuid(\%opts);
391             $string->nanoid(\%opts);
392             $string->cuid(\%opts);
393             $string->cuid2(\%opts);
394             $string->ulid(\%opts);
395             $string->regex($regex, \%opts);
396             $string->includes($substring, \%opts);
397             $string->startsWith($prefix, \%opts);
398             $string->endsWith($suffix, \%opts);
399             $string->ip(\%opts);
400             $string->date(\%opts);
401             $string->time(\%opts);
402             $string->datetime(\%opts);
403             $string->duration(\%opts);
404             $string->base64(\%opts);
405              
406             # Add transformations
407             $string->trim;
408             $string->toLowerCase;
409             $string->toUpperCase;
410              
411             =head1 DESCRIPTION
412              
413             This module provides a set of methods for validating and transforming strings. It includes rules for checking string length, format, and content, as well as transformations for modifying the string.
414              
415             =head2 METHODS
416              
417             =over 4
418              
419             =item rule
420              
421             $string->rule($value);
422             Validates that the value is a defined, non-reference string.
423              
424             =item coerce
425              
426             my $coerced_value = $string->coerce($value);
427             Coerces the value to a string.
428              
429             =item max
430              
431             $string->max($max_length, \%opts);
432             Adds a rule to ensure the string does not exceed the specified maximum length.
433              
434             =item min
435              
436             $string->min($min_length, \%opts);
437             Adds a rule to ensure the string is at least the specified minimum length.
438              
439             =item email
440              
441             $string->email(\%opts);
442             Adds a rule to validate that the string is a valid email address.
443              
444             =item url
445              
446             $string->url(\%opts);
447             Adds a rule to validate that the string is a valid URL.
448              
449             =item emoji
450              
451             $string->emoji(\%opts);
452             Adds a rule to validate that the string contains an emoji.
453              
454             =item uuid
455              
456             $string->uuid(\%opts);
457             Adds a rule to validate that the string is a valid UUID.
458              
459             =item nanoid
460              
461             $string->nanoid(\%opts);
462             Adds a rule to validate that the string is a valid NanoID.
463              
464             =item cuid
465              
466             $string->cuid(\%opts);
467             Adds a rule to validate that the string is a valid CUID.
468              
469             =item cuid2
470              
471             $string->cuid2(\%opts);
472             Adds a rule to validate that the string is a valid CUID2.
473              
474             =item ulid
475              
476             $string->ulid(\%opts);
477             Adds a rule to validate that the string is a valid ULID.
478              
479             =item regex
480              
481             $string->regex($regex, \%opts);
482             Adds a rule to validate that the string matches the specified regular expression.
483              
484             =item includes
485              
486             $string->includes($substring, \%opts);
487             Adds a rule to validate that the string includes the specified substring.
488              
489             =item startsWith
490              
491             $string->startsWith($prefix, \%opts);
492             Adds a rule to validate that the string starts with the specified prefix.
493              
494             =item endsWith
495              
496             $string->endsWith($suffix, \%opts);
497             Adds a rule to validate that the string ends with the specified suffix.
498              
499             =item ip
500              
501             $string->ip(\%opts);
502             Adds a rule to validate that the string is a valid IP address (IPv4 or IPv6).
503              
504             =item trim
505              
506             $string->trim;
507             Adds a transformation to trim whitespace from the string.
508              
509             =item toLowerCase
510              
511             $string->toLowerCase;
512             Adds a transformation to convert the string to lowercase.
513              
514             =item toUpperCase
515              
516             $string->toUpperCase;
517             Adds a transformation to convert the string to uppercase.
518              
519             =item date
520              
521             $string->date(\%opts);
522             Adds a rule to validate that the string is a valid date (YYYY-MM-DD).
523              
524             =item time
525              
526             $string->time(\%opts);
527             Adds a rule to validate that the string is a valid time (HH:MM:SS).
528              
529             =item datetime
530              
531             $string->datetime(\%opts);
532             Adds a rule to validate that the string is a valid ISO8601 datetime.
533              
534             =item duration
535              
536             $string->duration(\%opts);
537             Adds a rule to validate that the string is a valid ISO8601 duration.
538              
539             =item base64
540              
541             $string->base64(\%opts);
542             Adds a rule to validate that the string is a valid base64 encoded string.
543              
544             =back
545              
546             =head1 LICENSE
547              
548             Copyright (C) ytnobody.
549              
550             This library is free software; you can redistribute it and/or modify
551             it under the same terms as Perl itself.
552              
553             =head1 AUTHOR
554              
555             ytnobody E<lt>ytnobody@gmail.comE<gt>
556              
557             =cut