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   140475 use strict;
  37         103  
  37         1235  
6 37     37   210 use warnings;
  37         80  
  37         1228  
7 37     37   1049 use English qw< -no_match_vars >;
  37         6626  
  37         248  
8 37     37   13706 use Exporter 'import';
  37         85  
  37         2332  
9             our $VERSION = '0.738';
10              
11 37     37   1324 use Log::Log4perl::Tiny qw< :easy :dead_if_first >;
  37         27991  
  37         287  
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   32 my $module = shift;
37 13         88 (my $packfile = $module . '.pm') =~ s{::}{/}gmxs;
38 13         4354 require $packfile;
39 13         299 return $module;
40             } ## end sub _load_module
41              
42             sub args_array_with_options {
43 111     111 1 618 my %defaults = %{pop @_};
  111         516  
44 111 100 100     776 %defaults = (%defaults, %{pop @_})
  55         212  
45             if @_ && (ref($_[-1]) eq 'HASH');
46 111         461 return ([@_], \%defaults);
47             } ## end sub args_array_with_options
48              
49             sub assert_all_different {
50 24 50 33 24 1 127 my $keys = (@_ && ref($_[0])) ? $_[0] : \@_;
51 24         51 my %flag_for;
52 24         66 for my $key (@$keys) {
53 75 50       218 die {message => $key} if $flag_for{$key}++;
54             }
55 24         102 return 1;
56             } ## end sub assert_all_different
57              
58             sub _compile_capture {
59 9     9   32 my %h = @_;
60 37     37   24136 use feature 'state';
  37         95  
  37         16542  
61              
62 9         17 state $quoted = qr{(?mxs:
63             (?: "(?: [^\\"]+ | \\. )*") # double quotes
64             | (?: '[^']*') # single quotes
65             )};
66              
67             my ($key, $value, $kvs, $cs) =
68 9         25 @h{qw< key value key_value_separator chunks_separator>};
69              
70 9 50       20 if (!defined($key)) {
71 9         15 my $admitted = $h{key_admitted};
72 9 50       18 $admitted = qr{[\Q$admitted\E]} unless ref $admitted;
73 9         313 $key = qr{(?mxs: $quoted | (?:(?:$admitted | \\.)+?))};
74             }
75              
76 9 50       33 if (!defined($value)) {
77 9         14 my $admitted = $h{value_admitted};
78 9 50       18 $admitted = qr{[\Q$admitted\E]} unless ref $admitted;
79 9         222 $value = qr{(?mxs: $quoted | (?:(?:$admitted | \\.)+?))};
80             }
81              
82 9         57 my $close = qr{(?<close>$h{close})};
83 9         625 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   291 use feature 'state';
  37         101  
  37         78820  
