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   3233 use 5.014000;
  8         17  
4 8     8   24 use strict;
  8         6  
  8         124  
5 8     8   23 use warnings;
  8         11  
  8         224  
6 8     8   3702 no if $] >= 5.017011, warnings => 'experimental::smartmatch';
  8         54  
  8         41  
7 8     8   442 use utf8;
  8         8  
  8         33  
8              
9 8     8   3900 use Data::Dumper;
  8         38013  
  8         415  
10 8     8   37 use JSON qw();
  8         7  
  8         5204  
11              
12             our $VERSION = '0.20';
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 49 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 8 sub die_without_line { die remove_at_line shift }
25              
26             sub size ($) {
27 138 100 100 138 0 6281 ref $_[0] eq 'ARRAY' and @{ $_[0] } or ref $_[0] eq 'HASH' and %{ $_[0] }
  63   100     232  
  45         184  
28             }
29              
30             sub values_ref ($) {
31 4         17 ref $_[0] eq 'ARRAY' ? @{ $_[0] }
32 14 100   14 0 2202 : ref $_[0] eq 'HASH' ? (values %{ $_[0] })
  4 100       16  
    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 140546 my $select = shift // return;
52 150   100     465 my $args = shift // {};
53 150         113 my @select_path;
54             # split by '.' exept '\.'
55 150         690 for (split /(? $select) {
56             # now we can do unescape '\.'
57 465         462 s/\\\././g;
58 465         304 my $type = '';
59             # {....}
60 465 100       1599 if (s/^\{(.*)\}$/$1/) {
    100          
    100          
61 117         92 $type = 'HASH';
62 117 100       179 if ($args->{'ignore_case'}) {
63 3         2 $type = 'HASH_IC';
64 3         6 $_ = lc($_);
65             }
66             }
67             # [....]
68             elsif (s/^\[(.*)\]$/$1/) {
69 114         92 $type = 'SLICE';
70 114         427 s/^\s*|\s*$//g;
71             # '2, 3, -2' -> [2, 3, 4]
72 114         146 my $list = [];
73 114         80 my $err;
74 114         186 for (split ',') {
75 285         627 s/^\s*|\s*$//g;
76 285 100       367 next unless length $_;
77 243 100       481 unless (m/^[+-]?[0-9]++$/) {
78 3         15 warn "Failed to parse select: '$_' not a number\n";
79 3         9 $err = 1;
80 3         6 next;
81             }
82 240         394 push @$list, int($_);
83             }
84 114 100 100     437 die "Failed to parse select: '$_' not a number or list of numbers\n"
85             if $err or not @$list;
86 109         110 $_ = $list;
87             }
88             # /..../
89             elsif (s/^\/(.*)\/$/$1/) {
90 116         91 $type = 'REGEXP';
91 116         318 local $SIG{__DIE__} = \&die_without_line;
92 116         108 my $pat = $_;
93 116 100       190 $pat = '(?i)' . $pat if $args->{'ignore_case'};
94 116 100       112 eval { $_ = qr/$pat/ } or die "Failed to parse select: $@";
  116         893  
95             }
96             else {
97 118         91 $type = 'UNKNOWN';
98 8     8   36 no warnings 'uninitialized';
  8         11  
  8         9412  
99 118         212 s/^\\(\/)|\\(\/)$/$1$2/g; # \/...\/ -> /.../
100 118         181 s/^\\(\{)|\\(\})$/$1$2/g; # \{...\} -> {...}
101 118         198 s/^\\(\[)|\\(\])$/$1$2/g; # \[...\] -> [...]
102 118 100       186 if ($args->{'ignore_case'}) {
103 3         3 $type = 'UNKNOWN_IC';
104 3         4 $_ = lc($_);
105             }
106             }
107 458         898 push @select_path, {
108             type => $type,
109             val => $_,
110             };
111             }
112 143         445 return @select_path;
113             }
114              
115             sub select_by_path {
116 231     231 0 47300 my $data = shift;
117              
118             # no path
119 231 100       426 return $data unless @_;
120             # we can select only at ARRAY or HASH
121 124 100       425 return undef unless ref $data ~~ [qw/HASH ARRAY/];
122              
123 106         126 my $current = shift;
124 106         101 my $type = $current->{'type'};
125 106         85 my $val = $current->{'val'};
126 106 100       215 if (ref $data eq 'HASH') {
    50          
127 57         50 given ($type) {
128 57         90 when ([qw/HASH UNKNOWN/]) {
129 33 100       55 return undef unless exists $data->{$val};
130 31         56 my $selected = select_by_path($data->{$val}, @_);
131 31 100 100     78 return undef if @_ and not defined $selected;
132 29         82 return { $val => $selected };
133             }
134 24         42 when ([qw/HASH_IC UNKNOWN_IC/]) {
135 6         9 my %selected = ();
136 6         8 for (grep { lc($_) eq $val } keys %$data) {
  24         29  
137 8         11 my $selected = select_by_path($data->{$_}, @_);
138 8 50 33     15 next if @_ and not defined $selected;
139 8         29 $selected{$_} = $selected;
140             }
141 6 100       21 return %selected ? \%selected : undef;
142             }
143 18         18 when ('REGEXP') {
144 7         11 my %selected = ();
145 7         15 for (grep {m/$val/} keys %$data) {
  38         90  
146 12         22 my $selected = select_by_path($data->{$_}, @_);
147 12 50 66     30 next if @_ and not defined $selected;
148 12         16 $selected{$_} = $selected;
149             }
150 7 100       28 return %selected ? \%selected : undef;
151             }
152 11         10 default { return undef }
  11         31  
153             }
154             }
155             elsif (ref $data eq 'ARRAY') {
156 49         72 given ($type) {
157 49         58 when ('SLICE') {
158 7         12 my @slice = @$data[@$val];
159 7 50       11 return undef unless @slice;
160 7         7 my @selected;
161 7         10 for (@slice) {
162 23         26 my $selected = select_by_path($_, @_);
163 23 50 66     39 next if @_ and not defined $selected;
164 23         28 push @selected, $selected;
165             }
166 7 50       29 return @selected ? \@selected : undef;
167             }
168 42         33 when ('REGEXP') {
169 8         6 my @selected;
170 8         10 for (grep {m/$val/} keys @$data) {
  52         110  
171 28         33 my $selected = select_by_path($data->[$_], @_);
172 28 100 100     61 next if @_ and not defined $selected;
173 20         22 push @selected, $selected;
174             }
175 8 100       25 return @selected ? \@selected : undef;
176             }
177 34         53 when ([qw/UNKNOWN UNKNOWN_IC/]) {
178 24 100       111 return undef unless $val =~ m/^[+-]?[0-9]++$/;
179 23 100       39 return undef unless exists $data->[$val];
180 21         42 my $selected = select_by_path($data->[$val], @_);
181 21 50 33     45 return undef if @_ and not defined $selected;
182 21         49 return [$selected];
183             }
184 10         13 default { return undef }
  10         30  
185             }
186             }
187 0         0 return undef;
188             }
189              
190             sub filter {
191 121     121 0 25977 my ($data, $key_pat, $val_pat, $i, $visited, $r) = @_;
192              
193             # Nothing to filter if we have no filter patterns
194 121 100 100     292 return $data unless defined $key_pat or defined $val_pat;
195              
196             # $i - invert match flag
197              
198             # Deep recursion protection
199 112   100     186 $r //= 0;
200 112 50       139 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       259 return $data unless ref $data ~~ [qw/ARRAY HASH/];
207              
208             # If we have been already visited this ref
209 100   100     167 $visited //= {};
210 100 50       143 return $visited->{$data} if $visited->{$data};
211              
212 100         60 my $ret;
213              
214 100 100       134 if (ref $data eq 'HASH') {
    50          
215 61         46 $ret = {};
216 61         109 for (keys %$data) {
217 137 100 100     1359 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         67 $ret->{$_} = $data->{$_};
234             }
235             elsif (ref $data->{$_} ~~ [qw/ARRAY HASH/]) {
236 46         62 my $filtered = filter($data->{$_}, $key_pat, $val_pat, $i, $visited, $r);
237 46 100       48 $ret->{$_} = $filtered if size($filtered);
238             }
239             else {
240 50         63 next;
241             }
242             }
243             }
244             elsif (ref $data eq 'ARRAY') {
245 39         31 $ret = [];
246 39         48 for (@$data) {
247 105 100 66     644 if (ref $_ ~~ [qw/HASH ARRAY/]) {
    100 66        
248 32         38 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         39 push @$ret, $_;
259             }
260             }
261             }
262              
263 100         258 return $visited->{$data} = $ret;
264             }
265              
266             sub modify_data {
267 10 50   10 0 631 return if @_ == 1;
268              
269 10         5 my $r = 0;
270 10         10 my $visited = {};
271 10 100       12 if (@_ > 2) {
272 9         7 $r = pop;
273 9         5 $visited = pop;
274             }
275 10         7 my $hooks = pop;
276 10 50       10 return unless size $hooks;
277              
278 10 50       14 if (++$r > MAX_RECURSION) {
279 0         0 warn "Too deep modification\n";
280 0         0 return;
281             }
282              
283 10 100       15 if (ref $_[0] eq 'HASH') {
    100          
284 3 100       7 return if $visited->{ $_[0] };
285 2         1 modify_data($_, $hooks, $visited, $r) for values %{ $_[0] };
  2         8  
286 2         6 $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         3  
291 1         3 $visited->{ $_[0] } = 1;
292             }
293             else {
294 6         10 $_->($_[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__