File Coverage

blib/lib/Data/Dumper/Compact.pm
Criterion Covered Total %
statement 182 230 79.1
branch 68 120 56.6
condition 15 39 38.4
subroutine 37 42 88.1
pod 6 6 100.0
total 308 437 70.4


line stmt bran cond sub pod time code
1             package Data::Dumper::Compact;
2              
3 1     1   70829 use List::Util qw(sum);
  1         3  
  1         122  
4 1     1   8 use Scalar::Util qw(blessed reftype);
  1         2  
  1         44  
5 1     1   652 use Data::Dumper ();
  1         7127  
  1         30  
6 1     1   487 use Mu::Tiny;
  1         1122  
  1         6  
7              
8             our $VERSION = '0.005002';
9             $VERSION =~ tr/_//d;
10              
11             sub import {
12 1     1   8 my ($class, $ddc, $opts) = @_;
13 1 50       1704 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   204 no strict 'refs';
  1         2  
  1         477  
18 0         0 *{"${targ}::${ddc}"} = $cb;
  0         0  
19             }
20              
21 28     28   201 lazy max_width => sub { 78 };
22              
23 28     28   186 lazy width => sub { shift->max_width };
24              
25 28     28   199 lazy indent_width => sub { length($_[0]->indent_by) };
26              
27 621     621   1089 sub _next_width { $_[0]->width - $_[0]->indent_width }
28              
29 28     28   175 lazy indent_by => sub { ' ' };
30              
31 28     28   199 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 297     297   503 my ($self, $string) = @_;
37 297         523 my $ib = $self->indent_by;
38 297         1733 $string =~ s/^/$ib/msg;
39 297         1885 $string;
40             }
41              
42             lazy dumper => sub {
43 19     19   141 my ($self) = @_;
44 19         78 my $dd = Data::Dumper->new([]);
45 19 50       592 $dd->Trailingcomma(1) if $dd->can('Trailingcomma');
46 19         130 $dd->Terse(1)->Indent(1)->Useqq(1)->Deparse(1)->Quotekeys(0)->Sortkeys(1);
47 19         646 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 19         32 my $dp_new = do {
51 19         122 require B::Deparse;
52 19         43 my $orig = \&B::Deparse::new;
53 0     0   0 sub { my ($self, @args) = @_; $self->$orig('-si'.$indent_width, @args) }
  0         0  
54 19         98 };
55             sub {
56 1     1   8 no warnings 'redefine';
  1         2  
  1         1312  
57 186     186   749 local *B::Deparse::new = $dp_new;
58 186         474 $dd->Values([ $_[0] ])->Dump
59             },
60 19         99 };
61              
62 254     254   547 sub _dumper { $_[0]->dumper->($_[1]) }
63              
64             sub _optify {
65 37     37   87 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 37 0 33     107 and delete @{$self}{grep !$opts->{$_}, qw(indent_width dumper)};
  0   33     0  
      0        
72 37 50 50     223 ref($self) or $self = $self->new($opts||{});
73 37         1075 $self->$method(@args);
74             }
75              
76             sub dump {
77 28     28 1 6809 my ($self, $data, $opts) = @_;
78             $self->_optify($opts, sub {
79 28     28   59 my ($self) = @_;
80 28         73 $self->format($self->transform($self->transforms, $self->expand($data)));
81 28         160 });
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 333     333 1 622 my ($self, $data) = @_;
91 333 100 66     1154 if (ref($data) eq 'HASH') {
    100          
    100          
92             return [ hash => [
93             [ sort keys %$data ],
94 40         219 { map +($_ => $self->expand($data->{$_})), keys %$data }
95             ] ];
96             } elsif (ref($data) eq 'ARRAY') {
97 53         129 return [ array => [ map $self->expand($_), @$data ] ];
98             } elsif (blessed($data) and my $ret = $self->_expand_blessed($data)) {
99 3         15 return $ret;
100             }
101 237         423 (my $thing = $self->_dumper($data)) =~ s/\n\Z//;
102              
103             # -foo and friends automatically become 'key' type, all else stays 'string'
104 237 100       4689 if (my ($string) = $thing =~ /^"(.*)"$/) {
105 207 100       1042 return [ ($string =~ /^-[a-zA-Z]\w*$/ ? 'key' : 'string') => $string ];
106             }
107 30         153 return [ thing => $thing ];
108             }
109              
110             sub _expand_blessed {
111 3     3   12 my ($self, $blessed) = @_;
112 3 50       14 return unless grep { $_ eq 'ARRAY' or $_ eq 'HASH' } reftype($blessed);
  3 50       19  
113 3 50       19 my $cursed = reftype($blessed) eq 'ARRAY' ? [ @$blessed ] : { %$blessed };
114 3         29 return [ blessed => [ $self->expand($cursed), blessed($blessed) ] ];
115             }
116              
117             sub transform {
118 28     28 1 65 my ($self, $tfspec, $exp) = @_;
119 28 50       67 return $exp unless $tfspec;
120             # This is redundant from ->dump but consistent for direct user calls
121 28         65 local $self->{transforms} = $tfspec;
122 28         80 $self->_transform($exp, []);
123             }
124              
125             sub _transform {
126 300     300   557 my ($self, $exp, $path) = @_;
127 300         560 my ($type, $payload) = @$exp;
128 300 100       640 if ($type eq 'hash') {
    100          
129 37         46 my %h = %{$payload->[1]};
  37         116  
130             $payload = [
131             $payload->[0],
132             { map +(
133 37         190 $_ => $self->_transform($h{$_}, [ @$path, $_ ])
134             ), keys %h
135             },
136             ];
137             } elsif ($type eq 'array') {
138 50         117 my @a = @$payload;
139 50         231 $payload = [ map $self->_transform($a[$_], [ @$path, $_ ]), 0..$#a ];
140             }
141 300         427 TF: foreach my $this_tf (@{$self->transforms}) {
  300         556  
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 300         1736 return [ $type, $payload ];
174             }
175              
176             sub format {
177 28     28 1 56 my ($self, $exp) = @_;
178 28         63 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 1048     1048   1786 my ($self, $exp) = @_;
191 1048         1786 my ($type, $payload) = @$exp;
192 1048 50 33     2626 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   11 no warnings 'exiting';
  1         2  
  1         1886  
197 0         0 goto VERTICAL;
198             }
199 1048         4444 return $self->${\"_format_${type}"}($payload);
  1048         2880  
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 168     168   313 my ($self, $payload) = @_;
214 168         399 $self->_format_arraylike('[', ']', $payload);
215             }
216              
217             sub _format_el {
218 400     400   686 my ($self, $el) = @_;
219 400 100       888 return $el->[1].' =>' if $el->[0] eq 'key';
220 370         675 return $self->_format($el).',';
221             }
222              
223             sub _format_arraylike {
224 168     168   336 my ($self, $l, $r, $payload) = @_;
225 168 50       351 if ($self->{vertical}) {
226 0         0 return join("\n", $l,
227             (map $self->_indent($self->_format($_).','), @$payload),
228             $r);
229             }
230 168 50       392 return $l.$r unless my @pl = @$payload;
231 168         252 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 168         247 my @oneline = do {
237 168         333 local $self->{oneline} = 1;
238 168         358 ((map $self->_format_el($_), @pl), $self->_format($last_pl));
239             };
240 168 100       641 if (!grep /\n/, @oneline) {
241 166         441 my $try = join(' ', $l, @oneline, $r);
242 166 100 100     922 return $try if $self->{oneline} or length $try <= $self->width;
243             }
244 44         234 local $self->{width} = $self->_next_width;
245 44 100       252 if (@$payload == 1) {
246             # single entry, re-format the payload without oneline set
247 12         32 return $self->_format_single($l, $r, $self->_format($payload->[0]));
248             }
249 32 100 100     90 if (@$payload == 2 and $payload->[0][0] eq 'key') {
250 3         9 my $s = (my $k = $self->_format_el($payload->[0]))
251             .' '.$self->_format(my $p = $payload->[1]);
252 3         7 return $self->_format_single($l, $r, do {
253 3         7 $s =~ /\A(.{0,${\$self->width}})(?:\n|\Z)/
254             ? $s
255 3 50       7 : $k."\n".do {
256 0         0 local $self->{width} = $self->_next_width;
257 0         0 $self->_indent($self->_format($p));
258             }
259             });
260             }
261 29         49 my @lines;
262             my @bits;
263 29         64 $oneline[-1] .= ','; # going into multiline mode, *now* we add the comma
264 29         88 foreach my $idx (0..$#$payload) {
265 187         352 my $spare = $self->width - sum((scalar @bits)+1, map length($_), @bits);
266 187         1062 my $f = $oneline[$idx];
267 187 50       412 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 187 100       370 if (length($f) <= $spare) {
271 107         201 push @bits, $f;
272 107         190 next;
273             }
274 80 100       153 if (length($f) <= $self->width) {
275 65         315 push(@lines, join(' ', @bits));
276 65         131 @bits = ($f);
277 65         115 next;
278             }
279             }
280             # If it didn't format as a single line, re-format to avoid confusion
281 15         94 $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 15 50 33     281 if ($spare > 0 and $f =~ s/^(.{0,${spare}})\n//sm) {
286 15         58 push @bits, $1;
287             }
288 15 50       54 push(@lines, join(' ', @bits)) if @bits;
289 15         35 @bits = ();
290             # if the last line is less than our available width, turn that into
291             # an entry in a new line
292 15 50       25 if ($f =~ s/(?:\A|\n)(.{0,${\$self->width}})\Z//sm) {
  15         43  
293 15         936 push @bits, $1;
294             }
295             # stuff whatever's left from the middle into the line array
296 15 50       77 push(@lines, $f) if length($f);
297             }
298 29 50       90 push @lines, join(' ', @bits) if @bits;
299 29         82 return join("\n", $l, (map $self->_indent($_), @lines), $r);
300             }
301              
302             sub _format_hashkey {
303 261     261   453 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 261 100       1237 : do {
308 17         56 s/^" //, s/"\n\Z// for my $s = $self->_dumper(" $key");
309 17         525 $self->_format_string($s)
310             }
311             ).' =>';
312             }
313              
314             sub _format_hash {
315 185     185   326 my ($self, $payload) = @_;
316 185         301 my ($keys, $hash) = @$payload;
317 185 50       320 return '{}' unless @$keys;
318 185         793 my %k = (map +(
319             $_ => $self->_format_hashkey($_)), @$keys
320             );
321 185 50       530 if ($self->{vertical}) {
322             return join("\n", '{',
323 0         0 (map $self->_indent($k{$_}.' '.$self->_format($hash->{$_}).','), @$keys),
324             '}');
325             }
326 185         256 my $oneline = do {
327 185         341 local $self->{oneline} = 1;
328             join(' ', '{', join(', ',
329 185         487 map $k{$_}.' '.$self->_format($hash->{$_}), @$keys
330             ), '}');
331             };
332 185 100       816 return $oneline if $self->{oneline};
333 31 100 66     119 return $oneline if $oneline !~ /\n/ and length($oneline) <= $self->width;
334 27         144 my $width = local $self->{width} = $self->_next_width;
335             my @f = map {
336 27         153 my $s = $k{$_}.' '.$self->_format(my $p = $hash->{$_});
  47         138  
337             $s =~ /\A(.{0,${width}})(?:\n|\Z)/
338             ? $s
339 47 100       865 : $k{$_}."\n".do {
340 7         21 local $self->{width} = $self->_next_width;
341 7         48 $self->_indent($self->_format($p));
342             }
343             } @$keys;
344 27         70 local $self->{width} = $self->_next_width;
345 27 100       198 if (@f == 1) {
346 15         41 return $self->_format_single('{', '}', $f[0]);
347             }
348 12         46 return join("\n",
349             '{',
350             (map $self->_indent($_).',', @f),
351             '}',
352             );
353             }
354              
355 3     3   11 sub _format_key { shift->_format_string(@_) }
356              
357             sub _format_string {
358 490     490   875 my ($self, $str) = @_;
359 490 100       1116 my $q = $str =~ /[\\']/ ? q{"} : q{'};
360 490 50       963 my $w = $self->{vertical} ? 20 : $self->_next_width;
361 490 100       3818 return $q.$str.$q if length($str) <= $w;
362 4         8 $w--;
363 4         6 my @f;
364 4         19 while (length(my $chunk = substr($str, 0, $w, ''))) {
365 16         111 push @f, $q.$chunk.$q;
366             }
367 4         27 return join("\n.", @f);
368             }
369              
370 60     60   202 sub _format_thing { $_[1] }
371              
372             sub _format_single {
373 30     30   117 my ($self, $l, $r, $raw) = @_;
374 30         155 my ($first, @lines) = split /\n/, $raw;
375 30 100       81 return join("\n", $l, $self->_indent($first), $r) unless @lines;
376 26         59 (my $pad = $self->indent_by) =~ s/^ //;
377 26 100       211 my $last = $lines[-1] =~ /^[\}\]\)]/ ? (pop @lines).$pad: '';
378 26         56 local $self->{width} = $self->_next_width;
379 26 100       190 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 4     4   16 my ($self, $payload) = @_;
388 4         8 my ($content, $class) = @$payload;
389 4         9 return 'bless( '.$self->_format($content).qq{, "${class}"}.' )';
390             }
391              
392             1;
393             __END__