File Coverage

blib/lib/JQ/Lite/Util/Paths.pm
Criterion Covered Total %
statement 443 510 86.8
branch 273 412 66.2
condition 41 83 49.4
subroutine 40 41 97.5
pod n/a
total 797 1046 76.2


line stmt bran cond sub pod time code
1             package JQ::Lite::Util;
2              
3 176     176   1286 use strict;
  176         389  
  176         7501  
4 176     176   913 use warnings;
  176         327  
  176         9890  
5              
6 176     176   1045 use JSON::PP ();
  176         333  
  176         4134  
7 176     176   890 use Scalar::Util qw(looks_like_number);
  176         367  
  176         1293554  
8              
9             sub _apply_assignment {
10 16     16   49 my ($self, $item, $path, $value_spec, $operator) = @_;
11              
12 16 50       37 return $item unless defined $item;
13 16 100 66     80 return $item unless defined $path && length $path;
14              
15 15   50     35 $operator //= '=';
16              
17 15         41 my $value = _resolve_assignment_value($self, $item, $value_spec);
18              
19 15 100       39 if ($operator ne '=') {
20 6         14 my $current = _clone_for_assignment(_get_path_value($item, $path));
21 6         14 my $current_num = _coerce_number($current);
22 6         12 my $value_num = _coerce_number($value);
23              
24 6 50 33     24 return $item unless defined $current_num && defined $value_num;
25              
26 6         9 my $result;
27 6 100       23 if ($operator eq '+=') {
    100          
    100          
    50          
28 2         3 $result = $current_num + $value_num;
29             }
30             elsif ($operator eq '-=') {
31 1         3 $result = $current_num - $value_num;
32             }
33             elsif ($operator eq '*=') {
34 1         4 $result = $current_num * $value_num;
35             }
36             elsif ($operator eq '/=') {
37 2 100       7 return $item if $value_num == 0;
38 1         3 $result = $current_num / $value_num;
39             }
40             else {
41 0         0 return $item;
42             }
43              
44 5         8 $value = $result;
45             }
46              
47 14         62 _set_path_value($item, $path, $value);
48              
49 14         93 return $item;
50             }
51              
52             sub _get_path_value {
53 6     6   15 my ($target, $path) = @_;
54              
55 6 50       14 return undef unless defined $target;
56 6 50 33     24 return undef unless defined $path && length $path;
57              
58 6         28 my @segments = _parse_path_segments($path);
59 6 50       13 return undef unless @segments;
60              
61 6         11 my $cursor = $target;
62 6         18 for my $index (0 .. $#segments) {
63 12         19 my $segment = $segments[$index];
64 12         20 my $is_last = ($index == $#segments);
65              
66 12 50       23 if ($segment->{type} eq 'key') {
67 12 50       25 return undef unless ref $cursor eq 'HASH';
68 12         19 my $key = $segment->{value};
69              
70 12 100       48 return $cursor->{$key} if $is_last;
71              
72 6 50       14 return undef unless exists $cursor->{$key};
73 6         12 $cursor = $cursor->{$key};
74 6         11 next;
75             }
76              
77 0 0       0 if ($segment->{type} eq 'index') {
78 0 0       0 return undef unless ref $cursor eq 'ARRAY';
79              
80 0         0 my $idx = $segment->{value};
81 0         0 my $numeric = int($idx);
82 0 0       0 if ($idx =~ /^-?\d+$/) {
83 0 0       0 $numeric += @$cursor if $numeric < 0;
84             }
85              
86 0 0 0     0 return undef if $numeric < 0 || $numeric > $#$cursor;
87              
88 0 0       0 return $cursor->[$numeric] if $is_last;
89              
90 0         0 $cursor = $cursor->[$numeric];
91 0         0 next;
92             }
93             }
94              
95 0         0 return undef;
96             }
97              
98             sub _coerce_number {
99 12     12   18 my ($value) = @_;
100              
101 12 100       21 return 0 if !defined $value;
102              
103 11 50       27 if (JSON::PP::is_bool($value)) {
104 0 0       0 return $value ? 1 : 0;
105             }
106              
107 11 50       77 return 0 + $value if looks_like_number($value);
108              
109 0         0 return undef;
110             }
111              
112             sub _resolve_assignment_value {
113 15     15   32 my ($self, $item, $value_spec) = @_;
114              
115 15 50       33 return undef unless defined $value_spec;
116              
117 15 100 66     71 if ($value_spec->{type} && $value_spec->{type} eq 'path') {
118 1   50     5 my $path = $value_spec->{value} // '';
119 1         3 $path =~ s/^\.//;
120              
121 1         7 my @values = _traverse($item, $path);
122 1         5 return _clone_for_assignment($values[0]);
123             }
124              
125 14 100 66     62 if ($value_spec->{type} && $value_spec->{type} eq 'expression') {
126 1   50     5 my $expr = $value_spec->{value} // '';
127              
128 1         5 my ($values, $ok) = _evaluate_value_expression($self, $item, $expr);
129 1 50       4 if ($ok) {
130 1 50       7 return _clone_for_assignment(@$values ? $values->[0] : undef);
131             }
132              
133 0 0 0     0 if (defined $self && $self->can('run_query')) {
134 0         0 my @outputs = $self->run_query(_encode_json($item), $expr);
135 0 0       0 return _clone_for_assignment($outputs[0]) if @outputs;
136             }
137              
138 0         0 return _clone_for_assignment($expr);
139             }
140              
141 13         37 return _clone_for_assignment($value_spec->{value});
142             }
143              
144             sub _set_path_value {
145 14     14   32 my ($target, $path, $value) = @_;
146              
147 14 50       34 return unless defined $target;
148              
149 14         35 my @segments = _parse_path_segments($path);
150 14 50       48 return unless @segments;
151              
152 14         24 my $cursor = $target;
153 14         39 for my $index (0 .. $#segments) {
154 28         64 my $segment = $segments[$index];
155 28         49 my $is_last = ($index == $#segments);
156              
157 28 100       67 if ($segment->{type} eq 'key') {
158 26 50       86 return unless ref $cursor eq 'HASH';
159 26         48 my $key = $segment->{value};
160              
161 26 100       55 if ($is_last) {
162 14         35 $cursor->{$key} = $value;
163 14         63 last;
164             }
165              
166 12 50 33     59 if (!exists $cursor->{$key} || !defined $cursor->{$key}) {
167 0         0 my $next = $segments[$index + 1];
168 0 0       0 $cursor->{$key} = ($next->{type} eq 'index') ? [] : {};
169             }
170              
171 12         23 $cursor = $cursor->{$key};
172 12         25 next;
173             }
174              
175 2 50       7 if ($segment->{type} eq 'index') {
176 2 50       7 return unless ref $cursor eq 'ARRAY';
177              
178 2         7 my $idx = $segment->{value};
179 2         5 my $numeric = int($idx);
180 2 50       11 if ($idx =~ /^-?\d+$/) {
181 2 50       6 $numeric += @$cursor if $numeric < 0;
182             }
183              
184 2 50       6 return if $numeric < 0;
185              
186 2 50       6 if ($is_last) {
187 0         0 $cursor->[$numeric] = $value;
188 0         0 last;
189             }
190              
191 2 50       8 if (!defined $cursor->[$numeric]) {
192 0         0 my $next = $segments[$index + 1];
193 0 0       0 $cursor->[$numeric] = ($next->{type} eq 'index') ? [] : {};
194             }
195              
196 2         4 $cursor = $cursor->[$numeric];
197 2         5 next;
198             }
199             }
200              
201 14         54 return;
202             }
203              
204             sub _parse_path_segments {
205 20     20   43 my ($path) = @_;
206              
207 20   50     49 $path //= '';
208 20         104 $path =~ s/^\s+|\s+$//g;
209              
210 20         31 my @segments;
211 20         72 for my $chunk (split /\./, $path) {
212 39 50       74 next if $chunk eq '';
213              
214 39         76 while (length $chunk) {
215 40 100       101 if ($chunk =~ s/^\[(\-?\d+)\]//) {
216 2         10 push @segments, { type => 'index', value => $1 };
217 2         7 next;
218             }
219              
220 38 50       140 if ($chunk =~ s/^([^\[]+)//) {
221 38         147 push @segments, { type => 'key', value => $1 };
222 38         96 next;
223             }
224              
225 0         0 last;
226             }
227             }
228              
229 20         58 return @segments;
230             }
231              
232             sub _clone_for_assignment {
233 21     21   38 my ($value) = @_;
234              
235 21 100       41 return undef unless defined $value;
236 19 50       61 return $value unless ref $value;
237              
238 0         0 my $json = _encode_json($value);
239 0         0 return _decode_json($json);
240             }
241              
242             sub _map {
243 0     0   0 my ($self, $data, $filter) = @_;
244              
245 0 0       0 if (ref $data ne 'ARRAY') {
246 0         0 warn "_map expects array reference";
247 0         0 return ();
248             }
249              
250 0         0 my @mapped;
251 0         0 for my $item (@$data) {
252 0         0 push @mapped, $self->run_query(_encode_json($item), $filter);
253             }
254              
255 0         0 return @mapped;
256             }
257              
258             sub _apply_all {
259 6     6   14 my ($self, $value, $expr) = @_;
260              
261 6 100       33 if (ref $value eq 'ARRAY') {
262 5 100       16 return JSON::PP::true unless @$value;
263              
264 4         8 for my $item (@$value) {
265 9 100       65 if (defined $expr) {
266 4         15 my @evaluated = $self->run_query(_encode_json($item), $expr);
267 4 50       7 return JSON::PP::false unless @evaluated;
268 4 100       7 return JSON::PP::false if grep { !_is_truthy($_) } @evaluated;
  4         19  
269             }
270             else {
271 5 100       11 return JSON::PP::false unless _is_truthy($item);
272             }
273             }
274              
275 2         11 return JSON::PP::true;
276             }
277              
278 1 50       4 if (defined $expr) {
279 0         0 my @evaluated = $self->run_query(_encode_json($value), $expr);
280 0 0       0 return JSON::PP::false unless @evaluated;
281 0 0       0 return grep { !_is_truthy($_) } @evaluated ? JSON::PP::false : JSON::PP::true;
  0         0  
282             }
283              
284 1 50       5 return _is_truthy($value) ? JSON::PP::true : JSON::PP::false;
285             }
286              
287             sub _apply_any {
288 6     6   33 my ($self, $value, $expr) = @_;
289              
290 6 100       21 if (ref $value eq 'ARRAY') {
291 4 50       10 return JSON::PP::false unless @$value;
292              
293 4         12 for my $item (@$value) {
294 9 100       66 if (defined $expr) {
295 4         16 my @evaluated = $self->run_query(_encode_json($item), $expr);
296 4 100       10 return JSON::PP::true if grep { _is_truthy($_) } @evaluated;
  4         10  
297             }
298             else {
299 5 100       12 return JSON::PP::true if _is_truthy($item);
300             }
301             }
302              
303 2         10 return JSON::PP::false;
304             }
305              
306 2 50       7 if (defined $expr) {
307 0         0 my @evaluated = $self->run_query(_encode_json($value), $expr);
308 0 0       0 return grep { _is_truthy($_) } @evaluated ? JSON::PP::true : JSON::PP::false;
  0         0  
309             }
310              
311 2 100       8 return _is_truthy($value) ? JSON::PP::true : JSON::PP::false;
312             }
313              
314             sub _is_truthy {
315 45     45   92 my ($value) = @_;
316              
317 45 100       106 return 0 unless defined $value;
318              
319 41 100       136 if (JSON::PP::is_bool($value)) {
320 33 100       557 return $value ? 1 : 0;
321             }
322              
323 8 100       57 if (ref $value eq 'ARRAY') {
324 2 100       8 return @$value ? 1 : 0;
325             }
326              
327 6 50       23 if (ref $value eq 'HASH') {
328 0 0       0 return scalar(keys %$value) ? 1 : 0;
329             }
330              
331 6 50       27 if (!ref $value) {
332 6 50       16 return 0 if $value eq '';
333 6 100       33 if (looks_like_number($value)) {
334 4 100       21 return $value != 0 ? 1 : 0;
335             }
336 2         51 return 1;
337             }
338              
339 0         0 return 1;
340             }
341              
342             sub _apply_case_transform {
343 36     36   91 my ($value, $mode) = @_;
344              
345 36 50       97 if (!defined $value) {
346 0         0 return undef;
347             }
348              
349 36 100       105 if (ref $value eq 'ARRAY') {
350 6         14 return [ map { _apply_case_transform($_, $mode) } @$value ];
  14         37  
351             }
352              
353 30 100       67 if (!ref $value) {
354 28 100       111 return uc $value if $mode eq 'upper';
355 17 100       84 return lc $value if $mode eq 'lower';
356 6         18 return _to_titlecase($value);
357             }
358              
359 2         7 return $value;
360             }
361              
362             sub _apply_ascii_case_transform {
363 12     12   61 my ($value, $mode) = @_;
364              
365 12 50       27 if (!defined $value) {
366 0         0 return undef;
367             }
368              
369 12 100       35 if (ref $value eq 'ARRAY') {
370 2         24 return [ map { _apply_ascii_case_transform($_, $mode) } @$value ];
  6         17  
371             }
372              
373 10 50       64 if (!ref $value) {
374 10         17 my $copy = $value;
375 10 100       27 if ($mode eq 'upper') {
    50          
376 5         13 $copy =~ tr/a-z/A-Z/;
377             }
378             elsif ($mode eq 'lower') {
379 5         12 $copy =~ tr/A-Z/a-z/;
380             }
381 10         49 return $copy;
382             }
383              
384 0         0 return $value;
385             }
386              
387             sub _to_titlecase {
388 6     6   11 my ($value) = @_;
389              
390 6         13 my $result = lc $value;
391 6         37 $result =~ s/(^|[^\p{L}\p{N}])(\p{L})/$1 . uc($2)/ge;
  7         45  
392 6         35 return $result;
393             }
394              
395             sub _apply_trim {
396 19     19   42 my ($value) = @_;
397              
398 19 100       71 if (!defined $value) {
399 1         5 return undef;
400             }
401              
402 18 100       71 if (!ref $value) {
403 13         29 my $copy = $value;
404 13         45 $copy =~ s/^\s+//;
405 13         42 $copy =~ s/\s+$//;
406 13         55 return $copy;
407             }
408              
409 5 100       20 if (ref $value eq 'ARRAY') {
410 4         12 return [ map { _apply_trim($_) } @$value ];
  12         26  
411             }
412              
413 1         9 return $value;
414             }
415              
416             sub _apply_trimstr {
417 34     34   78 my ($value, $needle, $mode) = @_;
418              
419 34 100       60 if (!defined $value) {
420 2         4 return undef;
421             }
422              
423 32 100       68 if (ref $value eq 'ARRAY') {
424 4         10 return [ map { _apply_trimstr($_, $needle, $mode) } @$value ];
  16         25  
425             }
426              
427 28 100       61 if (ref $value) {
428 5         20 return $value;
429             }
430              
431 23 100       85 return $value if !_is_string_scalar($value);
432              
433 19 50       39 $needle = '' unless defined $needle;
434 19         35 my $target = "$value";
435 19         32 my $pattern = "$needle";
436 19         31 my $len = length $pattern;
437              
438 19 100       43 return $target if $len == 0;
439              
440 17 100       35 if ($mode eq 'left') {
441 9 100       33 return $target if index($target, $pattern) != 0;
442 5         16 return substr($target, $len);
443             }
444              
445 8 50       23 if ($mode eq 'right') {
446 8 100       30 return $target if $len > length($target);
447 6 100       23 return $target unless substr($target, -$len) eq $pattern;
448 4         26 return substr($target, 0, length($target) - $len);
449             }
450              
451 0         0 return $target;
452             }
453              
454             sub _apply_paths {
455 5     5   13 my ($value) = @_;
456              
457 5 100 66     40 if (!ref $value || JSON::PP::is_bool($value)) {
458 2         7 return [];
459             }
460              
461 3         65 my @paths;
462 3         19 _collect_paths($value, [], \@paths);
463 3         12 return \@paths;
464             }
465              
466             sub _apply_scalar_paths {
467 3     3   7 my ($value) = @_;
468              
469 3 100       10 return [] if _is_scalar_value($value);
470              
471 1         2 my @paths;
472 1         4 _collect_scalar_paths($value, [], \@paths);
473 1         3 return \@paths;
474             }
475              
476             sub _apply_leaf_paths {
477 6     6   12 my ($value) = @_;
478              
479 6 100       15 if (_is_leaf_value($value)) {
480 2         9 return [ [] ];
481             }
482              
483 4         8 my @paths;
484 4         13 _collect_leaf_paths($value, [], \@paths);
485 4         16 return \@paths;
486             }
487              
488             sub _validate_path_array {
489 43     43   104 my ($path, $caller) = @_;
490              
491 43   50     130 $caller //= 'getpath';
492              
493 43 50       113 die "$caller(): path must be an array" if ref($path) ne 'ARRAY';
494              
495 43         95 for my $segment (@$path) {
496 64   100     172 my $is_boolean = ref($segment) && JSON::PP::is_bool($segment);
497              
498 64 100       282 die "$caller(): path elements must be defined" if !defined $segment;
499 62 100 100     246 die "$caller(): path elements must be scalars" if ref($segment) && !$is_boolean;
500             }
501              
502 39         146 return [ @$path ];
503             }
504              
505             sub _apply_getpath {
506 14     14   42 my ($self, $value, $expr) = @_;
507              
508 14 50       78 return undef unless defined $value;
509              
510 14   50     41 $expr //= '';
511 14         79 $expr =~ s/^\s+|\s+$//g;
512 14 50       39 return undef if $expr eq '';
513              
514 14         23 my @paths;
515              
516 14         27 my $decoded = eval { _decode_json($expr) };
  14         46  
517 14 100 66     3639 if (!$@ && defined $decoded) {
518 13 100       45 if (ref $decoded eq 'ARRAY') {
519 12 100 100     68 if (@$decoded && ref $decoded->[0] eq 'ARRAY') {
520 1         4 for my $path (@$decoded) {
521 2         7 push @paths, _validate_path_array($path, 'getpath');
522             }
523             }
524             else {
525 11         35 push @paths, _validate_path_array($decoded, 'getpath');
526             }
527             }
528             else {
529 1         25 die 'getpath(): path must be an array';
530             }
531             }
532              
533 13 100       29 if (!@paths) {
534 1         5 my @outputs = $self->run_query(_encode_json($value), $expr);
535 1         4 for my $output (@outputs) {
536 7 50       16 next unless defined $output;
537              
538 7 50       18 if (ref $output eq 'ARRAY') {
539 7 50 33     29 if (@$output && ref $output->[0] eq 'ARRAY') {
540 0         0 for my $path (@$output) {
541 0         0 push @paths, _validate_path_array($path, 'getpath');
542             }
543             }
544             else {
545 7         15 push @paths, _validate_path_array($output, 'getpath');
546             }
547             }
548             else {
549 0         0 die 'getpath(): path must be an array';
550             }
551             }
552             }
553              
554 13 50       35 return undef unless @paths;
555              
556 13         27 my @values = map { _traverse_path_array($value, $_) } @paths;
  20         54  
557 13 100       86 return @values == 1 ? $values[0] : \@values;
558             }
559              
560             sub _apply_setpath {
561 10     10   30 my ($self, $value, $paths_expr, $value_expr) = @_;
562              
563 10 50       30 return $value unless defined $value;
564              
565 10   50     41 $paths_expr //= '';
566 10         80 $paths_expr =~ s/^\s+|\s+$//g;
567 10 50       30 return $value if $paths_expr eq '';
568              
569 10         27 my @paths = _resolve_paths_from_expr($self, $value, $paths_expr);
570 7 50       18 return $value unless @paths;
571              
572 7         25 my $replacement = _evaluate_setpath_value($self, $value, $value_expr);
573 7         16 my $result = $value;
574              
575 7         18 for my $path (@paths) {
576 11         47 $result = _set_value_at_path($result, [@$path], $replacement);
577             }
578              
579 7         65 return $result;
580             }
581              
582             sub _resolve_paths_from_expr {
583 10     10   70 my ($self, $value, $expr) = @_;
584              
585 10 50       24 return () unless defined $expr;
586              
587 10         21 my $clean = $expr;
588 10         70 $clean =~ s/^\s+|\s+$//g;
589 10 50       42 return () if $clean eq '';
590              
591 10         16 my @paths;
592              
593 10         21 my $decoded = eval { _decode_json($clean) };
  10         34  
594 10 100 66     5942 if (!$@ && defined $decoded) {
595 9 100       35 if (ref $decoded eq 'ARRAY') {
596 8 50 33     38 if (@$decoded && ref $decoded->[0] eq 'ARRAY') {
597 0         0 push @paths, map { _validate_path_array($_, 'setpath') } @$decoded;
  0         0  
598             }
599             else {
600 8         74 push @paths, _validate_path_array($decoded, 'setpath');
601             }
602             }
603             else {
604 1         26 die 'setpath(): path must be an array';
605             }
606             }
607              
608 7 100       18 if (!@paths) {
609 1         6 my @outputs = $self->run_query(_encode_json($value), $clean);
610 1         3 for my $output (@outputs) {
611 5 50       111 next unless defined $output;
612              
613 5 50 0     15 if (ref $output eq 'ARRAY') {
    0          
614 5 50 33     31 if (@$output && ref $output->[0] eq 'ARRAY') {
    50 33        
615 0         0 push @paths, map { _validate_path_array($_, 'setpath') } @$output;
  0         0  
616             }
617             elsif (!@$output || !ref $output->[0]) {
618 5         11 push @paths, _validate_path_array($output, 'setpath');
619             }
620             }
621             elsif (!ref $output || JSON::PP::is_bool($output)) {
622 0         0 die 'setpath(): path must be an array';
623             }
624             }
625             }
626              
627 7         31 return @paths;
628             }
629              
630             sub _evaluate_setpath_value {
631 7     7   20 my ($self, $context, $expr) = @_;
632              
633 7 50       18 return undef unless defined $expr;
634              
635 7         27 my $clean = $expr;
636 7         42 $clean =~ s/^\s+|\s+$//g;
637 7 50       22 return undef if $clean eq '';
638              
639 7         14 my $decoded = eval { _decode_json($clean) };
  7         31  
640 7 100       3357 if (!$@) {
641 6         20 return $decoded;
642             }
643              
644 1 50       7 if ($clean =~ /^'(.*)'$/) {
645 0         0 my $text = $1;
646 0         0 $text =~ s/\\'/'/g;
647 0         0 return $text;
648             }
649              
650 1 50       8 if ($clean =~ /^\.(.+)$/) {
651 1         5 my $path = $1;
652 1         7 my @values = _traverse($context, $path);
653 1 50       24 return @values ? $values[0] : undef;
654             }
655              
656 0         0 my @outputs = $self->run_query(_encode_json($context), $clean);
657 0 0       0 return @outputs ? $outputs[0] : undef;
658             }
659              
660             sub _set_value_at_path {
661 26     26   60 my ($current, $path, $replacement) = @_;
662              
663 26 50       56 return _deep_clone($replacement) unless @$path;
664              
665 26         67 my ($segment, @rest) = @$path;
666              
667 26 100       72 if (ref $current eq 'HASH') {
668 23         74 my $key = _coerce_hash_key($segment);
669 23 50       52 return $current unless defined $key;
670              
671 23         118 my %copy = %$current;
672 23 100       72 if (@rest) {
673 12 50       33 my $next_value = exists $copy{$key} ? $copy{$key} : _guess_container_for_segment($rest[0]);
674 12         55 $copy{$key} = _set_value_at_path($next_value, \@rest, $replacement);
675             }
676             else {
677 11         42 $copy{$key} = _deep_clone($replacement);
678             }
679              
680 23         373 return \%copy;
681             }
682              
683 3 100       13 if (ref $current eq 'ARRAY') {
684 1         6 my $index = _normalize_array_index_for_set($segment, scalar @$current);
685 1 50       5 return $current unless defined $index;
686              
687 1         3 my @copy = @$current;
688 1         7 _ensure_array_length(\@copy, $index);
689              
690 1 50       3 if (@rest) {
691 1 50       25 my $next_value = defined $copy[$index] ? $copy[$index] : _guess_container_for_segment($rest[0]);
692 1         5 $copy[$index] = _set_value_at_path($next_value, \@rest, $replacement);
693             }
694             else {
695 0         0 $copy[$index] = _deep_clone($replacement);
696             }
697              
698 1         6 return \@copy;
699             }
700              
701 2         5 my $container = _guess_container_for_segment($segment);
702 2         12 return _set_value_at_path($container, $path, $replacement);
703             }
704              
705             sub _coerce_hash_key {
706 45     45   86 my ($segment) = @_;
707              
708 45 50       97 return undef if !defined $segment;
709              
710 45 100       1689 if (JSON::PP::is_bool($segment)) {
711 4 100       29 return $segment ? 'true' : 'false';
712             }
713              
714 41 50       305 return undef if ref $segment;
715              
716 41         108 return "$segment";
717             }
718              
719             sub _guess_container_for_segment {
720 3     3   8 my ($segment) = @_;
721              
722 3 50       9 return [] if _is_numeric_segment($segment);
723 3         8 return {};
724             }
725              
726             sub _is_numeric_segment {
727 7     7   11 my ($segment) = @_;
728              
729 7 50       35 return 0 if !defined $segment;
730              
731 7 100       17 if (JSON::PP::is_bool($segment)) {
732 4         26 return 1;
733             }
734              
735 3 50       52 return 0 if ref $segment;
736              
737 3 50       26 return ($segment =~ /^-?\d+$/) ? 1 : 0;
738             }
739              
740             sub _normalize_array_index_for_set {
741 1     1   4 my ($segment, $length) = @_;
742              
743 1 50       4 return undef if !defined $segment;
744              
745 1 50       3 if (JSON::PP::is_bool($segment)) {
746 0 0       0 $segment = $segment ? 1 : 0;
747             }
748              
749 1 50       8 return undef if ref $segment;
750 1 50       9 return undef if $segment !~ /^-?\d+$/;
751              
752 1         3 my $index = int($segment);
753 1 50       4 $index += $length if $index < 0;
754              
755 1 50       4 return undef if $index < 0;
756              
757 1         2 return $index;
758             }
759              
760             sub _normalize_array_index_for_get {
761 6     6   14 my ($segment, $length) = @_;
762              
763 6 50       42 return undef if !defined $segment;
764              
765 6 100       15 if (JSON::PP::is_bool($segment)) {
766 2 100       23 $segment = $segment ? 1 : 0;
767             }
768              
769 6 50       72 return undef if ref $segment;
770 6 50       39 return undef if $segment !~ /^-?\d+$/;
771              
772 6         24 my $index = int($segment);
773 6 100       22 $index += $length if $index < 0;
774              
775 6 50       18 return undef if $index < 0;
776              
777 6         14 return $index;
778             }
779              
780             sub _ensure_array_length {
781 1     1   2 my ($array_ref, $index) = @_;
782              
783 1 50       5 return unless ref $array_ref eq 'ARRAY';
784              
785 1         5 while (@$array_ref <= $index) {
786 1         4 push @$array_ref, undef;
787             }
788             }
789              
790             sub _collect_paths {
791 8     8   25 my ($value, $current_path, $paths) = @_;
792              
793 8 100       30 if (ref $value eq 'HASH') {
794 6         29 for my $key (sort keys %$value) {
795 11         28 my $child = $value->{$key};
796 11         25 my @next = (@$current_path, $key);
797 11         32 push @$paths, [@next];
798              
799 11 100 100     57 if (ref $child eq 'HASH' || ref $child eq 'ARRAY') {
800 5         76 _collect_paths($child, \@next, $paths);
801             }
802             }
803 6         19 return;
804             }
805              
806 2 50       8 if (ref $value eq 'ARRAY') {
807 2         9 for my $index (0 .. $#$value) {
808 4         34 my $child = $value->[$index];
809 4         10 my @next = (@$current_path, $index);
810 4         19 push @$paths, [@next];
811              
812 4 50 33     26 if (ref $child eq 'HASH' || ref $child eq 'ARRAY') {
813 0         0 _collect_paths($child, \@next, $paths);
814             }
815             }
816 2         9 return;
817             }
818              
819 0         0 push @$paths, [@$current_path];
820             }
821              
822             sub _collect_scalar_paths {
823 3     3   6 my ($value, $current_path, $paths) = @_;
824              
825 3 100       8 if (ref $value eq 'HASH') {
826 2         7 for my $key (sort keys %$value) {
827 3         6 my $child = $value->{$key};
828 3         6 my @next = (@$current_path, $key);
829              
830 3 100 33     5 if (_is_scalar_value($child)) {
    50          
831 2         18 push @$paths, [@next];
832             }
833             elsif (ref $child eq 'HASH' || ref $child eq 'ARRAY') {
834 1         6 _collect_scalar_paths($child, \@next, $paths);
835             }
836             }
837 2         4 return;
838             }
839              
840 1 50       3 if (ref $value eq 'ARRAY') {
841 1         3 for my $index (0 .. $#$value) {
842 2         4 my $child = $value->[$index];
843 2         4 my @next = (@$current_path, $index);
844              
845 2 100 33     10 if (_is_scalar_value($child)) {
    50          
846 1         4 push @$paths, [@next];
847             }
848             elsif (ref $child eq 'HASH' || ref $child eq 'ARRAY') {
849 1         9 _collect_scalar_paths($child, \@next, $paths);
850             }
851             }
852 1         3 return;
853             }
854             }
855              
856             sub _traverse_path_array {
857 20     20   43 my ($value, $path) = @_;
858              
859 20 50       41 return undef unless defined $value;
860 20 50       47 return $value unless defined $path;
861 20 50       54 return $value if ref($path) ne 'ARRAY';
862              
863 20         34 my $cursor = $value;
864 20         39 for my $segment (@$path) {
865 25 50       52 return undef unless defined $cursor;
866              
867 25 100       81 if (ref $cursor eq 'HASH') {
868 18         44 my $key = _coerce_hash_key($segment);
869 18 50       58 return undef unless defined $key;
870 18 100       54 return undef unless exists $cursor->{$key};
871 17         33 $cursor = $cursor->{$key};
872 17         37 next;
873             }
874              
875 7 100       22 if (ref $cursor eq 'ARRAY') {
876 6         24 my $index = _normalize_array_index_for_get($segment, scalar @$cursor);
877 6 50       17 return undef unless defined $index;
878              
879 6 50       17 return undef if $index > $#$cursor;
880              
881 6         13 $cursor = $cursor->[$index];
882 6         15 next;
883             }
884              
885 1         6 return undef;
886             }
887              
888 18         61 return $cursor;
889             }
890              
891             sub _collect_leaf_paths {
892 9     9   21 my ($value, $current_path, $paths) = @_;
893              
894 9 100       18 if (ref $value eq 'HASH') {
895 5         21 for my $key (sort keys %$value) {
896 7         14 my $child = $value->{$key};
897 7         16 my @next = (@$current_path, $key);
898              
899 7 100       11 if (_is_leaf_value($child)) {
900 3         33 push @$paths, [@next];
901             }
902             else {
903 4         38 _collect_leaf_paths($child, \@next, $paths);
904             }
905             }
906 5         10 return;
907             }
908              
909 4 50       12 if (ref $value eq 'ARRAY') {
910 4         13 for my $index (0 .. $#$value) {
911 6         13 my $child = $value->[$index];
912 6         13 my @next = (@$current_path, $index);
913              
914 6 100       11 if (_is_leaf_value($child)) {
915 5         24 push @$paths, [@next];
916             }
917             else {
918 1         5 _collect_leaf_paths($child, \@next, $paths);
919             }
920             }
921 4         11 return;
922             }
923              
924 0         0 push @$paths, [@$current_path];
925             }
926              
927             sub _is_leaf_value {
928 27     27   70 my ($value) = @_;
929              
930 27 100       73 return 1 unless ref $value;
931 17 100       67 return 1 if JSON::PP::is_bool($value);
932 12 100       96 return 0 if ref($value) eq 'ARRAY';
933 7 50       34 return 0 if ref($value) eq 'HASH';
934 0         0 return 1;
935             }
936              
937             sub _is_scalar_value {
938 8     8   30 return _is_leaf_value(@_);
939             }
940              
941             1;