File Coverage

blib/lib/Perinci/Result/Format/Lite.pm
Criterion Covered Total %
statement 267 383 69.7
branch 121 214 56.5
condition 71 125 56.8
subroutine 18 20 90.0
pod 1 2 50.0
total 478 744 64.2


line stmt bran cond sub pod time code
1             ## no critic: Subroutines::ProhibitSubroutinePrototypes
2              
3             use 5.010001;
4 1     1   84860 use strict;
  1         12  
5 1     1   5 #IFUNBUILT
  1         2  
  1         21  
6             # use warnings;
7             #END IFUNBUILT
8              
9             use Exporter qw(import);
10 1     1   4 use List::Util qw(first max);
  1         2  
  1         25  
11 1     1   5  
  1         1  
  1         1382  
12             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
13             our $DATE = '2022-05-16'; # DATE
14             our $DIST = 'Perinci-Result-Format-Lite'; # DIST
15             our $VERSION = '0.287'; # VERSION
16              
17             our @EXPORT_OK = qw(format);
18              
19             # copy-pasted from List::MoreUtils::PP
20             my $f = shift;
21             foreach my $i ( 0 .. $#_ )
22 40     40 0 55 {
23 40         65 local *_ = \$_[$i];
24             return $i if $f->();
25 101         139 }
26 101 100       120 return -1;
27             }
28 10         26  
29             state $json = do {
30             if (eval { require Cpanel::JSON::XS; 1 }) { Cpanel::JSON::XS->new->canonical(1)->convert_blessed->allow_nonref }
31             elsif (eval { require JSON::Tiny::Subclassable; 1 }) { JSON::Tiny::Subclassable->new }
32 4     4   5 elsif (eval { require JSON::PP; 1 }) { JSON::PP->new->canonical(1)->convert_blessed->allow_nonref }
33 1 50       3 else { die "Can't find any JSON module" }
  1 0       4  
  1 0       2  
  1         9  
34 0         0 };
  0         0  
  0         0  
35 0         0 $json;
  0         0  
  0         0  
36 0         0 };
37              
38 4         35 state $cleanser = do {
39             eval { require Data::Clean::JSON; 1 };
40             if ($@) {
41             undef;
42 2     2   3 } else {
43 1         2 Data::Clean::JSON->get_cleanser;
  1         849  
  1         5674  
44 1 50       4 }
45 0         0 };
46             if ($cleanser) {
47 1         6 $cleanser->clean_in_place($_[0]);
48             } else {
49             $_[0];
50 2 50       2989 }
51 2         11 }
52              
53 0         0 my ($data, $header_row, $resmeta, $format) = @_;
54              
55             $resmeta //= {};
56              
57             # column names
58 12     12   27 my @columns;
59             if ($header_row) {
60 12   100     29 @columns = @{$data->[0]};
61             } else {
62             @columns = map {"col$_"} 0..@{$data->[0]}-1;
63 12         15 }
64 12 50       26  
65 12         16 my $column_orders; # e.g. [col2, col1, col3, ...]
  12         31  
