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