File Coverage

blib/lib/Pinto/Util.pm
Criterion Covered Total %
statement 155 189 82.0
branch 43 78 55.1
condition 7 12 58.3
subroutine 48 53 90.5
pod 32 35 91.4
total 285 367 77.6


line stmt bran cond sub pod time code
1             # ABSTRACT: Static utility functions for Pinto
2              
3             package Pinto::Util;
4              
5 66     66   78332 use strict;
  66         131  
  66         14081  
6 66     66   395 use warnings;
  66         122  
  66         1840  
7 66     66   8581 use version;
  66         51260  
  66         402  
8 66     66   5435 use base qw(Exporter);
  66         156  
  66         5661  
9              
10 66     66   12874 use URI;
  66         150855  
  66         3789  
11 66     66   22784 use URI::file;
  66         165236  
  66         1903  
12 66     66   511 use Carp;
  66         175  
  66         4363  
13 66     66   38481 use DateTime;
  66         26074263  
  66         3157  
14 66     66   19708 use File::Temp;
  66         421489  
  66         5393  
15 66     66   11539 use Path::Class;
  66         397543  
  66         3274  
16 66     66   491 use Digest::MD5;
  66         152  
  66         2157  
17 66     66   19763 use Digest::SHA;
  66         105280  
  66         2973  
18 66     66   462 use Scalar::Util;
  66         141  
  66         2098  
19 66     66   21935 use UUID::Tiny;
  66         527808  
  66         7021  
20 66     66   11538 use Readonly;
  66         106965  
  66         3901  
21              
22 66     66   21578 use Pinto::Globals;
  66         267  
  66         2603  
23 66     66   11562 use Pinto::Constants qw(:all);
  66         239  
  66         3020  
24 66     66   44258 use Pinto::Types qw(DiffStyle);
  66         235  
  66         623  
