File Coverage

blib/lib/Data/Dumper/Compact.pm
Criterion Covered Total %
statement 198 249 79.5
branch 82 136 60.2
condition 16 39 41.0
subroutine 38 43 88.3
pod 6 6 100.0
total 340 473 71.8


line stmt bran cond sub pod time code
1             package Data::Dumper::Compact;
2              
3 2     2   137052 use List::Util qw(sum);
  2         12  
  2         198  
4 2     2   13 use Scalar::Util qw(blessed reftype);
  2         3  
  2         77  
5 2     2   1151 use Data::Dumper ();
  2         12346  
  2         59  
6 2     2   837 use Mu::Tiny;
  2         2019  
  2         11  
7              
8             our $VERSION = '0.006000';
9             $VERSION =~ tr/_//d;
10              
11             sub import {
12 2     2   16 my ($class, $ddc, $opts) = @_;
13 2 50       3083 return unless defined($ddc);
14 0 0 0     0 die "Don't know how to export '$ddc'" unless ($ddc||'') =~ /^[jd]dc$/;
15 0         0 my $targ = caller;
16 0   0     0 my $cb = $class->new($opts||{})->dump_cb;
17 2     2   337 no strict 'refs';
  2         5  
  2         841  
18 0         0 *{"${targ}::${ddc}"} = $cb;
  0         0  
19             }
20              
21 30     30   204 lazy max_width => sub { 78 };
22              
23 30     30   257 lazy width => sub { shift->max_width };
24              
25 30     30   181 lazy indent_width => sub { length($_[0]->indent_by) };
26              
27 629     629   980 sub _next_width { $_[0]->width - $_[0]->indent_width }
28              
29 30     30   182 lazy indent_by => sub { ' ' };
30              
31 30     30   229 lazy transforms => sub { [] };
32              
33 0     0 1 0 sub add_transform { push(@{$_[0]->transforms}, $_[1]); $_[0] }
  0         0  
  0         0  
