File Coverage

blib/lib/Data/Visitor/Callback.pm
Criterion Covered Total %
statement 171 185 92.4
branch 53 72 73.6
condition 22 27 81.4
subroutine 33 33 100.0
pod 13 15 86.6
total 292 332 87.9


line stmt bran cond sub pod time code
1             package Data::Visitor::Callback;
2 7     7   417451 use Moose;
  7         2849687  
  7         58  
3             # ABSTRACT: A Data::Visitor with callbacks.
4              
5             our $VERSION = '0.32';
6 7     7   59584 use Data::Visitor ();
  7         35  
  7         295  
7              
8 7     7   64 use Carp qw(carp);
  7         18  
  7         605  
9 7     7   88 use Scalar::Util qw/blessed refaddr reftype/;
  7         39  
  7         428  
10              
11 7     7   61 no warnings 'recursion';
  7         15  
  7         369  
12              
13 7     7   42 use namespace::clean -except => 'meta';
  7         138  
  7         49  
14              
15 7     7   2996 use constant DEBUG => Data::Visitor::DEBUG();
  7         32  
  7         759  
16 7     7   133 use constant FIVE_EIGHT => ( $] >= 5.008 );
  7         18  
  7         1970  
17              
18             extends qw(Data::Visitor);
19              
20             has callbacks => (
21             isa => "HashRef",
22             is => "rw",
23             default => sub { {} },
24             );
25              
26             has class_callbacks => (
27             isa => "ArrayRef",
28             is => "rw",
29             default => sub { [] },
30             );
31              
32             has ignore_return_values => (
33             isa => "Bool",
34             is => "rw",
35             );
36              
37             sub BUILDARGS {
38 26     26 1 93 my ( $class, @args ) = @_;
39              
40 26         152 my $args = $class->SUPER::BUILDARGS(@args);
41              
42 26         377 my %init_args = map { $_->init_arg => undef } $class->meta->get_all_attributes;
  130         2860  
43              
44 26         114 my %callbacks = map { $_ => $args->{$_} } grep { not exists $init_args{$_} } keys %$args;
  158         315  
  177         331  
45              
46 26         62 my @class_callbacks = do {
47 7     7   58 no strict 'refs';
  7         17  
  7         4430  
48             grep {
49             # this check can be half assed because an ->isa check will be
50             # performed later. Anything that cold plausibly be a class name
51             # should be included in the list, even if the class doesn't
52             # actually exist.
53              
54 26         76 m{ :: | ^[A-Z] }x # if it looks kinda lack a class name
55             or
56 158 100       529 scalar keys %{"${_}::"} # or it really is a class
  122         450  
57             } keys %callbacks;
58             };
59              
60             # sort from least derived to most derived
61 26         90 @class_callbacks = sort { !$a->isa($b) <=> !$b->isa($a) } @class_callbacks;
  33         205  
62              
63             return {
64 26         1008 %$args,
65             callbacks => \%callbacks,
66             class_callbacks => \@class_callbacks,
67             };
68             }
69              
70             sub visit {
71 280     280 1 3227 my $self = shift;
72              
73 280   100     838 my $replaced_hash = local $self->{_replaced} = ($self->{_replaced} || {}); # delete it after we're done with the whole visit
74              
75 280         467 my @ret;
76              
77 280         537 for my $data (@_) {
78 280   66     890 my $refaddr = ref($data) && refaddr($data); # we need this early, it may change by the time we write replaced hash
79              
80 280         585 local *_ = \$data; # alias $_
81              
82 280 100 100     810 if ( $refaddr and exists $replaced_hash->{ $refaddr } ) {
83 1         2 if ( FIVE_EIGHT ) {
84 1         2 $self->trace( mapping => replace => $data, with => $replaced_hash->{$refaddr} ) if DEBUG;
85 1         3 push @ret, $data = $replaced_hash->{$refaddr};
86 1         3 next;
87             } else {
88             carp(q{Assignment of replacement value for already seen reference } . overload::StrVal($data) . q{ to container doesn't work on Perls older than 5.8, structure shape may have lost integrity.});
89             }
90             }
91              
92 279         368 my $ret;
93              
94 279 100       513 if ( defined wantarray ) {
95 256         516 $ret = $self->SUPER::visit( $self->callback( visit => $data ) );
96             } else {
97 23         92 $self->SUPER::visit( $self->callback( visit => $data ) );
98             }
99              
100 279 100 100     1056 $replaced_hash->{$refaddr} = $_ if $refaddr and ( not ref $_ or $refaddr ne refaddr($_) );
      100        
101              
102 279 100       796 push @ret, $ret if defined wantarray;
103             }
104              
105 280 50       1428 return ( @_ == 1 ? $ret[0] : @ret );
106             }
107              
108             sub visit_ref {
109 102     102 1 187 my ( $self, $data ) = @_;
110              
111 102         216 my $mapped = $self->callback( ref => $data );
112              
113 102 50       218 if ( ref $mapped ) {
114 102         304 return $self->SUPER::visit_ref($mapped);
115             } else {
116 0         0 return $self->visit($mapped);
117             }
118             }
119              
120             sub visit_seen {
121 9     9 1 24 my ( $self, $data, $result ) = @_;
122              
123 9         25 my $mapped = $self->callback( seen => $data, $result );
124              
125 7     7   57 no warnings 'uninitialized';
  7         15  
  7         3294  
126 9 100       45 if ( refaddr($mapped) == refaddr($data) ) {
127 7         32 return $result;
128             } else {
129 2         7 return $mapped;
130             }
131             }
132              
133             sub visit_value {
134 187     187 1 316 my ( $self, $data ) = @_;
135              
136 187         345 $data = $self->callback_and_reg( value => $data );
137 187 100       480 $self->callback_and_reg( ( ref($data) ? "ref_value" : "plain_value" ) => $data );
138             }
139              
140             sub visit_object {
141 24     24 1 67 my ( $self, $data ) = @_;
142              
143 24         56 $self->trace( flow => visit_object => $data ) if DEBUG;
144              
145 24         56 $data = $self->callback_and_reg( object => $data );
146              
147 24         43 my $class_cb = 0;
148              
149 24         39 foreach my $class ( grep { $data->isa($_) } @{ $self->class_callbacks } ) {
  33         150  
  24         655  
150 5 50       22 last unless blessed($data);
151 5 50       23 die "Unexpected object $data found"
152             unless $data->isa($class);
153 5         8 $self->trace( flow => class_callback => $class, on => $data ) if DEBUG;
154              
155 5         7 $class_cb++;
156 5         12 $data = $self->callback_and_reg( $class => $data );
157             }
158              
159 24 100       90 $data = $self->callback_and_reg( object_no_class => $data ) unless $class_cb;
160              
161 24 50       97 $data = $self->callback_and_reg( object_final => $data )
162             if blessed($data);
163              
164 24         98 $data;
165             }
166              
167             sub visit_scalar {
168 4     4 1 11 my ( $self, $data ) = @_;
169 4         11 my $new_data = $self->callback_and_reg( scalar => $data );
170 4 50 50     39 if ( (reftype($new_data)||"") =~ /^(?: SCALAR | REF | LVALUE | VSTRING ) $/x ) {
171 4         23 my $visited = $self->SUPER::visit_scalar( $new_data );
172              
173 7     7   69 no warnings "uninitialized";
  7         31  
  7         1424  
174 4 50       21 if ( refaddr($visited) != refaddr($data) ) {
175 4         15 return $self->_register_mapping( $data, $visited );
176             } else {
177 0         0 return $visited;
178             }
179             } else {
180 0         0 return $self->_register_mapping( $data, $self->visit( $new_data ) );
181             }
182             }
183              
184             BEGIN {
185 7     7   38 foreach my $reftype ( qw/array hash glob code/ ) {
186 28         94 my $name = "visit_$reftype";
187 28 50 100 7 1 3769 eval '
  7 50 50 7 1 76  
  7 100 50 7 1 20  
  7 50 100 7 1 691  
  7 50   16   57  
  7 50   34   35  
  7 50   3   695  
  7 50   45   74  
  7 100       19  
  7         673  
  7         52  
  7         17  
  7         636  
  16         48  
  16         47  
  16         85  
  15         63  
  15         76  
  15         46  
  0         0  
  1         5  
  34         122  
  34         83  
  34         157  
  34         109  
  34         133  
  0         0  
  34         140  
  0         0  
  3         11  
  3         11  
  3         32  
  3         19  
  3         33  
  3         13  
  0         0  
  0         0  
  45         145  
  45         138  
  45         218  
  44         181  
  44         211  
  44         131  
  0         0  
  1         4  
188             sub '.$name.' {
189             my ( $self, $data ) = @_;
190             my $new_data = $self->callback_and_reg( '.$reftype.' => $data );
191             if ( "'.uc($reftype).'" eq (reftype($new_data)||"") ) {
192             my $visited = $self->SUPER::'.$name.'( $new_data );
193              
194             no warnings "uninitialized";
195             if ( refaddr($visited) != refaddr($data) ) {
196             return $self->_register_mapping( $data, $visited );
197             } else {
198             return $visited;
199             }
200             } else {
201             return $self->_register_mapping( $data, $self->visit( $new_data ) );
202             }
203             }
204             1;
205             ' or die $@;
206             }
207             }
208              
209             sub visit_hash_entry {
210 106     106 1 273 my ( $self, $key, $value, $hash ) = @_;
211              
212 106         299 my ( $new_key, $new_value ) = $self->callback( hash_entry => $_[1], $_[2], $_[3] );
213              
214 106 100       3007 unless ( $self->ignore_return_values ) {
215 7     7   55 no warnings 'uninitialized';
  7         16  
  7         2606  
216 87 50 66     401 if ( ref($value) and refaddr($value) != refaddr($new_value) ) {
217 0         0 $self->_register_mapping( $value, $new_value );
218 0 0       0 if ( $key ne $new_key ) {
219 0         0 return $self->SUPER::visit_hash_entry($new_key, $new_value, $_[3]);
220             } else {
221 0         0 return $self->SUPER::visit_hash_entry($_[1], $new_value, $_[3]);
222             }
223             } else {
224 87 50       179 if ( $key ne $new_key ) {
225 0         0 return $self->SUPER::visit_hash_entry($new_key, $_[2], $_[3]);
226             } else {
227 87         257 return $self->SUPER::visit_hash_entry($_[1], $_[2], $_[3]);
228             }
229             }
230             } else {
231 19         78 return $self->SUPER::visit_hash_entry($_[1], $_[2], $_[3]);
232             }
233             }
234              
235             sub callback {
236 1054     1054 0 1951 my ( $self, $name, $data, @args ) = @_;
237              
238 1054 100       29201 if ( my $code = $self->callbacks->{$name} ) {
239 104         151 $self->trace( flow => callback => $name, on => $data ) if DEBUG;
240 104 100       203 if ( wantarray ) {
241 32         83 my @ret = $self->$code( $data, @args );
242 32 50       1052 return $self->ignore_return_values ? ( $data, @args ) : @ret;
243             } else {
244 72         207 my $ret = $self->$code( $data, @args );
245 72 100       2341 return $self->ignore_return_values ? $data : $ret ;
246             }
247             } else {
248 950 100       2951 return wantarray ? ( $data, @args ) : $data;
249             }
250             }
251              
252             sub callback_and_reg {
253 558     558 0 1025 my ( $self, $name, $data, @args ) = @_;
254              
255 558         979 my $new_data = $self->callback( $name, $data, @args );
256              
257 558 100       15385 unless ( $self->ignore_return_values ) {
258 7     7   104 no warnings 'uninitialized';
  7         32  
  7         1372  
259 451 100       891 if ( ref $data ) {
260 221 100       607 if ( refaddr($data) != refaddr($new_data) ) {
261 14         44 return $self->_register_mapping( $data, $new_data );
262             }
263             }
264              
265 437         3049 return $new_data;
266             }
267              
268 107         589 return $data;
269             }
270              
271             sub visit_tied {
272 9     9 1 23 my ( $self, $tied, @args ) = @_;
273 9         30 $self->SUPER::visit_tied( $self->callback_and_reg( tied => $tied, @args ), @args );
274             }
275              
276             __PACKAGE__->meta->make_immutable if __PACKAGE__->meta->can("make_immutable");
277              
278             __PACKAGE__;
279              
280             __END__
281              
282             =pod
283              
284             =encoding UTF-8
285              
286             =head1 NAME
287              
288             Data::Visitor::Callback - A Data::Visitor with callbacks.
289              
290             =head1 VERSION
291              
292             version 0.32
293              
294             =head1 SYNOPSIS
295              
296             use Data::Visitor::Callback;
297              
298             my $v = Data::Visitor::Callback->new(
299             # you can provide callbacks
300             # $_ will contain the visited value
301              
302             value => sub { ... },
303             array => sub { ... },
304              
305              
306             # you can also delegate to method names
307             # this specific example will force traversal on objects, by using the
308             # 'visit_ref' callback which normally traverse unblessed references
309              
310             object => "visit_ref",
311              
312              
313             # you can also use class names as callbacks
314             # the callback will be invoked on all objects which inherit that class
315              
316             'Some::Class' => sub {
317             my ( $v, $obj ) = @_; # $v is the visitor
318              
319             ...
320             },
321             );
322              
323             $v->visit( $some_perl_value );
324              
325             =head1 DESCRIPTION
326              
327             This is a L<Data::Visitor> subclass that lets you invoke callbacks instead of
328             needing to subclass yourself.
329              
330             =head1 METHODS
331              
332             =over 4
333              
334             =item new %opts, %callbacks
335              
336             Construct a new visitor.
337              
338             The options supported are:
339              
340             =over 4
341              
342             =item ignore_return_values
343              
344             When this is true (off by default) the return values from the callbacks are
345             ignored, thus disabling the fmapping behavior as documented in
346             L<Data::Visitor>.
347              
348             This is useful when you want to modify $_ directly
349              
350             =item tied_as_objects
351              
352             Whether or not to visit the L<perlfunc/tied> of a tied structure instead of
353             pretending the structure is just a normal one.
354              
355             See L<Data::Visitor/visit_tied>.
356              
357             =back
358              
359             =back
360              
361             =head1 CALLBACKS
362              
363             Use these keys for the corresponding callbacks.
364              
365             The callback is in the form:
366              
367             sub {
368             my ( $visitor, $data ) = @_;
369              
370             # or you can use $_, it's aliased
371              
372             return $data; # or modified data
373             }
374              
375             Within the callback $_ is aliased to the data, and this is also passed in the
376             parameter list.
377              
378             Any method can also be used as a callback:
379              
380             object => "visit_ref", # visit objects anyway
381              
382             =over 4
383              
384             =item visit
385              
386             Called for all values
387              
388             =item value
389              
390             Called for non objects, non container (hash, array, glob or scalar ref) values.
391              
392             =item ref_value
393              
394             Called after C<value>, for references to regexes, globs and code.
395              
396             =item plain_value
397              
398             Called after C<value> for non references.
399              
400             =item object
401              
402             Called for blessed objects.
403              
404             Since L<Data::Visitor/visit_object> will not recurse downwards unless you
405             delegate to C<visit_ref>, you can specify C<visit_ref> as the callback for
406             C<object> in order to enter objects.
407              
408             It is recommended that you specify the classes (or base classes) you want
409             though, instead of just visiting any object forcefully.
410              
411             =item Some::Class
412              
413             You can use any class name as a callback. This is called only after the
414             C<object> callback.
415              
416             If the object C<isa> the class then the callback will fire.
417              
418             These callbacks are called from least derived to most derived by comparing the
419             classes' C<isa> at construction time.
420              
421             =item object_no_class
422              
423             Called for every object that did not have a class callback.
424              
425             =item object_final
426              
427             The last callback called for objects, useful if you want to post process the
428             output of any class callbacks.
429              
430             =item array
431              
432             Called for array references.
433              
434             =item hash
435              
436             Called for hash references.
437              
438             =item glob
439              
440             Called for glob references.
441              
442             =item scalar
443              
444             Called for scalar references.
445              
446             =item tied
447              
448             Called on the return value of C<tied> for all tied containers. Also passes in
449             the variable as the second argument.
450              
451             =item seen
452              
453             Called for a reference value encountered a second time.
454              
455             Passes in the result mapping as the second argument.
456              
457             =back
458              
459             =for Pod::Coverage DEBUG
460             FIVE_EIGHT
461             callback
462             callback_and_reg
463             subname
464              
465             =head1 SUPPORT
466              
467             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Data-Visitor>
468             (or L<bug-Data-Visitor@rt.cpan.org|mailto:bug-Data-Visitor@rt.cpan.org>).
469              
470             =head1 AUTHORS
471              
472             =over 4
473              
474             =item *
475              
476             Yuval Kogman <nothingmuch@woobling.org>
477              
478             =item *
479              
480             Marcel Grünauer <marcel@cpan.org>
481              
482             =back
483              
484             =head1 COPYRIGHT AND LICENCE
485              
486             This software is copyright (c) 2023 by Yuval Kogman.
487              
488             This is free software; you can redistribute it and/or modify it under
489             the same terms as the Perl 5 programming language system itself.
490              
491             =cut