File Coverage

blib/lib/Data/Reach.pm
Criterion Covered Total %
statement 122 123 99.1
branch 87 102 85.2
condition 56 66 84.8
subroutine 15 15 100.0
pod 3 3 100.0
total 283 309 91.5


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