File Coverage

blib/lib/Data/Tersify.pm
Criterion Covered Total %
statement 138 139 99.2
branch 60 62 96.7
condition 12 18 66.6
subroutine 16 16 100.0
pod 2 2 100.0
total 228 237 96.2


line stmt bran cond sub pod time code
1             package Data::Tersify;
2              
3 2     2   75333 use strict;
  2         11  
  2         59  
4 2     2   11 use warnings;
  2         4  
  2         56  
5 2     2   10 no warnings 'uninitialized';
  2         4  
  2         67  
6              
7 2     2   908 use parent 'Exporter';
  2         599  
  2         11  
8             our @EXPORT_OK = qw(tersify tersify_many);
9              
10             # Have you updated the version number in the POD below?
11             our $VERSION = '1.003';
12             $VERSION = eval $VERSION;
13              
14 2     2   230 use Carp;
  2         3  
  2         134  
15 2     2   1016 use Devel::OverloadInfo 0.005;
  2         24349  
  2         98  
16 2     2   940 use Module::Pluggable require => 1, force_search_all_paths => 1;
  2         16530  
  2         16  
17 2     2   203 use Scalar::Util qw(blessed refaddr reftype);
  2         5  
  2         2706  
18              
19             =head1 NAME
20              
21             Data::Tersify - generate terse equivalents of complex data structures
22              
23             =head1 VERSION
24              
25             This is version 1.003 of Data::Tersify.
26              
27             =head1 SYNOPSIS
28              
29             use Data::Dumper;
30             use Data::Tersify qw(tersify);
31            
32             my $complicated_data_structure = ...;
33            
34             print Dumper(tersify($complicated_data_structure));
35             # Your scrollback is not full of DateTime, DBIx::Class, Moose etc.
36             # spoor which you weren't interested in.
37              
38             =head1 DESCRIPTION
39              
40             Complex data structures are useful; necessary, even. But they're not
41             I. In particular, when you're buried in the guts of some code
42             you don't fully understand and you have a variable you want to inspect,
43             and you say C in the debugger, or C from
44             your code, or something very similar with the dumper module of your choice,
45             and you then get I because C<$foo>
46             contained, I one or more references to a DateTime, DBIx::Class,
47             Moose or other verbose object ... you didn't need that.
48              
49             Data::Tersify looks at any data structure it's given, and if it finds a
50             blessed object that it knows about, anywhere, it replaces it in the data
51             structure by a terser equivalent, designed to (a) not use up all of your
52             scrollback, but (b) be blatantly clear that this is I the original object
53             that was in that data structure originally, but a terser equivalent.
54              
55             Do not use Data::Tersify as part of any serialisation implementation! By
56             design, Data::Tersify is lossy and will throw away information! That's because
57             it supposes that that if you're using it, you want to dump information about a
58             complex data structure, and you don't I about the fine details.
59              
60             If you find yourself saying C in the debugger a lot, consider adding
61             Data::Tersify::perldb to your .perldb file, or something like it.
62              
63             =head2 tersify
64              
65             In: $data_structure
66             In: $terser_data_structure
67              
68             Supplied with a data structure, returns a data structure with the complicated
69             bits summarised. Every attempt is made to preserve those parts of the data
70             structure that don't need summarising.
71              
72             Objects are only summarised if (1) they're blessed objects, (2) they're
73             not the root structure passed to tersify (so if you actually to want to dump a
74             complex DBIx::Class object, for instance, you still can), and (3) a
75             plugin has been registered that groks that type of object, I they
76             contain as an element one such object.
77              
78             Summaries are either scalar references of the form "I (I)
79             I", e.g. "DateTime (0xdeadbeef) 2017-08-15 12:34:56", blessed into the
80             Data::Tersify::Summary class, I copies of the
81             object's internal state with any sub-objects tersified as above, blessed into
82             the Data::Tersify::Summary::I::0xI class, where I is the
83             class the object was originally blessed into and I the object's
84             original address.
85              
86             So, if you had the plugin Data::Tersify::Plugin::DateTime installed,
87             passing a DateTime object to tersify would return that same object, untouched;
88             but passing
89              
90             {
91             name => 'Now',
92             description => 'The time it currently is, not a time in the future',
93             datetime => DateTime->now
94             }
95              
96             to tersify would return something like this:
97              
98             {
99             name => 'Now',
100             description => 'The time it currently is, not a time in the future',
101             datetime => bless \"DateTime (0xdeadbeef) 2018-08-12 17:15:00",
102             "Data::Tersify::Summary",
103             }
104              
105             If the hashref had been blessed into the class "Time::Description",
106             and had a refaddr of 0xcafebabe, you would get back a hash as above, but
107             blessed into the class
108             C.
109              
110             Note that point 2 above (objects aren't tersified if they're the root
111             structure) applies only to plugins. If the object contains other objects
112             that could be tersified, they will be. One design consequence of this is that
113             you should consider writing plugins for I, rather
114             than the ur-object that they might be part of.
115              
116             =cut
117              
118             my (%seen_refaddr, %refaddr_tersified_as, %safe_to_mess_with_refaddr);
119              
120             sub tersify {
121 38     38 1 52225 my ($data_structure) = @_;
122              
123 38         79 %seen_refaddr = ();
124 38         69 %refaddr_tersified_as = ();
125 38         50 %safe_to_mess_with_refaddr = ();
126 38         79 ($data_structure) = _tersify($data_structure);
127 38         80 while (%refaddr_tersified_as) {
128 23         50 my @known_refaddrs = keys %refaddr_tersified_as;
129 23         40 %seen_refaddr = ();
130 23         37 ($data_structure) = _tersify($data_structure);
131 23         76 delete @refaddr_tersified_as{@known_refaddrs};
132             }
133 38         92 return $data_structure;
134             }
135              
136             sub _tersify {
137 293     293   333 my ($data_structure) = @_;
138              
139             # If this is a data structure that we've tersified already, replace it.
140 293 100       604 if (my $terse_object = $refaddr_tersified_as{refaddr($data_structure)}) {
141 13         22 return ($terse_object, 1);
142             }
143              
144             # If this is a simple scalar, there's nothing to change.
145 280 100       434 if (!ref($data_structure)) {
146 64         102 return ($data_structure, 0);
147             }
148              
149             # If it's a reference to something, tersify *that* and take a reference
150             # to it.
151 216 100       327 if (ref($data_structure) eq 'REF') {
152 2         4 my $referenced_data_structure = $$data_structure;
153 2         3 my ($maybe_new_data_structure, $changed)
154             = _tersify($referenced_data_structure);
155 2 100       5 if (!$changed) {
156 1         2 return ($data_structure, 0);
157             }
158 1         2 my $ref = \$maybe_new_data_structure;
159 1         3 return ($ref, 1);
160             }
161              
162             # Don't loop infinitely through a complex structure.
163 214 100       470 return ($data_structure, 0) if $seen_refaddr{refaddr($data_structure)}++;
164            
165             # If this is a blessed object, see if we know how to tersify it.
166 200 100       366 if (blessed($data_structure)) {
167 73         118 my ($object, $changed) = _tersify_object($data_structure);
168 73 100       125 if ($changed) {
169 35         70 $refaddr_tersified_as{refaddr($data_structure)} = $object;
170             }
171 73         150 return ($object, $changed);
172             }
173              
174             # For arrays and hashes, check if any of the elements changed, and if so
175             # create a fresh array or hash.
176 127         128 my $changed;
177             my $get_new_value = sub {
178 214     214   264 my ($old_value) = @_;
179 214         293 my ($new_value, $this_value_changed) = _tersify($old_value);
180 214         264 $changed += $this_value_changed;
181 214 100       429 return $this_value_changed ? $new_value : $old_value;
182 127         330 };
183 127         155 my $new_structure;
184 127 100       274 if (ref($data_structure) eq 'ARRAY') {
    100          
185 14         18 my @new_array;
186 14         21 for my $element (@$data_structure) {
187 55         73 push @new_array, $get_new_value->($element);
188             }
189 14 100       20 if (!$changed) {
190 10         56 return ($data_structure, 0);
191             }
192 4         8 $new_structure = \@new_array;
193             } elsif (ref($data_structure) eq 'HASH') {
194 112         115 my %new_hash;
195 112         220 for my $key (keys %$data_structure) {
196 159         220 $new_hash{$key} = $get_new_value->($data_structure->{$key});
197             }
198 112 100       190 if (!$changed) {
199 72         211 return ($data_structure, 0);
200             }
201 40         57 $new_structure = \%new_hash;
202             } else {
203 1         4 return ($data_structure, 0);
204             }
205              
206             # If it's safe to mess with the existing data structure (e.g. because this
207             # is the second pass, or later, that we've done through a data structure
208             # and this is an arrayref or hashref that we already anonymised earlier),
209             # just update its contents. Otherwise mark it as a new data structure.
210 44 100       86 if (!$safe_to_mess_with_refaddr{refaddr($data_structure)}) {
211 42         74 $refaddr_tersified_as{ refaddr($data_structure) } = $new_structure;
212 42         75 $safe_to_mess_with_refaddr{refaddr($new_structure)} = 1;
213 42         132 return ($new_structure, 1);
214             } else {
215 2         4 _replace_contents_of_structure_with($data_structure,
216             $new_structure);
217 2         8 return ($data_structure, 0);
218             }
219             }
220              
221             sub _tersify_object {
222 73     73   91 my ($data_structure) = @_;
223              
224             # A summary has, by definition, already been tersified.
225 73 100       133 if (ref($data_structure) eq 'Data::Tersify::Summary') {
226 27         46 return ($data_structure, 0);
227             }
228              
229             # We might know how to tersify such an object directly, via a
230             # plugin.
231 46         62 my $terse_object = _tersify_via_plugin($data_structure);
232 46   66     208 my $changed = blessed($terse_object)
233             && $terse_object->isa('Data::Tersify::Summary');
234              
235             # OK, but does it overload stringification?
236 46 100       86 if (!$changed) {
237 19 50       47 if (my $overload_info
238             = Devel::OverloadInfo::overload_info($data_structure))
239             {
240 19 100       16936 if ($overload_info->{'""'}) {
241             return (
242 2         26 _summarise_object_as_string(
243             $data_structure, "$data_structure"
244             ),
245             1
246             );
247             }
248             }
249             }
250              
251             # Although if this is the root structure passed to tersify, we want
252             # to pass it through as-is; we only tersify complicated objects
253             # that feature somewhere deeper in the data structure, possibly
254             # unexpectedly.
255 44         190 my ($caller_sub) = (caller(2))[3];
256 44 100 100     128 if ($changed && $caller_sub ne 'Data::Tersify::tersify') {
257 25         60 return ($terse_object, $changed);
258             }
259              
260             # If we didn't tersify this object, maybe we can tersify its internal
261             # structure?
262 19         24 my $object_contents;
263 19 100       56 if (reftype($data_structure) eq 'HASH') {
    100          
264 14         73 $object_contents = {%$data_structure};
265             } elsif (reftype($data_structure) eq 'ARRAY') {
266 2         6 $object_contents = [@$data_structure];
267             }
268 19 100       34 if ($object_contents) {
269 16         16 my $maybe_new_structure;
270 16         27 ($maybe_new_structure, $changed) = _tersify($object_contents);
271 16 100       32 if ($changed) {
272             # We might need to build a new Data::Tersify::Summary object.
273 8 100       18 if (!$safe_to_mess_with_refaddr{refaddr($data_structure)}) {
274             # No need to remember that we messed with $object_contents;
275             # that was a temporary variable we created purely to see if
276             # we could tersify it, and it's not referenced anywhere.
277 5         12 delete $refaddr_tersified_as{refaddr($object_contents)};
278             # Just create a new blessed object; the calling code will
279             # realise that we created a new object and update
280             # %refaddr_tersified_as with the proper values.
281 5         7 $terse_object = $maybe_new_structure;
282 5         59 bless $terse_object =>
283             sprintf('Data::Tersify::Summary::%s::0x%x',
284             ref($data_structure), refaddr($data_structure));
285 5         12 $safe_to_mess_with_refaddr{refaddr($terse_object)}++;
286             } else {
287             # We can reuse the existing one, which is now *even terser*!
288             # There's no danger of blatting existing data structures,
289             # because we've *already* replaced the previous data structure
290             # with one of ours, as part of generating a new object.
291 3         5 _replace_contents_of_structure_with($data_structure,
292             $maybe_new_structure);
293             }
294 8         24 return ($terse_object, $changed);
295             }
296             }
297              
298             # OK, return this object unchanged.
299 11         52 return ($data_structure, 0);
300             }
301              
302             sub _replace_contents_of_structure_with {
303 5     5   8 my ($safe_structure, $new_contents) = @_;
304              
305 5 100 66     32 if (reftype($safe_structure) eq 'HASH'
    50 33        
306             && reftype($new_contents) eq 'HASH')
307             {
308 3         12 %$safe_structure = %$new_contents;
309             } elsif (reftype($safe_structure) eq 'ARRAY'
310             && reftype($new_contents) eq 'ARRAY')
311             {
312 2         6 @$safe_structure = @$new_contents;
313             } else {
314 0         0 croak sprintf(q{Want to put %s in existing %s, but that's a %s?!},
315             $new_contents, $safe_structure, reftype($new_contents));
316             }
317             }
318              
319             =head2 tersify_many
320              
321             In: @data_structures
322             Out: @terser_data_structures
323              
324             A simple wrapper around L that expects to be passed one or more
325             variables. Note that as each value is passed to L, none of the values
326             in @data_structures will be tersified if they're objects recognised by plugins.
327             (Whereas they would have been if you'd said C.
328              
329             This is intended to be used by e.g. the Perl debugger's x command.
330              
331             =cut
332              
333             sub tersify_many {
334 1     1 1 2317 return map { tersify($_) } @_;
  4         8  
335             }
336              
337             =head2 PLUGINS
338              
339             Data::Tersify can be extended by plugins. See L for
340             a general description of plugins; for examples of plugins, see
341             L and L,
342             provided in separate distributions.
343              
344             =cut
345              
346             {
347             my (%handled_by_plugin, %plugin_handles_subclasses);
348             my $subclass_lookup_initialised;
349              
350             sub _tersify_via_plugin {
351 46     46   59 my ($object) = @_;
352              
353             # A simple lookup of "this type of object is handled by this class".
354             # If the plugin doesn't handle subclasses of objects, this is fine.
355 46 100       80 if (!keys %handled_by_plugin) {
356 1         4 for my $plugin (plugins()) {
357 3         3802 for my $class ($plugin->handles) {
358 4         15 $handled_by_plugin{$class} = $plugin;
359             }
360             }
361             }
362              
363             # A more complex lookup of "this class handles subclasses".
364 46 100       74 if (!$subclass_lookup_initialised) {
365 1         4 for my $plugin (plugins()) {
366 3 100 66     1953 if ( $plugin->can('handles_subclasses')
367             && $plugin->handles_subclasses)
368             {
369 1         7 $plugin_handles_subclasses{$plugin} = [ $plugin->handles ];
370             }
371             }
372 1         6 $subclass_lookup_initialised = 1;
373             }
374              
375             # With that in mind, look for a plugin that handles this object
376             # one way or another.
377 46         61 my $chosen_plugin = $handled_by_plugin{ref($object)};
378 46 100 66     122 if (!$chosen_plugin && keys %plugin_handles_subclasses) {
379             plugin:
380 21         49 for my $plugin (sort keys %plugin_handles_subclasses) {
381 21         25 for my $class (@{ $plugin_handles_subclasses{$plugin} }) {
  21         39  
382 21 100       136 if ($object->isa($class)) {
383 2         4 $chosen_plugin = $plugin;
384 2         5 last plugin;
385             }
386             }
387             }
388             }
389              
390             # And use it to summarise the object if we can.
391 46 100       70 if ($chosen_plugin) {
392 27         74 return _summarise_object_as_string($object,
393             $chosen_plugin->tersify($object));
394             }
395 19         30 return $object;
396             }
397             }
398              
399             sub _summarise_object_as_string {
400 29     29   371 my ($object, $string) = @_;
401 29         123 my $summary
402             = sprintf('%s (0x%x) %s', ref($object), refaddr($object), $string);
403 29         88 return bless \$summary => 'Data::Tersify::Summary';
404             }
405              
406             =head1 LICENSE
407              
408             This is free software; you can redistribute it and/or modify it under the same
409             terms as Perl 5.
410              
411             =head1 BUGS
412              
413             If you find any bugs, or have any feature suggestions, please report them
414             via L.
415              
416             =head1 SEE ALSO
417              
418             L will tersify data structures as part of its standard
419             output.
420              
421             =cut
422              
423             1;