66             SET_COLUMN_ORDERS: {
67 0         0  
  0         0  
  0         0  
68             # find column orders from 'table_column_orders' in result metadata (or
69             # from env)
70 12         17 my $tcos;
71             if ($ENV{FORMAT_PRETTY_TABLE_COLUMN_ORDERS}) {
72             $tcos = _json->encode($ENV{FORMAT_PRETTY_TABLE_COLUMN_ORDERS});
73             } elsif (my $rfos = ($resmeta->{'cmdline.format_options'} //
74             $resmeta->{format_options})) {
75 12         18 my $rfo = $rfos->{'text-pretty'} // $rfos->{text} // $rfos->{any};
  12         18  
76 12 50 33     56 if ($rfo) {
    50          
77 0         0 $tcos = $rfo->{table_column_orders};
78             }
79             }
80 0   0     0 if ($tcos) {
      0        
81 0 0       0 # find an entry in tcos that @columns contains all the columns of
82 0         0 COLS:
83             for my $cols (@$tcos) {
84             for my $col (@$cols) {
85 12 50       28 next COLS unless first {$_ eq $col} @columns;
86             }
87             $column_orders = $cols;
88 0         0 last SET_COLUMN_ORDERS;
89 0         0 }
90 0 0   0   0 }
  0         0  
91              
92 0         0 if ($resmeta->{'table.field_orders'}) {
93 0         0 $column_orders = $resmeta->{'table.field_orders'};
94             last SET_COLUMN_ORDERS;
95             }
96              
97 12 100       28 # find column orders from table spec
98 1         2 $column_orders = $resmeta->{'table.fields'};
99 1         3 }
100              
101             # reorder each row according to requested column order
102             if ($column_orders) {
103 11         16 require Sort::BySpec;
104             my $cmp = Sort::BySpec::cmp_by_spec(spec => $column_orders);
105             # 0->2, 1->0, ... (map column position from unordered to ordered)
106             my @map0 = sort { $cmp->($a->[1], $b->[1]) }
107 12 100       28 map {[$_, $columns[$_]]} 0..$#columns;
108 11         821 #use DD; dd \@map0;
109 11         1105 my @map;
110             for (0..$#map0) {
111 57         3781 $map[$_] = $map0[$_][0];
112 11         229 }
  47         110  
113             #use DD; dd \@map;
114 11         787 my $newdata = [];
115 11         27 for my $row (@$data) {
116 47         67 my @newrow;
117             for (0..$#map) { $newrow[$_] = $row->[$map[$_]] }
118             push @$newdata, \@newrow;
119 11         22 }
120 11         20 $data = $newdata;
121 42         44 my @newcolumns;
122 42         58 for (@map) { push @newcolumns, $columns[$_] }
  174         241  
123 42         80 @columns = @newcolumns;
124             }
125 11         18  
126 11         13 my @field_idxs; # map column to index in table.fields
127 11         20 {
  47         63  
128 11         152 my $tff = $resmeta->{'table.fields'} or last;
129             for my $i (0..$#columns) {
130             $field_idxs[$i] = firstidx { $_ eq $columns[$i] } @$tff;
131 12         24 }
132             }
133 12 100       33  
134 10         24 # determine field labels
135 40     101   112 {
  101         253  
136             last unless $header_row && @$data;
137             my $tff = $resmeta->{'table.fields'} or last;
138             my $tfl = $resmeta->{'table.field_labels'};
139             my $tfu = $resmeta->{'table.field_units'};
140             for my $i (0..$#columns) {
141 12 50 33     15 my $field_idx = $field_idxs[$i];
  12         18  
  12         46  
142 12 100       32 next unless $field_idx >= 0;
143 10         13 if ($tfl && defined $tfl->[$field_idx]) {
144 10         16 $data->[0][$i] = $tfl->[$field_idx];
145 10         19 } elsif ($tfu && defined $tfu->[$field_idx]) {
146 40         47 # add field units as label suffix to header (" (UNIT)")
147 40 100       69 $data->[0][$i] .= " ($tfu->[$field_idx])";
148 30 100 100     88 }
    100 66        
149 2         4 }
150             }
151              
152 3         7 FORMAT_CELLS:
153             {
154             my $tffmt = $resmeta->{'table.field_formats'};
155             my $tffmt_code = $resmeta->{'table.field_format_code'};
156             my $tffmt_default = $resmeta->{'table.default_field_format'};
157             last unless $tffmt || $tffmt_code || $tffmt_default;
158              
159 12         16 my (@fmt_names, @fmt_opts); # key: column index
  12         20  
160 12         16 for my $i (0..$#columns) {
161 12         20 my $field_idx = $field_idxs[$i];
162 12 50 66     63 my $fmt = $tffmt_code ? $tffmt_code->($columns[$i]) : undef;
      66        
163             $fmt //= $tffmt->[$field_idx] if $field_idx >= 0;
164 3         7 $fmt //= $tffmt_default;
165 3         6 if (ref $fmt eq 'ARRAY') {
166 12         16 $fmt_names[$i] = $fmt->[0];
167 12 100       24 $fmt_opts [$i] = $fmt->[1] // {};
168 12 100 66     47 } else {
169 12   100     29 $fmt_names[$i] = $fmt;
170 12 100       22 $fmt_opts [$i] = {};
171 6         8 }
172 6   50     14 }
173              
174 6         9 my $nf;
175 6         10  
176             for my $i (0..$#{$data}) {
177             next if $i==0 && $header_row;
178             my $row = $data->[$i];
179 3         6 for my $j (0..$#columns) {
180             next unless defined $row->[$j];
181 3         5 my $fmt_name = $fmt_names[$j];
  3         8  
182 6 100 66     16 #say "D:j=$j fmt_name=$fmt_name";
183 3         5 next unless $fmt_name;
184 3         6 my $fmt_opts = $fmt_opts [$j];
185 12 50       773 if ($fmt_name eq 'iso8601_datetime' || $fmt_name eq 'iso8601_date' || $fmt_name eq 'datetime' || $fmt_name eq 'date') {
186 12         17 if ($row->[$j] =~ /\A[0-9]+(\.[0-9]*)?\z/) {
187             my $frac = $1 ? "0$1"+0 : 0;
188 12 100       23 my @t = gmtime($row->[$j]);
189 10         11 if ($fmt_name eq 'iso8601_datetime' || $fmt_name eq 'datetime') {
190 10 100 100     71 $row->[$j] = sprintf(
    50 66        
    50 66        
    50          
    50          
    50          
191 4 50       22 "%04d-%02d-%02dT%02d:%02d:".($frac ? "%06.3f" : "%02d")."Z",
192 4 50       13 $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]+$frac);
193 4         23 } else {
194 4 100 66     17 $row->[$j] = sprintf(
195 1 50       11 "%04d-%02d-%02d",
196             $t[5]+1900, $t[4]+1, $t[3]);
197             }
198             }
199 3         20 } elsif ($fmt_name eq 'boolstr') {
200             $row->[$j] = $row->[$j] ? "yes" : "no";
201             } elsif ($fmt_name eq 'filesize') {
202             require Format::Human::Bytes;
203             $row->[$j] = Format::Human::Bytes::base2($row->[$j], 0);
204             } elsif ($fmt_name eq 'sci2dec') {
205 0 0       0 if ($row->[$j] =~ /\A(?:[+-]?)(?:\d+\.|\d*\.(\d+))[eE]([+-]?\d+)\z/) {
206             my $n = length($1 || "") - $2; $n = 0 if $n < 0;
207 0         0 $row->[$j] = sprintf("%.${n}f", $row->[$j]);
208 0         0 }
209             } elsif ($fmt_name eq 'percent') {
210 0 0       0 my $fmt = $fmt_opts->{sprintf} // '%.2f%%';
211 0 0 0     0 $row->[$j] = sprintf($fmt, $row->[$j] * 100);
  0         0  
212 0         0 } elsif ($fmt_name eq 'number') {
213             require Number::Format::BigFloat;
214             $row->[$j] = Number::Format::BigFloat::format_number(
215 0   0     0 $row->[$j], {
216 0         0 thousands_sep => $fmt_opts->{thousands_sep} // ',',
217             decimal_point => $fmt_opts->{decimal_point} // '.',
218 6         22 decimal_digits => $fmt_opts->{precision} // 0,
219             # XXX decimal_fill
220             });
221             }
222             }
223 6   50     44 }
      50        
      50        
224             }
225              
226             if ($format eq 'text-pretty') {
227             ALIGN_COLUMNS:
228             {
229             last unless @$data;
230              
231 12 50       375 # note: the logic of this block of code now also put in Number::Pad
    0          
    0          
    0          
    0          
232              
233             # XXX we just want to turn off 'uninitialized' and 'negative repeat
234 12 50       15 # count does nothing' from the operator x
  12         24  
235             no warnings;
236              
237             my $tfa = $resmeta->{'table.field_aligns'};
238             my $tfa_code = $resmeta->{'table.field_align_code'};
239             my $tfa_default = $resmeta->{'table.default_field_align'};
240 1     1   6  
  1         2  
  1         929  
241             # align numbers by default, with 'right' currently as 'number' is too slow
242 12         22 unless ($tfa || $tfa_code || $tfa_default) {
243 12         20 $tfa = [map { undef } 0 .. $#columns];
244 12         18 COLUMN:
245             for my $colidx (0 .. $#columns) {
246             for my $i (0 .. $#{$data}) {
247 12 50 66     42 next if $header_row && $i == 0;
      66        
248 9         24 my $cell = $data->[$i][$colidx];
  37         53  
249             next unless defined $cell;
250 9         21 next COLUMN unless $cell =~ /\A[+-]?[0-9]+(?:\.[0-9]*)?(?:[Ee][+-]?[0-9]+)?(?:%)?\z/;
251 37         52 }
  37         64  
252 118 100 66     263 $tfa->[$colidx] = 'right';
253 81         98 }
254 81 100       133 }
255 36 100       147 #use DD; dd $tfa;
256             #say "D1";
257 32         53  
258             for my $colidx (0..$#columns) {
259             my $field_idx = $field_idxs[$colidx];
260             my $align = $tfa_code ? $tfa_code->($columns[$colidx]) : undef;
261             $align //= $tfa->[$field_idx] if $field_idx >= 0;
262             $align //= $tfa_default;
263 12         22 next unless $align;
264 49         61  
265 49 100       81 # determine max widths
266 49 100 66     166 my $maxw;
267 49   100     102 my ($maxw_bd, $maxw_d, $maxw_ad); # before digit, digit, after d
268 49 100       85 if ($align eq 'number') {
269             my (@w_bd, @w_d, @w_ad);
270             for my $i (0..$#{$data}) {
271 36         41 my $row = $data->[$i];
272 36         47 if (@$row > $colidx) {
273 36 100       58 my $cell = $row->[$colidx];
274 3         6 if ($header_row && $i == 0) {
275 3         4 my $w = length($cell);
  3         5  
276 15         20 push @w_bd, 0;
277 15 50       21 push @w_bd, 0;
278 15         18 push @w_ad, 0;
279 15 100 66     78 } elsif ($cell =~ /\A([+-]?\d+)(\.?)(\d*)[%]?\z/) {
    100          
    50          
280 3         6 # decimal notation number (with optional percent sign). TODO: allow arbitraty units after number, e.g. ml, mcg, etc? but should we align the unit too?
281 3         5 push @w_bd, length($1);
282 3         4 push @w_d , length($2);
283 3         6 push @w_ad, length($3);
284             } elsif ($cell =~ /\A([+-]?\d+\.?\d*)([eE])([+-]?\d+)\z/) {
285             # scientific notation number
286 8         21 push @w_bd, length($1);
287 8         12 push @w_d , length($2);
288 8         17 push @w_ad, length($3);
289             } else {
290             # not a number
291 4         9 push @w_bd, length($cell);
292 4         6 push @w_bd, 0;
293 4         7 push @w_ad, 0;
294             }
295             } else {
296 0         0 push @w_bd, 0;
297 0         0 push @w_d , 0;
298 0         0 push @w_ad, 0;
299             }
300             }
301 0         0 $maxw_bd = max(@w_bd);
302 0         0 $maxw_d = max(@w_d);
303 0         0 $maxw_ad = max(@w_ad);
304             if ($header_row) {
305             my $w = length($data->[0][$colidx]);
306 3         6 if ($maxw_d == 0 && $maxw_ad == 0) {
307 3         7 $maxw_bd = $w;
308 3         6 }
309 3 50       18 }
310 3         5 }
311 3 100 66     9  
312 1         3 $maxw = max(map {
313             @$_ > $colidx ? length($_->[$colidx]) : 0
314             } @$data);
315              
316             # do the alignment
317             for my $i (0..$#{$data}) {
318 36 50       58 my $row = $data->[$i];
  140         271  
319             for my $i (0..$#{$data}) {
320             my $row = $data->[$i];
321             next unless @$row > $colidx;
322 36         46 my $cell = $row->[$colidx];
  36         69  
323 140         177 next unless defined($cell);
324 140         149 if ($align eq 'number') {
  140         210  
325 612         709 my ($bd, $d, $ad);
326 612 50       892 if ($header_row && $i == 0) {
327 612         685 } elsif (($bd, $d, $ad) = $cell =~ /\A([+-]?\d+)(\.?)(\d*)\z/) {
328 612 100       898 $cell = join(
329 441 100 100     756 '',
    100          
    100          
330 75         87 (' ' x ($maxw_bd - length($bd))), $bd,
331 75 100 66     291 $d , (' ' x ($maxw_d - length($d ))),
    100          
    100          
332             $ad, (' ' x ($maxw_ad - length($ad))),
333 8         24 );
334             } elsif (($bd, $d, $ad) = $cell =~ /\A([+-]?\d+\.?\d*)([eE])([+-]?\d+)\z/) {
335             $cell = join(
336             '',
337             (' ' x ($maxw_bd - length($bd))), $bd,
338             $d , (' ' x ($maxw_d - length($d ))),
339             $ad, (' ' x ($maxw_ad - length($ad))),
340 4         10 );
341             }
342             my $w = length($cell);
343             $cell = (' ' x ($maxw - $w)) . $cell
344             if $maxw > $w;
345             } elsif ($align eq 'right') {
346             $cell = (' ' x ($maxw - length($cell))) . $cell;
347 75         85 } elsif ($align eq 'middle' || $align eq 'center') {
348 75 100       125 my $w = length($cell);
349             my $n = int(($maxw-$w)/2);
350             $cell = (' ' x $n) . $cell . (' ' x ($maxw-$w-$n));
351 241         360 } else {
352             # assumed left
353 50         53 $cell .= (' ' x ($maxw - length($cell)));
354 50         70  
355 50         79 }
356             $row->[$colidx] = $cell;
357             }
358 75         105 }
359             } # for $colidx
360             } # END align columns
361 441         696 #say "D2";
362              
363             my $fres;
364             my $backend = $ENV{FORMAT_PRETTY_TABLE_BACKEND};
365             $backend //= "Text::Table::Org" if $ENV{INSIDE_EMACS};
366             if ($backend) {
367             require Text::Table::Any;
368 12         19 $fres = Text::Table::Any::table(
369 12         20 rows => $data,
370 12 50 0     29 header_row => $header_row,
371 12 50       22 backend => $backend,
372 0         0 (caption => $resmeta->{caption}) x !!defined($resmeta->{caption}),
373             );
374             } else {
375             require Text::Table::Sprintf;
376             $fres = Text::Table::Sprintf::table(rows=>$data, header_row=>$header_row);
377 0         0 }
378             $fres .= "\n" unless $fres =~ /\R\z/ || !length($fres);
379             $fres;
380 12         1183 } elsif ($format eq 'csv') {
381 12         95 no warnings 'uninitialized';
382             join(
383 12 50 33     1216 "",
384 12         131 map {
385             my $row = $_;
386 1     1   7 join(
  1         2  
  1         110  
387             ",",
388             map {
389             my $cell = $_;
390 0         0 $cell =~ s/"/""/g;
  0         0  
391             qq("$cell");
392             } @$row)."\n";
393             } @$data
394 0         0 );
  0         0  
395 0         0 } elsif ($format eq 'tsv') {
396 0         0 no warnings 'uninitialized';
397             join("", map { my $row = $_; join("\t", @$row)."\n" } @$data);
398             } elsif ($format eq 'ltsv') {
399             no warnings 'uninitialized';
400             join("", map { my $row = $_; join("\t", map { "$columns[$_]:$row->[$_]" } 0 .. $#{$row})."\n" } @$data);
401 1     1   6 } elsif ($format eq 'html') {
  1         2  
  1         83  
402 0         0 no warnings 'uninitialized';
  0         0  
  0         0  
403             require HTML::Entities;
404 1     1   6  
  1         1  
  1         118  
405 0         0 my $tfa = $resmeta->{'table.field_aligns'};
  0         0  
  0         0  
  0         0  
  0         0  
406              
407 1     1   6 my @res;
  1         2  
  1         295  
408 0         0 push @res, "<table".($resmeta->{'table.html_class'} ?
409             " class=\"".HTML::Entities::encode_entities(
410 0         0 $resmeta->{'table.html_class'})."\"" : "").
411             ">\n";
412 0         0 for my $i (0..$#{$data}) {
413             my $data_elem = $i == 0 ? "th" : "td";
414             push @res, "<thead>\n" if $i == 0;
415 0 0       0 push @res, "<tbody>\n" if $i == 1;
416             push @res, " <tr>\n";
417 0         0 my $row = $data->[$i];
  0         0  
418 0 0       0 for my $j (0..$#{$row}) {
419 0 0       0 my $field_idx = $field_idxs[$j];
420 0 0       0 my $align;
421 0         0 if ($field_idx >= 0 && $tfa->[$field_idx]) {
422 0         0 $align = $tfa->[$field_idx];
423 0         0 $align = "right" if $align eq 'number';
  0         0  
424 0         0 $align = "middle" if $align eq 'center';
425 0         0 }
426 0 0 0     0 push @res, " <$data_elem",
427 0         0 ($align ? " align=\"$align\"" : ""),
428 0 0       0 ">", HTML::Entities::encode_entities($row->[$j]),
429 0 0       0 "</$data_elem>\n";
430             }
431 0 0       0 push @res, " </tr>\n";
432             push @res, "</thead>\n" if $i == 0;
433             }
434             push @res, "</tbody>\n";
435             push @res, "</table>\n";
436 0         0 join '', @res;
437 0 0       0 } else {
438             no warnings 'uninitialized';
439 0         0 shift @$data if $header_row;
440 0         0 join("", map {join("\t", @$_)."\n"} @$data);
441 0         0 }
442             }
443 1     1   6  
  1         2  
  1         391  
444 0 0       0 my ($res, $format, $is_naked, $cleanse) = @_;
445 0         0  
  0         0  
446             if ($format =~ /\A(text|text-simple|text-pretty|csv|tsv|ltsv|html)\z/) {
447             $format = $format eq 'text' ?
448             ((-t STDOUT) ? 'text-pretty' : 'text-simple') : $format;
449             no warnings 'uninitialized';
450 18     18 1 79132 if ($res->[0] !~ /^(2|304)/) {
451             my $fres = "ERROR $res->[0]: $res->[1]";
452 18 100       110 if (my $prev = $res->[3]{prev}) {
453 16 0       49 $fres .= " ($prev->[0]: $prev->[1])";
    50          
454             }
455 1     1   7 return "$fres\n";
  1         1  
  1         510  
456 16 100 66     100 } elsif ($res->[3] && $res->[3]{"x.hint.result_binary"}) {
    50          
457 1         4 return $res->[2];
458 1 50       4 } else {
459 0         0 require Data::Check::Structure;
460             my $data = $res->[2];
461 1         14 my $max = 1000;
462             if (!ref($data)) {
463 0         0 $data //= "";
464             $data .= "\n" unless !length($data) || $data =~ /\n\z/;
465 15         811 return $data;
466 15         1300 } elsif (ref($data) eq 'ARRAY' && !@$data) {
467 15         24 return "";
468 15 100 66     92 } elsif (Data::Check::Structure::is_aos($data, {max=>$max})) {
    50          
    50          
    50          
    100          
    50          
469 3   100     10 return join("", map {"$_\n"} @$data);
470 3 100 100     14 } elsif (Data::Check::Structure::is_aoaos($data, {max=>$max})) {
471 3         24 my $header_row = 0;
472             my $data = $data;
473 0         0 if ($res->[3]{'table.fields'}) {
474             $data = [$res->[3]{'table.fields'}, @$data];
475 0         0 $header_row = 1;
  0         0  
476             }
477 0         0 return __gen_table($data, $header_row, $res->[3], $format);
478 0         0 } elsif (Data::Check::Structure::is_hos($data, {max=>$max})) {
479 0 0       0 $data = [map {[$_, $data->{$_}]} sort keys %$data];
480 0         0 unshift @$data, ["key", "value"];
481 0         0 return __gen_table($data, 1, $res->[3], $format);
482             } elsif (Data::Check::Structure::is_aohos($data, {max=>$max})) {
483 0         0 # collect all mentioned fields
484             my @fieldnames;
485 1         36 if ($res->[3] && $res->[3]{'table.fields'} &&
  1         4  
486 1         3 $res->[3]{'table.hide_unknown_fields'}) {
487 1         4 @fieldnames = @{ $res->[3]{'table.fields'} };
488             } else {
489             my %fieldnames;
490 11         1315 for my $row (@$data) {
491 11 100 66     64 $fieldnames{$_}++ for keys %$row;
      100        
492             }
493 1         3 @fieldnames = sort keys %fieldnames;
  1         3  
494             }
495 10         16 my $newdata = [];
496 10         22 for my $row (@$data) {
497 28         79 push @$newdata, [map {$row->{$_}} @fieldnames];
498             }
499 10         56 unshift @$newdata, \@fieldnames;
500             return __gen_table($newdata, 1, $res->[3], $format);
501 11         25 } else {
502 11         22 $format = 'json-pretty';
503 31         50 }
  127         197  
504             }
505 11         24 }
506 11         32  
507             my $tff = $res->[3]{'table.fields'};
508 0         0 $res = $res->[2] if $is_naked;
509              
510             if ($format eq 'perl') {
511             my $use_color = $ENV{COLOR} // (-t STDOUT);
512             if ($use_color && eval { require Data::Dump::Color; 1 }) {
513 2         5 return Data::Dump::Color::dump($res);
514 2 50       6 } elsif (eval { require Data::Dump; 1 }) {
515             return Data::Dump::dump($res);
516 2 50       5 } else {
517 0   0     0 no warnings 'once';
518 0 0 0     0 require Data::Dumper;
  0 0       0  
  0         0  
519 0         0 local $Data::Dumper::Terse = 1;
520 0         0 local $Data::Dumper::Indent = 1;
  0         0  
521 0         0 local $Data::Dumper::Useqq = 1;
522             local $Data::Dumper::Deparse = 1;
523 1     1   6 local $Data::Dumper::Quotekeys = 0;
  1         2  
  1         357  
524 0         0 local $Data::Dumper::Sortkeys = 1;
525 0         0 local $Data::Dumper::Trailingcomma = 1;
526 0         0 return Data::Dumper::Dumper($res);
527 0         0 }
528 0         0 }
529 0         0  
530 0         0 unless ($format =~ /\Ajson(-pretty)?\z/) {
531 0         0 warn "Unknown format '$format', fallback to json-pretty";
532 0         0 $format = 'json-pretty';
533             }
534             __cleanse($res) if ($cleanse//1);
535             if ($format =~ /json/) {
536 2 100       10 if ($tff && _json->can("sort_by") &&
537 1         47 eval { require Sort::ByExample; 1}) {
538 1         5 my $cmp = Sort::ByExample->cmp($tff);
539             _json->sort_by(sub { $cmp->($JSON::PP::a, $JSON::PP::b) });
540 2 50 50     14 }
541 2 50       200  
542 2 0 33     6 if ($format eq 'json') {
      33        
543 0         0 return _json->encode($res) . "\n";
  0         0  
544 0         0 } else {
545 0     0   0 _json->pretty(1);
  0         0  
546             return _json->encode($res);
547             }
548 2 50       5 }
549 0         0 }
550              
551 2         5 1;
552 2         3 # ABSTRACT: Format enveloped result
553              
554              
555             =pod
556              
557             =encoding UTF-8
558              
559             =head1 NAME
560              
561             Perinci::Result::Format::Lite - Format enveloped result
562              
563             =head1 VERSION
564              
565             This document describes version 0.287 of Perinci::Result::Format::Lite (from Perl distribution Perinci-Result-Format-Lite), released on 2022-05-16.
566              
567             =head1 SYNOPSIS
568              
569             =head1 DESCRIPTION
570              
571             This module formats L<enveloped result structure|Rinci::function/"Enveloped
572             result"> to "pretty text" if it can do so, e.g. the structure can be represented
573             as a 2-dimensional table. Otherwise, it falls back to JSON or Perl. The table
574             formats supported include CSV, TSV, LTSV, or HTML. More table formats (e.g. Org,
575             Markdown) are supported via L<Text::Table::Any> when you set
576             L</"FORMAT_PRETTY_TABLE_BACKEND">.
577              
578             This module is a more lightweight version of L<Perinci::Result::Format> but the
579             long-term goal is to reunite the two formatting modules back to a
580             modular/pluggable module.
581              
582             =for Pod::Coverage ^(firstidx)$
583              
584             =head1 FUNCTIONS
585              
586             =head2 format($res, $format[ , $is_naked=0, $cleanse=1 ]) => str
587              
588             =head1 ENVIRONMENT
589              
590             =head2 FORMAT_PRETTY_TABLE_BACKEND
591              
592             Str, optional. If this is set, will render text table using L<Text::Table::Any>
593             (with C<backend> set to the value of this environment variable) instead of the
594             default L<Text::Table::Sprintf>. This is useful if you want to output text table
595             in a different format, for example to generate Org tables (make sure
596             L<Text::Table::Org> backend is already installed):
597              
598             % FORMAT_PRETTY_TABLE_BACKEND=Text::Table::Org lcpan rdeps Getopt::Lucid
599              
600             For convenience, a default is chosen for you under certain condition. When
601             inside Emacs (environment C<INSIDE_EMACS> is set), C<Text::Table::Org> is used
602             as default.
603              
604             =head2 FORMAT_PRETTY_TABLE_COLUMN_ORDERS => array (json)
605              
606             Set the default of C<table_column_orders> in C<format_options> in result
607             metadata, similar to what's implemented in L<Perinci::Result::Format> and
608             L<Data::Format::Pretty::Console>.
609              
610             =head2 COLOR => bool
611              
612             =head1 HOMEPAGE
613              
614             Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Result-Format-Lite>.
615              
616             =head1 SOURCE
617              
618             Source repository is at L<https://github.com/perlancar/perl-Perinci-Result-Format-Lite>.
619              
620             =head1 SEE ALSO
621              
622             L<Perinci::Result::Format>, a more heavyweight version of this module.
623              
624             L<Perinci::CmdLine::Lite> uses this module to format enveloped result.
625              
626             =head1 AUTHOR
627              
628             perlancar <perlancar@cpan.org>
629              
630             =head1 CONTRIBUTING
631              
632              
633             To contribute, you can send patches by email/via RT, or send pull requests on
634             GitHub.
635              
636             Most of the time, you don't need to build the distribution yourself. You can
637             simply modify the code, then test via:
638              
639             % prove -l
640              
641             If you want to build the distribution (e.g. to try to install it locally on your
642             system), you can install L<Dist::Zilla>,
643             L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
644             L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
645             Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
646             that are considered a bug and can be reported to me.
647              
648             =head1 COPYRIGHT AND LICENSE
649              
650             This software is copyright (c) 2022, 2021, 2020, 2018, 2017, 2016, 2015 by perlancar <perlancar@cpan.org>.
651              
652             This is free software; you can redistribute it and/or modify it under
653             the same terms as the Perl 5 programming language system itself.
654              
655             =head1 BUGS
656              
657             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Result-Format-Lite>
658              
659             When submitting a bug or request, please include a test-file or a
660             patch to an existing test-file that illustrates the bug or desired
661             feature.
662              
663             =cut