File Coverage

blib/lib/App/yajg.pm
Criterion Covered Total %
statement 178 217 82.0
branch 91 130 70.0
condition 64 91 70.3
subroutine 18 22 81.8
pod 1 12 8.3
total 352 472 74.5


line stmt bran cond sub pod time code
1             package App::yajg;
2              
3 7     7   3994 use 5.014000;
  7         15  
4 7     7   21 use strict;
  7         9  
  7         114  
5 7     7   24 use warnings;
  7         3  
  7         249  
6 7     7   3591 no if $] >= 5.017011, warnings => 'experimental::smartmatch';
  7         50  
  7         36  
7 7     7   400 use utf8;
  7         7  
  7         35  
8              
9 7     7   3628 use Data::Dumper;
  7         30578  
  7         386  
10 7     7   4333 use JSON::XS qw();
  7         25659  
  7         623  
11              
12             our $VERSION = '0.18';
13              
14             our %CAN;
15              
16             BEGIN {
17 7     7   12 $CAN{'highlight'} = eval {
18             # FIXME: probably will not work if not unix os
19 7 50       22609 qx(which highlight 2>/dev/null) and $? == 0
20             };
21             # $CAN{'highlight'}->{'error'} = $@;
22 7         25 $CAN{'ddp'} = eval { require Data::Printer };
  7         1266  
23 7         5385 $CAN{'_error'}->{'ddp'} = $@;
24             }
25              
26             sub MAX_RECURSION () {300}
27              
28             {
29             my $inc = caller() ? $INC{ __PACKAGE__ =~ s/::/\//r . '.pm' } : undef;
30             my $at = join '|' => "\Q$0\E", '\(eval [0-9]++\)', '-[eE]', $inc ? "\Q$inc\E" : ();
31             my $re = qr/at (?:$at) line [0-9]++(?:\.|, <> (?:chunk|line) [0-9]++\.)/;
32 4   50 4 0 56 sub remove_at_line ($) { (shift // '') =~ s/$re//r }
33             }
34              
35 0     0 0 0 sub warn_without_line { warn remove_at_line shift }
36 4     4 0 9 sub die_without_line { die remove_at_line shift }
37              
38             sub size ($) {
39 138 100 100 138 0 6644 ref $_[0] eq 'ARRAY' and @{ $_[0] } or ref $_[0] eq 'HASH' and %{ $_[0] }
  63   100     315  
  45         254  
40             }
41              
42             sub values_ref ($) {
43 4         24 ref $_[0] eq 'ARRAY' ? @{ $_[0] }
44 14 100   14 0 2267 : ref $_[0] eq 'HASH' ? (values %{ $_[0] })
  4 100       13  
    100          
45             : wantarray ? ()
46             : 0
47             }
48              
49             sub read_next_json_file () {
50 0     0 0 0 local $/;
51 0         0 local $SIG{__WARN__} = \&warn_without_line;
52 0         0 while (<>) {
53 0 0       0 utf8::encode($_) if utf8::is_utf8($_);
54 0         0 $_ = eval { JSON::XS::decode_json($_) };
  0         0  
55 0 0       0 warn "Failed to parse $ARGV: $@" if $@;
56 0 0       0 next unless ref $_;
57 0         0 return $_;
58             }
59 0         0 return;
60             }
61              
62             sub parse_select ($;$) {
63 150   50 150 0 133601 my $select = shift // return;
64 150   100     488 my $args = shift // {};
65 150         127 my @select_path;
66             # split by '.' exept '\.'
67 150         685 for (split /(? $select) {
68             # now we can do unescape '\.'
69 465         457 s/\\\././g;
70 465         308 my $type = '';
71             # {....}
72 465 100       1546 if (s/^\{(.*)\}$/$1/) {
    100          
    100          
73 117         93 $type = 'HASH';
74 117 100       203 if ($args->{'ignore_case'}) {
75 3         3 $type = 'HASH_IC';
76 3         5 $_ = lc($_);
77             }
78             }
79             # [....]
80             elsif (s/^\[(.*)\]$/$1/) {
81 114         87 $type = 'SLICE';
82 114         480 s/^\s*|\s*$//g;
83             # '2, 3, -2' -> [2, 3, 4]
84 114         115 my $list = [];
85 114         87 my $err;
86 114         184 for (split ',') {
87 285         605 s/^\s*|\s*$//g;
88 285 100       404 next unless length $_;
89 243 100       497 unless (m/^[+-]?[0-9]++$/) {
90 3         15 warn "Failed to parse select: '$_' not a number\n";
91 3         7 $err = 1;
92 3         5 next;
93             }
94 240         405 push @$list, int($_);
95             }
96 114 100 100     392 die "Failed to parse select: '$_' not a number or list of numbers\n"
97             if $err or not @$list;
98 109         120 $_ = $list;
99             }
100             # /..../
101             elsif (s/^\/(.*)\/$/$1/) {
102 116         100 $type = 'REGEXP';
103 116         307 local $SIG{__DIE__} = \&die_without_line;
104 116         115 my $pat = $_;
105 116 100       171 $pat = '(?i)' . $pat if $args->{'ignore_case'};
106 116 100       116 eval { $_ = qr/$pat/ } or die "Failed to parse select: $@";
  116         883  
107             }
108             else {
109 118         84 $type = 'UNKNOWN';
110 7     7   37 no warnings 'uninitialized';
  7         10  
  7         11013  
111 118         225 s/^\\(\/)|\\(\/)$/$1$2/g; # \/...\/ -> /.../
112 118         195 s/^\\(\{)|\\(\})$/$1$2/g; # \{...\} -> {...}
113 118         170 s/^\\(\[)|\\(\])$/$1$2/g; # \[...\] -> [...]
114 118 100       219 if ($args->{'ignore_case'}) {
115 3         4 $type = 'UNKNOWN_IC';
116 3         3 $_ = lc($_);
117             }
118             }
119 458         901 push @select_path, {
120             type => $type,
121             val => $_,
122             };
123             }
124 143         448 return @select_path;
125             }
126              
127             sub select_by_path {
128 231     231 0 34262 my $data = shift;
129              
130             # no path
131 231 100       375 return $data unless @_;
132             # we can select only at ARRAY or HASH
133 124 100       370 return undef unless ref $data ~~ [qw/HASH ARRAY/];
134              
135 106         98 my $current = shift;
136 106         92 my $type = $current->{'type'};
137 106         91 my $val = $current->{'val'};
138 106 100       181 if (ref $data eq 'HASH') {
    50          
139 57         45 given ($type) {
140 57         90 when ([qw/HASH UNKNOWN/]) {
141 33 100       51 return undef unless exists $data->{$val};
142 31         51 my $selected = select_by_path($data->{$val}, @_);
143 31 100 100     81 return undef if @_ and not defined $selected;
144 29         73 return { $val => $selected };
145             }
146 24         35 when ([qw/HASH_IC UNKNOWN_IC/]) {
147 6         7 my %selected = ();
148 6         7 for (grep { lc($_) eq $val } keys %$data) {
  24         27  
149 8         12 my $selected = select_by_path($data->{$_}, @_);
150 8 50 33     12 next if @_ and not defined $selected;
151 8         13 $selected{$_} = $selected;
152             }
153 6 100       20 return %selected ? \%selected : undef;
154             }
155 18         18 when ('REGEXP') {
156 7         10 my %selected = ();
157 7         32 for (grep {m/$val/} keys %$data) {
  38         72  
158 12         18 my $selected = select_by_path($data->{$_}, @_);
159 12 50 66     25 next if @_ and not defined $selected;
160 12         13 $selected{$_} = $selected;
161             }
162 7 100       21 return %selected ? \%selected : undef;
163             }
164 11         8 default { return undef }
  11         30  
165             }
166             }
167             elsif (ref $data eq 'ARRAY') {
168 49         41 given ($type) {
169 49         49 when ('SLICE') {
170 7         11 my @slice = @$data[@$val];
171 7 50       12 return undef unless @slice;
172 7         4 my @selected;
173 7         10 for (@slice) {
174 23         27 my $selected = select_by_path($_, @_);
175 23 50 66     37 next if @_ and not defined $selected;
176 23         27 push @selected, $selected;
177             }
178 7 50       21 return @selected ? \@selected : undef;
179             }
180 42         37 when ('REGEXP') {
181 8         6 my @selected;
182 8         12 for (grep {m/$val/} keys @$data) {
  52         107  
183 28         31 my $selected = select_by_path($data->[$_], @_);
184 28 100 100     59 next if @_ and not defined $selected;
185 20         25 push @selected, $selected;
186             }
187 8 100       25 return @selected ? \@selected : undef;
188             }
189 34         42 when ([qw/UNKNOWN UNKNOWN_IC/]) {
190 24 100       86 return undef unless $val =~ m/^[+-]?[0-9]++$/;
191 23 100       34 return undef unless exists $data->[$val];
192 21         42 my $selected = select_by_path($data->[$val], @_);
193 21 50 33     42 return undef if @_ and not defined $selected;
194 21         39 return [$selected];
195             }
196 10         8 default { return undef }
  10         26  
197             }
198             }
199 0         0 return undef;
200             }
201              
202             sub filter {
203 121     121 0 29481 my ($data, $key_pat, $val_pat, $i, $visited, $r) = @_;
204              
205             # Nothing to filter if we have no filter patterns
206 121 100 100     337 return $data unless defined $key_pat or defined $val_pat;
207              
208             # $i - invert match flag
209              
210             # Deep recursion protection
211 112   100     196 $r //= 0;
212 112 50       212 if (++$r > MAX_RECURSION) {
213 0         0 warn "Too deep filtering\n";
214 0         0 return $data;
215             }
216              
217             # for $val_pat we do grep at array or hash loops
218 112 100       326 return $data unless ref $data ~~ [qw/ARRAY HASH/];
219              
220             # If we have been already visited this ref
221 100   100     282 $visited //= {};
222 100 50       179 return $visited->{$data} if $visited->{$data};
223              
224 100         83 my $ret;
225              
226 100 100       196 if (ref $data eq 'HASH') {
    50          
227 61         93 $ret = {};
228 61         183 for (keys %$data) {
229 137 100 100     1877 if (
    100 100        
      100        
      100        
      66        
      66        
      66        
      66        
      66        
      66        
      66        
      66        
230             # only key_pat
231             (defined $key_pat and not defined $val_pat and m/$key_pat/)
232             # otherwise data must be defined scalar
233             or (not ref $data->{$_} and defined $data->{$_}
234             and (not defined $key_pat or m/$key_pat/)
235             and (not defined $val_pat or ($data->{$_} =~ m/$val_pat/ xor $i))
236             )
237             # if invert match and we have $val_pat we need to allow
238             # empty arrays, empty hashes, undef values and other refes
239             or ($i and defined $val_pat
240             and (not defined $data->{$_}
241             or ref $data->{$_} and not size($data->{$_})
242             )
243             )
244             ) {
245 41         132 $ret->{$_} = $data->{$_};
246             }
247             elsif (ref $data->{$_} ~~ [qw/ARRAY HASH/]) {
248 46         98 my $filtered = filter($data->{$_}, $key_pat, $val_pat, $i, $visited, $r);
249 46 100       60 $ret->{$_} = $filtered if size($filtered);
250             }
251             else {
252 50         70 next;
253             }
254             }
255             }
256             elsif (ref $data eq 'ARRAY') {
257 39         44 $ret = [];
258 39         60 for (@$data) {
259 105 100 66     929 if (ref $_ ~~ [qw/HASH ARRAY/]) {
    100 66        
260 32         38 my $filtered = filter($_, $key_pat, $val_pat, $i, $visited, $r);
261 32 100       42 push @$ret, $filtered if size($filtered);
262             }
263             elsif (defined $val_pat
264             and (defined $_ and not ref $_ and (m/$val_pat/ xor $i)
265             # if invert match and we have $val_pat we need to allow
266             # empty arrays, empty hashes, undef values and other refes
267             or ($i and (not defined $_ or ref $_ and not size($_)))
268             )
269             ) {
270 26         68 push @$ret, $_;
271             }
272             }
273             }
274              
275 100         386 return $visited->{$data} = $ret;
276             }
277              
278             sub modify_data {
279 10 50   10 0 736 return if @_ == 1;
280              
281 10         5 my $r = 0;
282 10         7 my $visited = {};
283 10 100       20 if (@_ > 2) {
284 9         5 $r = pop;
285 9         9 $visited = pop;
286             }
287 10         7 my $hooks = pop;
288 10 50       9 return unless size $hooks;
289              
290 10 50       15 if (++$r > MAX_RECURSION) {
291 0         0 warn "Too deep modification\n";
292 0         0 return;
293             }
294              
295 10 100       17 if (ref $_[0] eq 'HASH') {
    100          
296 3 100       6 return if $visited->{ $_[0] };
297 2         1 modify_data($_, $hooks, $visited, $r) for values %{ $_[0] };
  2         8  
298 2         7 $visited->{ $_[0] } = 1;
299             }
300             elsif (ref $_[0] eq 'ARRAY') {
301 1 50       2 return if $visited->{ $_[0] };
302 1         19 modify_data($_, $hooks, $visited, $r) for @{ $_[0] };
  1         3  
303 1         3 $visited->{ $_[0] } = 1;
304             }
305             else {
306 6         10 $_->($_[0]) for @$hooks;
307             }
308             }
309              
310             sub highlight ($$) {
311 0     0 1   my ($string, $lang) = @_;
312              
313 0 0         return $string unless $CAN{'highlight'};
314              
315             # IPC::Open2 hangs on big data so we will do like this
316 0           my $pid = open(my $hl_out, '-|');
317 0 0         if (not defined $pid) {
318 0           warn "highlight failed: $!\n";
319 0           return $string;
320             }
321 0           my $utf8 = utf8::is_utf8($string);
322              
323 0 0         unless ($pid) {
324 0 0         open(my $hl_in, '|-', 'highlight', '-O', 'ansi', '-S', $lang)
325             or die "$!\n";
326 0 0         utf8::encode($string) if utf8::is_utf8($string);
327 0           print $hl_in $string;
328 0           close $hl_in;
329 0           exit 0;
330             }
331              
332 0           local $/;
333 0           my $highlighted = <$hl_out>;
334 0           close $hl_out; # may be waitpid($pid, 0); ??
335 0 0         return $string unless $? == 0;
336 0 0 0       utf8::decode($highlighted) if $utf8 and not utf8::is_utf8($highlighted);
337              
338 0   0       return $highlighted || $string;
339             }
340              
341             our %OUT = (
342             'ddp' => sub {
343             my ($data, $args) = @_;
344             my $ddp = Data::Printer::np($data,
345             colored => $args->{'color'},
346             max_depth => $args->{'max_depth'},
347             );
348             utf8::encode($ddp) if utf8::is_utf8($ddp);
349             say $ddp;
350             },
351             'json' => sub {
352             my ($data, $args) = @_;
353             local $SIG{__WARN__} = \&warn_without_line;
354              
355             if ($args->{'max_depth'}) {
356             my $VAR1;
357             eval Data::Dumper->new([$data])->Maxdepth($args->{'max_depth'})->Dump();
358             if ($@ or not defined $VAR1) {
359             warn "max_depth failed: $@";
360             }
361             else {
362             $data = $VAR1;
363             }
364             }
365              
366             my $json = eval {
367             JSON::XS
368             ->new
369             ->pretty(int not $args->{'minimal'})
370             ->allow_nonref
371             ->canonical
372             ->encode($data)
373             };
374             warn($@), return if $@;
375             $json = highlight($json, 'js') if $args->{'color'};
376             utf8::encode($json) if utf8::is_utf8($json);
377             print $json;
378             },
379             'perl' => sub {
380             my ($data, $args) = @_;
381             local $SIG{__WARN__} = \&warn_without_line;
382             local $Data::Dumper::Useperl = 1;
383 7     7   34 no warnings 'redefine';
  7         14  
  7         1940  
384             local *Data::Dumper::qquote = sub {
385             my $d = shift;
386             utf8::encode $d if utf8::is_utf8($d);
387             return "'$d'";
388             };
389             my $perl = eval {
390             Data::Dumper->new([$data])
391             ->Indent(int not $args->{'minimal'})
392             ->Pair($args->{'minimal'} ? '=>' : ' => ')
393             ->Terse(1)
394             ->Sortkeys(int not $args->{'minimal'})
395             ->Useqq(1)
396             ->Maxdepth($args->{'max_depth'})
397             ->Dump()
398             };
399             warn($@), return if $@;
400             $perl = highlight($perl, 'perl') if $args->{'color'};
401             utf8::encode($perl) if utf8::is_utf8($perl);
402             print $perl;
403             },
404             );
405              
406             sub validate_output ($) {
407 0     0 0   my $output = shift;
408 0           die 'Output must be one of ' . join(', ' => map {"'$_'"} sort keys %OUT) . "\n"
409 0 0         unless $OUT{$output};
410             die "Can't require Data::Printer : $CAN{'_error'}->{'ddp'}\n"
411 0 0 0       if not $CAN{'ddp'} and $output eq 'ddp';
412             }
413              
414             1;
415              
416             __END__