File Coverage

blib/lib/Struct/Path.pm
Criterion Covered Total %
statement 197 197 100.0
branch 110 110 100.0
condition 32 32 100.0
subroutine 9 9 100.0
pod 4 4 100.0
total 352 352 100.0


line stmt bran cond sub pod time code
1             package Struct::Path;
2              
3 8     8   484246 use 5.006;
  8         90  
4 8     8   43 use strict;
  8         13  
  8         179  
5 8     8   35 use warnings FATAL => 'all';
  8         14  
  8         356  
6 8     8   3245 use parent 'Exporter';
  8         2176  
  8         39  
7              
8 8     8   411 use Carp 'croak';
  8         15  
  8         13663  
9              
10             our @EXPORT_OK = qw(
11             implicit_step
12             list_paths
13             path
14             path_delta
15             );
16              
17             =head1 NAME
18              
19             Struct::Path - Path for nested structures where path is also a structure
20              
21             =begin html
22              
23             Travis CI
24             Coverage Status
25             CPAN version
26              
27             =end html
28              
29             =head1 VERSION
30              
31             Version 0.84
32              
33             =cut
34              
35             our $VERSION = '0.84';
36              
37             =head1 SYNOPSIS
38              
39             use Struct::Path qw(list_paths path);
40              
41             $s = [
42             0,
43             {
44             two => {
45             three => 3,
46             four => 4
47             }
48             },
49             undef
50             ];
51              
52             @list = list_paths($s); # list paths and values
53             # @list == (
54             # [[0]], \0,
55             # [[1],{K => ['two']},{K => ['four']}], \4,
56             # [[1],{K => ['two']},{K => ['three']}], \3,
57             # [[2]], \undef
58             # )
59              
60             @r = path($s, [ [1],{K => ['two']} ]); # get refs to values
61             # @r == (\{four => 4,three => 3})
62              
63             =head1 DESCRIPTION
64              
65             Struct::Path provides functions to access/match/expand/list nested data
66             structures.
67              
68             Why L are not enough? This module has no
69             conflicts for paths like '/a/0/c', where C<0> may be an array index or a key
70             for hash (depends on passed structure). This is vital in some cases, for
71             example, when one need to define exact path in structure, but unable to
72             validate it's schema or when structure itself doesn't yet exist (see
73             option C for L).
74              
75             =head1 EXPORT
76              
77             Nothing is exported by default.
78              
79             =head1 ADDRESSING SCHEME
80              
81             Path is a list of 'steps', each represents nested level in the structure.
82              
83             Arrayref as a step stands for ARRAY and must contain desired items indexes or
84             be empty (means "all items"). Sequence for indexes define result sequence.
85              
86             Hashref represent HASH and may contain key C or be empty. C's value
87             should be a list of desired keys and compiled regular expressions. Empty
88             hash or empty list for C means all keys, sequence in the list define
89             resulting sequence.
90              
91             Coderef step is a hook - subroutine which may filter out items and/or modify
92             structure. Traversed path for first, stack of passed structured for secong and
93             path remainder for third agrument passed to hook when executed; all passed args
94             are arrayrefs. Among this two global variables available within hook: C<$_> is
95             set to current substructure and C<$_{opts}> contains c's options. Some
96             true (match) value or false (doesn't match) value expected as output.
97              
98             Sample:
99              
100             $path = [
101             [1,7], # first spep
102             {K => [qr/foo/,qr/bar/]} # second step
103             sub { exists $_->{bar} } # third step
104             ];
105              
106             Struct::Path designed to be machine-friendly. See L
107             and L for human friendly path definition.
108              
109             =head1 SUBROUTINES
110              
111             =head2 implicit_step
112              
113             $bool = implicit_step($step);
114              
115             Returns true value if step contains hooks or specified 'all' items or regexp
116             match.
117              
118             =cut
119              
120             sub implicit_step {
121 8 100   8 1 100 if (ref $_[0] eq 'ARRAY') {
    100          
122 2 100       3 return 1 unless (@{$_[0]});
  2         12  
123             } elsif (ref $_[0] eq 'HASH') {
124 5 100       15 return 1 unless (exists $_[0]->{K});
125 4 100       6 return 1 unless (@{$_[0]->{K}});
  4         11  
126 3   100     6 ref $_ eq 'Regexp' && return 1 for (@{$_[0]->{K}})
  3         15  
127             } else { # hooks
128 1         3 return 1;
129             }
130              
131 2         7 return undef;
132             }
133              
134             =head2 list_paths
135              
136             Returns list of paths and references to their values from structure.
137              
138             @list = list_paths($structure, %opts)
139              
140             =head3 Options
141              
142             =over 4
143              
144             =item depth C<< >>
145              
146             Don't dive into structure deeper than defined level.
147              
148             =back
149              
150             =cut
151              
152             sub list_paths($;@) {
153 7     7 1 26356 my @stack = ([], \shift); # init: (path, ref)
154 7         15 my %opts = @_;
155              
156 7         13 my (@out, $path, $ref);
157 7 100       17 my $depth = defined $opts{depth} ? $opts{depth} : -1;
158              
159 7         23 while (($path, $ref) = splice @stack, 0, 2) {
160 66 100 100     76 if (ref ${$ref} eq 'HASH' and @{$path} != $depth and keys %{${$ref}}) {
  66 100 100     174  
  24   100     66  
  19   100     20  
  19         39  
161 38         37 map { unshift @stack, [@{$path}, {K => [$_]}], \${$ref}->{$_} }
  38         74  
  38         91  
162 18         20 reverse sort keys %{${$ref}};
  18         16  
  18         44  
163 48         84 } elsif (ref ${$ref} eq 'ARRAY' and @{$path} != $depth and @{${$ref}}) {
  12         29  
  11         11  
  11         23  
164 21         20 map { unshift @stack, [@{$path}, [$_]], \${$ref}->[$_] }
  21         35  
  21         42  
165 9         10 reverse 0 .. $#{${$ref}}
  9         10  
  9         15  
166             } else {
167 39         82 push @out, $path, $ref;
168             }
169             }
170              
171 7         48 return @out;
172             }
173              
174             =head2 path
175              
176             Returns list of references from structure.
177              
178             @found = path($structure, $path, %opts)
179              
180             =head3 Options
181              
182             =over 4
183              
184             =item assign C<< >>
185              
186             Assign provided value to substructures pointed by path.
187              
188             =item delete C<< >>
189              
190             Delete specified by path items from structure.
191              
192             =item deref C<< >>
193              
194             Dereference result items.
195              
196             =item expand C<< >>
197              
198             Expand structure if specified in path items doesn't exist. All newly created
199             items initialized by C.
200              
201             =item paths C<< >>
202              
203             Return path for each result.
204              
205             =item stack C<< >>
206              
207             Return stack of references to substructures.
208              
209             =item strict C<< >>
210              
211             Croak if at least one element, specified by path, absent in the structure.
212              
213             =back
214              
215             All options are disabled (C) by default.
216              
217             =cut
218              
219             sub path($$;@) {
220 71     71 1 71930 my (undef, $init_path, %opts) = @_;
221              
222 71 100       364 croak "Arrayref expected for path" unless (ref $init_path eq 'ARRAY');
223             croak "Unable to remove passed thing entirely (empty path passed)"
224 70 100 100     166 if ($opts{delete} and not @{$init_path});
  16         196  
225              
226             # use alias for refs - to be able to rewrite passed scalar
227 69         146 my @stack = ([], [\$_[0]], [@{$_[1]}]);
  69         126  
228 69         115 my (@done, $items, $path, $pos, $refs, $rest, $step, $step_type);
229              
230 69         179 while (($path, $refs, $rest) = splice @stack, 0, 3) {
231 233 100       476 if (not ref $refs->[-1]) {
    100          
232 1         81 croak "Reference expected for refs stack entry, step #$pos";
233 232         403 } elsif (not @{$rest}) {
234 74 100       123 ${$refs->[-1]} = $opts{assign} if (exists $opts{assign});
  7         13  
235              
236 74 100       105 if ($opts{stack}) {
237 2 100       5 map { $_ = ${$_} } @{$refs} if ($opts{deref});
  4         6  
  4         7  
  1         2  
238             } else {
239 72 100       115 $refs = $opts{deref} ? ${$refs->[-1]} : $refs->[-1];
  5         9  
240             }
241              
242 74 100       116 push @done, ($opts{paths} ? ($path, $refs) : $refs);
243              
244 74         198 next;
245             }
246              
247 158         169 $step = shift @{$rest};
  158         199  
248 158         178 $pos = $#{$init_path} - @{$rest};
  158         189  
  158         186  
249              
250 158 100       332 if (($step_type = ref $step) eq 'HASH') {
    100          
    100          
251 77 100       79 if (ref ${$refs->[-1]} ne 'HASH') {
  77         163  
252 2         183 croak "HASH expected on step #$pos, got " . ref ${$refs->[-1]}
253 9 100       22 if ($opts{strict});
254 7 100       19 next unless ($opts{expand});
255 4         6 ${$refs->[-1]} = {};
  4         8  
256             }
257              
258 72         94 undef $items;
259              
260 72 100       120 if (exists $step->{K}) {
261             croak "Unsupported HASH definition, step #$pos"
262 63 100       78 if (keys %{$step} > 1);
  63         234  
263             croak "Unsupported HASH keys definition, step #$pos"
264 62 100       205 unless (ref $step->{K} eq 'ARRAY');
265              
266 61         72 for my $i (@{$step->{K}}) {
  61         119  
267 67 100       101 if (ref $i eq 'Regexp') {
268 9         12 push @{$items}, grep { /$i/ } keys %{${$refs->[-1]}};
  9         13  
  23         93  
  9         10  
  9         21  
269             } else {
270 58 100 100     106 unless ($opts{expand} or exists ${$refs->[-1]}->{$i}) {
  44         121  
271             croak "{$i} doesn't exist, step #$pos"
272 6 100       92 if $opts{strict};
273 5         7 next;
274             }
275 52         68 push @{$items}, $i;
  52         111  
276             }
277             }
278             } else {
279             croak "Unsupported HASH definition, step #$pos"
280 9 100       12 if (keys %{$step});
  9         97  
281             }
282              
283 68 100       121 for (exists $step->{K} ? @{$items} : keys %{${$refs->[-1]}}) {
  60         92  
  8         9  
  8         17  
284             push @stack,
285 79         167 [@{$path}, {K => [$_]}],
286 79         99 [@{$refs}, \${$refs->[-1]}->{$_}],
  79         133  
287 79         89 [@{$rest}];
  79         126  
288              
289 8         24 delete ${$refs->[-1]}->{$_}
290 79 100 100     228 if ($opts{delete} and not @{$rest});
  24         70  
291             }
292             } elsif ($step_type eq 'ARRAY') {
293 70 100       76 if (ref ${$refs->[-1]} ne 'ARRAY') {
  70         139  
294 2         263 croak "ARRAY expected on step #$pos, got " . ref ${$refs->[-1]}
295 17 100       30 if ($opts{strict});
296 15 100       38 next unless ($opts{expand});
297 7         10 ${$refs->[-1]} = [];
  7         11  
298             }
299              
300 60 100       69 $items = @{$step} ? $step : [0 .. $#${$refs->[-1]}];
  60         113  
  11         22  
301 60         74 for (@{$items}) {
  60         106  
302 85 100 100     153 unless (
    100          
303             $opts{expand} or
304 71         67 @{${$refs->[-1]}} > ($_ >= 0 ? $_ : abs($_ + 1))
  71         227  
305             ) {
306 7 100       173 croak "[$_] doesn't exist, step #$pos" if ($opts{strict});
307 5         8 next;
308             }
309              
310 78 100       130 if ($_ < 0) {
311 6 100       8 if (@{${$refs->[-1]}} < abs($_)) {
  6         7  
  6         20  
312             # expand smoothly for out of range negative indexes
313 3         4 $_ = @{${$refs->[-1]}};
  3         5  
  3         6  
314             } else {
315 3         3 $_ += @{${$refs->[-1]}};
  3         3  
  3         5  
316             }
317             }
318              
319             push @stack,
320 78         124 [@{$path}, [$_]],
321 78         92 [@{$refs}, \${$refs->[-1]}->[$_]],
  78         126  
322 78         87 [@{$rest}];
  78         146  
323             }
324              
325 58 100 100     163 if ($opts{delete} and not @{$rest}) {
  16         42  
326 9         9 for (reverse sort @{$items}) {
  9         26  
327 13         12 splice(@{${$refs->[-1]}}, $_, 1)
  13         32  
328 15 100       16 if ($_ < @{${$refs->[-1]}});
  15         15  
  15         23  
329             }
330             }
331             } elsif ($step_type eq 'CODE') {
332 10         12 local $_ = ${$refs->[-1]};
  10         14  
333 10         17 local $_{opts} = \%opts;
334              
335             $step->($path, $refs, $rest) and
336 10 100       31 push @stack, $path, $refs, [@{$rest}];
  7         54  
337             } else {
338 1         83 croak "Unsupported thing in the path, step #$pos";
339             }
340             }
341              
342 57         206 return @done;
343             }
344              
345             =head2 path_delta
346              
347             Returns delta for two passed paths. By delta means list of steps from the
348             second path without beginning common steps for both.
349              
350             @delta = path_delta($path1, $path2)
351              
352             =cut
353              
354             sub path_delta($$) {
355 16     16 1 14039 my ($frst, $scnd) = @_;
356              
357 16 100       191 croak "Second path must be an arrayref" unless (ref $scnd eq 'ARRAY');
358 15 100       45 return @{$scnd} unless (defined $frst);
  1         5  
359 14 100       144 croak "First path may be undef or an arrayref" unless (ref $frst eq 'ARRAY');
360              
361 13         67 require B::Deparse;
362 13         229 my $deparse = B::Deparse->new();
363 13         27 my $i = 0;
364              
365             MAIN:
366 13   100     17 while ($i < @{$frst} and ref $frst->[$i] eq ref $scnd->[$i]) {
  35         138  
367 31 100       91 if (ref $frst->[$i] eq 'ARRAY') {
    100          
    100          
368 10 100       32 last unless (@{$frst->[$i]} == @{$scnd->[$i]});
  10         17  
  10         20  
369 8         13 for (0 .. $#{$frst->[$i]}) {
  8         17  
370 15 100       30 last MAIN unless ($frst->[$i]->[$_] == $scnd->[$i]->[$_]);
371             }
372             } elsif (ref $frst->[$i] eq 'HASH') {
373 18 100       25 last unless (@{$frst->[$i]->{K}} == @{$scnd->[$i]->{K}});
  18         28  
  18         36  
374 16         23 for (0 .. $#{$frst->[$i]->{K}}) {
  16         45  
375             last MAIN unless (
376             $frst->[$i]->{K}->[$_] eq
377 21 100       55 $scnd->[$i]->{K}->[$_]
378             );
379             }
380             } elsif (ref $frst->[$i] eq 'CODE') {
381             last unless (
382 2 100       2169 $deparse->coderef2text($frst->[$i]) eq
383             $deparse->coderef2text($scnd->[$i])
384             );
385             } else {
386 1         188 croak "Unsupported thing in the path, step #$i";
387             }
388              
389 22         37 $i++;
390             }
391              
392 12         23 return @{$scnd}[$i .. $#{$scnd}];
  12         97  
  12         19  
393             }
394              
395             =head1 LIMITATIONS
396              
397             Struct::Path will fail on structures with loops in references.
398              
399             No object oriented interface provided.
400              
401             =head1 AUTHOR
402              
403             Michael Samoglyadov, C<< >>
404              
405             =head1 BUGS
406              
407             Please report any bugs or feature requests to C,
408             or through the web interface at
409             L. I will be
410             notified, and then you'll automatically be notified of progress on your bug
411             as I make changes.
412              
413             =head1 SUPPORT
414              
415             You can find documentation for this module with the perldoc command.
416              
417             perldoc Struct::Path
418              
419             You can also look for information at:
420              
421             =over 4
422              
423             =item * RT: CPAN's request tracker (report bugs here)
424              
425             L
426              
427             =item * AnnoCPAN: Annotated CPAN documentation
428              
429             L
430              
431             =item * CPAN Ratings
432              
433             L
434              
435             =item * Search CPAN
436              
437             L
438              
439             =back
440              
441             =head1 SEE ALSO
442              
443             L L L L L
444             L L L L L
445             L L L
446              
447             L L L
448              
449             =head1 LICENSE AND COPYRIGHT
450              
451             Copyright 2016-2019 Michael Samoglyadov.
452              
453             This program is free software; you can redistribute it and/or modify it
454             under the terms of either: the GNU General Public License as published
455             by the Free Software Foundation; or the Artistic License.
456              
457             See L for more information.
458              
459             =cut
460              
461             1; # End of Struct::Path