File Coverage

blib/lib/App/yajg.pm
Criterion Covered Total %
statement 170 193 88.0
branch 90 114 78.9
condition 64 82 78.0
subroutine 16 19 84.2
pod 1 11 9.0
total 341 419 81.3


line stmt bran cond sub pod time code
1             package App::yajg;
2              
3 8     8   3690 use 5.014000;
  8         19  
4 8     8   27 use strict;
  8         8  
  8         141  
5 8     8   25 use warnings;
  8         13  
  8         249  
6 8     8   3663 no if $] >= 5.017011, warnings => 'experimental::smartmatch';
  8         55  
  8         41  
7 8     8   451 use utf8;
  8         8  
  8         45  
8              
9 8     8   4006 use Data::Dumper;
  8         37356  
  8         446  
10 8     8   39 use JSON qw();
  8         9  
  8         4978  
11              
12             our $VERSION = '0.19';
13              
14             sub MAX_RECURSION () {300}
15              
16             {
17             my $inc = caller() ? $INC{ __PACKAGE__ =~ s/::/\//r . '.pm' } : undef;
18             my $at = join '|' => "\Q$0\E", '\(eval [0-9]++\)', '-[eE]', $inc ? "\Q$inc\E" : ();
19             my $re = qr/at (?:$at) line [0-9]++(?:\.|, <> (?:chunk|line) [0-9]++\.)/;
20 4   50 4 0 52 sub remove_at_line ($) { (shift // '') =~ s/$re//r }
21             }
22              
23 0     0 0 0 sub warn_without_line { warn remove_at_line shift }
24 4     4 0 7 sub die_without_line { die remove_at_line shift }
25              
26             sub size ($) {
27 138 100 100 138 0 6112 ref $_[0] eq 'ARRAY' and @{ $_[0] } or ref $_[0] eq 'HASH' and %{ $_[0] }
  63   100     236  
  45         181  
28             }
29              
30             sub values_ref ($) {
31 4         16 ref $_[0] eq 'ARRAY' ? @{ $_[0] }
32 14 100   14 0 2216 : ref $_[0] eq 'HASH' ? (values %{ $_[0] })
  4 100       15  
    100          
33             : wantarray ? ()
34             : 0
35             }
36              
37             sub read_next_json_file () {
38 0     0 0 0 local $/;
39 0         0 local $SIG{__WARN__} = \&warn_without_line;
40 0         0 while (<>) {
41 0 0       0 utf8::encode($_) if utf8::is_utf8($_);
42 0         0 $_ = eval { JSON::decode_json($_) };
  0         0  
43 0 0       0 warn "Failed to parse $ARGV: $@" if $@;
44 0 0       0 next unless ref $_;
45 0         0 return $_;
46             }
47 0         0 return;
48             }
49              
50             sub parse_select ($;$) {
51 150   50 150 0 151772 my $select = shift // return;
52 150   100     488 my $args = shift // {};
53 150         134 my @select_path;
54             # split by '.' exept '\.'
55 150         802 for (split /(? $select) {
56             # now we can do unescape '\.'
57 465         474 s/\\\././g;
58 465         332 my $type = '';
59             # {....}
60 465 100       1636 if (s/^\{(.*)\}$/$1/) {
    100          
    100          
61 117         107 $type = 'HASH';
62 117 100       208 if ($args->{'ignore_case'}) {
63 3         3 $type = 'HASH_IC';
64 3         5 $_ = lc($_);
65             }
66             }
67             # [....]
68             elsif (s/^\[(.*)\]$/$1/) {
69 114         114 $type = 'SLICE';
70 114         449 s/^\s*|\s*$//g;
71             # '2, 3, -2' -> [2, 3, 4]
72 114         125 my $list = [];
73 114         77 my $err;
74 114         197 for (split ',') {
75 285         624 s/^\s*|\s*$//g;
76 285 100       428 next unless length $_;
77 243 100       547 unless (m/^[+-]?[0-9]++$/) {
78 3         15 warn "Failed to parse select: '$_' not a number\n";
79 3         8 $err = 1;
80 3         7 next;
81             }
82 240         432 push @$list, int($_);
83             }
84 114 100 100     421 die "Failed to parse select: '$_' not a number or list of numbers\n"
85             if $err or not @$list;
86 109         116 $_ = $list;
87             }
88             # /..../
89             elsif (s/^\/(.*)\/$/$1/) {
90 116         104 $type = 'REGEXP';
91 116         354 local $SIG{__DIE__} = \&die_without_line;
92 116         101 my $pat = $_;
93 116 100       196 $pat = '(?i)' . $pat if $args->{'ignore_case'};
94 116 100       113 eval { $_ = qr/$pat/ } or die "Failed to parse select: $@";
  116         928  
95             }
96             else {
97 118         118 $type = 'UNKNOWN';
98 8     8   38 no warnings 'uninitialized';
  8         7  
  8         9148  
99 118         251 s/^\\(\/)|\\(\/)$/$1$2/g; # \/...\/ -> /.../
100 118         195 s/^\\(\{)|\\(\})$/$1$2/g; # \{...\} -> {...}
101 118         198 s/^\\(\[)|\\(\])$/$1$2/g; # \[...\] -> [...]
102 118 100       194 if ($args->{'ignore_case'}) {
103 3         3 $type = 'UNKNOWN_IC';
104 3         5 $_ = lc($_);
105             }
106             }
107 458         961 push @select_path, {
108             type => $type,
109             val => $_,
110             };
111             }
112 143         458 return @select_path;
113             }
114              
115             sub select_by_path {
116 231     231 0 43517 my $data = shift;
117              
118             # no path
119 231 100       385 return $data unless @_;
120             # we can select only at ARRAY or HASH
121 124 100       400 return undef unless ref $data ~~ [qw/HASH ARRAY/];
122              
123 106         101 my $current = shift;
124 106         98 my $type = $current->{'type'};
125 106         86 my $val = $current->{'val'};
126 106 100       188 if (ref $data eq 'HASH') {
    50          
127 57         53 given ($type) {
128 57         82 when ([qw/HASH UNKNOWN/]) {
129 33 100       49 return undef unless exists $data->{$val};
130 31         54 my $selected = select_by_path($data->{$val}, @_);
131 31 100 100     71 return undef if @_ and not defined $selected;
132 29         73 return { $val => $selected };
133             }
134 24         39 when ([qw/HASH_IC UNKNOWN_IC/]) {
135 6         7 my %selected = ();
136 6         8 for (grep { lc($_) eq $val } keys %$data) {
  24         28  
137 8         11 my $selected = select_by_path($data->{$_}, @_);
138 8 50 33     15 next if @_ and not defined $selected;
139 8         27 $selected{$_} = $selected;
140             }
141 6 100       20 return %selected ? \%selected : undef;
142             }
143 18         19 when ('REGEXP') {
144 7         8 my %selected = ();
145 7         13 for (grep {m/$val/} keys %$data) {
  38         76  
146 12         17 my $selected = select_by_path($data->{$_}, @_);
147 12 50 66     25 next if @_ and not defined $selected;
148 12         14 $selected{$_} = $selected;
149             }
150 7 100       20 return %selected ? \%selected : undef;
151             }
152 11         8 default { return undef }
  11         29  
153             }
154             }
155             elsif (ref $data eq 'ARRAY') {
156 49         47 given ($type) {
157 49         54 when ('SLICE') {
158 7         14 my @slice = @$data[@$val];
159 7 50       9 return undef unless @slice;
160 7         7 my @selected;
161 7         8 for (@slice) {
162 23         21 my $selected = select_by_path($_, @_);
163 23 50 66     38 next if @_ and not defined $selected;
164 23         25 push @selected, $selected;
165             }
166 7 50       25 return @selected ? \@selected : undef;
167             }
168 42         32 when ('REGEXP') {
169 8         6 my @selected;
170 8         13 for (grep {m/$val/} keys @$data) {
  52         115  
171 28         28 my $selected = select_by_path($data->[$_], @_);
172 28 100 100     64 next if @_ and not defined $selected;
173 20         22 push @selected, $selected;
174             }
175 8 100       24 return @selected ? \@selected : undef;
176             }
177 34         43 when ([qw/UNKNOWN UNKNOWN_IC/]) {
178 24 100       103 return undef unless $val =~ m/^[+-]?[0-9]++$/;
179 23 100       38 return undef unless exists $data->[$val];
180 21         33 my $selected = select_by_path($data->[$val], @_);
181 21 50 33     44 return undef if @_ and not defined $selected;
182 21         40 return [$selected];
183             }
184 10         10 default { return undef }
  10         24  
185             }
186             }
187 0         0 return undef;
188             }
189              
190             sub filter {
191 121     121 0 21604 my ($data, $key_pat, $val_pat, $i, $visited, $r) = @_;
192              
193             # Nothing to filter if we have no filter patterns
194 121 100 100     297 return $data unless defined $key_pat or defined $val_pat;
195              
196             # $i - invert match flag
197              
198             # Deep recursion protection
199 112   100     179 $r //= 0;
200 112 50       151 if (++$r > MAX_RECURSION) {
201 0         0 warn "Too deep filtering\n";
202 0         0 return $data;
203             }
204              
205             # for $val_pat we do grep at array or hash loops
206 112 100       261 return $data unless ref $data ~~ [qw/ARRAY HASH/];
207              
208             # If we have been already visited this ref
209 100   100     155 $visited //= {};
210 100 50       148 return $visited->{$data} if $visited->{$data};
211              
212 100         62 my $ret;
213              
214 100 100       142 if (ref $data eq 'HASH') {
    50          
215 61         49 $ret = {};
216 61         107 for (keys %$data) {
217 137 100 100     1331 if (
    100 100        
      100        
      100        
      66        
      66        
      66        
      66        
      66        
      66        
      66        
      66        
218             # only key_pat
219             (defined $key_pat and not defined $val_pat and m/$key_pat/)
220             # otherwise data must be defined scalar
221             or (not ref $data->{$_} and defined $data->{$_}
222             and (not defined $key_pat or m/$key_pat/)
223             and (not defined $val_pat or ($data->{$_} =~ m/$val_pat/ xor $i))
224             )
225             # if invert match and we have $val_pat we need to allow
226             # empty arrays, empty hashes, undef values and other refes
227             or ($i and defined $val_pat
228             and (not defined $data->{$_}
229             or ref $data->{$_} and not size($data->{$_})
230             )
231             )
232             ) {
233 41         60 $ret->{$_} = $data->{$_};
234             }
235             elsif (ref $data->{$_} ~~ [qw/ARRAY HASH/]) {
236 46         66 my $filtered = filter($data->{$_}, $key_pat, $val_pat, $i, $visited, $r);
237 46 100       54 $ret->{$_} = $filtered if size($filtered);
238             }
239             else {
240 50         57 next;
241             }
242             }
243             }
244             elsif (ref $data eq 'ARRAY') {
245 39         29 $ret = [];
246 39         46 for (@$data) {
247 105 100 66     621 if (ref $_ ~~ [qw/HASH ARRAY/]) {
    100 66        
248 32         35 my $filtered = filter($_, $key_pat, $val_pat, $i, $visited, $r);
249 32 100       41 push @$ret, $filtered if size($filtered);
250             }
251             elsif (defined $val_pat
252             and (defined $_ and not ref $_ and (m/$val_pat/ xor $i)
253             # if invert match and we have $val_pat we need to allow
254             # empty arrays, empty hashes, undef values and other refes
255             or ($i and (not defined $_ or ref $_ and not size($_)))
256             )
257             ) {
258 26         38 push @$ret, $_;
259             }
260             }
261             }
262              
263 100         241 return $visited->{$data} = $ret;
264             }
265              
266             sub modify_data {
267 10 50   10 0 401 return if @_ == 1;
268              
269 10         9 my $r = 0;
270 10         7 my $visited = {};
271 10 100       15 if (@_ > 2) {
272 9         6 $r = pop;
273 9         5 $visited = pop;
274             }
275 10         10 my $hooks = pop;
276 10 50       10 return unless size $hooks;
277              
278 10 50       13 if (++$r > MAX_RECURSION) {
279 0         0 warn "Too deep modification\n";
280 0         0 return;
281             }
282              
283 10 100       16 if (ref $_[0] eq 'HASH') {
    100          
284 3 100       6 return if $visited->{ $_[0] };
285 2         2 modify_data($_, $hooks, $visited, $r) for values %{ $_[0] };
  2         9  
286 2         7 $visited->{ $_[0] } = 1;
287             }
288             elsif (ref $_[0] eq 'ARRAY') {
289 1 50       2 return if $visited->{ $_[0] };
290 1         1 modify_data($_, $hooks, $visited, $r) for @{ $_[0] };
  1         4  
291 1         3 $visited->{ $_[0] } = 1;
292             }
293             else {
294 6         11 $_->($_[0]) for @$hooks;
295             }
296             }
297              
298             sub output ($) {
299 0     0 1   my $output = shift;
300 0           state $supported = {
301             'ddp' => 'App::yajg::Output::DDP',
302             'json' => 'App::yajg::Output::Json',
303             'perl' => 'App::yajg::Output::Perl',
304             'yaml' => 'App::yajg::Output::Yaml',
305             };
306 0           die 'Output must be one of ' . join(', ' => map {"'$_'"} sort keys %$supported) . "\n"
307 0 0         unless $supported->{$output};
308              
309 0           eval "require $supported->{$output}";
310 0 0         die "Can't init output $output: $@" if $@;
311              
312 0           return $supported->{$output}->new;
313             }
314              
315             1;
316              
317             __END__