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