File Coverage

blib/lib/Struct/Path.pm
Criterion Covered Total %
statement 197 197 100.0
branch 106 106 100.0
condition 33 33 100.0
subroutine 9 9 100.0
pod 4 4 100.0
total 349 349 100.0


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