File Coverage

blib/lib/Text/Yeti/Table.pm
Criterion Covered Total %
statement 68 68 100.0
branch 15 16 93.7
condition 7 14 50.0
subroutine 7 7 100.0
pod 1 2 50.0
total 98 107 91.5


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