File Coverage

blib/lib/CXC/Data/Visitor.pm
Criterion Covered Total %
statement 130 132 98.4
branch 43 58 74.1
condition 15 20 75.0
subroutine 20 20 100.0
pod 1 1 100.0
total 209 231 90.4


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 2     2   567467 use v5.20;
  2         10  
6 2     2   33 use strict;
  2         6  
  2         76  
7 2     2   42 use warnings;
  2         5  
  2         154  
8              
9              
10 2     2   14 use feature 'current_sub';
  2         8  
  2         408  
11 2     2   1424 use experimental 'signatures', 'lexical_subs', 'postderef';
  2         7007  
  2         13  
12              
13             our $VERSION = '0.08';
14              
15 2     2   266 use base 'Exporter::Tiny';
  2         4  
  2         1360  
16 2     2   14559 use Hash::Util 'lock_hash', 'unlock_hash', 'unlock_value';
  2         12887  
  2         18  
17 2     2   1478 use POSIX 'floor';
  2         18186  
  2         65  
18 2     2   4044 use Scalar::Util 'refaddr', 'looks_like_number';
  2         4  
  2         146  
19 2         224 use Ref::Util 'is_plain_arrayref', 'is_plain_hashref', 'is_coderef', 'is_plain_ref',
20 2     2   1291 'is_plain_refref';
  2         6322  
21 2     2   1297 use Feature::Compat::Defer;
  2         818  
  2         9  
22              
23             use constant {
24 2         298 CYCLE_DIE => 'die',
25             CYCLE_CONTINUE => 'continue',
26             CYCLE_TRUNCATE => 'truncate',
27 2     2   215 };
  2         4  
28 2     2   15 use constant CYCLE_QR => qr /\A die|continue|truncate \z/x;
  2         5  
  2         205  
29             use constant {
30 2         247 VISIT_HASH => 0b001,
31             VISIT_ARRAY => 0b010,
32             VISIT_CONTAINER => 0b011,
33             VISIT_LEAF => 0b100,
34             VISIT_ALL => 0b111,
35 2     2   15 };
  2         37  
36             use constant {
37 2         191 RESULT_RETURN => 0,
38             RESULT_CONTINUE => 1,
39             RESULT_REVISIT_CONTAINER => 2,
40             RESULT_REVISIT_ELEMENT => 3,
41             RESULT_STOP_DESCENT => 4,
42 2     2   15 };
  2         11  
43              
44 2     2   12 use constant { PASS_VISIT_ELEMENT => 1, PASS_REVISIT_ELEMENT => 2 };
  2         4  
  2         1213  
