File Coverage

blib/lib/Data/Tersify.pm
Criterion Covered Total %
statement 124 125 99.2
branch 52 54 96.3
condition 8 12 66.6
subroutine 16 16 100.0
pod 2 2 100.0
total 202 209 96.6


line stmt bran cond sub pod time code
1             package Data::Tersify;
2              
3 2     2   71012 use strict;
  2         25  
  2         52  
4 2     2   10 use warnings;
  2         2  
  2         47  
5 2     2   8 no warnings 'uninitialized';
  2         4  
  2         53  
6              
7 2     2   784 use parent 'Exporter';
  2         580  
  2         10  
8             our @EXPORT_OK = qw(tersify tersify_many);
9              
10             # Have you updated the version number in the POD below?
11             our $VERSION = '1.000';
12             $VERSION = eval $VERSION;
13              
14 2     2   143 use Carp;
  2         4  
  2         103  
15 2     2   814 use Devel::OverloadInfo 0.005;
  2         23876  
  2         86  
16 2     2   830 use Module::Pluggable require => 1, force_search_all_paths => 1;
  2         14356  
  2         14  
17 2     2   283 use Scalar::Util qw(blessed refaddr reftype);
  2         3  
  2         2122  
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.000 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 27     27 1 43547 my ($data_structure) = @_;
122              
123 27         49 %seen_refaddr = ();
124 27         50 %refaddr_tersified_as = ();
125 27         47 ($data_structure) = _tersify($data_structure);
126 27         52 while (%refaddr_tersified_as) {
127 13         32 my @known_refaddrs = keys %refaddr_tersified_as;
128 13         24 %seen_refaddr = ();
129 13         21 ($data_structure) = _tersify($data_structure);
130 13         38 delete @refaddr_tersified_as{@known_refaddrs};
131             }
132 27         71 return $data_structure;
133             }
134              
135             sub _tersify {
136 243     243   287 my ($data_structure) = @_;
137              
138             # If this is a data structure that we've tersified already, replace it.
139 243 100       518 if (my $terse_object = $refaddr_tersified_as{refaddr($data_structure)}) {
140 14         24 return ($terse_object, 1);
141             }
142              
143             # If this is a simple scalar, there's nothing to change.
144 229 100       359 if (!ref($data_structure)) {
145 66         114 return ($data_structure, 0);
146             }
147              
148             # If it's a reference to something, tersify *that* and take a reference
149             # to it.
150 163 100       245 if (ref($data_structure) eq 'REF') {
151 3         4 my $referenced_data_structure = $$data_structure;
152 3         7 my ($maybe_new_data_structure, $changed)
153             = _tersify($referenced_data_structure);
154 3 100       9 if (!$changed) {
155 2         4 return ($data_structure, 0);
156             }
157 1         1 my $ref = \$maybe_new_data_structure;
158 1         3 return ($ref, 1);
159             }
160              
161             # Don't loop infinitely through a complex structure.
162 160 100       383 return ($data_structure, 0) if $seen_refaddr{refaddr($data_structure)}++;
163            
164             # If this is a blessed object, see if we know how to tersify it.
165 140 100       260 if (blessed($data_structure)) {
166 51         114 my ($object, $changed) = _tersify_object($data_structure);
167 51 100       110 if ($changed) {
168 22         50 $refaddr_tersified_as{refaddr($data_structure)} = $object;
169             }
170 51         104 return ($object, $changed);
171             }
172              
173             # For arrays and hashes, check if any of the elements changed, and if so
174             # create a fresh array or hash.
175 89         92 my $changed;
176             my $get_new_value = sub {
177 183     183   296 my ($old_value) = @_;
178 183         267 my ($new_value, $this_value_changed) = _tersify($old_value);
179 183         223 $changed += $this_value_changed;
180 183 100       354 return $this_value_changed ? $new_value : $old_value;
181 89         275 };
182 89         116 my $new_structure;
183 89 100       170 if (ref($data_structure) eq 'ARRAY') {
    100          
184 15         15 my @new_array;
185 15         22 for my $element (@$data_structure) {
186 60         112 push @new_array, $get_new_value->($element);
187             }
188 15 100       29 if (!$changed) {
189 11         33 return ($data_structure, 0);
190             }
191 4         6 $new_structure = \@new_array;
192             } elsif (ref($data_structure) eq 'HASH') {
193 73         87 my %new_hash;
194 73         151 for my $key (keys %$data_structure) {
195 123         179 $new_hash{$key} = $get_new_value->($data_structure->{$key});
196             }
197 73 100       126 if (!$changed) {
198 53         156 return ($data_structure, 0);
199             }
200 20         31 $new_structure = \%new_hash;
201             } else {
202 1         4 return ($data_structure, 0);
203             }
204              
205             # If it's safe to mess with the existing data structure (e.g. because this
206             # is the second pass, or later, that we've done through a data structure
207             # and this is an arrayref or hashref that we already anonymised earlier),
208             # just update its contents. Otherwise mark it as a new data structure.
209 24 100       54 if (!$safe_to_mess_with_refaddr{refaddr($data_structure)}) {
210 21         34 $refaddr_tersified_as{ refaddr($data_structure) } = $new_structure;
211 21         41 $safe_to_mess_with_refaddr{refaddr($new_structure)} = 1;
212 21         84 return ($new_structure, 1);
213             } else {
214 3         5 _replace_contents_of_structure_with($data_structure,
215             $new_structure);
216 3         12 return ($data_structure, 0);
217             }
218             }
219              
220             sub _tersify_object {
221 51     51   67 my ($data_structure) = @_;
222              
223             # A summary has, by definition, already been tersified.
224 51 100       91 if (ref($data_structure) eq 'Data::Tersify::Summary') {
225 17         31 return ($data_structure, 0);
226             }
227              
228             # We might know how to tersify such an object directly, via a
229             # plugin.
230 34         52 my $terse_object = _tersify_via_plugin($data_structure);
231 34   66     223 my $changed = blessed($terse_object)
232             && $terse_object->isa('Data::Tersify::Summary');
233              
234             # OK, but does it overload stringification?
235 34 100       69 if (!$changed) {
236 20 50       49 if (my $overload_info
237             = Devel::OverloadInfo::overload_info($data_structure))
238             {
239 20 100       17437 if ($overload_info->{'""'}) {
240             return (
241 2         27 _summarise_object_as_string(
242             $data_structure, "$data_structure"
243             ),
244             1
245             );
246             }
247             }
248             }
249              
250             # Although if this is the root structure passed to tersify, we want
251             # to pass it through as-is; we only tersify complicated objects
252             # that feature somewhere deeper in the data structure, possibly
253             # unexpectedly.
254 32         138 my ($caller_sub) = (caller(2))[3];
255 32 100 100     86 if ($changed && $caller_sub ne 'Data::Tersify::tersify') {
256 12         27 return ($terse_object, $changed);
257             }
258              
259             # If we didn't tersify this object, maybe we can tersify its internal
260             # structure?
261 20         26 my $object_contents;
262 20 100       54 if (reftype($data_structure) eq 'HASH') {
    100          
263 15         74 $object_contents = {%$data_structure};
264             } elsif (reftype($data_structure) eq 'ARRAY') {
265 2         14 $object_contents = [@$data_structure];
266             }
267 20 100       40 if ($object_contents) {
268 17         21 my $maybe_new_structure;
269 17         26 ($maybe_new_structure, $changed) = _tersify($object_contents);
270 17 100       31 if ($changed) {
271             # We might need to build a new Data::Tersify::Summary object.
272 8 100       19 if (!$safe_to_mess_with_refaddr{refaddr($data_structure)}) {
273             # No need to remember that we messed with $object_contents;
274             # that was a temporary variable we created purely to see if
275             # we could tersify it, and it's not referenced anywhere.
276 5         11 delete $refaddr_tersified_as{refaddr($object_contents)};
277             # Just create a new blessed object; the calling code will
278             # realise that we created a new object and update
279             # %refaddr_tersified_as with the proper values.
280 5         7 $terse_object = $maybe_new_structure;
281 5         60 bless $terse_object =>
282             sprintf('Data::Tersify::Summary::%s::0x%x',
283             ref($data_structure), refaddr($data_structure));
284 5         16 $safe_to_mess_with_refaddr{refaddr($terse_object)}++;
285             } else {
286             # We can reuse the existing one, which is now *even terser*!
287             # There's no danger of blatting existing data structures,
288             # because we've *already* replaced the previous data structure
289             # with one of ours, as part of generating a new object.
290 3         5 _replace_contents_of_structure_with($data_structure,
291             $maybe_new_structure);
292             }
293 8         23 return ($terse_object, $changed);
294             }
295             }
296              
297             # OK, return this object unchanged.
298 12         29 return ($data_structure, 0);
299             }
300              
301             sub _replace_contents_of_structure_with {
302 6     6   11 my ($safe_structure, $new_contents) = @_;
303              
304 6 100 66     30 if (reftype($safe_structure) eq 'HASH'
    50 33        
305             && reftype($new_contents) eq 'HASH')
306             {
307 4         13 %$safe_structure = %$new_contents;
308             } elsif (reftype($safe_structure) eq 'ARRAY'
309             && reftype($new_contents) eq 'ARRAY')
310             {
311 2         6 @$safe_structure = @$new_contents;
312             } else {
313 0         0 croak sprintf(q{Want to put %s in existing %s, but that's a %s?!},
314             $new_contents, $safe_structure, reftype($new_contents));
315             }
316             }
317              
318             =head2 tersify_many
319              
320             In: @data_structures
321             Out: @terser_data_structures
322              
323             A simple wrapper around L that expects to be passed one or more
324             variables. Note that as each value is passed to L, none of the values
325             in @data_structures will be tersified if they're objects recognised by plugins.
326             (Whereas they would have been if you'd said C.
327              
328             This is intended to be used by e.g. the Perl debugger's x command.
329              
330             =cut
331              
332             sub tersify_many {
333 1     1 1 2246 return map { tersify($_) } @_;
  4         7  
334             }
335              
336             =head2 PLUGINS
337              
338             Data::Tersify can be extended by plugins. See L for
339             a general description of plugins; for examples of plugins, see
340             L and L,
341             provided in separate distributions.
342              
343             =cut
344              
345             {
346             my (%handled_by_plugin);
347              
348             sub _tersify_via_plugin {
349 34     34   49 my ($object) = @_;
350              
351 34 100       52 if (!keys %handled_by_plugin) {
352 1         4 for my $plugin (plugins()) {
353 2         2725 for my $class ($plugin->handles) {
354 3         11 $handled_by_plugin{$class} = $plugin;
355             }
356             }
357             }
358              
359             ### FIXME: subclasses also. Loop the other way, go through
360             ### the types we know about and see if $object->isa(...)
361             ### rather than hard-coding the ref($object).
362 34 100       75 if (my $plugin = $handled_by_plugin{ref($object)}) {
363 14         36 return _summarise_object_as_string($object,
364             $plugin->tersify($object));
365             }
366 20         31 return $object;
367             }
368             }
369              
370             sub _summarise_object_as_string {
371 16     16   178 my ($object, $string) = @_;
372 16         67 my $summary
373             = sprintf('%s (0x%x) %s', ref($object), refaddr($object), $string);
374 16         53 return bless \$summary => 'Data::Tersify::Summary';
375             }
376              
377             =head1 LICENSE
378              
379             This is free software; you can redistribute it and/or modify it under the same
380             terms as Perl 5.
381              
382             =head1 BUGS
383              
384             If you find any bugs, or have any feature suggestions, please report them
385             via L.
386              
387             =head1 SEE ALSO
388              
389             L will tersify data structures as part of its standard
390             output.
391              
392             =cut
393              
394             1;