File Coverage

blib/lib/Data/Tubes/Util.pm
Criterion Covered Total %
statement 221 236 93.6
branch 101 136 74.2
condition 24 34 70.5
subroutine 34 34 100.0
pod 19 19 100.0
total 399 459 86.9


line stmt bran cond sub pod time code
1             package Data::Tubes::Util;
2              
3             # vim: ts=3 sts=3 sw=3 et ai :
4              
5 37     37   169469 use strict;
  37         98  
  37         1197  
6 37     37   192 use warnings;
  37         73  
  37         1141  
7 37     37   1199 use English qw< -no_match_vars >;
  37         8046  
  37         296  
8 37     37   13049 use Exporter 'import';
  37         81  
  37         2089  
9             our $VERSION = '0.737';
10              
11 37     37   1514 use Log::Log4perl::Tiny qw< :easy :dead_if_first >;
  37         34361  
  37         243  
12              
13             our @EXPORT_OK = qw<
14             args_array_with_options
15             assert_all_different
16             generalized_hashy
17             load_module
18             load_sub
19             metadata
20             normalize_args
21             normalize_filename
22             pump
23             read_file
24             read_file_maybe
25             resolve_module
26             shorter_sub_names
27             sprintffy
28             test_all_equal
29             traverse
30             trim
31             tube
32             unzip
33             >;
34              
35             sub _load_module {
36 13     13   29 my $module = shift;
37 13         75 (my $packfile = $module . '.pm') =~ s{::}{/}gmxs;
38 13         4278 require $packfile;
39 13         253 return $module;
40             } ## end sub _load_module
41              
42             sub args_array_with_options {
43 111     111 1 585 my %defaults = %{pop @_};
  111         497  
44 111 100 100     759 %defaults = (%defaults, %{pop @_})
  55         216  
45             if @_ && (ref($_[-1]) eq 'HASH');
46 111         456 return ([@_], \%defaults);
47             } ## end sub args_array_with_options
48              
49             sub assert_all_different {
50 24 50 33 24 1 98 my $keys = (@_ && ref($_[0])) ? $_[0] : \@_;
51 24         38 my %flag_for;
52 24         50 for my $key (@$keys) {
53 75 50       242 die {message => $key} if $flag_for{$key}++;
54             }
55 24         98 return 1;
56             } ## end sub assert_all_different
57              
58             sub _compile_capture {
59 9     9   37 my %h = @_;
60 37     37   23481 use feature 'state';
  37         101  
  37         15708  
61              
62 9         24 state $quoted = qr{(?mxs:
63             (?: "(?: [^\\"]+ | \\. )*") # double quotes
64             | (?: '[^']*') # single quotes
65             )};
66              
67             my ($key, $value, $kvs, $cs) =
68 9         33 @h{qw< key value key_value_separator chunks_separator>};
69              
70 9 50       40 if (!defined($key)) {
71 9         18 my $admitted = $h{key_admitted};
72 9 50       28 $admitted = qr{[\Q$admitted\E]} unless ref $admitted;
73 9         421 $key = qr{(?mxs: $quoted | (?:(?:$admitted | \\.)+?))};
74             }
75              
76 9 50       36 if (!defined($value)) {
77 9         17 my $admitted = $h{value_admitted};
78 9 50       24 $admitted = qr{[\Q$admitted\E]} unless ref $admitted;
79 9         267 $value = qr{(?mxs: $quoted | (?:(?:$admitted | \\.)+?))};
80             }
81              
82 9         73 my $close = qr{(?<close>$h{close})};
83 9         663 return qr{(?mxs:
84             (?: (?<key> $key) $kvs)? # optional key with kv-separator
85             (?<value> $value) # a value, for sure
86             (?: $close | $cs $close?) # close or chunk separator next
87             )};
88             } ## end sub _compile_capture
89              
90             sub generalized_hashy {
91 37     37   284 use feature 'state';
  37         92  
  37         76667  
92 12     12 1 4239 state $admitted_default = qr{[^\\'":=\s,;\|/]};
93             state $kvdecoder = sub {
94 37     37   131 my $kv = shift;
95 37         79 my $first = substr $kv, 0, 1;
96 37 100 100     131 $kv = substr $kv, 1, length($kv) - 2
97             if ($first eq q{'}) || ($first eq q{"});
98 37 100       114 $kv =~ s{\\(.)}{$1}gmxs unless $first eq q{'};
99 37         80 return $kv;
100 12         35 };
101             state $default_handler_for = {
102             open => qr{(?mxs: \s* )},
103             key_value_separator => qr{(?mxs: \s* [:=] \s*)},
104             chunks_separator => qr{(?mxs: \s* [\s,;\|/] \s*)},
105             close => qr{(?mxs: \s*\z)},
106             key_admitted => $admitted_default,
107             value_admitted => $admitted_default,
108             key_decoder => $kvdecoder,
109             value_decoder => $kvdecoder,
110             key_duplicate => sub {
111 1     1   4 my ($h, $k, $v) = @_;
112 1 50       10 $h->{$k} = [$h->{$k}] unless ref $h->{$k};
113 1         3 push @{$h->{$k}}, $v;
  1         5  
114             },
115 12         52 };
116 12         46 my $args = normalize_args(@_, [$default_handler_for, 'text']);
117             $args->{key_default} = delete $args->{default_key}
118 12 100       51 if exists $args->{default_key};
119 12         26 my $text = $args->{text};
120              
121 12         91 my %h = (%$default_handler_for, %$args);
122 12   66     79 my $capture = $h{capture} ||= _compile_capture(%h);
123 12         47 my %retval = (capture => $capture);
124 12 100       54 return {%retval, failure => 'undefined input'} unless defined $text;
125              
126 9         20 my $len = length $text;
127 9   50     54 pos($text) = my $startpos = $args->{pos} || 0;
128 9         51 %retval = (%retval, pos => $startpos, res => ($len - $startpos));
129              
130             # let's check open first, no need to define anything otherwise
131 9 50       89 $text =~ m{\G$h{open}}gmxs or return {%retval, failure => 'no opening'};
132              
133             my ($dkey, $dupkey, $kdec, $vdec) =
134 9         37 @h{qw< key_default key_duplicate key_decoder value_decoder >};
135 9         19 my ($closed, %hash);
136 9   66     47 while (!$closed && pos($text) < length($text)) {
137 23         48 my $pos = pos($text);
138 23 100       924 $text =~ m{\G$capture}gcmxs
139             or return {
140             %retval,
141             failure => "failed match at $pos",
142             failpos => $pos
143             };
144              
145             my $key =
146             exists($+{key}) ? ($kdec ? $kdec->($+{key}) : $+{key})
147 22 50       236 : defined($dkey) ? (ref($dkey) ? $dkey->() : $dkey)
    50          
    100          
    100          
148             : undef;
149             return {
150 22 100       76 %retval,
151             failure => 'stand-alone value, no default key set',
152             failpos => $pos
153             }
154             unless defined $key;
155              
156 21 50       70 my $value = $vdec ? $vdec->($+{value}) : $+{value};
157              
158 21 100       78 if (!exists $hash{$key}) {
    50          
159 19         51 $hash{$key} = $value;
160             }
161             elsif ($dupkey) {
162 2         7 $dupkey->(\%hash, $key, $value);
163             }
164             else {
165             return {
166 0         0 %retval,
167             failure => "duplicate key $key",
168             failpos => $pos
169             };
170             } ## end else [ if (!exists $hash{$key...})]
171              
172 20         124 $closed = exists $+{close};
173             } ## end while (!$closed && pos($text...))
174              
175 6 50       18 return {%retval, failure => 'no closure found'} unless $closed;
176              
177 6         14 my $pos = pos $text;
178             return {
179 6         73 %retval,
180             pos => $pos,
181             res => ($len - $pos),
182             hash => \%hash,
183             };
184             } ## end sub generalized_hashy
185              
186             sub load_module {
187 7     7 1 23 return _load_module(resolve_module(@_));
188             } ## end sub load_module
189              
190             sub load_sub {
191 18     18 1 47 my ($locator, $prefix) = @_;
192 18 50       161 my ($module, $sub) =
193             ref($locator) ? @$locator : $locator =~ m{\A(.*)::(\w+)\z}mxs;
194 18         70 $module = resolve_module($module, $prefix);
195              
196             # optimistic first
197 18   66     235 return $module->can($sub) // _load_module($module)->can($sub);
198             } ## end sub load_sub
199              
200             sub metadata {
201 3     3 1 6 my $input = shift;
202 3         17 my %args = normalize_args(
203             @_,
204             {
205             chunks_separator => ' ',
206             key_value_separator => '=',
207             default_key => '',
208             }
209             );
210              
211             # split data into chunks, un-escape on the fly
212 3         11 my $separator = $args{chunks_separator};
213 3         7 my $qs = quotemeta($separator);
214 3         78 my $regexp = qr/((?:\\.|[^\\$qs])+)(?:$qs+)?/;
215 3         47 my @chunks = map { s{\\(.)}{$1}g; $_ } $input =~ m{$regexp}gc;
  7         18  
  7         18  
216              
217             # ensure we consumed the whole $input
218 3 50       12 die {message =>
219             "invalid metadata (separator: '$separator', input: [$input])\n"
220             }
221             if pos($input) < length($input);
222              
223 3         9 $separator = $args{key_value_separator};
224             return {
225             map {
226 3         8 my ($k, $v) = _split_pair($_, $separator);
  7         18  
227 7 100       40 defined($v) ? ($k, $v) : ($args{default_key} => $k);
228             } @chunks
229             };
230             } ## end sub metadata
231              
232             sub normalize_args {
233 225     225 1 4384 my $defaults = pop(@_);
234              
235 225         383 my %retval;
236 225 100       871 if (ref($defaults) eq 'ARRAY') {
237 184         403 ($defaults, my $key) = @$defaults;
238 184 100 66     923 $retval{$key} = shift(@_)
239             if (scalar(@_) % 2) && (ref($_[0]) ne 'HASH');
240             }
241             %retval = (
242             %$defaults, # defaults go first
243             %retval, # anything already present goes next
244 225 100 100     1941 ((@_ && ref($_[0]) eq 'HASH') ? %{$_[0]} : @_), # then... the rest
  10         37  
245             );
246              
247 225 100       1350 return %retval if wantarray();
248 33         79 return \%retval;
249             } ## end sub normalize_args
250              
251             sub normalize_filename {
252 25     25 1 56 my ($filename, $default_handle) = @_;
253 25 100       85 return unless defined $filename;
254 24 100       71 return $filename if ref($filename) eq 'GLOB';
255 23 100       60 return $filename if ref($filename) eq 'SCALAR';
256 14 50       30 return $default_handle if $filename eq '-';
257 14 50       47 return $filename if $filename =~ s{\Afile:}{}mxs;
258 14 50       56 if (my ($handlename) = $filename =~ m{\Ahandle:(?:std)?(.*)\z}imxs) {
259 0         0 $handlename = lc $handlename;
260 0 0       0 return \*STDOUT if $handlename eq 'out';
261 0 0       0 return \*STDIN if $handlename eq 'err';
262 0 0       0 return \*STDERR if $handlename eq 'in';
263 0         0 LOGDIE "normalize_filename: invalid filename '$filename', "
264             . "use 'file:$filename' if name is correct";
265             } ## end if (my ($handlename) =...)
266 14         39 return $filename;
267             } ## end sub normalize_filename
268              
269             sub pump {
270 13     13 1 30 my ($iterator, $sink) = @_;
271 13 50       29 if ($sink) {
272 0         0 while (my @items = $iterator->()) {
273 0         0 $sink->(@items);
274             }
275 0         0 return;
276             }
277 13         22 my $wa = wantarray();
278 13 50       32 if (! defined $wa) {
279 0         0 while (my @items = $iterator->()) {}
280 0         0 return;
281             }
282 13         19 my @records;
283 13         27 while (my @items = $iterator->()) {
284 19         120 push @records, @items;
285             }
286 13 100       94 return $wa ? @records : \@records;
287             }
288              
289             sub read_file {
290 16     16 1 6392 my %args = normalize_args(
291             @_,
292             [
293             {binmode => ':encoding(UTF-8)'},
294             'filename', # default key for "straight" unnamed parameter
295             ]
296             );
297 16 100       62 defined(my $filename = normalize_filename($args{filename}, \*STDIN))
298             or LOGDIE 'read_file(): undefined filename';
299              
300 15         26 my $fh;
301 15 50       29 if (ref($filename) eq 'GLOB') {
302 0         0 $fh = $filename;
303             }
304             else {
305 15 100       584 open $fh, '<', $filename
306             or LOGDIE "read_file() for <$args{filename}>: open(): $OS_ERROR";
307             }
308              
309 14 50       55 if (defined $args{binmode}) {
310             binmode $fh, $args{binmode}
311 14 100   1   189 or LOGDIE "read_file(): binmode()"
  1     1   7  
  1         1  
  1         8  
  1         8  
  1         2  
  1         4  
312             . " for $args{filename} failed: $OS_ERROR";
313             }
314              
315 13         1725 local $INPUT_RECORD_SEPARATOR;
316 13         448 return <$fh>;
317             } ## end sub read_file
318              
319             sub read_file_maybe {
320 52     52 1 81 my $x = shift;
321 52 100       150 return read_file(@$x) if ref($x) eq 'ARRAY';
322 42         104 return $x;
323             }
324              
325             sub resolve_module {
326 68     68 1 180 my ($module, $prefix) = @_;
327              
328             # Force a first character transforming from new interface if after 0.734
329 68 100       243 if ($Data::Tubes::API_VERSION gt '0.734') {
330 64 100       355 $module = '+' . $module unless $module =~ s{^[+^]}{!}mxs;
331             }
332              
333 68         239 my ($first) = substr $module, 0, 1;
334 68 100       234 return substr $module, 1 if $first eq '!';
335              
336 51   100     205 $prefix //= 'Data::Tubes::Plugin';
337 51 100       169 if ($first eq '+') {
    100          
338 49         120 $module = substr $module, 1;
339             }
340             elsif ($module =~ m{::}mxs) {
341 1         2 $prefix = undef;
342             }
343 51 100       145 return $module unless defined $prefix;
344 50         211 return $prefix . '::' . $module;
345             }
346              
347             sub shorter_sub_names {
348 33     33 1 150 my $stash = shift(@_) . '::';
349              
350 37     37   353 no strict 'refs';
  37         93  
  37         33785  
351              
352             # isolate all subs
353             my %sub_for =
354 33 100       670 map { *{$stash . $_}{CODE} ? ($_ => *{$stash . $_}{CODE}) : (); }
  2944         3466  
  2944         7118  
  983         2410  
355             keys %$stash;
356              
357             # iterate through inputs, work only on isolated subs and don't
358             # consider shortened ones
359 33         261 for my $prefix (@_) {
360 33         224 while (my ($name, $sub) = each %sub_for) {
361 983 100       2824 next if index($name, $prefix) < 0;
362 188         388 my $shortname = substr $name, length($prefix);
363 188         242 *{$stash . $shortname} = $sub;
  188         1032  
364             }
365             } ## end for my $prefix (@_)
366              
367 33         184 return;
368             } ## end sub shorter_sub_names
369              
370             sub _split_pair {
371 7     7   16 my ($input, $separator) = @_;
372 7         10 my $qs = quotemeta($separator);
373 7         69 my $regexp = qr{(?mxs:\A((?:\\.|[^\\$qs])+)$qs(.*)\z)};
374 7         52 my ($first, $second) = $input =~ m{$regexp};
375 7 100       22 ($first, $second) = ($input, undef) unless defined($first);
376 7         14 $first =~ s{\\(.)}{$1}gmxs; # unescape metadata
377 7         24 return ($first, $second);
378             } ## end sub _split_pair
379              
380             sub sprintffy {
381 34     34 1 85 my ($template, $substitutions) = @_;
382 34         64 my $len = length $template;
383 34         83 pos($template) = 0; # initialize
384 34         64 my @chunks;
385             QUEST:
386 34         97 while (pos($template) < $len) {
387 65         341 $template =~ m{\G (.*?) (% | \z)}mxscg;
388 65         208 my ($plain, $term) = ($1, $2);
389 65         103 my $pos = pos($template);
390 65         92 push @chunks, $plain;
391 65 100       186 last unless $term; # got a percent, have to continue
392             CANDIDATE:
393 31         137 for my $candidate ([qr{%} => '%'], @$substitutions) {
394 62         132 my ($regex, $value) = @$candidate;
395 62 100       1240 $template =~ m{\G$regex}cg or next CANDIDATE;
396 31 50       149 $value = $value->() if ref($value) eq 'CODE';
397 31         60 push @chunks, $value;
398 31         121 next QUEST;
399             } ## end CANDIDATE: for my $candidate ([qr{%}...])
400              
401             # didn't find a matchin thing... time to complain
402 0         0 die {message => "invalid sprintffy template '$template'"};
403             } ## end QUEST: while (pos($template) < $len)
404 34         137 return join '', @chunks;
405             } ## end sub sprintffy
406              
407             sub test_all_equal {
408 17     17 1 29 my $reference = shift;
409 17         38 for my $candidate (@_) {
410 16 100       53 return if $candidate ne $reference;
411             }
412 13         87 return 1;
413             } ## end sub test_all_equal
414              
415             sub traverse {
416 38     38 1 114 my ($data, @keys) = @_;
417 38         102 for my $key (@keys) {
418 58 50       125 if (ref($data) eq 'HASH') {
    0          
419 58         103 $data = $data->{$key};
420             }
421             elsif (ref($data) eq 'ARRAY') {
422 0         0 $data = $data->[$key];
423             }
424             else {
425 0         0 return undef;
426             }
427 58 50       154 return undef unless defined $data;
428             } ## end for my $key (@keys)
429 38         112 return $data;
430             } ## end sub traverse
431              
432             sub trim {
433 15     15 1 192 s{\A\s+|\s+\z}{}gmxs for @_;
434             }
435              
436             sub tube {
437 18     18 1 42 my $opts = {};
438 18 100 66     110 $opts = shift(@_) if (@_ && ref($_[0]) eq 'HASH');
439 18 50       73 my @prefix = exists($opts->{prefix}) ? ($opts->{prefix}) : ();
440 18         42 my $locator = shift;
441 18         67 return load_sub($locator, @prefix)->(@_);
442             }
443              
444             sub unzip {
445 24 50 33 24 1 132 my $items = (@_ && ref($_[0])) ? $_[0] : \@_;
446 24         42 my $n_items = scalar @$items;
447 24         42 my (@evens, @odds);
448 24         34 my $i = 0;
449 24         60 while ($i < $n_items) {
450 75         146 push @evens, $items->[$i++];
451 75 100       196 push @odds, $items->[$i++] if $i < $n_items;
452             }
453 24         82 return (\@evens, \@odds);
454             } ## end sub unzip
455              
456             1;