45              
46             our %EXPORT_TAGS = (
47             funcs => [qw( visit )],
48             results => [
49             qw( RESULT_RETURN RESULT_CONTINUE RESULT_REVISIT_CONTAINER
50             RESULT_REVISIT_ELEMENT RESULT_STOP_DESCENT ),
51             ],
52             cycles => [qw( CYCLE_DIE CYCLE_CONTINUE CYCLE_TRUNCATE )],
53             visits => [qw( VISIT_ARRAY VISIT_HASH VISIT_CONTAINER VISIT_LEAF VISIT_ALL )],
54             passes => [qw( PASS_VISIT_ELEMENT PASS_REVISIT_ELEMENT )],
55             constants => [qw( :results :cycles :visits )],
56             );
57              
58             our @EXPORT_OK = map { $EXPORT_TAGS{$_}->@* } keys %EXPORT_TAGS;
59              
60             my sub croak {
61 1     1   113 require Carp;
62 1         223 goto \&Carp::croak;
63             }
64              
65              
66             ## no critic (Subroutines::ProhibitManyArgs Subroutines::ProhibitExcessComplexity)
67 66     66   75 my sub _visit ( $node, $code, $context, $cycle, $visit, $meta ) {
  66         102  
  66         70  
  66         73  
  66         74  
  66         65  
  66         72  
  66         70  
68              
69 66         84 my $path = $meta->{path};
70 66         77 my $ancestors = $meta->{ancestors};
71 66         86 my $revisit_limit = $meta->{revisit_limit};
72              
73 66         83 my $refaddr = refaddr( $node );
74 66 100       227 if ( exists $meta->{seen}{$refaddr} ) {
75              
76 19 50       30 my $lcycle
77             = is_coderef( $cycle )
78             ? $cycle->( $node, $context, $meta )
79             : $cycle;
80              
81 19 100       29 $lcycle eq CYCLE_TRUNCATE and return !!1;
82 18 100       31 $lcycle eq CYCLE_DIE
83             and croak( __PACKAGE__ . '::visit: cycle detected: ', join( '->', $path->@* ) );
84              
85 17 50       24 $lcycle eq CYCLE_CONTINUE
86             or croak( __PACKAGE__ . "::visit: unkown cycle parameter value: $lcycle" );
87             }
88              
89             # after this call to _visit, will have visited all descendents of
90             # $node, so don't need this any longer.
91 64         125 $meta->{seen}{$refaddr} = ();
92 64         68 defer { delete $meta->{seen}{$refaddr} }
  64         176  
93              
94 64         229 my %meta = $meta->%*;
95 64         116 $meta{container} = $node;
96              
97             # deal with bare next in $code body
98 2     2   18 use warnings FATAL => 'exiting';
  2         22  
  2         3293  
99              
100 64         83 my $is_hashref = is_plain_hashref( $node );
101              
102 64         115 push $ancestors->@*, $node;
103 64         59 defer { pop $ancestors->@* };
  64         124  
104              
105 64         94 my $visit_leaf = !!( $visit & VISIT_LEAF );
106 64         85 my $visit_hash = !!( $visit & VISIT_HASH );
107 64         78 my $visit_array = !!( $visit & VISIT_ARRAY );
108              
109             SCAN: {
110 64 50       74 last unless --$revisit_limit;
  65         99  
111              
112             my @idx
113             = $is_hashref
114             ? (
115             $meta->{sort_keys}
116 65 100       235 ? ( sort { $meta->{sort_keys}->( $a, $b ) } keys $node->%* )
  4 100       9  
117             : sort keys $node->%*
118             )
119             : keys $node->@*;
120              
121 65         109 for my $idx ( @idx ) {
122              
123 111         170 push $path->@*, $idx;
124 111         110 defer { pop $path->@* }
  111         210  
125              
126 111 100       214 my $vref = \( $is_hashref ? $node->{$idx} : $node->[$idx] );
127              
128 111 100       212 my $visit_element
    100          
129             = is_plain_hashref( $$vref ) ? $visit_hash
130             : is_plain_arrayref( $$vref ) ? $visit_array
131             : $visit_leaf;
132              
133 111         123 my $revisit_element = !!0;
134              
135 111         155 $meta{pass} = PASS_VISIT_ELEMENT;
136 111 100 100     260 if ( $visit_element
137             and ( my $result = $code->( $idx, $vref, $context, \%meta ) ) != RESULT_CONTINUE )
138             {
139 6 100       91 redo SCAN if $result == RESULT_REVISIT_CONTAINER;
140 5 100       14 return !!0 if $result == RESULT_RETURN;
141 3 100       12 next if $result == RESULT_STOP_DESCENT; # this works for both leaves and containers
142              
143 1 50       4 if ( $result == RESULT_REVISIT_ELEMENT ) {
    0          
144 1         3 $revisit_element = !!1;
145             }
146             elsif ( $result != RESULT_CONTINUE ) {
147 0         0 croak( "unknown return value from visit: $result" );
148             }
149             }
150              
151 106 100       76556 next unless is_plain_refref( $vref );
152              
153 51         61 my $ref = $vref->$*;
154 51 50 66     138 if ( is_plain_arrayref( $ref ) || is_plain_hashref( $ref ) ) {
155 51 100       177 __SUB__->( $ref, $code, $context, $cycle, $visit, \%meta ) || return !!0;
156              
157 29 100       65 if ( $revisit_element ) {
158 1         5 $meta{pass} = PASS_REVISIT_ELEMENT;
159 1         3 my $result = $code->( $idx, $vref, $context, \%meta );
160 1 50       19 return !!0 if $result == RESULT_RETURN;
161 1 50       5 next if $result == RESULT_CONTINUE;
162 0         0 croak( "unknown return value from visit: $result" );
163             }
164             }
165             }
166             }
167 40 50       64 croak( "exceeded limit ($meta->{revisit_limit}) on revisiting containers" )
168             unless $revisit_limit;
169              
170 40         87 return !!1;
171             }
172             ## critic (Subroutines::ProhibitManyArgs Subroutines::ProhibitExcessComplexity)
173              
174              
175              
176              
177              
178              
179              
180              
181              
182              
183              
184              
185              
186              
187              
188              
189              
190              
191              
192              
193              
194              
195              
196              
197              
198              
199              
200              
201              
202              
203              
204              
205              
206              
207              
208              
209              
210              
211              
212              
213              
214              
215              
216              
217              
218              
219              
220              
221              
222              
223              
224              
225              
226              
227              
228              
229              
230              
231              
232              
233              
234              
235              
236              
237              
238              
239              
240              
241              
242              
243              
244              
245              
246              
247              
248              
249              
250              
251              
252              
253              
254              
255              
256              
257              
258              
259              
260              
261              
262              
263              
264              
265              
266              
267              
268              
269              
270              
271              
272              
273              
274              
275              
276              
277              
278              
279              
280              
281              
282              
283              
284              
285              
286              
287              
288              
289              
290              
291              
292              
293              
294              
295              
296              
297              
298              
299              
300              
301              
302              
303              
304              
305              
306              
307              
308              
309              
310              
311              
312              
313              
314              
315              
316              
317              
318              
319              
320              
321              
322              
323              
324              
325              
326              
327              
328              
329              
330              
331              
332              
333              
334              
335              
336              
337              
338              
339              
340              
341              
342              
343              
344              
345              
346              
347              
348              
349              
350              
351              
352              
353              
354              
355              
356              
357              
358              
359              
360              
361              
362              
363              
364              
365              
366              
367              
368              
369              
370              
371              
372              
373              
374              
375              
376              
377              
378              
379              
380              
381              
382              
383              
384              
385              
386              
387              
388              
389              
390              
391              
392              
393              
394              
395              
396              
397              
398              
399              
400              
401              
402              
403              
404              
405              
406              
407              
408              
409              
410              
411              
412              
413              
414              
415              
416 15     15 1 398236 sub visit ( $struct, $callback, %opts ) {
  15         24  
  15         20  
  15         52  
  15         15  
417              
418 15 50       57 is_coderef( $callback )
419             or croak( q{parameter 'callback' must be a coderef} );
420              
421             croak( q{parameter 'sort_keys' must be a coderef} )
422 15 50 66     44 if exists $opts{sort_keys} && !is_coderef( $opts{sort_keys} );
423              
424 15   100     65 my $context = delete $opts{context} // {};
425              
426             my %metadata = (
427             path => [],
428             seen => {},
429             ancestors => [],
430             container => undef,
431             revisit_limit => delete $opts{revisit_limit} // 10,
432             sort_keys => delete $opts{sort_keys},
433 15   50     101 );
434              
435             croak( "illegal value for 'revisit_limit' : $metadata{revisit_limit}" )
436             unless looks_like_number( $metadata{revisit_limit} )
437 15 50 33     116 && floor( $metadata{revisit_limit} ) == $metadata{revisit_limit};
438              
439              
440 15   100     55 my $cycle = delete $opts{cycle} // 'die';
441 15   100     32 my $visit = delete $opts{visit} // VISIT_ALL;
442              
443 15 50       70 $cycle =~ CYCLE_QR
444             or croak( "illegal value for cycle parameter: $cycle" );
445              
446 15 50       29 %opts
447             and croak( 'illegal parameters: ', join( q{, }, keys %opts ) );
448              
449 15         49 lock_hash( %metadata );
450 15         391 unlock_value( %metadata, 'container' );
451 15         136 my $completed = _visit( $struct, $callback, $context, $cycle, $visit, \%metadata );
452 14         46 unlock_hash( %metadata );
453              
454 14         227 delete $metadata{ancestors}; # should be empty, but just in case,
455             # don't want to keep references
456             # around.
457              
458 14         41 return ( $completed, $context, \%metadata );
459             }
460              
461             1;
462              
463             #
464             # This file is part of CXC-Data-Visitor
465             #
466             # This software is Copyright (c) 2024 by Smithsonian Astrophysical Observatory.
467             #
468             # This is free software, licensed under:
469             #
470             # The GNU General Public License, Version 3, June 2007
471             #
472              
473             __END__