File Coverage

blib/lib/Data/Dumper/Compact.pm
Criterion Covered Total %
statement 181 230 78.7
branch 68 120 56.6
condition 15 39 38.4
subroutine 36 42 85.7
pod 6 6 100.0
total 306 437 70.0


line stmt bran cond sub pod time code
1             package Data::Dumper::Compact;
2              
3 1     1   72320 use List::Util qw(sum);
  1         2  
  1         118  
4 1     1   7 use Scalar::Util qw(blessed reftype);
  1         3  
  1         52  
5 1     1   683 use Data::Dumper ();
  1         7424  
  1         29  
6 1     1   521 use Mu::Tiny;
  1         1160  
  1         14  
7              
8             our $VERSION = '0.005001';
9             $VERSION =~ tr/_//d;
10              
11             sub import {
12 1     1   7 my ($class, $ddc, $opts) = @_;
13 1 50       1637 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 1     1   216 no strict 'refs';
  1         2  
  1         529  
18 0         0 *{"${targ}::${ddc}"} = $cb;
  0         0  
19             }
20              
21 10     10   69 lazy max_width => sub { 78 };
22              
23 10     10   51 lazy width => sub { shift->max_width };
24              
25 10     10   47 lazy indent_width => sub { length($_[0]->indent_by) };
26              
27 437     437   777 sub _next_width { $_[0]->width - $_[0]->indent_width }
28              
29 10     10   53 lazy indent_by => sub { ' ' };
30              
31 10     10   99 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 171     171   305 my ($self, $string) = @_;
37 171         324 my $ib = $self->indent_by;
38 171         1012 $string =~ s/^/$ib/msg;
39 171         944 $string;
40             }
41              
42             lazy dumper => sub {
43 10     10   44 my ($self) = @_;
44 10         33 my $dd = Data::Dumper->new([]);
45 10 50       345 $dd->Trailingcomma(1) if $dd->can('Trailingcomma');
46 10         68 $dd->Terse(1)->Indent(1)->Useqq(1)->Deparse(1)->Quotekeys(0)->Sortkeys(1);
47 10         287 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 10         17 my $dp_new = do {
51 10         53 require B::Deparse;
52 10         57 my $orig = \&B::Deparse::new;
53 0     0   0 sub { my ($self, @args) = @_; $self->$orig('-si'.$indent_width, @args) }
  0         0  
54 10         58 };
55             sub {
56 1     1   11 no warnings 'redefine';
  1         2  
  1         1347  
57 118     118   454 local *B::Deparse::new = $dp_new;
58 118         327 $dd->Values([ $_[0] ])->Dump
59             },
60 10         47 };
61              
62 118     118   248 sub _dumper { $_[0]->dumper->($_[1]) }
63              
64             sub _optify {
65 10     10   25 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 10 0 33     29 and delete @{$self}{grep !$opts->{$_}, qw(indent_width dumper)};
  0   33     0  
      0        
72 10 50 50     70 ref($self) or $self = $self->new($opts||{});
73 10         295 $self->$method(@args);
74             }
75              
76             sub dump {
77 10     10 1 1194 my ($self, $data, $opts) = @_;
78             $self->_optify($opts, sub {
79 10     10   22 my ($self) = @_;
80 10         22 $self->format($self->transform($self->transforms, $self->expand($data)));
81 10         60 });
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 153     153 1 299 my ($self, $data) = @_;
91 153 100 66     478 if (ref($data) eq 'HASH') {
    100          
    100          
92             return [ hash => [
93             [ sort keys %$data ],
94 26         125 { map +($_ => $self->expand($data->{$_})), keys %$data }
95             ] ];
96             } elsif (ref($data) eq 'ARRAY') {
97 25         72 return [ array => [ map $self->expand($_), @$data ] ];
98             } elsif (blessed($data) and my $ret = $self->_expand_blessed($data)) {
99 1         5 return $ret;
100             }
101 101         193 (my $thing = $self->_dumper($data)) =~ s/\n\Z//;
102              
103             # -foo and friends automatically become 'key' type, all else stays 'string'
104 101 100       2556 if (my ($string) = $thing =~ /^"(.*)"$/) {
105 89 100       543 return [ ($string =~ /^-[a-zA-Z]\w*$/ ? 'key' : 'string') => $string ];
106             }
107 12         69 return [ thing => $thing ];
108             }
109              
110             sub _expand_blessed {
111 1     1   3 my ($self, $blessed) = @_;
112 1 50       4 return unless grep { $_ eq 'ARRAY' or $_ eq 'HASH' } reftype($blessed);
  1 50       8  
113 1 50       7 my $cursed = reftype($blessed) eq 'ARRAY' ? [ @$blessed ] : { %$blessed };
114 1         4 return [ blessed => [ $self->expand($cursed), blessed($blessed) ] ];
115             }
116              
117             sub transform {
118 10     10 1 26 my ($self, $tfspec, $exp) = @_;
119 10 50       24 return $exp unless $tfspec;
120             # This is redundant from ->dump but consistent for direct user calls
121 10         24 local $self->{transforms} = $tfspec;
122 10         27 $self->_transform($exp, []);
123             }
124              
125             sub _transform {
126 142     142   242 my ($self, $exp, $path) = @_;
127 142         235 my ($type, $payload) = @$exp;
128 142 100       305 if ($type eq 'hash') {
    100          
129 25         49 my %h = %{$payload->[1]};
  25         75  
130             $payload = [
131             $payload->[0],
132             { map +(
133 25         160 $_ => $self->_transform($h{$_}, [ @$path, $_ ])
134             ), keys %h
135             },
136             ];
137             } elsif ($type eq 'array') {
138 24         111 my @a = @$payload;
139 24         137 $payload = [ map $self->_transform($a[$_], [ @$path, $_ ]), 0..$#a ];
140             }
141 142         209 TF: foreach my $this_tf (@{$self->transforms}) {
  142         258  
142 0         0 my $tf = $this_tf;
143 0         0 my $last_tf = 0;
144 0         0 while ($tf != $last_tf) {
145 0         0 $last_tf = $tf;
146 0 0       0 if (ref($tf) eq 'ARRAY') {
    0          
147 0         0 my @match = @$tf;
148 0         0 $tf = pop @match;
149 0 0       0 next TF if @match > @$path; # not deep enough
150 0         0 MATCH: foreach my $idx (0..$#match) {
151 0 0       0 next MATCH unless defined(my $m = $match[$idx]);
152 0         0 my $rpv = $path->[$idx-@match];
153 0 0       0 if (!ref($m)) {
    0          
    0          
154 0 0       0 next TF unless $rpv eq $m;
155             } elsif (ref($m) eq 'Regexp') {
156 0 0       0 next TF unless $rpv =~ $m;
157             } elsif (ref($m) eq 'CODE') {
158 0         0 local $_ = $rpv;
159 0 0       0 next TF unless $m->($rpv);
160             } else {
161 0         0 die "Unknown path match type for $m";
162             }
163             }
164             } elsif (ref($tf) eq 'HASH') {
165 0 0 0     0 next TF unless $tf = $tf->{$type}||$tf->{_};
166             }
167             }
168             ($type, $payload) = @{
169 0 0       0 $self->$tf($type, $payload, $path)
  0         0  
170             || [ $type, $payload ]
171             };
172             }
173 142         820 return [ $type, $payload ];
174             }
175              
176             sub format {
177 10     10 1 23 my ($self, $exp) = @_;
178 10         23 return $self->_format($exp)."\n";
179             # If we realise we've flat run out of horizontal space, we need to be able
180             # to jump back up the call stack to the top and start again - hence the
181             # presence of this label to jump to from _format - of course, if that
182             # clause never gets hit then our first _format call returns and therefore
183             # the label is never reached.
184             VERTICAL:
185 0         0 local $self->{vertical} = 1;
186 0         0 return $self->_format($exp)."\n";
187             }
188              
189             sub _format {
190 681     681   1088 my ($self, $exp) = @_;
191 681         1146 my ($type, $payload) = @$exp;
192 681 50 33     1774 if (!$self->{vertical} and $self->width <= 0) {
193             # We've run out of horizontal space, engage 'vertical sprawl mode' and
194             # restart from the top by jumping back up the current call stack to the
195             # VERTICAL label in the top-level call to format.
196 1     1   9 no warnings 'exiting';
  1         2  
  1         1898  
197 0         0 goto VERTICAL;
198             }
199 681         2976 return $self->${\"_format_${type}"}($payload);
  681         1909  
200             }
201              
202             sub _format_list {
203 0     0   0 my ($self, $payload) = @_;
204 0         0 my @plain = grep !/\s/, map $_->[1], grep $_->[0] eq 'string', @$payload;
205 0 0       0 if (@plain == @$payload) {
206 0         0 my $try = 'qw('.join(' ', @plain).')';
207 0 0 0     0 return $try if $self->{oneline} or length($try) <= $self->width;
208             }
209 0         0 return $self->_format_arraylike('(', ')', $payload);
210             }
211              
212             sub _format_array {
213 119     119   204 my ($self, $payload) = @_;
214 119         267 $self->_format_arraylike('[', ']', $payload);
215             }
216              
217             sub _format_el {
218 297     297   486 my ($self, $el) = @_;
219 297 100       1014 return $el->[1].' =>' if $el->[0] eq 'key';
220 270         509 return $self->_format($el).',';
221             }
222              
223             sub _format_arraylike {
224 119     119   212 my ($self, $l, $r, $payload) = @_;
225 119 50       220 if ($self->{vertical}) {
226 0         0 return join("\n", $l,
227             (map $self->_indent($self->_format($_).','), @$payload),
228             $r);
229             }
230 119 50       259 return $l.$r unless my @pl = @$payload;
231 119         182 my $last_pl = pop @pl;
232             # We don't want 'foo =>' at the end of the array, so for the last
233             # entry use plain _format which will render key-as-string, and don't
234             # add a comma yet because we don't want a trailing comma on a single
235             # line render
236 119         166 my @oneline = do {
237 119         218 local $self->{oneline} = 1;
238 119         232 ((map $self->_format_el($_), @pl), $self->_format($last_pl));
239             };
240 119 100       402 if (!grep /\n/, @oneline) {
241 118         361 my $try = join(' ', $l, @oneline, $r);
242 118 100 100     638 return $try if $self->{oneline} or length $try <= $self->width;
243             }
244 19         98 local $self->{width} = $self->_next_width;
245 19 100       207 if (@$payload == 1) {
246             # single entry, re-format the payload without oneline set
247 4         14 return $self->_format_single($l, $r, $self->_format($payload->[0]));
248             }
249 15 100 100     41 if (@$payload == 2 and $payload->[0][0] eq 'key') {
250 1         5 my $s = (my $k = $self->_format_el($payload->[0]))
251             .' '.$self->_format(my $p = $payload->[1]);
252 1         3 return $self->_format_single($l, $r, do {
253 1         3 $s =~ /\A(.{0,${\$self->width}})(?:\n|\Z)/
254             ? $s
255 1 50       2 : $k."\n".do {
256 0         0 local $self->{width} = $self->_next_width;
257 0         0 $self->_indent($self->_format($p));
258             }
259             });
260             }
261 14         24 my @lines;
262             my @bits;
263 14         29 $oneline[-1] .= ','; # going into multiline mode, *now* we add the comma
264 14         42 foreach my $idx (0..$#$payload) {
265 75         132 my $spare = $self->width - sum((scalar @bits)+1, map length($_), @bits);
266 75         383 my $f = $oneline[$idx];
267 75 50       159 if ($f !~ /\n/) {
268             # single line entry, add to the bits for the current line if it'll fit
269             # otherwise collapse bits into a line and start afresh with this entry
270 75 100       132 if (length($f) <= $spare) {
271 44         77 push @bits, $f;
272 44         72 next;
273             }
274 31 100       62 if (length($f) <= $self->width) {
275 23         104 push(@lines, join(' ', @bits));
276 23         45 @bits = ($f);
277 23         53 next;
278             }
279             }
280             # If it didn't format as a single line, re-format to avoid confusion
281 8         38 $f = $self->_format_el($payload->[$idx]);
282              
283             # if we can fit the first line in the available remaining space in the
284             # current line, do that
285 8 50 33     192 if ($spare > 0 and $f =~ s/^(.{0,${spare}})\n//sm) {
286 8         32 push @bits, $1;
287             }
288 8 50       28 push(@lines, join(' ', @bits)) if @bits;
289 8         18 @bits = ();
290             # if the last line is less than our available width, turn that into
291             # an entry in a new line
292 8 50       21 if ($f =~ s/(?:\A|\n)(.{0,${\$self->width}})\Z//sm) {
  8         26  
293 8         554 push @bits, $1;
294             }
295             # stuff whatever's left from the middle into the line array
296 8 50       36 push(@lines, $f) if length($f);
297             }
298 14 50       57 push @lines, join(' ', @bits) if @bits;
299 14         36 return join("\n", $l, (map $self->_indent($_), @lines), $r);
300             }
301              
302             sub _format_hashkey {
303 242     242   417 my ($self, $key) = @_;
304             ($key =~ /^-?[a-zA-Z_]\w*$/
305             ? $key
306             # stick a space on the front to force dumping of e.g. 123, then strip it
307 242 100       1228 : do {
308 17         49 s/^" //, s/"\n\Z// for my $s = $self->_dumper(" $key");
309 17         517 $self->_format_string($s)
310             }
311             ).' =>';
312             }
313              
314             sub _format_hash {
315 159     159   390 my ($self, $payload) = @_;
316 159         255 my ($keys, $hash) = @$payload;
317 159 50       288 return '{}' unless @$keys;
318 159         336 my %k = (map +(
319             $_ => $self->_format_hashkey($_)), @$keys
320             );
321 159 50       404 if ($self->{vertical}) {
322             return join("\n", '{',
323 0         0 (map $self->_indent($k{$_}.' '.$self->_format($hash->{$_}).','), @$keys),
324             '}');
325             }
326 159         209 my $oneline = do {
327 159         308 local $self->{oneline} = 1;
328             join(' ', '{', join(', ',
329 159         428 map $k{$_}.' '.$self->_format($hash->{$_}), @$keys
330             ), '}');
331             };
332 159 100       762 return $oneline if $self->{oneline};
333 20 100 66     69 return $oneline if $oneline !~ /\n/ and length($oneline) <= $self->width;
334 16         79 my $width = local $self->{width} = $self->_next_width;
335             my @f = map {
336 16         93 my $s = $k{$_}.' '.$self->_format(my $p = $hash->{$_});
  30         94  
337             $s =~ /\A(.{0,${width}})(?:\n|\Z)/
338             ? $s
339 30 100       590 : $k{$_}."\n".do {
340 3         11 local $self->{width} = $self->_next_width;
341 3         20 $self->_indent($self->_format($p));
342             }
343             } @$keys;
344 16         67 local $self->{width} = $self->_next_width;
345 16 100       115 if (@f == 1) {
346 8         18 return $self->_format_single('{', '}', $f[0]);
347             }
348 8         20 return join("\n",
349             '{',
350             (map $self->_indent($_).',', @f),
351             '}',
352             );
353             }
354              
355 0     0   0 sub _format_key { shift->_format_string(@_) }
356              
357             sub _format_string {
358 371     371   642 my ($self, $str) = @_;
359 371 100       847 my $q = $str =~ /[\\']/ ? q{"} : q{'};
360 371 50       753 my $w = $self->{vertical} ? 20 : $self->_next_width;
361 371 100       2997 return $q.$str.$q if length($str) <= $w;
362 2         14 $w--;
363 2         3 my @f;
364 2         11 while (length(my $chunk = substr($str, 0, $w, ''))) {
365 8         30 push @f, $q.$chunk.$q;
366             }
367 2         14 return join("\n.", @f);
368             }
369              
370 47     47   159 sub _format_thing { $_[1] }
371              
372             sub _format_single {
373 13     13   58 my ($self, $l, $r, $raw) = @_;
374 13         72 my ($first, @lines) = split /\n/, $raw;
375 13 100       44 return join("\n", $l, $self->_indent($first), $r) unless @lines;
376 12         24 (my $pad = $self->indent_by) =~ s/^ //;
377 12 100       93 my $last = $lines[-1] =~ /^[\}\]\)]/ ? (pop @lines).$pad: '';
378 12         26 local $self->{width} = $self->_next_width;
379 12 100       94 return join("\n",
380             $l.($l eq '{' ? ' ' : $pad).$first,
381             (map $self->_indent($_), @lines),
382             $last.$r
383             );
384             }
385              
386             sub _format_blessed {
387 2     2   14 my ($self, $payload) = @_;
388 2         5 my ($content, $class) = @$payload;
389 2         7 return 'bless( '.$self->_format($content).qq{, "${class}"}.' )';
390             }
391              
392             1;
393             __END__