File Coverage

blib/lib/JQ/Lite/Util/Paths.pm
Criterion Covered Total %
statement 445 509 87.4
branch 273 408 66.9
condition 41 83 49.4
subroutine 41 42 97.6
pod n/a
total 800 1042 76.7


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