25              
26             #-------------------------------------------------------------------------------
27              
28             our $VERSION = '0.14'; # VERSION
29              
30             #-------------------------------------------------------------------------------
31              
32             Readonly our @EXPORT_OK => qw(
33             author_dir
34             body_text
35             current_author_id
36             current_utc_time
37             current_time_offset
38             current_username
39             debug
40             decamelize
41             default_diff_style
42             indent_text
43             interpolate
44             is_blank
45             is_not_blank
46             is_interactive
47             is_remote_repo
48             is_system_prop
49             isa_perl
50             itis
51             make_uri
52             md5
53             mksymlink
54             mtime
55             parse_dist_path
56             mask_uri_passwords
57             sha256
58             tempdir
59             title_text
60             throw
61             trim_text
62             truncate_text
63             user_palette
64             uuid
65             whine
66             );
67              
68             Readonly our %EXPORT_TAGS => ( all => \@EXPORT_OK );
69              
70             #-------------------------------------------------------------------------------
71              
72              
73             sub throw {
74 109     109 1 23042001 my ($error) = @_;
75              
76             # Rethrowing...
77 109 100       513 $error->throw if itis( $error, 'Pinto::Exception' );
78              
79 67         662 require Pinto::Exception;
80 67         1614 Pinto::Exception->throw( message => "$error" );
81              
82 0         0 return; # Should never get here
83             }
84              
85             #-------------------------------------------------------------------------------
86              
87              
88             sub debug {
89 6958     6958 1 210211 my ($it) = @_;
90              
91             # TODO: Use Carp instead?
92              
93 6958 50       32694 return 1 if not $ENV{PINTO_DEBUG};
94              
95 0 0       0 $it = $it->() if ref $it eq 'CODE';
96 0         0 my ( $file, $line ) = (caller)[ 1, 2 ];
97 0         0 print {*STDERR} "$it in $file at line $line\n";
  0         0  
98              
99 0         0 return 1;
100             }
101              
102             #-------------------------------------------------------------------------------
103              
104              
105             sub whine {
106 10     10 1 81 my ($message) = @_;
107              
108 10 50       48 if ( $ENV{DEBUG} ) {
109 0         0 Carp::cluck($message);
110 0         0 return 1;
111             }
112              
113 10         34 chomp $message;
114 10         286 warn $message . "\n";
115              
116 10         320 return 1;
117             }
118              
119             #-------------------------------------------------------------------------------
120              
121              
122             sub author_dir { ## no critic (ArgUnpacking)
123 132     132 1 149309 my $author = uc pop;
124 132         541 my @base = @_;
125              
126 132         1147 return dir( @base, substr( $author, 0, 1 ), substr( $author, 0, 2 ), $author );
127             }
128              
129             #-------------------------------------------------------------------------------
130              
131              
132             sub itis {
133 2138     2138 1 7325 my ( $var, $class ) = @_;
134              
135 2138   100     21959 return ref $var && Scalar::Util::blessed($var) && $var->isa($class);
136             }
137              
138             #-------------------------------------------------------------------------------
139              
140              
141             sub parse_dist_path {
142 103     103 1 1231 my ($path) = @_;
143              
144             # eg: /yadda/authors/id/A/AU/AUTHOR/subdir1/subdir2/Foo-1.0.tar.gz
145             # or: A/AU/AUTHOR/subdir/Foo-1.0.tar.gz
146              
147 103 50       1263 if ( $path =~ s{^ (?:.*/authors/id/)? (.*) $}{$1}mx ) {
148              
149             # $path = 'A/AU/AUTHOR/subdir/Foo-1.0.tar.gz'
150 103         598 my @path_parts = split m{ / }mx, $path;
151 103         271 my $author = $path_parts[2]; # AUTHOR
152 103         205 my $archive = $path_parts[-1]; # Foo-1.0.tar.gz
153 103         504 return ( $author, $archive );
154             }
155              
156 0         0 throw "Unable to parse path: $path";
157             }
158              
159             #-------------------------------------------------------------------------------
160              
161              
162             sub isa_perl {
163 0     0 1 0 my ($path_or_uri) = @_;
164              
165 0         0 return $path_or_uri =~ m{ / perl-[\d.]+ \.tar \.(?: gz|bz2 ) $ }mx;
166             }
167              
168             #-------------------------------------------------------------------------------
169              
170              
171             sub mtime {
172 161     161 1 1480 my ($file) = @_;
173              
174 161 50       859 throw 'Must supply a file' if not $file;
175 161 50       1494 throw "$file does not exist" if not -e $file;
176              
177 161         9996 return ( stat $file )[9];
178             }
179              
180             #-------------------------------------------------------------------------------
181              
182              
183             sub md5 {
184 161     161 1 5820 my ($file) = @_;
185              
186 161 50       474 throw 'Must supply a file' if not $file;
187 161 50       1116 throw "$file does not exist" if not -e $file;
188              
189 161         5305 my $fh = $file->openr();
190 161         28769 my $md5 = Digest::MD5->new->addfile($fh)->hexdigest();
191              
192 161         2319 return $md5;
193             }
194              
195             #-------------------------------------------------------------------------------
196              
197              
198             sub sha256 {
199 443     443 1 52344 my ($file) = @_;
200              
201 443 50       2454 throw 'Must supply a file' if not $file;
202 443 50       4218 throw "$file does not exist" if not -e $file;
203              
204 443         30290 my $fh = $file->openr();
205 443         77236 my $sha256 = Digest::SHA->new(256)->addfile($fh)->hexdigest();
206              
207 443         36583 return $sha256;
208             }
209              
210             #-------------------------------------------------------------------------------
211              
212              
213             sub validate_property_name {
214 269     269 1 606 my ($prop_name) = @_;
215              
216 269 100       1624 throw "Invalid property name $prop_name" if $prop_name !~ $PINTO_PROPERTY_NAME_REGEX;
217              
218 268         3071 return $prop_name;
219             }
220              
221             #-------------------------------------------------------------------------------
222              
223              
224             sub validate_stack_name {
225 0     0 1 0 my ($stack_name) = @_;
226              
227 0 0       0 throw "Invalid stack name $stack_name" if $stack_name !~ $PINTO_STACK_NAME_REGEX;
228              
229 0         0 return $stack_name;
230             }
231              
232             #-------------------------------------------------------------------------------
233              
234              
235             sub current_utc_time {
236              
237             ## no critic qw(PackageVars)
238 519 100   519 1 2271 return $Pinto::Globals::current_utc_time
239             if defined $Pinto::Globals::current_utc_time;
240              
241 515         2186 return time;
242             }
243              
244             #-------------------------------------------------------------------------------
245              
246              
247             sub current_time_offset {
248              
249             ## no critic qw(PackageVars)
250 76 100   76 1 320 return $Pinto::Globals::current_time_offset
251             if defined $Pinto::Globals::current_time_offset;
252              
253 75         324 my $now = current_utc_time;
254 75         1240 my $time = DateTime->from_epoch( epoch => $now, time_zone => 'local' );
255              
256 75         115774 return $time->offset;
257             }
258              
259             #-------------------------------------------------------------------------------
260              
261              
262             sub current_username {
263              
264             ## no critic qw(PackageVars)
265 85 100   85 1 3664 return $Pinto::Globals::current_username
266             if defined $Pinto::Globals::current_username;
267              
268 9   33     160 my $username = $ENV{PINTO_USERNAME} || $ENV{USER} || $ENV{LOGIN} || $ENV{USERNAME} || $ENV{LOGNAME};
269              
270 9 50       52 throw "Unable to determine your username. Set PINTO_USERNAME." if not $username;
271              
272 0         0 return $username;
273             }
274              
275             #-------------------------------------------------------------------------------
276              
277              
278             sub current_author_id {
279              
280             ## no critic qw(PackageVars)
281 20 50   20 1 497 return $Pinto::Globals::current_author_id
282             if defined $Pinto::Globals::current_author_id;
283              
284 0         0 my $author_id = $ENV{PINTO_AUTHOR_ID};
285 0 0       0 return uc $author_id if $author_id;
286              
287 0         0 my $username = current_username;
288 0         0 $username =~ s/[^a-zA-Z0-9]//g;
289              
290 0         0 return uc $username;
291             }
292              
293             #-------------------------------------------------------------------------------
294              
295              
296             sub is_interactive {
297              
298             ## no critic qw(PackageVars)
299 78 50   78 1 626 return $Pinto::Globals::is_interactive
300             if defined $Pinto::Globals::is_interactive;
301              
302 0         0 return -t STDOUT;
303             }
304              
305             #-------------------------------------------------------------------------------
306              
307              
308             sub interpolate {
309 0     0 1 0 my $string = shift;
310              
311 0         0 return eval qq{"$string"}; ## no critic qw(Eval)
312             }
313              
314             #-------------------------------------------------------------------------------
315              
316              
317             sub trim_text {
318 119     119 1 323 my $string = shift;
319              
320 119         338 $string =~ s/^ \s+ //x;
321 119         332 $string =~ s/ \s+ $//x;
322              
323 119         1158 return $string;
324             }
325              
326             #-------------------------------------------------------------------------------
327              
328              
329             sub title_text {
330 14     14 1 307 my $string = shift;
331              
332 14         55 my $nl = index $string, "\n";
333 14 100       93 return $nl < 0 ? $string : substr $string, 0, $nl;
334             }
335              
336             #-------------------------------------------------------------------------------
337              
338              
339             sub body_text {
340 5     5 1 80 my $string = shift;
341              
342 5         18 my $nl = index $string, "\n";
343 5 100 66     59 return '' if $nl < 0 or $nl == length $string;
344 2         13 return substr $string, $nl + 1;
345             }
346              
347             #-------------------------------------------------------------------------------
348              
349              
350             sub truncate_text {
351 2     2 1 5 my ( $string, $max_length, $elipses ) = @_;
352              
353 2 50       6 return $string if not $max_length;
354 2 50       12 return $string if length $string <= $max_length;
355              
356 0 0       0 $elipses = '...' if not defined $elipses;
357              
358 0         0 my $truncated = substr $string, 0, $max_length;
359              
360 0         0 return $truncated . $elipses;
361             }
362              
363             #-------------------------------------------------------------------------------
364              
365              
366             sub decamelize {
367 378     378 1 655 my $string = shift;
368              
369 378 50       1007 return if not defined $string;
370              
371 378         1172 $string =~ s/ ([a-z]) ([A-Z]) /$1_$2/xg;
372              
373 378         1120 return lc $string;
374             }
375              
376             #-------------------------------------------------------------------------------
377              
378              
379             sub indent_text {
380 3     3 1 9 my ( $string, $spaces ) = @_;
381              
382 3 50       13 return $string if not $spaces;
383 3 50       7 return $string if not $string;
384              
385 3         13 my $indent = ' ' x $spaces;
386 3         14 $string =~ s/^ /$indent/xmg;
387              
388 3         11 return $string;
389             }
390              
391             #-------------------------------------------------------------------------------
392              
393              
394             sub mksymlink {
395 253     253 1 18173 my ( $from, $to ) = @_;
396              
397             # TODO: Try to add Win32 support here, somehow.
398 253         1023 debug "Linking $to to $from";
399 253 50       913 symlink $to, $from or throw "symlink to $to from $from failed: $!";
400              
401 253         17014 return 1;
402             }
403              
404             #-------------------------------------------------------------------------------
405              
406              
407             sub is_system_prop {
408 0     0 1 0 my $string = shift;
409              
410 0 0       0 return 0 if not $string;
411 0         0 return $string =~ m/^ pinto- /x;
412             }
413              
414             #-------------------------------------------------------------------------------
415              
416              
417             sub uuid {
418 183     183 1 2383 return UUID::Tiny::create_uuid_as_string(UUID::Tiny::UUID_V4);
419             }
420              
421             #-------------------------------------------------------------------------------
422              
423              
424             sub user_palette {
425             my $palette = $ENV{PINTO_PALETTE}
426 6   33 6 1 243 || $ENV{PINTO_COLORS} || $ENV{PINTO_COLOURS}; # For backcompat
427              
428 6 100       86 return $PINTO_DEFAULT_PALETTE if not $palette;
429              
430 1         31 return [ split m/\s* , \s*/x, $palette ];
431             }
432              
433             #-------------------------------------------------------------------------------
434              
435              
436             sub is_blank {
437 74     74 1 220 my ($string) = @_;
438              
439 74 50       265 return 1 if not $string;
440 74 100       2541 return 0 if $string =~ m/ \S /x;
441 2         17 return 1;
442             }
443              
444             #-------------------------------------------------------------------------------
445              
446              
447             sub is_not_blank {
448 73     73 1 264 my ($string) = @_;
449              
450 73         413 return !is_blank($string);
451             }
452              
453             #-------------------------------------------------------------------------------
454              
455              
456             sub mask_uri_passwords {
457 48     48 1 76 my ($uri) = @_;
458              
459 48         78 $uri =~ s{ (https?://[^:/@]+ :) [^@/]+@}{$1*password*@}gx;
460              
461 48         107 return $uri;
462             }
463              
464             #-------------------------------------------------------------------------------
465              
466              
467             sub is_remote_repo {
468 0     0 1 0 my ($uri) = @_;
469              
470 0 0       0 return if not $uri;
471 0         0 return $uri =~ m{^https?://}x;
472             }
473              
474             #-------------------------------------------------------------------------------
475              
476             sub tempdir {
477              
478 124     124 0 4973 return Path::Class::dir(File::Temp::tempdir(CLEANUP => 1));
479             }
480              
481              
482             #-------------------------------------------------------------------------------
483              
484              
485             sub default_diff_style {
486              
487 20 100   20 0 113 if (my $style = $ENV{PINTO_DIFF_STYLE}) {
488              
489 11 50       75 throw "PINTO_DIFF_STYLE ($style) is invalid. Must be one of (@PINTO_DIFF_STYLES)"
490             unless DiffStyle->check($style);
491              
492 11         10597 return $style;
493             }
494              
495 9         82 return $PINTO_DIFF_STYLE_CONCISE;
496             }
497              
498             #-------------------------------------------------------------------------------
499              
500             sub make_uri {
501 196     196 0 614 my ($it) = @_;
502              
503 196 100       1222 return $it
504             if itis( $it, 'URI' );
505              
506 161 50       629 return URI::file->new( $it->absolute )
507             if itis( $it, 'Path::Class::File' );
508              
509 0 0         return URI::file->new( file($it)->absolute )
510             if -e $it;
511              
512 0           return URI->new($it);
513             }
514              
515             #-------------------------------------------------------------------------------
516             1;
517              
518             __END__
519              
520             =pod
521              
522             =encoding UTF-8
523              
524             =for :stopwords Jeffrey Ryan Thalhammer
525              
526             =head1 NAME
527              
528             Pinto::Util - Static utility functions for Pinto
529              
530             =head1 VERSION
531              
532             version 0.14
533              
534             =head1 DESCRIPTION
535              
536             This is a private module for internal use only. There is nothing for
537             you to see here (yet). All API documentation is purely for my own
538             reference.
539              
540             =head1 FUNCTIONS
541              
542             =head2 throw($message)
543              
544             =head2 throw($exception_object)
545              
546             Throws a L<Pinto::Exception> with the given message. If given a reference
547             to a L<Pinto::Exception> object, then it just throws it again.
548              
549             =head2 debug( $message )
550              
551             =head2 debug( sub {...} )
552              
553             Writes the message on STDERR if the C<PINTO_DEBUG> environment variable is true.
554             If the argument is a subroutine, it will be invoked and its output will be
555             written instead. Always returns true.
556              
557             =head2 whine( $message )
558              
559             Just calls warn(), but always appends the newline so that line numbers are
560             suppressed.
561              
562             =head2 author_dir( @base, $author )
563              
564             Given the name of an C<$author>, returns the directory where the
565             distributions for that author belong (as a L<Path::Class::Dir>). The
566             optional C<@base> can be a series of L<Path::Class:Dir> or path parts
567             (as strings). If C<@base> is given, it will be prepended to the
568             directory that is returned.
569              
570             =head2 itis( $var, $class )
571              
572             Asserts whether var is a blessed reference and is an instance of the
573             C<$class>.
574              
575             =head2 parse_dist_path( $path )
576              
577             Parses a path like the ones you would see in a full URI to a
578             distribution in a CPAN repository, or the URI fragment you would see
579             in a CPAN index. Returns the author and file name of the
580             distribution. Subdirectories between the author name and the file
581             name are discarded.
582              
583             =head2 isa_perl( $path_or_uri )
584              
585             Return true if C<$path_or_uri> appears to point to a release of perl
586             itself. This is based on some file naming patterns that I've seen in
587             the wild. It may not be completely accurate.
588              
589             =head2 mtime( $file )
590              
591             Returns the last modification time (in epoch seconds) for the C<file>.
592             The argument is required and the file must exist or an exception will
593             be thrown.
594              
595             =head2 md5( $file )
596              
597             Returns the C<MD-5> digest (as a hex string) for the C<$file>. The
598             argument is required and the file must exist on an exception will be
599             thrown.
600              
601             =head2 sha256( $file )
602              
603             Returns the C<SHA-256> digest (as a hex string) for the C<$file>. The
604             argument is required and the file must exist on an exception will be
605             thrown.
606              
607             =head2 validate_property_name( $prop_name )
608              
609             Throws an exception if the property name is invalid. Currently,
610             property names must be alphanumeric plus any underscores or hyphens.
611              
612             =head2 validate_stack_name( $stack_name )
613              
614             Throws an exception if the stack name is invalid. Currently, stack
615             names must be alphanumeric plus underscores or hyphens.
616              
617             =head2 current_utc_time()
618              
619             Returns the current time (in epoch seconds) unless the current time has been
620             overridden by C<$Pinto::Globals::current_utc_time>.
621              
622             =head2 current_time_offset()
623              
624             Returns the offset between current UTC time and the local time in
625             seconds, unless overridden by C<$Pinto::Globals::current_time_offset>.
626             The C<current_time> function is used to determine the current UTC
627             time.
628              
629             =head2 current_username()
630              
631             Returns the username of the current user unless it has been overridden by
632             C<$Pinto::Globals::current_username>. The username can be defined through
633             a number of environment variables. Throws an exception if no username
634             can be determined.
635              
636             =head2 current_author_id()
637              
638             Returns the author id of the current user unless it has been overridden by
639             C<$Pinto::Globals::current_author_id>. The author id can be defined through
640             environment variables. Otherwise it defaults to the upper-case form of the
641             C<current_username>. And since PAUSE only allows letters and numbers in the
642             author id, then we remove all of those from the C<current_username> too.
643              
644             =head2 is_interactive()
645              
646             Returns true if the process is connected to an interactive terminal
647             (i.e. a keyboard & screen) unless it has been overridden by
648             C<$Pinto::Globals::is_interactive>.
649              
650             =head2 interpolate($string)
651              
652             Performs interpolation on a literal string. The string should not
653             include anything that looks like a variable. Only metacharacters
654             (like \n) will be interpolated correctly.
655              
656             =head2 trim_text($string)
657              
658             Returns the string with all leading and trailing whitespace removed.
659              
660             =head2 title_text($string)
661              
662             Returns all the characters in C<$string> before the first newline. If
663             there is no newline, returns the entire C<$string>.
664              
665             =head2 body_text($string)
666              
667             Returns all the characters in C<$string> after the first newline. If
668             there is no newline, returns an empty string.
669              
670             =head2 truncate_text($string, $length, $elipses)
671              
672             Truncates the C<$string> and appends C<$elipses> if the C<$string> is
673             longer than C<$length> characters. C<$elipses> defaults to '...' if
674             not specified.
675              
676             =head2 decamelize($string)
677              
678             Returns the string forced to lower case and words separated by underscores.
679             For example C<FooBar> becomes C<foo_bar>.
680              
681             =head2 indent_text($string, $n)
682              
683             Returns a copy of C<$string> with each line indented by C<$n> spaces.
684             In other words, it puts C<4n> spaces immediately after each newline
685             in C<$string>. The original C<$string> is not modified.
686              
687             =head2 mksymlink($from => $to)
688              
689             Creates a symlink between the two files. No checks are performed to see
690             if either path is valid or already exists. Throws an exception if the
691             operation fails or is not supported.
692              
693             =head2 is_system_prop($string)
694              
695             Returns true if C<$string> is the name of a system property.
696              
697             =head2 uuid()
698              
699             Returns a UUID as a string. Currently, the UUID is derived from
700             random numbers.
701              
702             =head2 user_palette()
703              
704             Returns a reference to an array containing the names of the colors pinto
705             can use. This can be influenced by setting the C<PINTO_PALETTE> environment
706             variable.
707              
708             =head2 is_blank($string)
709              
710             Returns true if the string is undefined, empty, or contains only whitespace.
711              
712             =head2 is_not_blank($string)
713              
714             Returns true if the string contains any non-whitespace characters.
715              
716             =head2 mask_uri_passwords($string)
717              
718             Masks the parts the string that look like a password embedded in an http or
719             https URI. For example, C<http://joe:secret@foo.com> would return
720             C<http://joe:*password*@foo.com>
721              
722             =head2 is_remote_repo {
723              
724             Returns true if the argument looks like a URI to a remote repository
725              
726             =head1 AUTHOR
727              
728             Jeffrey Ryan Thalhammer <jeff@stratopan.com>
729              
730             =head1 COPYRIGHT AND LICENSE
731              
732             This software is copyright (c) 2015 by Jeffrey Ryan Thalhammer.
733              
734             This is free software; you can redistribute it and/or modify it under
735             the same terms as the Perl 5 programming language system itself.
736              
737             =cut