File Coverage

blib/lib/Text/Yeti/Table.pm
Criterion Covered Total %
statement 69 69 100.0
branch 15 16 93.7
condition 7 14 50.0
subroutine 7 7 100.0
pod 1 2 50.0
total 99 108 91.6


line stmt bran cond sub pod time code
1              
2             package Text::Yeti::Table;
3             $Text::Yeti::Table::VERSION = '0.3.0';
4             # ABSTRACT: Render a table like "docker ps" does
5              
6 3     3   333906 use 5.010001;
  3         25  
7 3     3   14 use Mojo::Base -strict;
  3         5  
  3         20  
8              
9 3     3   366 use Exporter 'import';
  3         4  
  3         2624  
10             our @EXPORT_OK = qw(render_table);
11              
12             # default stringification
13             my $TO_S = sub { defined $_[0] ? "$_[0]" : "" };
14              
15             # default header computation (from column key)
16             my $TO_H = sub { local $_ = $_[0]; s/([a-z])([A-Z])/$1 $2/g; uc };
17              
18             sub _compile_table_spec {
19 8     8   10 my $spec = shift;
20              
21             # 'key'
22             # [ 'key', $to_s, 'head' ]
23             # { k => , h => , s => , x => }
24              
25             # { I => $i, K => 'key', H => 'head', S => $to_s, X => $exc }
26              
27 8         10 my @columns;
28              
29 8         9 my $i = 0;
30 8         16 for (@$spec) {
31 26         28 my %c;
32 26         35 $c{I} = $i++;
33 26 100       44 if ( ref eq 'HASH' ) {
34 2         7 my %spec = %$_;
35 2         3 $c{K} = $spec{k};
36 2   33     6 $c{H} = $spec{h} // $TO_H->( $spec{k} );
37 2   33     6 $c{S} = $spec{s} // $TO_S;
38 2 50       6 $c{X} = $spec{x} if $spec{x};
39             }
40             else {
41 24 100       44 my @spec = ref $_ ? @$_ : ($_);
42 24         27 $c{K} = $spec[0];
43 24   66     52 $c{H} = $spec[2] // $TO_H->( $spec[0] );
44 24   66     59 $c{S} = $spec[1] // $TO_S;
45             }
46 26         45 push @columns, \%c;
47             }
48              
49 8         23 my $r = { C => \@columns };
50 8 100       35 if ( my @x = map $_->{I}, grep $_->{X}, @columns ) {
51 2         3 $r->{X} = \@x;
52             }
53 8         13 return $r;
54             }
55              
56             sub _render_table {
57 8     8   16 my ( $items, $spec, $io ) = ( shift, shift, shift );
58              
59 8         14 my $t = _compile_table_spec($spec);
60 8         11 my $c = $t->{C};
61              
62 8         11 my ( @rows, @len );
63              
64             # Compute table headers
65 8         22 my @h = map { $_->{H} } @$c;
  26         41  
66 8         10 @len = map { length $_ } @h;
  26         35  
67              
68             # Compute table rows, keep track of max length
69 8         15 my @i = 0 .. $#$c;
70 8         11 my @k = map { $_->{K} } @$c;
  26         34  
71 8         10 my @s = map { $_->{S} } @$c;
  26         31  
72 8         14 for my $item (@$items) {
73 13         14 my @v = map { $s[$_]->( $item->{ $k[$_] }, $item ) } @i;
  40         59  
74 13         35 $len[$_] = max( $len[$_], length $v[$_] ) for @i;
75 13         22 push @rows, \@v;
76             }
77              
78             # Exclude columns conditionally
79 8 100       13 if ( $t->{X} ) {
80 2         3 my %x; # Compute exclusions
81 2         2 for my $i ( @{ $t->{X} } ) {
  2         4  
82 2         3 my @c = map { $_->[$i] } @rows; # Column values
  4         6  
83 2 100       5 $x{$i}++ if $c->[$i]{X}( \@c );
84             }
85 2 100       28 if (%x) { # Exclude
86 1         2 my @keep = grep { !$x{$_} } @i;
  3         7  
87 1         3 @$_ = @{$_}[@keep] for @rows, \@len, \@h;
  4         9  
88             }
89             }
90              
91             # Compute the table format
92 8         12 my @fmt = map {"%-${_}s"} @len;
  25         46  
93 8         19 $fmt[-1] = '%s';
94 8         19 my $fmt = join( ' ' x 3, @fmt ) . "\n";
95              
96             # Render the table
97 8         9 printf {$io} $fmt, @h;
  8         35  
98 8         16 printf {$io} $fmt, @$_ for @rows;
  13         65  
99             }
100              
101             sub render_table {
102 8   50 8 1 5055 _render_table( shift, shift, shift // \*STDOUT );
103             }
104              
105 40 100   40 0 69 sub max { $_[0] >= $_[1] ? $_[0] : $_[1] }
106              
107             1;
108              
109             #pod =encoding utf8
110             #pod
111             #pod =head1 SYNOPSIS
112             #pod
113             #pod use Text::Yeti::Table qw(render_table);
114             #pod
115             #pod render_table( $list, $spec );
116             #pod
117             #pod =head1 DESCRIPTION
118             #pod
119             #pod L renders a table of data into text.
120             #pod Given a table (which is an arrayref of hashrefs) and a specification,
121             #pod it creates output such as below.
122             #pod
123             #pod CONTAINER ID IMAGE CREATED STATUS NAME
124             #pod 632495650e4e alpine:latest 5 days ago Exited 5 days ago zealous_galileo
125             #pod 6459c004a7b4 postgres:9.6.1-alpine 23 days ago Up 23 days hardcore_sammet
126             #pod 63a4c1b60c9f f348af3681e0 2 weeks ago Exited 12 days ago elastic_ride
127             #pod
128             #pod The specification can be as simple as:
129             #pod
130             #pod [ 'key1', 'key2', 'key3' ]
131             #pod
132             #pod For complex values, a function can be given for the text conversion.
133             #pod
134             #pod [ 'name', 'id', 'node', 'address', [ 'tags', sub {"@{$_[0]}"} ] ]
135             #pod
136             #pod Usually headers are computed from keys, but that can be overriden.
137             #pod
138             #pod [ 'ServiceName', 'ServiceID', 'Node', [ 'Datacenter', undef, 'DC' ] ]
139             #pod
140             #pod =head1 EXAMPLE
141             #pod
142             #pod The following code illustrates a full example:
143             #pod
144             #pod my @items = (
145             #pod { ContainerId => '632495650e4e',
146             #pod Image => 'alpine:latest',
147             #pod Created => { unit => 'days', amount => 5 },
148             #pod ExitedAt => { unit => 'days', amount => 5 },
149             #pod Name => '/zealous_galileo',
150             #pod },
151             #pod { ContainerId => '6459c004a7b4',
152             #pod Image => 'postgres:9.6.1-alpine',
153             #pod Created => { unit => 'days', amount => 23 },
154             #pod StartedAt => { unit => 'days', amount => 23 },
155             #pod Running => true,
156             #pod Name => '/hardcore_sammet',
157             #pod },
158             #pod { ContainerId => '63a4c1b60c9f',
159             #pod Image => 'f348af3681e0',
160             #pod Created => { unit => 'weeks', amount => 2 },
161             #pod ExitedAt => { unit => 'days', amount => 12 },
162             #pod Name => '/elastic_ride',
163             #pod },
164             #pod );
165             #pod
166             #pod sub status_of {
167             #pod my ( $running, $item ) = ( shift, shift );
168             #pod $running
169             #pod ? "Up $item->{StartedAt}{amount} $item->{StartedAt}{unit}"
170             #pod : "Exited $item->{ExitedAt}{amount} $item->{ExitedAt}{unit} ago";
171             #pod }
172             #pod
173             #pod my @spec = (
174             #pod 'ContainerId',
175             #pod 'Image',
176             #pod [ 'Created', sub {"$_[0]->{amount} $_[0]->{unit} ago"} ],
177             #pod [ 'Running', \&status_of, 'STATUS' ],
178             #pod [ 'Name', sub { substr( shift, 1 ) } ],
179             #pod );
180             #pod
181             #pod render_table( \@items, \@spec );
182             #pod
183             #pod The corresponding output is the table in L.
184             #pod
185             #pod =head1 FUNCTIONS
186             #pod
187             #pod L implements the following functions, which can be imported individually.
188             #pod
189             #pod =head2 render_table
190             #pod
191             #pod render_table( \@items, $spec );
192             #pod render_table( \@items, $spec, $io );
193             #pod
194             #pod The C<$spec> is an arrayref whose entries can be:
195             #pod
196             #pod =over 4
197             #pod
198             #pod =item *
199             #pod
200             #pod a string (like C<'key>'), which is equivalent to
201             #pod
202             #pod ['key']
203             #pod
204             #pod =item *
205             #pod
206             #pod an arrayref, with up to 3 entries
207             #pod
208             #pod ['key', $to_s, $header]
209             #pod
210             #pod C<$to_s> is a function to convert the value under C<'key'> to text.
211             #pod By default, it stringifies the value, except for C which
212             #pod becomes C<< "" >>.
213             #pod
214             #pod C<$header> is the header for the corresponding column.
215             #pod By default, it is computed from the key, as in the examples below:
216             #pod
217             #pod "image" -> "IMAGE"
218             #pod "ContainerID" -> "CONTAINER ID"
219             #pod
220             #pod =item *
221             #pod
222             #pod a hashref, with keys
223             #pod
224             #pod k => 'key', required
225             #pod s => $to_s,
226             #pod h => $header,
227             #pod x => $exclude,
228             #pod
229             #pod where
230             #pod
231             #pod C<$to_s> is a function to convert the value under C to text.
232             #pod By default, C becomes C<< '' >>, and everything else
233             #pod is stringfied.
234             #pod
235             #pod C<$header> is the header for the corresponding column.
236             #pod If not given, it is computed from the key as above.
237             #pod
238             #pod C<$exclude> is a coderef which given all the values of a column
239             #pod (as an arrayref) should return true if the column should be excluded
240             #pod or false if the column is to be kept. As an example,
241             #pod
242             #pod use List::Util 'all';
243             #pod (x => sub { all { $_ eq '' } @{$_[0]} })
244             #pod
245             #pod will exclude the corresponding column if all values collapse to C<< '' >>.
246             #pod
247             #pod =back
248             #pod
249             #pod The C<$io> is a handle. By default, output goes to C.
250             #pod
251             #pod =cut
252              
253             __END__