File Coverage

blib/lib/CXC/Data/Visitor.pm
Criterion Covered Total %
statement 200 201 99.5
branch 87 110 79.0
condition 18 29 62.0
subroutine 22 22 100.0
pod 1 1 100.0
total 328 363 90.3


line stmt bran cond sub pod time code
1             package CXC::Data::Visitor;
2              
3             # ABSTRACT: Invoke a callback on every element at every level of a data structure.
4              
5 3     3   702619 use v5.20;
  3         11  
6 3     3   15 use strict;
  3         6  
  3         69  
7 3     3   15 use warnings;
  3         4  
  3         139  
8              
9              
10 3     3   12 use feature 'current_sub';
  3         7  
  3         482  
11 3     3   1685 use experimental 'signatures', 'lexical_subs', 'postderef';
  3         8873  
  3         14  
12              
13             #<<< no tidy
14             our $VERSION = '0.12';
15             #>>>
16              
17 3     3   326 use base 'Exporter::Tiny';
  3         6  
  3         1624  
18 3     3   16603 use Hash::Util 'lock_hash', 'unlock_hash', 'unlock_value';
  3         10199  
  3         29  
19 3     3   1755 use POSIX 'floor';
  3         20802  
  3         24  
20 3     3   4566 use Scalar::Util 'refaddr', 'looks_like_number';
  3         4  
  3         195  
21 3         280 use Ref::Util 'is_plain_arrayref', 'is_plain_hashref', 'is_coderef', 'is_plain_ref',
22 3     3   1626 'is_plain_refref';
  3         7639  
23 3     3   1620 use Feature::Compat::Defer;
  3         1055  
  3         13  
24              
25             use constant {
26 3         339 CYCLE_DIE => 'die',
27             CYCLE_CONTINUE => 'continue',
28             CYCLE_TRUNCATE => 'truncate',
29 3     3   267 };
  3         5  
30 3     3   21 use constant CYCLE_QR => qr /\A die|continue|truncate \z/x;
  3         6  
  3         220  
31             use constant {
32 3         309 VISIT_HASH => 0b0001,
33             VISIT_ARRAY => 0b0010,
34             VISIT_CONTAINER => 0b0011,
35             VISIT_LEAF => 0b0100,
36             VISIT_ALL => 0b0111,
37             VISIT_ROOT => 0b1000,
38 3     3   16 };
  3         6  
39             use constant {
40 3         271 RESULT_NULL => 0b000000,
41             RESULT_RETURN => 0b000001,
42             RESULT_CONTINUE => 0b000010,
43             RESULT_REVISIT_CONTENTS => 0b000100,
44             RESULT_REVISIT_CONTAINER => 0b000100, # back compat
45             RESULT_REVISIT_ELEMENT => 0b001000,
46             RESULT_STOP_DESCENT => 0b010000,
47             RESULT_REVISIT_ROOT => 0b100000,
48 3     3   29 };
  3         8  
49              
50             use constant {
51 3         1678 PASS_VISIT_ELEMENT => 0b01,
52             PASS_REVISIT_ELEMENT => 0b10,
53 3     3   12 };
  3         5  
