File Coverage

blib/lib/Data/Annotation/Traverse.pm
Criterion Covered Total %
statement 70 84 83.3
branch 19 52 36.5
condition 8 23 34.7
subroutine 9 9 100.0
pod 4 4 100.0
total 110 172 63.9


line stmt bran cond sub pod time code
1             package Data::Annotation::Traverse;
2 3     3   58 use v5.24;
  3         12  
3 3     3   15 use experimental qw< signatures >;
  3         4  
  3         18  
4 3     3   454 use Scalar::Util qw< blessed refaddr reftype >;
  3         5  
  3         251  
5              
6 3     3   19 use Exporter qw< import >;
  3         5  
  3         319  
7             our @EXPORT_OK = qw< MISSING crumble kpath means_missing traverse_plain >;
8             our %EXPORT_TAGS = (all => \@EXPORT_OK);
9              
10 3     3   26 use constant MISSING => \"Bubù-non-c'è-più";
  3         5  
  3         4881  
11              
12 18     18 1 28 sub crumble ($input) {
  18         59  
  18         28  
13 18 50       42 return unless defined $input;
14 18 100       59 return $input if ref($input);
15            
16 7         51 $input =~ s{\A\s+|\s+\z}{}gmxs;
17 7 50       47 return [] unless length $input;
18            
19 7         35 my $sq = qr{(?mxs: ' [^']* ' )}mxs;
20 7         21 my $dq = qr{(?mxs: " (?:[^\\"] | \\.)* " )}mxs;
21 7         23 my $ud = qr{(?mxs: \w+ )}mxs;
22 7         369 my $chunk = qr{(?mxs: $sq | $dq | $ud)+}mxs;
23            
24             # save and reset current pos() on $input
25 7         72 my $prepos = pos($input);
26 7         26 pos($input) = undef;
27            
28 7         49 my @path;
29 7         369 push @path, $1 while $input =~ m{\G [.]? ($chunk) }cgmxs;
30            
31             # save and restore pos() on $input - FIXME do we really need this?!?
32 7         22 my $postpos = pos($input);
33 7         50 pos($input) = $prepos;
34            
35 7 50       23 return unless defined $postpos;
36 7 50       22 return if ($postpos != length($input));
37            
38             # cleanup @path components
39 7         19 for my $part (@path) {
40 14         24 my @subparts;
41 14   100     63 while ((pos($part) || 0) < length($part)) {
42 14 50       377 if ($part =~ m{\G ($sq) }cgmxs) {
    50          
    50          
43 0         0 push @subparts, substr $1, 1, length($1) - 2;
44             }
45             elsif ($part =~ m{\G ($dq) }cgmxs) {
46 0         0 my $subpart = substr $1, 1, length($1) - 2;
47 0         0 $subpart =~ s{\\(.)}{$1}gmxs;
48 0         0 push @subparts, $subpart;
49             }
50             elsif ($part =~ m{\G ($ud) }cgmxs) {
51 14         71 push @subparts, $1;
52             }
53             else { # shouldn't happen ever
54 0         0 return;
55             }
56             } ## end while ((pos($part) || 0) ...)
57 14         47 $part = join '', @subparts;
58             } ## end for my $part (@path)
59            
60 7         61 return \@path;
61             } ## end sub crumble
62              
63 11     11 1 19 sub kpath ($input) {
  11         19  
  11         19  
64 11 50       27 return unless defined $input;
65 11 50       28 $input = crumble($input) unless ref($input);
66             return join '.',
67 11         45 map { s{([.%])}{sprintf('%%%02x', ord($1))}regmxs } $input->@*;
  11         79  
  0         0  
68             }
69              
70 11 50   11 1 28 sub means_missing ($x) { ref($x) && refaddr($x) == refaddr(MISSING) }
  11         20  
  11         15  
  11         46  
71              
72             # The following function is long and complex because it deals with many
73             # different cases. It is kept as-is to avoid too many calls to other
74             # subroutines; for this reason, it's reasonably commented.
75 11     11 1 19 sub traverse_plain ($node, $crumbs, %opts) {
  11         20  
  11         17  
  11         43  
  11         18  
76              
77             # figure out what to do with blessed objects, based on configuration
78 11   50     30 my $traverse_methods = $opts{traverse_methods} || 0;
79 11         25 my ($strict_blessed, $method_pre) = (0, 0);
80 11 50       27 if ($traverse_methods) {
81 11   50     47 $strict_blessed = $opts{strict_blessed} || 0;
82 11   50     51 $method_pre = ((! $strict_blessed) && $opts{method_over_key}) || 0;
83             }
84              
85 11         31 for my $key ($crumbs->@*) {
86            
87             # $ref tells me how to look down into $$ref_to_child, i.e. as
88             # an ARRAY or a HASH or a CODE or an object.
89 11         25 my $ref = reftype($node);
90              
91             # if $ref is not true, we hit a wall and cannot go past
92 11 50       27 return MISSING unless $ref;
93              
94             # set up for the tests
95 11         16 my $is_blessed = blessed($node);
96 11   33     30 my $method = $is_blessed && $traverse_methods && $node->can($key);
97            
98             # DWIM dispatch table
99 11 50 33     90 if ($is_blessed && $strict_blessed) {
    50 33        
    50 0        
    50          
    0          
    0          
100 0 0       0 return MISSING unless $method;
101 0 0       0 ($node) = $node->$method or return MISSING;
102             }
103             elsif ($method && $method_pre) {
104 0 0       0 ($node) = $node->$method or return MISSING;
105             }
106             elsif ($ref eq 'CODE') {
107 0 0       0 ($node) = $node->($key) or return MISSING;
108             }
109             elsif ($ref eq 'HASH') {
110 11 50       48 return MISSING unless exists($node->{$key});
111 11         36 $node = $node->{$key};
112             }
113             elsif ($ref eq 'ARRAY') {
114 0 0 0     0 return MISSING
115             if $key !~ m{\A (?: 0 | [1-9] \d*) \z}mxs || $key > $node->$#*;
116 0         0 $node = $node->[$key];
117             }
118             elsif ($method && $traverse_methods) {
119 0 0       0 ($node) = $node->$method or return MISSING;
120             }
121             else {
122 0         0 return MISSING;
123             }
124              
125             } ## end for my $crumb (@$crumbs)
126              
127 11         39 return $node;
128             } ## end sub traverse
129              
130             1;