File Coverage

blib/lib/Data/Reach.pm
Criterion Covered Total %
statement 124 125 99.2
branch 87 102 85.2
condition 56 66 84.8
subroutine 16 16 100.0
pod 3 3 100.0
total 286 312 91.6


line stmt bran cond sub pod time code
1             package Data::Reach;
2 9     9   617477 use 5.014;
  9         103  
3 9     9   52 use strict;
  9         15  
  9         208  
4 9     9   42 use warnings;
  9         17  
  9         296  
5 9     9   54 use Carp qw/carp croak/;
  9         13  
  9         616  
6 9     9   74 use Scalar::Util qw/blessed reftype/;
  9         19  
  9         545  
7 9     9   10586 use overload;
  9         8602  
  9         55  
8            
9             our $VERSION = '2.01';
10            
11            
12             #======================================================================
13             # reach() and utility functions
14             #======================================================================
15             # main entry point
16             sub reach ($@) {
17 172     172 1 10865 my ($root, @path) = @_;
18            
19             # loop until either @path or the datastructure under $root is exhausted
20 172         254 while (1) {
21            
22             # exit conditions
23 413 100       3774 return undef if !defined $root;
24 405 100       954 return $root if !@path;
25 252         395 my $path0 = shift @path;
26 252 100       450 return undef if !defined $path0;
27            
28             # otherwise, walk down one step into the datastructure and loop again
29 251 100       665 $root = blessed $root ? _step_down_obj($root, $path0)
30             : _step_down_raw($root, $path0);
31             }
32             }
33            
34             # get inner data within a raw datastructure
35             sub _step_down_raw {
36 206     206   360 my ($data, $key) = @_;
37            
38 206   100     502 my $reftype = reftype $data || '';
39            
40 206 100       396 if ($reftype eq 'HASH') {
    100          
41 127         337 return $data->{$key};
42             }
43             elsif ($reftype eq 'ARRAY') {
44 72 100       298 if ($key =~ /^-?\d+$/) {
45 71         184 return $data->[$key];
46             }
47             else {
48 1         186 croak "cannot reach index '$key' within an array";
49             }
50             }
51             else {
52 7 50       24 my $kind = $reftype ? "${reftype}REF"
    100          
53             : defined ref $data ? "SCALAR"
54             : "undef";
55 7 50       27 my $article = $kind =~ /^[aeiou]/i ? "an" : "a";
56 7         684 croak "cannot reach '$key' within $article $kind";
57             }
58             }
59            
60            
61             # get inner data within an object
62             sub _step_down_obj {
63 52     52   96 my ($obj, $key) = @_;
64            
65             # pragmata that may modify our algorithm -- see L
66 52         278 my $hint_hash = (caller(1))[10];
67 52   100     203 my $use_overloads = $hint_hash->{'Data::Reach/use_overloads'} // 1; # default
68 52   100     133 my $peek_blessed = $hint_hash->{'Data::Reach/peek_blessed'} // 1; # default
69            
70             # choice 1 : call named method in object
71 52   100     124 my $meth_name = $hint_hash->{'Data::Reach/reach_method'} || '';
72 52 100       215 return $obj->$meth_name($key) if $obj->can($meth_name);
73            
74             # choice 2 : use overloaded methods -- active by default
75 43 100       95 if ($use_overloads) {
76             # overloaded array dereferencing is tried first but only if the key is numeric.
77             # Otherwise, the hash dereferencing is tried.
78 41 100 100     95 return $obj->[$key] if overload::Method($obj, '@{}')
79             && $key =~ /^-?\d+$/;
80 9 100       398 return $obj->{$key} if overload::Method($obj, '%{}');
81             }
82            
83             # choice 3 : use the object's internal representation -- active by default
84 9 100       261 if ($peek_blessed) {
85 7         13 return _step_down_raw($obj, $key);
86             }
87             else {
88 2         295 croak "cannot reach '$key' within an object of class " . ref $obj;
89             }
90             }
91            
92            
93             #======================================================================
94             # map_paths()
95             #======================================================================
96            
97             sub map_paths (&+;$$$); # the prototype must be declared beforehand, because the sub is recursive
98             sub map_paths (&+;$$$) {
99 124     124 1 2683 my ($coderef, $tree, $max_depth, $path, $recurse)= @_;
100 124   100     227 $max_depth //= -1;
101 124   100     236 $path //= []; # only used for recursive calls
102 124   100     549 $recurse //= reftype $tree // ''; # only used for recursive calls
      100        
103            
104 124         528 my $hint_hash = (caller(1))[10];
105 124         271 my $ignore_empty_subtrees = ! $hint_hash->{'Data::Reach/keep_empty_subtrees'};
106            
107 124 50       203 if ($max_depth) {
108 124 100 100     656 if ($recurse eq 'ARRAY' and (@$tree or $ignore_empty_subtrees)) {
    100 100        
    100 100        
      66        
109 9         58 return map {map_paths(\&$coderef, $tree->[$_], $max_depth-1, [@$path, $_])} 0 .. $#$tree;
  62         414  
110             }
111             elsif ($recurse eq 'HASH' and (my @k = sort keys %$tree or $ignore_empty_subtrees)) {
112 19         37 return map {map_paths(\&$coderef, $tree->{$_}, $max_depth-1, [@$path, $_])} @k;
  50         318  
113             }
114             elsif (blessed $tree) {
115             # try to call named method in object
116 4 100       10 if (my $meth_name = $hint_hash->{'Data::Reach/paths_method'}) {
117 1 50       7 if ($tree->can($meth_name)) {
118 1         6 my @paths = $tree->$meth_name();
119 1         8 return map {map_paths(\&$coderef, reach($tree, $_), $max_depth-1, [@$path, $_])} @paths;
  4         25  
120             }
121             }
122            
123             # otherwise, try to use overloaded methods, or else use the object's internal representation (if allowed)
124 3   100     10 my $use_overloads = $hint_hash->{'Data::Reach/use_overloads'} // 1; # default
125 3   100     8 my $peek_blessed = $hint_hash->{'Data::Reach/peek_blessed'} // 1; # default
126 3 50 66     16 $recurse = $use_overloads && overload::Method($tree, '@{}') ? 'ARRAY'
    50 33        
    100          
127             : $use_overloads && overload::Method($tree, '%{}') ? 'HASH'
128             : $peek_blessed ? reftype $tree
129             : undef;
130            
131             # recursive call if appropriate
132 3 100       142 return map_paths(\&$coderef, $tree, $max_depth, $path, $recurse) if $recurse;
133            
134             # if all else failed, treat this object as an opaque leaf (see base case below)
135             }
136             }
137            
138             # base case
139 93         148 for ($tree) {return $coderef->(@$path)}; # @_ contains the path, $_ contains the leaf
  93         174  
140             }
141            
142            
143            
144             #======================================================================
145             # each_path()
146             #======================================================================
147            
148             sub each_path (+;$) {
149 122     122 1 5267 my ($tree, $max_depth) = @_;
150 122   100     219 $max_depth //= -1;
151 122         547 my $hint_hash = (caller(1))[10];
152 122   100     418 my $use_overloads = $hint_hash->{'Data::Reach/use_overloads'} // 1; # default
153 122   100     300 my $peek_blessed = $hint_hash->{'Data::Reach/peek_blessed'} // 1; # default
154 122         156 my $keep_empty_subtrees = $hint_hash->{'Data::Reach/keep_empty_subtrees'};
155            
156             # local boolean variable to avoid returning the same result multiple times
157 122         141 my $is_consumed = 0;
158            
159             # closure to be used at tree leaves
160 122 100   186   390 my $leaf = sub {return $is_consumed++ ? () : ([], $tree)};
  186         478  
161            
162 122         184 my $paths_method = $hint_hash->{'Data::Reach/paths_method'};
163 122 50 66     420 my $recurse = !blessed $tree ? reftype $tree
    50 66        
    100 33        
    100          
    100          
164             : $paths_method && $tree->can($paths_method) ? 'OBJECT'
165             : $use_overloads && overload::Method($tree, '@{}') ? 'ARRAY'
166             : $use_overloads && overload::Method($tree, '%{}') ? 'HASH'
167             : $peek_blessed ? reftype $tree
168             : undef;
169            
170             # either this tree is a leaf, or we must recurse into subtrees
171 122 100 66     561 if (!$recurse || $recurse !~ /^(OBJECT|HASH|ARRAY)$/ || !$max_depth) {
172 91         408 return $leaf;
173             }
174             else {
175 31 50       213 my @paths = $recurse eq 'OBJECT' ? $tree->$paths_method()
    100          
    100          
176             : $recurse eq 'HASH' ? sort keys %$tree
177             : $recurse eq 'ARRAY' ? (0 .. $#$tree)
178             : ();
179 31 100 100     126 if (!@paths && $keep_empty_subtrees) {
180 2         8 return $leaf;
181             }
182             else {
183 29         39 my $next_subpath; # iterator into next subtree
184            
185             return sub {
186 260     260   682 while (1) {
187 376 100       634 if (!$next_subpath) { # if there is no current iterator
188 145 100 66     419 if (!$is_consumed && @paths) { # if there is a chance to get a new iterator
189 116         197 my $subtree = reach $tree, $paths[0];
190 116         223 $next_subpath = each_path($subtree, $max_depth-1); # build an iterator on next subtree
191             }
192             else { # end of data
193 29         56 $is_consumed++;
194 29         59 return ();
195             }
196             }
197 347 100       474 if (my ($subpath, $subval) = $next_subpath->()) { # try to get content from the current iterator
198 231         658 return ([$paths[0], @$subpath], $subval); # found a path, return it
199             }
200             else { # mark that the iterator on this subtree ..
201 116         293 $next_subpath = undef; # .. is finished and move to the next data item
202 116         189 shift @paths;
203             }
204             }
205             }
206 29         206 }
207             }
208             }
209            
210            
211            
212            
213             #======================================================================
214             # class methods: import and unimport
215             #======================================================================
216            
217             # the 'import' method does 2 things : a) export the required functions,
218             # like the regular Exporter, but possibly with a change of name;
219             # b) implement optional changes to the algorithm, lexically scoped
220             # through the %^H hint hash (see L).
221            
222             my $exported_functions = qr/^(?: reach | each_path | map_paths )$/x;
223             my $hint_options = qr/^(?: peek_blessed | use_overloads | keep_empty_subtrees )$/x;
224            
225             sub import {
226 15     15   120 my $class = shift;
227 15         45 my $pkg = caller;
228            
229             # defaults
230 15 100       69 my %export_as = map {($_ => $_)} qw/reach each_path map_paths/ if !@_;
  9         26  
231 15         25 my $last_func = 'reach';
232            
233             # loop over args passed to import()
234 15         56 while (my $option = shift) {
235 14 100       168 if ($option =~ $exported_functions) {
    100          
    100          
    100          
    50          
236 3         10 $export_as{$option} = $option;
237 3         10 $last_func = $option;
238             }
239             elsif ($option eq 'as') {
240 3 100       272 my $alias = shift
241             or croak "use Data::Reach : no export name after 'as'";
242 2         7 $export_as{$last_func} = $alias;
243             }
244             elsif ($option =~ /^(reach|call)_method$/) {
245 5 50       30 warn q{"use Data::Reach call_method => .." is obsolete; use "reach_method => .."} if $1 eq 'call';
246 5 50       15 my $method = shift
247             or croak "use Data::Reach : no method name after 'reach_method'";
248 5         31 $^H{"Data::Reach/reach_method"} = $method;
249             }
250             elsif ($option eq 'paths_method') {
251 1 50       4 my $method = shift
252             or croak "use Data::Reach : no method name after 'paths_method'";
253 1         5 $^H{"Data::Reach/paths_method"} = $method;
254             }
255             elsif ($option =~ $hint_options) {
256 2         17 $^H{"Data::Reach/$option"} = 1;
257             }
258             else {
259 0         0 croak "use Data::Reach : unknown option : $option";
260             }
261             }
262            
263             # export into caller's package, under the required alias names
264 14         267 while (my ($func, $alias) = each %export_as) {
265 9     9   16818 no strict 'refs';
  9         43  
  9         1846  
266 17 50       54 *{$pkg . "::" . $alias} = \&$func if $alias;
  17         6369  
267             }
268             }
269            
270            
271             sub unimport {
272 5     5   3412 my $class = shift;
273 5         15 while (my $option = shift) {
274 7 50       944 $^H{"Data::Reach/$option"} = '' if $option =~ $hint_options;
275             # NOTE : mark with a false value, instead of deleting from the
276             # hint hash, in order to distinguish options explicitly turned off
277             # from default options
278             }
279             }
280            
281            
282             1;
283            
284            
285             __END__