92 12     12 1 3112 state $admitted_default = qr{[^\\'":=\s,;\|/]};
93             state $kvdecoder = sub {
94 37     37   108 my $kv = shift;
95 37         65 my $first = substr $kv, 0, 1;
96 37 100 100     123 $kv = substr $kv, 1, length($kv) - 2
97             if ($first eq q{'}) || ($first eq q{"});
98 37 100       91 $kv =~ s{\\(.)}{$1}gmxs unless $first eq q{'};
99 37         68 return $kv;
100 12         32 };
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   3 my ($h, $k, $v) = @_;
112 1 50       6 $h->{$k} = [$h->{$k}] unless ref $h->{$k};
113 1         2 push @{$h->{$k}}, $v;
  1         3  
114             },
115 12         39 };
116 12         36 my $args = normalize_args(@_, [$default_handler_for, 'text']);
117             $args->{key_default} = delete $args->{default_key}
118 12 100       41 if exists $args->{default_key};
119 12         18 my $text = $args->{text};
120              
121 12         108 my %h = (%$default_handler_for, %$args);
122 12   66     65 my $capture = $h{capture} ||= _compile_capture(%h);
123 12         40 my %retval = (capture => $capture);
124 12 100       46 return {%retval, failure => 'undefined input'} unless defined $text;
125              
126 9         15 my $len = length $text;
127 9   50     42 pos($text) = my $startpos = $args->{pos} || 0;
128 9         36 %retval = (%retval, pos => $startpos, res => ($len - $startpos));
129              
130             # let's check open first, no need to define anything otherwise
131 9 50       73 $text =~ m{\G$h{open}}gmxs or return {%retval, failure => 'no opening'};
132              
133             my ($dkey, $dupkey, $kdec, $vdec) =
134 9         27 @h{qw< key_default key_duplicate key_decoder value_decoder >};
135 9         14 my ($closed, %hash);
136 9   66     33 while (!$closed && pos($text) < length($text)) {
137 23         31 my $pos = pos($text);
138 23 100       802 $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       195 : defined($dkey) ? (ref($dkey) ? $dkey->() : $dkey)
    50          
    100          
    100          
148             : undef;
149             return {
150 22 100       59 %retval,
151             failure => 'stand-alone value, no default key set',
152             failpos => $pos
153             }
154             unless defined $key;
155              
156 21 50       54 my $value = $vdec ? $vdec->($+{value}) : $+{value};
157              
158 21 100       67 if (!exists $hash{$key}) {
    50          
159 19         44 $hash{$key} = $value;
160             }
161             elsif ($dupkey) {
162 2         6 $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         111 $closed = exists $+{close};
173             } ## end while (!$closed && pos($text...))
174              
175 6 50       14 return {%retval, failure => 'no closure found'} unless $closed;
176              
177 6         12 my $pos = pos $text;
178             return {
179 6         58 %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 32 return _load_module(resolve_module(@_));
188             } ## end sub load_module
189              
190             sub load_sub {
191 18     18 1 41 my ($locator, $prefix) = @_;
192 18 50       164 my ($module, $sub) =
193             ref($locator) ? @$locator : $locator =~ m{\A(.*)::(\w+)\z}mxs;
194 18         67 $module = resolve_module($module, $prefix);
195              
196             # optimistic first
197 18   66     228 return $module->can($sub) // _load_module($module)->can($sub);
198             } ## end sub load_sub
199              
200             sub metadata {
201 3     3 1 7 my $input = shift;
202 3         23 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         8 my $qs = quotemeta($separator);
214 3         88 my $regexp = qr/((?:\\.|[^\\$qs])+)(?:$qs+)?/;
215 3         47 my @chunks = map { s{\\(.)}{$1}g; $_ } $input =~ m{$regexp}gc;
  7         17  
  7         20  
216              
217             # ensure we consumed the whole $input
218 3 50       16 die {message =>
219             "invalid metadata (separator: '$separator', input: [$input])\n"
220             }
221             if pos($input) < length($input);
222              
223 3         12 $separator = $args{key_value_separator};
224             return {
225             map {
226 3         8 my ($k, $v) = _split_pair($_, $separator);
  7         22  
227 7 100       45 defined($v) ? ($k, $v) : ($args{default_key} => $k);
228             } @chunks
229             };
230             } ## end sub metadata
231              
232             sub normalize_args {
233 225     225 1 3777 my $defaults = pop(@_);
234              
235 225         374 my %retval;
236 225 100       758 if (ref($defaults) eq 'ARRAY') {
237 184         419 ($defaults, my $key) = @$defaults;
238 184 100 66     868 $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     2081 ((@_ && ref($_[0]) eq 'HASH') ? %{$_[0]} : @_), # then... the rest
  10         35  
245             );
246              
247 225 100       1461 return %retval if wantarray();
248 33         79 return \%retval;
249             } ## end sub normalize_args
250              
251             sub normalize_filename {
252 25     25 1 59 my ($filename, $default_handle) = @_;
253 25 100       85 return unless defined $filename;
254 24 100       77 return $filename if ref($filename) eq 'GLOB';
255 23 100       69 return $filename if ref($filename) eq 'SCALAR';
256 14 50       33 return $default_handle if $filename eq '-';
257 14 50       50 return $filename if $filename =~ s{\Afile:}{}mxs;
258 14 50       57 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         42 return $filename;
267             } ## end sub normalize_filename
268              
269             sub pump {
270 13     13 1 30 my ($iterator, $sink) = @_;
271 13 50       37 if ($sink) {
272 0         0 while (my @items = $iterator->()) {
273 0         0 $sink->(@items);
274             }
275 0         0 return;
276             }
277 13         27 my $wa = wantarray();
278 13 50       35 if (! defined $wa) {
279 0         0 while (my @items = $iterator->()) {}
280 0         0 return;
281             }
282 13         21 my @records;
283 13         35 while (my @items = $iterator->()) {
284 19         126 push @records, @items;
285             }
286 13 100       99 return $wa ? @records : \@records;
287             }
288              
289             sub read_file {
290 16     16 1 7748 my %args = normalize_args(
291             @_,
292             [
293             {binmode => ':encoding(UTF-8)'},
294             'filename', # default key for "straight" unnamed parameter
295             ]
296             );
297 16 100       76 defined(my $filename = normalize_filename($args{filename}, \*STDIN))
298             or LOGDIE 'read_file(): undefined filename';
299              
300 15         26 my $fh;
301 15 50       37 if (ref($filename) eq 'GLOB') {
302 0         0 $fh = $filename;
303             }
304             else {
305 15 100       547 open $fh, '<', $filename
306             or LOGDIE "read_file() for <$args{filename}>: open(): $OS_ERROR";
307             }
308              
309 14 50       63 if (defined $args{binmode}) {
310             binmode $fh, $args{binmode}
311 14 100   1   215 or LOGDIE "read_file(): binmode()"
  1     1   7  
  1         2  
  1         6  
  1         7  
  1         2  
  1         7  
312             . " for $args{filename} failed: $OS_ERROR";
313             }
314              
315 13         1845 local $INPUT_RECORD_SEPARATOR;
316 13         487 return <$fh>;
317             } ## end sub read_file
318              
319             sub read_file_maybe {
320 52     52 1 98 my $x = shift;
321 52 100       160 return read_file(@$x) if ref($x) eq 'ARRAY';
322 42         105 return $x;
323             }
324              
325             sub resolve_module {
326 68     68 1 200 my ($module, $prefix) = @_;
327              
328             # Force a first character transforming from new interface if after 0.734
329 68 100       268 if ($Data::Tubes::API_VERSION gt '0.734') {
330 64 100       340 $module = '+' . $module unless $module =~ s{^[+^]}{!}mxs;
331             }
332              
333 68         277 my ($first) = substr $module, 0, 1;
334 68 100       253 return substr $module, 1 if $first eq '!';
335              
336 51   100     228 $prefix //= 'Data::Tubes::Plugin';
337 51 100       172 if ($first eq '+') {
    100          
338 49         130 $module = substr $module, 1;
339             }
340             elsif ($module =~ m{::}mxs) {
341 1         3 $prefix = undef;
342             }
343 51 100       149 return $module unless defined $prefix;
344 50         214 return $prefix . '::' . $module;
345             }
346              
347             sub shorter_sub_names {
348 33     33 1 166 my $stash = shift(@_) . '::';
349              
350 37     37   408 no strict 'refs';
  37         84  
  37         35171  
351              
352             # isolate all subs
353             my %sub_for =
354 33 100       658 map { *{$stash . $_}{CODE} ? ($_ => *{$stash . $_}{CODE}) : (); }
  2944         3397  
  2944         7307  
  983         2382  
355             keys %$stash;
356              
357             # iterate through inputs, work only on isolated subs and don't
358             # consider shortened ones
359 33         263 for my $prefix (@_) {
360 33         242 while (my ($name, $sub) = each %sub_for) {
361 983 100       2629 next if index($name, $prefix) < 0;
362 188         384 my $shortname = substr $name, length($prefix);
363 188         252 *{$stash . $shortname} = $sub;
  188         1065  
364             }
365             } ## end for my $prefix (@_)
366              
367 33         194 return;
368             } ## end sub shorter_sub_names
369              
370             sub _split_pair {
371 7     7   17 my ($input, $separator) = @_;
372 7         12 my $qs = quotemeta($separator);
373 7         76 my $regexp = qr{(?mxs:\A((?:\\.|[^\\$qs])+)$qs(.*)\z)};
374 7         59 my ($first, $second) = $input =~ m{$regexp};
375 7 100       26 ($first, $second) = ($input, undef) unless defined($first);
376 7         15 $first =~ s{\\(.)}{$1}gmxs; # unescape metadata
377 7         28 return ($first, $second);
378             } ## end sub _split_pair
379              
380             sub sprintffy {
381 34     34 1 73 my ($template, $substitutions) = @_;
382 34         68 my $len = length $template;
383 34         92 pos($template) = 0; # initialize
384 34         71 my @chunks;
385             QUEST:
386 34         79 while (pos($template) < $len) {
387 65         384 $template =~ m{\G (.*?) (% | \z)}mxscg;
388 65         207 my ($plain, $term) = ($1, $2);
389 65         121 my $pos = pos($template);
390 65         108 push @chunks, $plain;
391 65 100       145 last unless $term; # got a percent, have to continue
392             CANDIDATE:
393 31         143 for my $candidate ([qr{%} => '%'], @$substitutions) {
394 62         137 my ($regex, $value) = @$candidate;
395 62 100       1267 $template =~ m{\G$regex}cg or next CANDIDATE;
396 31 50       156 $value = $value->() if ref($value) eq 'CODE';
397 31         70 push @chunks, $value;
398 31         132 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         143 return join '', @chunks;
405             } ## end sub sprintffy
406              
407             sub test_all_equal {
408 17     17 1 39 my $reference = shift;
409 17         39 for my $candidate (@_) {
410 16 100       55 return if $candidate ne $reference;
411             }
412 13         63 return 1;
413             } ## end sub test_all_equal
414              
415             sub traverse {
416 38     38 1 107 my ($data, @keys) = @_;
417 38         91 for my $key (@keys) {
418 58 50       168 if (ref($data) eq 'HASH') {
    0          
419 58         116 $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       143 return undef unless defined $data;
428             } ## end for my $key (@keys)
429 38         122 return $data;
430             } ## end sub traverse
431              
432             sub trim {
433 15     15 1 199 s{\A\s+|\s+\z}{}gmxs for @_;
434             }
435              
436             sub tube {
437 18     18 1 38 my $opts = {};
438 18 100 66     107 $opts = shift(@_) if (@_ && ref($_[0]) eq 'HASH');
439 18 50       63 my @prefix = exists($opts->{prefix}) ? ($opts->{prefix}) : ();
440 18         38 my $locator = shift;
441 18         66 return load_sub($locator, @prefix)->(@_);
442             }
443              
444             sub unzip {
445 24 50 33 24 1 158 my $items = (@_ && ref($_[0])) ? $_[0] : \@_;
446 24         58 my $n_items = scalar @$items;
447 24         45 my (@evens, @odds);
448 24         35 my $i = 0;
449 24         67 while ($i < $n_items) {
450 75         156 push @evens, $items->[$i++];
451 75 100       223 push @odds, $items->[$i++] if $i < $n_items;
452             }
453 24         90 return (\@evens, \@odds);
454             } ## end sub unzip
455              
456             1;