54              
55             our %EXPORT_TAGS = (
56             funcs => [qw( visit )],
57             results => [ qw(
58             RESULT_NULL
59             RESULT_RETURN
60             RESULT_CONTINUE
61             RESULT_REVISIT_ROOT
62             RESULT_REVISIT_CONTENTS
63             RESULT_REVISIT_CONTAINER
64             RESULT_REVISIT_ELEMENT
65             RESULT_STOP_DESCENT
66             ),
67             ],
68              
69             cycles => [ qw(
70             CYCLE_DIE
71             CYCLE_CONTINUE
72             CYCLE_TRUNCATE
73             ),
74             ],
75              
76             visits => [ qw(
77             VISIT_ARRAY
78             VISIT_HASH
79             VISIT_CONTAINER
80             VISIT_LEAF
81             VISIT_ALL
82             VISIT_ROOT
83             ),
84             ],
85              
86             passes => [ qw(
87             PASS_VISIT_ELEMENT
88             PASS_REVISIT_ELEMENT
89             ),
90             ],
91              
92             constants => [qw( :results :cycles :visits :passes )],
93             );
94              
95             our @EXPORT_OK = map { $EXPORT_TAGS{$_}->@* } keys %EXPORT_TAGS;
96              
97             my sub croak {
98 1     1   896 require Carp;
99 1         217 goto \&Carp::croak;
100             }
101              
102              
103             ## no critic (Subroutines::ProhibitManyArgs Subroutines::ProhibitExcessComplexity)
104 109     109   145 my sub visit_node ( $node, $code, $context, $cycle, $visit, $meta ) {
  109         169  
  109         159  
  109         132  
  109         163  
  109         137  
  109         127  
  109         129  
105              
106 109         169 my $path = $meta->{path};
107 109         151 my $ancestors = $meta->{ancestors};
108              
109 109         183 my $sort_key_mode = $meta->{sort_key_mode};
110 109         161 my $sort_idx_mode = $meta->{sort_idx_mode};
111 109         149 my $key_sort = $meta->{key_sort};
112 109         163 my $idx_sort = $meta->{idx_sort};
113              
114 109         171 my $visit_leaf = !!( $visit & VISIT_LEAF );
115 109         159 my $visit_hash = !!( $visit & VISIT_HASH );
116 109         145 my $visit_array = !!( $visit & VISIT_ARRAY );
117              
118 109         159 my $refaddr = refaddr( $node );
119 109 100       295 if ( exists $meta->{seen}{$refaddr} ) {
120              
121 19 50       25 my $lcycle
122             = is_coderef( $cycle )
123             ? $cycle->( $node, $context, $meta )
124             : $cycle;
125              
126 19 100       28 $lcycle eq CYCLE_TRUNCATE and return !!1;
127 18 100       30 $lcycle eq CYCLE_DIE
128             and croak( __PACKAGE__ . '::visit: cycle detected: ', join( '->', $path->@* ) );
129              
130 17 50       25 $lcycle eq CYCLE_CONTINUE
131             or croak( __PACKAGE__ . "::visit: unkown cycle parameter value: $lcycle" );
132             }
133              
134             # after this call to visit_node, will have visited all descendents of
135             # $node, so don't need this any longer.
136 107         249 $meta->{seen}{$refaddr} = ();
137 107         140 defer { delete $meta->{seen}{$refaddr} }
  107         312  
138              
139 107         543 my %meta = $meta->%*;
140 107         223 $meta{container} = $node;
141              
142             # deal with bare next in $code body
143 3     3   35 use warnings FATAL => 'exiting';
  3         6  
  3         6731  
144              
145 107         170 my $is_hashref = is_plain_hashref( $node );
146              
147 107         178 push $ancestors->@*, $node;
148 107         174 defer { pop $ancestors->@* };
  107         320  
149              
150 107         174 my $revisit_limit = $meta->{revisit_limit};
151 107         223 @meta{ 'visit', 'idx' } = ( 0, -1 );
152              
153             SCAN: {
154 107 50       148 last unless --$revisit_limit;
  110         213  
155              
156 110         151 $meta{visit}++;
157 110         164 $meta{idx} = -1;
158              
159 110         152 my $rescan_container = !!0;
160              
161 110         145 my $kydx_arr = do {
162              
163 110 100       199 if ( $is_hashref ) {
164 62 100       286 $sort_key_mode == 0 ? $key_sort->( [ keys $node->%* ] )
    100          
165             : $sort_key_mode == 1 ? [ sort keys $node->%* ]
166             : [ keys $node->%* ];
167             }
168              
169             # must be an arrayref
170             else {
171 48 100       165 $sort_idx_mode == 0
172             ? $idx_sort->( 0+ $node->@* )
173             : [ 0 .. ( $node->@* - 1 ) ];
174             }
175             };
176              
177 110         232 for my $kydx ( $kydx_arr->@* ) {
178 232         402 $meta{idx}++;
179              
180 232         362 push $path->@*, $kydx;
181 232         305 defer { pop $path->@* }
  232         529  
182              
183 232 100       465 my $vref = \( $is_hashref ? $node->{$kydx} : $node->[$kydx] );
184              
185 232 100       514 my $visit_element
    100          
186             = is_plain_hashref( $$vref ) ? $visit_hash
187             : is_plain_arrayref( $$vref ) ? $visit_array
188             : $visit_leaf;
189              
190 232         329 my $revisit_element = !!0;
191              
192 232         335 $meta{pass} = PASS_VISIT_ELEMENT;
193 232 100 100     608 if ( $visit_element
194             and ( my $result = $code->( $kydx, $vref, $context, \%meta ) ) != RESULT_CONTINUE )
195             {
196             # immediate rescan if explicitly set to value,
197             # otherwise it will happen after the container is
198             # completely visited
199 9 100       111 redo SCAN if $result == RESULT_REVISIT_CONTENTS;
200 7 100       16 return RESULT_RETURN if $result == RESULT_RETURN;
201 5 100       10 return RESULT_REVISIT_ROOT if $result == RESULT_REVISIT_ROOT;
202              
203 4         7 $rescan_container = $result & RESULT_REVISIT_CONTENTS;
204              
205 4 100       11 next if $result & RESULT_STOP_DESCENT; # this works for both leaves and containers
206              
207 1         2 $revisit_element = $result & RESULT_REVISIT_ELEMENT;
208              
209 1 50 33     4 croak( "unknown return value from visit: $result" )
210             if !$revisit_element && !$result & RESULT_CONTINUE;
211             }
212              
213 224 100       83368 next unless is_plain_refref( $vref );
214              
215 83         183 my $ref = $vref->$*;
216 83 50 66     249 if ( is_plain_arrayref( $ref ) || is_plain_hashref( $ref ) ) {
217 83         276 my $result = __SUB__->( $ref, $code, $context, $cycle, $visit, \%meta );
218 80 100       188 return RESULT_RETURN if $result == RESULT_RETURN;
219 58 100       124 return RESULT_REVISIT_ROOT if $result == RESULT_REVISIT_ROOT;
220 57 100       152 if ( $revisit_element ) {
221 1         2 $meta{pass} = PASS_REVISIT_ELEMENT;
222 1         4 $result = $code->( $kydx, $vref, $context, \%meta );
223 1 50       8 return RESULT_RETURN if $result == RESULT_RETURN;
224 1 50       3 return RESULT_REVISIT_ROOT if $result == RESULT_REVISIT_ROOT;
225 1 50       2 croak( "unexpected return value from visit: $result" )
226             if $result & ~( RESULT_CONTINUE | RESULT_REVISIT_CONTENTS );
227 1         3 $rescan_container |= $result & RESULT_REVISIT_CONTENTS;
228             }
229             }
230             }
231 79 100       254 redo SCAN if $rescan_container;
232             }
233 78 50       150 croak( "exceeded limit ($meta{revisit_limit}) on revisiting containers" )
234             unless $revisit_limit;
235              
236 78         137 return RESULT_CONTINUE;
237             }
238              
239 5     5   11 my sub visit_root ( $root, $code, $context, $cycle, $visit, $meta ) {
  5         9  
  5         13  
  5         70  
  5         11  
  5         9  
  5         9  
  5         13  
240              
241 5         31 my %meta = $meta->%*;
242 5         16 my $revisit_limit = $meta{revisit_limit};
243 5         14 $meta{pass} = PASS_VISIT_ELEMENT;
244 5         22 @meta{ 'visit', 'idx' } = ( 0, 0 );
245              
246             FROOT_LOOP:
247             {
248 5         8 $meta{visit}++;
  6         16  
249 6 50       18 last unless --$revisit_limit;
250              
251 6         23 my $result = $code->( undef, \$root, $context, \%meta );
252              
253 6 100       102 redo FROOT_LOOP if $result == RESULT_REVISIT_ROOT;
254              
255 5 100       22 return !!0 if $result == RESULT_RETURN;
256 4 100       16 return !!1 if $result == RESULT_STOP_DESCENT;
257              
258 3         7 my $revisit_element = $result & RESULT_REVISIT_ELEMENT;
259              
260 3 50 66     17 croak( "unknown return value from visit: $result" )
261             if !$revisit_element && !$result & RESULT_CONTINUE;
262              
263 3         11 my $status = visit_node( $root, $code, $context, $cycle, $visit, \%meta );
264 3 50       13 return !!0 if $status == RESULT_RETURN;
265              
266 3 100       13 if ( $revisit_element ) {
267 1         3 $meta{pass} = PASS_REVISIT_ELEMENT;
268 1         4 $result = $code->( undef, \$root, $context, \%meta );
269 1 50       16 return !!0 if $result == RESULT_RETURN;
270 1 50       9 return !!1 if $result == RESULT_CONTINUE;
271 0         0 croak( "unexpected return value while revisiting root: $result" );
272             }
273             }
274 2 50       6 croak( "exceeded limit ($meta{revisit_limit}) while revisiting root" )
275             unless $revisit_limit;
276              
277 2         11 return !!1;
278             }
279              
280              
281              
282             ## critic (Subroutines::ProhibitManyArgs Subroutines::ProhibitExcessComplexity)
283 27     27 1 759429 sub visit ( $root, $callback, %opts ) {
  27         72  
  27         43  
  27         64  
  27         37  
284              
285 27 50       105 is_coderef( $callback )
286             or croak( q{parameter 'callback' must be a coderef} );
287              
288 27   100     133 my $context = delete $opts{context} // {};
289              
290             # back compat
291 27 100       87 if ( defined( my $sort_keys = delete $opts{sort_keys} ) ) {
292             croak( q{specify only one of 'key_sort' or 'sort_keys'} )
293 1 50       5 if defined $opts{key_sort};
294              
295 2         5 $opts{key_sort} = is_coderef( $sort_keys )
296 2     2   5 ? sub ( $array ) {
  2         3  
297 2         11 [ sort { $sort_keys->( $a, $b ) } $array->@* ];
  4         16  
298             }
299 1 50       10 : $sort_keys;
300             }
301              
302             croak( "illegal value for 'revisit_limit' : $opts{revisit_limit}" )
303             if defined $opts{revisit_limit}
304             && !(looks_like_number( $opts{revisit_limit} )
305 27 50 0     98 && floor( $opts{revisit_limit} ) == $opts{revisit_limit} );
      33        
306              
307             my %metadata = (
308             path => [],
309             seen => {},
310             ancestors => [],
311             container => undef,
312             revisit_limit => delete $opts{revisit_limit} // 10,
313             key_sort => delete $opts{key_sort},
314             idx_sort => delete $opts{idx_sort},
315 27   50     311 );
316              
317             {
318 27         43 my $key_sort = $metadata{key_sort};
  27         48  
319 27         53 my $idx_sort = $metadata{idx_sort};
320              
321             # $sort_key_mode =
322             # 0 if passed coderef
323             # 1 if should sort
324             # 2 if should not sort
325             $metadata{sort_key_mode}
326 27 100       91 = defined $key_sort ? ( is_coderef( $key_sort ) ? 0 : $key_sort ? 1 : 2 ) : 1;
    100          
    100          
327              
328             # sorting indices is different than sorting keys,
329             # as unlike for keys, indices are intrinsicly sorted
330              
331             # $sort_idx_modes =
332             # 0 if passed coderef
333             # 1 otherwise
334 27 100 66     103 $metadata{sort_idx_mode} = defined( $idx_sort ) && is_coderef( $idx_sort ) ? 0 : 1;
335             }
336              
337 27   100     106 my $cycle = delete $opts{cycle} // 'die';
338 27   100     132 my $visit = delete $opts{visit} // VISIT_ALL;
339 27 100       69 $visit |= VISIT_ALL if $visit == VISIT_ROOT;
340              
341 27 50       140 $cycle =~ CYCLE_QR
342             or croak( "illegal value for cycle parameter: $cycle" );
343              
344 27 50       87 %opts
345             and croak( 'illegal parameters: ', join( q{, }, keys %opts ) );
346              
347 27         127 lock_hash( %metadata );
348 27         986 unlock_value( %metadata, 'container' );
349              
350 27         254 my $completed;
351              
352 27 100       99 if ( $visit & VISIT_ROOT ) {
353 5         21 $completed = visit_root( $root, $callback, $context, $cycle, $visit, \%metadata );
354             }
355             else {
356 22         40 my $revisit_limit = $metadata{revisit_limit};
357 22         49 while ( --$revisit_limit ) {
358 23 50       53 last unless --$revisit_limit;
359 23         62 $completed = visit_node( $root, $callback, $context, $cycle, $visit, \%metadata );
360 22 100       68 last unless $completed == RESULT_REVISIT_ROOT;
361             }
362 21 50       38 croak( "exceeded limit ($metadata{revisit_limit}) while revisiting root" )
363             unless $revisit_limit;
364 21         38 $completed = $completed != RESULT_RETURN;
365             }
366              
367 26         104 unlock_hash( %metadata );
368              
369 26         747 delete $metadata{ancestors}; # should be empty, but just in case,
370             # don't want to keep references
371             # around.
372              
373 26         98 return ( $completed, $context, \%metadata );
374             }
375              
376             1;
377              
378             #
379             # This file is part of CXC-Data-Visitor
380             #
381             # This software is Copyright (c) 2024 by Smithsonian Astrophysical Observatory.
382             #
383             # This is free software, licensed under:
384             #
385             # The GNU General Public License, Version 3, June 2007
386             #
387              
388             __END__