34              
35             sub _indent {
36 307     307   513 my ($self, $string) = @_;
37 307         437 my $ib = $self->indent_by;
38 307         1506 $string =~ s/^/$ib/msg;
39 307         2095 $string;
40             }
41              
42             lazy dumper => sub {
43 20     20   94 my ($self) = @_;
44 20         111 my $dd = Data::Dumper->new([]);
45 20 50       839 $dd->Trailingcomma(1) if $dd->can('Trailingcomma');
46 20         199 $dd->Terse(1)->Indent(1)->Useqq(1)->Deparse(1)->Quotekeys(0)->Sortkeys(1);
47 20         806 my $indent_width = $self->indent_width;
48             # feed the indent width down into B::Deparse - not using tabs because
49             # it has no way to tell it how wide a tab is that I could find
50 20         30 my $dp_new = do {
51 20         155 require B::Deparse;
52 20         59 my $orig = \&B::Deparse::new;
53 0     0   0 sub { my ($self, @args) = @_; $self->$orig('-si'.$indent_width, @args) }
  0         0  
54 20         104 };
55             sub {
56 2     2   15 no warnings 'redefine';
  2         4  
  2         2309  
57 188     188   674 local *B::Deparse::new = $dp_new;
58 188         451 $dd->Values([ $_[0] ])->Dump
59             },
60 20         93 };
61              
62 258     258   489 sub _dumper { $_[0]->dumper->($_[1]) }
63              
64             sub _optify {
65 39     39   104 my ($self, $opts, $method, @args) = @_;
66             # if we're an object, localize anything provided in the options,
67             # and also blow away the dependent attributes if indent_by is changed
68             ref($self) and $opts
69 0         0 and (local @{$self}{keys %$opts} = values %$opts, 1)
70             and $opts->{indent_by}
71 39 0 33     112 and delete @{$self}{grep !$opts->{$_}, qw(indent_width dumper)};
  0   33     0  
      0        
72 39 50 50     253 ref($self) or $self = $self->new($opts||{});
73 39         1105 $self->$method(@args);
74             }
75              
76             sub dump {
77 30     30 1 7510 my ($self, $data, $opts) = @_;
78             $self->_optify($opts, sub {
79 30     30   59 my ($self) = @_;
80 30         104 $self->format($self->transform($self->transforms, $self->expand($data)));
81 30         170 });
82             }
83              
84             sub dump_cb {
85 0     0 1 0 my ($self) = @_;
86 0     0   0 return sub { $self->dump(@_) };
  0         0  
87             }
88              
89             sub expand {
90 347     347 1 592 my ($self, $data, $p) = @_;
91 347 100       638 local $self->{expand_seen} = {} unless $self->{expand_seen};
92             my $this_path = [
93 347 100       543 ($self->{expand_path} ? @{$self->{expand_path}} : ()),
  317 100       640  
94             (defined($p) ? ($p) : ())
95             ];
96 347 100       659 if (ref($data)) {
97 107 100       220 if (my $seen_path = $self->{expand_seen}{$data}) {
98 2         23 return [ ref => $seen_path ];
99             } else {
100 105         257 $self->{expand_seen}{$data} = $this_path;
101             }
102             }
103 345         525 local $self->{expand_path} = $this_path;
104 345 100 66     985 if (ref($data) eq 'HASH') {
    100          
    100          
105             return [ hash => [
106             [ sort keys %$data ],
107 46         302 { map +($_ => $self->expand($data->{$_}, [ key => $_ ])), sort keys %$data }
108             ] ];
109             } elsif (ref($data) eq 'ARRAY') {
110 55         70 my $idx = 0;
111 55         205 return [ array => [ map $self->expand($_, [ idx => $idx++ ]), @$data ] ];
112             } elsif (blessed($data) and my $ret = $self->_expand_blessed($data)) {
113 3         23 return $ret;
114             }
115 241         404 (my $thing = $self->_dumper($data)) =~ s/\n\Z//;
116              
117             # -foo and friends automatically become 'key' type, all else stays 'string'
118 241 100       4141 if (my ($string) = $thing =~ /^"(.*)"$/) {
119 207 100       1517 return [ ($string =~ /^-[a-zA-Z]\w*$/ ? 'key' : 'string') => $string ];
120             }
121 34         225 return [ thing => $thing ];
122             }
123              
124             sub _expand_blessed {
125 3     3   10 my ($self, $blessed) = @_;
126 3 50       12 return unless grep { $_ eq 'ARRAY' or $_ eq 'HASH' } reftype($blessed);
  3 50       22  
127 3 50       20 my $cursed = reftype($blessed) eq 'ARRAY' ? [ @$blessed ] : { %$blessed };
128 3         12 return [ blessed => [ $self->expand($cursed), blessed($blessed) ] ];
129             }
130              
131             sub transform {
132 30     30 1 66 my ($self, $tfspec, $exp) = @_;
133 30 50       64 return $exp unless $tfspec;
134             # This is redundant from ->dump but consistent for direct user calls
135 30         67 local $self->{transforms} = $tfspec;
136 30         89 $self->_transform($exp, []);
137             }
138              
139             sub _transform {
140 314     314   459 my ($self, $exp, $path) = @_;
141 314         437 my ($type, $payload) = @$exp;
142 314 100       564 if ($type eq 'hash') {
    100          
143 43         54 my %h = %{$payload->[1]};
  43         135  
144             $payload = [
145             $payload->[0],
146             { map +(
147 43         242 $_ => $self->_transform($h{$_}, [ @$path, $_ ])
148             ), sort keys %h
149             },
150             ];
151             } elsif ($type eq 'array') {
152 52         114 my @a = @$payload;
153 52         227 $payload = [ map $self->_transform($a[$_], [ @$path, $_ ]), 0..$#a ];
154             }
155 314         379 TF: foreach my $this_tf (@{$self->transforms}) {
  314         501  
156 0         0 my $tf = $this_tf;
157 0         0 my $last_tf = 0;
158 0         0 while ($tf != $last_tf) {
159 0         0 $last_tf = $tf;
160 0 0       0 if (ref($tf) eq 'ARRAY') {
    0          
161 0         0 my @match = @$tf;
162 0         0 $tf = pop @match;
163 0 0       0 next TF if @match > @$path; # not deep enough
164 0         0 MATCH: foreach my $idx (0..$#match) {
165 0 0       0 next MATCH unless defined(my $m = $match[$idx]);
166 0         0 my $rpv = $path->[$idx-@match];
167 0 0       0 if (!ref($m)) {
    0          
    0          
168 0 0       0 next TF unless $rpv eq $m;
169             } elsif (ref($m) eq 'Regexp') {
170 0 0       0 next TF unless $rpv =~ $m;
171             } elsif (ref($m) eq 'CODE') {
172 0         0 local $_ = $rpv;
173 0 0       0 next TF unless $m->($rpv);
174             } else {
175 0         0 die "Unknown path match type for $m";
176             }
177             }
178             } elsif (ref($tf) eq 'HASH') {
179 0 0 0     0 next TF unless $tf = $tf->{$type}||$tf->{_};
180             }
181             }
182             ($type, $payload) = @{
183 0 0       0 $self->$tf($type, $payload, $path)
  0         0  
184             || [ $type, $payload ]
185             };
186             }
187 314         1619 return [ $type, $payload ];
188             }
189              
190             sub format {
191 36     36 1 69 my ($self, $exp) = @_;
192 36         83 return $self->_format($exp)."\n";
193             # If we realise we've flat run out of horizontal space, we need to be able
194             # to jump back up the call stack to the top and start again - hence the
195             # presence of this label to jump to from _format - of course, if that
196             # clause never gets hit then our first _format call returns and therefore
197             # the label is never reached.
198             VERTICAL:
199 0         0 local $self->{vertical} = 1;
200 0         0 return $self->_format($exp)."\n";
201             }
202              
203             sub _format {
204 1086     1086   1446 my ($self, $exp) = @_;
205 1086         1552 my ($type, $payload) = @$exp;
206 1086 50 33     2291 if (!$self->{vertical} and $self->width <= 0) {
207             # We've run out of horizontal space, engage 'vertical sprawl mode' and
208             # restart from the top by jumping back up the current call stack to the
209             # VERTICAL label in the top-level call to format.
210 2     2   15 no warnings 'exiting';
  2         4  
  2         3528  
211 0         0 goto VERTICAL;
212             }
213 1086         3887 return $self->${\"_format_${type}"}($payload);
  1086         2638  
214             }
215              
216             sub _format_list {
217 0     0   0 my ($self, $payload) = @_;
218 0         0 my @plain = grep !/\s/, map $_->[1], grep $_->[0] eq 'string', @$payload;
219 0 0       0 if (@plain == @$payload) {
220 0         0 my $try = 'qw('.join(' ', @plain).')';
221 0 0 0     0 return $try if $self->{oneline} or length($try) <= $self->width;
222             }
223 0         0 return $self->_format_arraylike('(', ')', $payload);
224             }
225              
226             sub _format_array {
227 172     172   257 my ($self, $payload) = @_;
228 172         312 $self->_format_arraylike('[', ']', $payload);
229             }
230              
231             sub _format_el {
232 401     401   556 my ($self, $el) = @_;
233 401 100       691 return $el->[1].' =>' if $el->[0] eq 'key';
234 371         522 return $self->_format($el).',';
235             }
236              
237             sub _format_arraylike {
238 172     172   263 my ($self, $l, $r, $payload) = @_;
239 172 50       290 if ($self->{vertical}) {
240 0         0 return join("\n", $l,
241             (map $self->_indent($self->_format($_).','), @$payload),
242             $r);
243             }
244 172 50       383 return $l.$r unless my @pl = @$payload;
245 172         232 my $last_pl = pop @pl;
246             # We don't want 'foo =>' at the end of the array, so for the last
247             # entry use plain _format which will render key-as-string, and don't
248             # add a comma yet because we don't want a trailing comma on a single
249             # line render
250 172         208 my @oneline = do {
251 172         279 local $self->{oneline} = 1;
252 172         361 ((map $self->_format_el($_), @pl), $self->_format($last_pl));
253             };
254 172 100       534 if (!grep /\n/, @oneline) {
255 167         451 my $try = join(' ', $l, @oneline, $r);
256 167 100 100     856 return $try if $self->{oneline} or length $try <= $self->width;
257             }
258 47         224 local $self->{width} = $self->_next_width;
259 47 100       263 if (@$payload == 1) {
260             # single entry, re-format the payload without oneline set
261 12         31 return $self->_format_single($l, $r, $self->_format($payload->[0]));
262             }
263 35 100 100     130 if (@$payload == 2 and $payload->[0][0] eq 'key') {
264 3         8 my $s = (my $k = $self->_format_el($payload->[0]))
265             .' '.$self->_format(my $p = $payload->[1]);
266 3         7 return $self->_format_single($l, $r, do {
267 3         8 $s =~ /\A(.{0,${\$self->width}})(?:\n|\Z)/
268             ? $s
269 3 50       5 : $k."\n".do {
270 0         0 local $self->{width} = $self->_next_width;
271 0         0 $self->_indent($self->_format($p));
272             }
273             });
274             }
275 32         51 my @lines;
276             my @bits;
277 32         69 $oneline[-1] .= ','; # going into multiline mode, *now* we add the comma
278 32         108 foreach my $idx (0..$#$payload) {
279 193         313 my $spare = $self->width - sum((scalar @bits)+1, map length($_), @bits);
280 193         862 my $f = $oneline[$idx];
281 193 100       353 if ($f !~ /\n/) {
282             # single line entry, add to the bits for the current line if it'll fit
283             # otherwise collapse bits into a line and start afresh with this entry
284 190 100       395 if (length($f) <= $spare) {
285 110         167 push @bits, $f;
286 110         176 next;
287             }
288 80 100       143 if (length($f) <= $self->width) {
289 65         262 push(@lines, join(' ', @bits));
290 65         118 @bits = ($f);
291 65         102 next;
292             }
293             }
294             # If it didn't format as a single line, re-format to avoid confusion
295 18         92 $f = $self->_format_el($payload->[$idx]);
296              
297             # if we can fit the first line in the available remaining space in the
298             # current line, do that
299 18 50 33     412 if ($spare > 0 and $f =~ s/^(.{0,${spare}})\n//sm) {
300 18         78 push @bits, $1;
301             }
302 18 50       72 push(@lines, join(' ', @bits)) if @bits;
303 18         39 @bits = ();
304             # if the last line is less than our available width, turn that into
305             # an entry in a new line
306 18 50       32 if ($f =~ s/(?:\A|\n)(.{0,${\$self->width}})\Z//sm) {
  18         58  
307 18         934 push @bits, $1;
308             }
309             # stuff whatever's left from the middle into the line array
310 18 100       80 push(@lines, $f) if length($f);
311             }
312 32 50       101 push @lines, join(' ', @bits) if @bits;
313 32         111 return join("\n", $l, (map $self->_indent($_), @lines), $r);
314             }
315              
316             sub _format_hashkey {
317 265     265   383 my ($self, $key) = @_;
318             ($key =~ /^-?[a-zA-Z_]\w*$/
319             ? $key
320             # stick a space on the front to force dumping of e.g. 123, then strip it
321 265 100       1135 : do {
322 17         52 s/^" //, s/"\n\Z// for my $s = $self->_dumper(" $key");
323 17         504 $self->_format_string($s)
324             }
325             ).' =>';
326             }
327              
328             sub _format_hash {
329 199     199   323 my ($self, $payload) = @_;
330 199         265 my ($keys, $hash) = @$payload;
331 199 50       327 return '{}' unless @$keys;
332 199         461 @$keys = sort @$keys;
333 199         370 my %k = (map +(
334             $_ => $self->_format_hashkey($_)), @$keys
335             );
336 199 50       572 if ($self->{vertical}) {
337             return join("\n", '{',
338 0         0 (map $self->_indent($k{$_}.' '.$self->_format($hash->{$_}).','), @$keys),
339             '}');
340             }
341 199         225 my $oneline = do {
342 199         322 local $self->{oneline} = 1;
343             join(' ', '{', join(', ',
344 199         471 map $k{$_}.' '.$self->_format($hash->{$_}), @$keys
345             ), '}');
346             };
347 199 100       827 return $oneline if $self->{oneline};
348 36 100 100     131 return $oneline if $oneline !~ /\n/ and length($oneline) <= $self->width;
349 29         131 my $width = local $self->{width} = $self->_next_width;
350             my @f = map {
351 29         149 my $s = $k{$_}.' '.$self->_format(my $p = $hash->{$_});
  50         144  
352             $s =~ /\A(.{0,${width}})(?:\n|\Z)/
353             ? $s
354 50 100       917 : $k{$_}."\n".do {
355 7         21 local $self->{width} = $self->_next_width;
356 7         45 $self->_indent($self->_format($p));
357             }
358             } @$keys;
359 29         64 local $self->{width} = $self->_next_width;
360 29 100       175 if (@f == 1) {
361 16         51 return $self->_format_single('{', '}', $f[0]);
362             }
363 13         41 return join("\n",
364             '{',
365             (map $self->_indent($_).',', @f),
366             '}',
367             );
368             }
369              
370 3     3   9 sub _format_key { shift->_format_string(@_) }
371              
372             sub _format_string {
373 490     490   679 my ($self, $str) = @_;
374 490 100       962 my $q = $str =~ /[\\']/ ? q{"} : q{'};
375 490 50       879 my $w = $self->{vertical} ? 20 : $self->_next_width;
376 490 100       3291 return $q.$str.$q if length($str) <= $w;
377 4         6 $w--;
378 4         6 my @f;
379 4         14 while (length(my $chunk = substr($str, 0, $w, ''))) {
380 16         45 push @f, $q.$chunk.$q;
381             }
382 4         21 return join("\n.", @f);
383             }
384              
385 62     62   191 sub _format_thing { $_[1] }
386              
387             sub _format_single {
388 31     31   147 my ($self, $l, $r, $raw) = @_;
389 31         156 my ($first, @lines) = split /\n/, $raw;
390 31 100       91 return join("\n", $l, $self->_indent($first), $r) unless @lines;
391 27         61 (my $pad = $self->indent_by) =~ s/^ //;
392 27 100       211 my $last = $lines[-1] =~ /^[\}\]\)]/ ? (pop @lines).$pad: '';
393 27         54 local $self->{width} = $self->_next_width;
394 27 100       190 return join("\n",
395             $l.($l eq '{' ? ' ' : $pad).$first,
396             (map $self->_indent($_), @lines),
397             $last.$r
398             );
399             }
400              
401             sub _format_blessed {
402 4     4   9 my ($self, $payload) = @_;
403 4         7 my ($content, $class) = @$payload;
404 4         10 return 'bless( '.$self->_format($content).qq{, "${class}"}.' )';
405             }
406              
407             sub _format_ref {
408 1     1   3 my ($self, $payload) = @_;
409             return '$_->'.join('',
410             map {
411 1 50       2 if ($_->[0] eq 'key') {
  1 0       3  
412 1         13 my $quoted = quotemeta($_->[1]);
413 1 50       4 if ($_->[1] eq $quoted) {
414 1         13 '{'.$quoted.'}'
415             } else {
416 0           '{"'.$quoted.'"}'
417             }
418             } elsif ($_->[0] eq 'idx') {
419 0           '['.$_->[1].']'
420             } else {
421 0           die "Invalid ref element type ".$_->[0];
422             }
423             } @$payload
424             );
425             }
426              
427             1;
428             __END__