File Coverage

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