File Coverage

blib/lib/Mojar/Util.pm
Criterion Covered Total %
statement 104 154 67.5
branch 38 82 46.3
condition 49 61 80.3
subroutine 17 22 77.2
pod 8 13 61.5
total 216 332 65.0


line stmt bran cond sub pod time code
1             package Mojar::Util;
2 3     3   42107 use Mojo::Base -strict;
  3         7  
  3         18  
3              
4             our $VERSION = 0.371;
5              
6 3     3   343 use B;
  3         6  
  3         124  
7 3     3   16 use Carp 'croak';
  3         5  
  3         115  
8 3     3   16 use Exporter 'import';
  3         6  
  3         81  
9 3     3   1180 use Mojo::File;
  3         271883  
  3         129  
10 3     3   27 use Scalar::Util 'reftype';
  3         8  
  3         108  
11 3     3   1892 use Storable 'dclone';
  3         7162  
  3         457  
12              
13             our @EXPORT_OK = qw(as_bool been_numeric check_exists dumper hash_or_hashref
14             loaded_path lc_keys merge slurp_chomped snakecase spurt transcribe
15             unsnakecase);
16              
17             # Public functions
18              
19             sub as_bool {
20 13     13 1 805 my ($val) = shift;
21 13 100 100     32 return !! $val if been_numeric($val) or not defined $val;
22 11         26 $val = lc "$val";
23 11 100 100     82 return !! 1
      100        
      100        
24             if $val eq '1' or $val eq 'true' or $val eq 'yes' or $val eq 'on';
25             return !! undef
26 7 100 100     55 if $val eq '0' or $val eq 'false' or $val eq 'no' or $val eq 'off';
      100        
      100        
27 3         13 return !! $val;
28             }
29              
30             sub dumper {
31 3     3   20 no warnings 'once';
  3         6  
  3         5551  
32 11     11 1 1658 require Data::Dumper;
33 11         21 local $Data::Dumper::Terse = 1;
34 11         18 local $Data::Dumper::Indent = 1;
35 11         18 local $Data::Dumper::Quotekeys = 0;
36 11         18 local $Data::Dumper::Sortkeys = 1;
37 11         37 my $dump = Data::Dumper::Dumper(@_);
38 11         570 $dump =~ s/\n\z//;
39 11         49 return $dump;
40             }
41              
42             sub lc_keys {
43 0     0 0 0 my ($hr) = @_;
44 0 0       0 croak q{Missing required hashref} unless reftype $hr eq 'HASH';
45 0         0 %$hr = map +(lc $_ => $$hr{$_}), keys %$hr;
46 0         0 return $hr;
47             }
48              
49             sub slurp_chomped {
50 0     0 0 0 my $t = Mojo::File->new(@_)->slurp;
51 0         0 () while chomp $t;
52 0         0 $t
53             }
54              
55             sub snakecase {
56 20     20 1 2357 my ($string, $syllable_sep) = @_;
57 20   100     68 $syllable_sep //= '_';
58 20 50       46 return undef unless defined $string;
59            
60 20         29 my @words;
61             # Absorb any leading lowercase chars
62 20 100       59 push @words, $1 if $string =~ s/^([^A-Z]+)//;
63             # Absorb each titlecase substring
64 20         172 push @words, lcfirst $1 while $string =~ s/\A([A-Z][^A-Z]*)//;
65 20         50 for (0 .. $#words - 1) {
66 15 100       55 $words[$_] .= $syllable_sep unless $words[$_] =~ /[^a-z]$/;
67             }
68 20         98 return join '', @words;
69             }
70              
71             sub unsnakecase {
72 27     27 1 3946 my ($string, $separator, $want_camelcase) = @_;
73 27   100     113 $separator //= '_';
74 27 50       57 return undef unless defined $string;
75            
76 27         38 my @words;
77             # Absorb any leading separators
78 27 100       169 push @words, $1 if $string =~ s/\A(\Q$separator\E+)//;
79             # Absorb any leading component if doing camelcase
80 27 100 66     93 if ($want_camelcase
81             and $string =~ s/\A([^\Q$separator\E]+)\Q$separator\E?//) {
82 2         5 push @words, $1;
83 2 50       14 push @words, $1 if $string =~ s/\A(\Q$separator\E+)//;
84             }
85             # Absorb each substring as titlecase
86 27         184 while ($string =~ s/\A([^\Q$separator\E]+)\Q$separator\E?//) {
87 37         98 push @words, ucfirst lc $1;
88 37 100       209 push @words, $1 if $string =~ s/\A(\Q$separator\E+)//;
89             }
90             # Fix any trailing separators
91 27 100 100     145 $words[-1] .= $separator if @words && $words[-1] =~ /\A\Q$separator\E/;
92 27         138 return join '', @words;
93             }
94              
95             sub transcribe {
96 14     14 1 1533 my $string = shift;
97 14 100       39 my $translator = ref $_[-1] eq 'CODE' ? pop : undef;
98 14 100       42 return undef unless defined $string;
99              
100 13         28 my $parts = [ $string ]; # arrayref tree with strings at leaves
101 13         24 my @joiners = (); # joining string for each level
102 13         29 my @level_parts = ( $parts ); # array of arrayrefs, each containing a string
103 13         17 my @next_level_parts = (); # array of arrayrefs, each containing a string
104 13         23 my ($old, $new);
105 13   66     66 while (($old, $new) = (shift, shift) and defined $new) {
106 20         38 push @joiners, $new;
107 20         29 foreach my $p (@level_parts) {
108             # $p is arrayref containing a string
109 35         219 my @components = split /\Q$old/, $p->[0], -1;
110             # Modify $parts tree
111 35   50     176 @$p = map [ $_ // '' ], @components;
112             # $p is arrayref containing arrayrefs, each containing a string
113             # Set up next level
114 35         81 push @next_level_parts, @$p;
115             }
116 20         40 @level_parts = @next_level_parts;
117 20         89 @next_level_parts = ();
118             }
119 13   100     47 while ($translator and my $p = shift @level_parts) {
120 22         62 $p->[0] = $translator->($p->[0]);
121             }
122              
123 13         30 my @traverse = ( [0, $parts] );
124 13         34 while (my $next = pop @traverse) {
125 42         72 my ($depth, $ref) = @$next[0,1];
126 42 100       95 if (ref $$ref[0]) {
127 41 100       106 if (my @deeper = grep ref($_->[0]), @$ref) {
128             # Found some children not ready to be joined
129 9         46 push @traverse, [$depth, $ref], map [$depth + 1, $_], @deeper;
130             }
131             else {
132             # Children all strings => join them
133 32   100     208 @$ref = join $joiners[$depth], map +($_->[0] //= ''), @$ref;
134             }
135             }
136             # else string => do nothing
137             }
138              
139 13   100     107 return $parts->[0] // '';
140             }
141              
142             sub loaded_path {
143 0     0 0 0 my ($self) = @_;
144             # Try .pm
145 0   0     0 (my $module = (ref $self // $self) .'.pm') =~ s{::}{/};
146 0 0       0 return $INC{$module} if exists $INC{$module};
147              
148             # Try .pl
149 0         0 $module =~ s{\.pm$}{.pl};
150 0 0       0 return $INC{$module} if exists $INC{$module};
151              
152 0         0 return undef;
153             }
154              
155             sub been_numeric {
156 13     13 1 21 my $value = shift;
157             # From Mojo::JSON
158 13 50 66     172 return 1 if B::svref_2object(\$value)->FLAGS & (B::SVp_IOK | B::SVp_NOK)
      66        
159             and 0 + $value eq $value and $value * 0 == 0;
160             }
161              
162             sub spurt (@) {
163 4     4 0 3422 my $path = shift;
164 4 100       15 my $lines = ref $_[-1] eq 'ARRAY' ? pop : \@_;
165 4         7 my $count = 0;
166              
167 4 50       213 die qq{Can't open file "$path": $!} unless open my $file, '>', $path;
168 4         29 $file->syswrite('');
169 4         52 local $_;
170 4   33     14 $file->syswrite($_), $file->syswrite($/) and ++$count for @$lines;
171 4         178 close $file;
172 4         29 return $count;
173             }
174              
175             sub hash_or_hashref {
176 10 100   10 1 1594 return { @_ } if @_ % 2 == 0; # hash
177 5 50 66     34 return $_[0] if ref $_[0] eq 'HASH' or reftype $_[0] eq 'HASH';
178 0         0 croak sprintf 'Hash not identified (%s)', join ',', @_;
179             }
180              
181             sub check_exists {
182 6     6 1 3680 my $requireds = shift;
183 6         15 my $param = hash_or_hashref(@_);
184 6 100       23 $requireds = [$requireds] unless ref $requireds eq 'ARRAY';
185              
186 6   66     315 exists $param->{$_} or croak "Missing required param ($_)" for @$requireds;
187 4         25 return @$param{@$requireds};
188             }
189              
190             # Private function
191             sub _merge ($;$) {
192 0     0     my ($left, $right) = @_;
193 0 0         if (reftype $left eq 'ARRAY') {
194 0 0         if (reftype $right eq 'ARRAY') {
195 0           %{$left->[0]} = (%{$left->[0]}, %{ dclone($right->[0]) });
  0            
  0            
  0            
196             }
197             else {
198             # $right : HASHREF
199 0           %{$left->[0]} = (%{$left->[0]}, %{ dclone($right) });
  0            
  0            
  0            
200             }
201             }
202             else {
203             # $left : HASHREF
204 0 0         if (reftype($right) eq 'ARRAY') {
205 0           %$left = (%$left, %{ dclone($right->[0]) });
  0            
206             }
207             else {
208             # $right : HASHREF
209 0           %$left = (%$left, %{ dclone($right) });
  0            
210             }
211             }
212 0           return $left;
213             }
214              
215             sub merge (@);
216             sub merge (@) {
217             # Both class & object function
218             # my $class = (@_ and not ref $_[0]) ? shift : undef;
219 0 0   0 0   my $class = shift unless ref $_[0];
220             # defined($class) <=> class method
221 0 0         return undef unless @_;
222 0           my $left = shift;
223              
224             # $left is a ref; @right could be various things
225              
226             # If called as object method
227             # 'owning' (ie leftmost) object gets modified
228             # If called as class method
229             # a new object is created for the result
230              
231             # It is important that the merge associates to the left
232             # [ie ($a merge $b) merge $c], in contrast to Hash::Util::Simple.
233              
234             # class method => new object
235             # this is done at most once per original call
236 0 0         if ($class) {
237 0 0         if ($left->can('clone')) {
    0          
    0          
238 0           return merge $left->clone, @_;
239             }
240             elsif ($left->can('new')) {
241 0           return merge $left->new, @_;
242             }
243             elsif (ref $left eq 'HASH') {
244 0           $left = dclone($left);
245             }
246             else {
247 0           croak "Unable to clone first argument\n". dumper $left;
248             }
249             }
250              
251             # Base case
252 0 0         unless (@_) {
    0          
    0          
    0          
253 0           return $left;
254             }
255             # Recurse
256 0 0         elsif (@_ == 1 and ref $_[0]) {
257             # object or maybe hash ref
258 0           return _merge($left, $_[0]);
259             }
260 0 0         elsif (@_ > 1 and ref $_[0]) {
261             # object or maybe hash ref
262 0           my $right = shift;
263 0           return merge _merge($left, $right), @_;
264             }
265 0 0         elsif (@_ > 1 and @_ % 2 == 0) {
266             # assume plain hash
267 0           return _merge($left, { @_ });
268             }
269             else {
270 0           croak 'Tried to merge incompatible/non-object'. $/ . dumper(@_);
271             }
272             }
273              
274             1;
275             __END__