File Coverage

blib/lib/Text/Yeti/Table.pm
Criterion Covered Total %
statement 35 35 100.0
branch 4 4 100.0
condition 5 8 62.5
subroutine 6 6 100.0
pod 1 2 50.0
total 51 55 92.7


line stmt bran cond sub pod time code
1              
2             package Text::Yeti::Table;
3             $Text::Yeti::Table::VERSION = '0.1.0';
4             # ABSTRACT: Render a table like "docker ps" does
5              
6 2     2   92476 use 5.010001;
  2         8  
7 2     2   13 use Mojo::Base -strict;
  2         5  
  2         18  
8              
9 2     2   341 use Exporter 'import';
  2         4  
  2         1279  
10             our @EXPORT_OK = qw(render_table);
11              
12             # default stringification
13             my $TO_S = sub { defined $_[0] ? "$_[0]" : "" };
14              
15             sub _render_table {
16 5     5   19 my ( $items, $spec, $io ) = ( shift, shift, shift );
17              
18 5         11 my ( @rows, @len );
19 5 100       16 my @spec = map { ref $_ ? $_ : [$_] } @$spec;
  17         72  
20 5         13 my @c = map { $_->[0] } @spec;
  17         51  
21              
22             # Compute table headers
23             my @h = map {
24 5   66     14 $_->[2] // do { local $_ = $_->[0]; s/([a-z])([A-Z])/$1 $2/g; uc }
  17         61  
  16         38  
  16         70  
  16         87  
25             } @spec;
26 5         14 @len = map { length $_ } @h;
  17         46  
27 5         15 push @rows, \@h;
28              
29             # Compute table rows, keep track of max length
30 5   66     15 my @to_s = map { $_->[1] // $TO_S } @spec;
  17         82  
31 5         16 for my $item (@$items) {
32 7         24 my @v = map { $to_s[$_]->( $item->{ $c[$_] }, $item ) } 0 .. $#c;
  22         75  
33 7         49 $len[$_] = max( $len[$_], length $v[$_] ) for 0 .. $#c;
34 7         24 push @rows, \@v;
35             }
36              
37             # Compute the table format
38 5         16 my $fmt = join( " " x 3, map {"%-${_}s"} @len ) . "\n";
  17         73  
39              
40             # Render the table
41 5         19 printf {$io} $fmt, @$_ for @rows;
  12         113  
42             }
43              
44             sub render_table {
45 5   50 5 1 5237 _render_table( shift, shift, shift // \*STDOUT );
46             }
47              
48 22 100   22 0 91 sub max { $_[0] >= $_[1] ? $_[0] : $_[1] }
49              
50             1;
51              
52             #pod =encoding utf8
53             #pod
54             #pod =head1 SYNOPSIS
55             #pod
56             #pod use Text::Yeti::Table qw(render_table);
57             #pod
58             #pod render_table( $list, $spec );
59             #pod
60             #pod =head1 DESCRIPTION
61             #pod
62             #pod L renders a table of data into text.
63             #pod Given a table (which is an arrayref of hashrefs) and a specification,
64             #pod it creates output such as below.
65             #pod
66             #pod CONTAINER ID IMAGE CREATED STATUS NAME
67             #pod 632495650e4e alpine:latest 5 days ago Exited 5 days ago zealous_galileo
68             #pod 6459c004a7b4 postgres:9.6.1-alpine 23 days ago Up 23 days hardcore_sammet
69             #pod 63a4c1b60c9f f348af3681e0 2 weeks ago Exited 12 days ago elastic_ride
70             #pod
71             #pod The specification can be as simple as:
72             #pod
73             #pod [ 'key1', 'key2', 'key3' ]
74             #pod
75             #pod For complex values, a function can be given for the text conversion.
76             #pod
77             #pod [ 'name', 'id', 'node', 'address', [ 'tags', sub {"@{$_[0]}"} ] ]
78             #pod
79             #pod Usually headers are computed from keys, but that can be overriden.
80             #pod
81             #pod [ 'ServiceName', 'ServiceID', 'Node', [ 'Datacenter', undef, 'DC' ] ]
82             #pod
83             #pod =head1 EXAMPLE
84             #pod
85             #pod The following code illustrates a full example:
86             #pod
87             #pod my @items = (
88             #pod { ContainerId => '632495650e4e',
89             #pod Image => 'alpine:latest',
90             #pod Created => { unit => 'days', amount => 5 },
91             #pod ExitedAt => { unit => 'days', amount => 5 },
92             #pod Name => '/zealous_galileo',
93             #pod },
94             #pod { ContainerId => '6459c004a7b4',
95             #pod Image => 'postgres:9.6.1-alpine',
96             #pod Created => { unit => 'days', amount => 23 },
97             #pod StartedAt => { unit => 'days', amount => 23 },
98             #pod Running => true,
99             #pod Name => '/hardcore_sammet',
100             #pod },
101             #pod { ContainerId => '63a4c1b60c9f',
102             #pod Image => 'f348af3681e0',
103             #pod Created => { unit => 'weeks', amount => 2 },
104             #pod ExitedAt => { unit => 'days', amount => 12 },
105             #pod Name => '/elastic_ride',
106             #pod },
107             #pod );
108             #pod
109             #pod sub status_of {
110             #pod my ( $running, $item ) = ( shift, shift );
111             #pod $running
112             #pod ? "Up $item->{StartedAt}{amount} $item->{StartedAt}{unit}"
113             #pod : "Exited $item->{ExitedAt}{amount} $item->{ExitedAt}{unit} ago";
114             #pod }
115             #pod
116             #pod my @spec = (
117             #pod 'ContainerId',
118             #pod 'Image',
119             #pod [ 'Created', sub {"$_[0]->{amount} $_[0]->{unit} ago"} ],
120             #pod [ 'Running', \&status_of, 'STATUS' ],
121             #pod [ 'Name', sub { substr( shift, 1 ) } ],
122             #pod );
123             #pod
124             #pod render_table( \@items, \@spec );
125             #pod
126             #pod The corresponding output is the table in L.
127             #pod
128             #pod =head1 FUNCTIONS
129             #pod
130             #pod L implements the following functions, which can be imported individually.
131             #pod
132             #pod =head2 render_table
133             #pod
134             #pod render_table( \@items, $spec );
135             #pod render_table( \@items, $spec, $io );
136             #pod
137             #pod The C<$spec> is an arrayref whose entries are:
138             #pod
139             #pod =over 4
140             #pod
141             #pod =item *
142             #pod
143             #pod a string (like C<'key>'), which is equivalent to
144             #pod
145             #pod ['key']
146             #pod
147             #pod =item *
148             #pod
149             #pod an arrayref, with up to 3 entries
150             #pod
151             #pod ['key', $to_s, $header]
152             #pod
153             #pod C<$to_s> is a function to convert the value under C<'key'> to text.
154             #pod By default, it stringifies the value, except for C which
155             #pod becomes C<< "" >>.
156             #pod
157             #pod C<$header> is the header for the corresponding column.
158             #pod By default, it is computed from the key, as in the examples below:
159             #pod
160             #pod "image" -> "IMAGE"
161             #pod "ContainerID" -> "CONTAINER ID"
162             #pod
163             #pod =back
164             #pod
165             #pod The C<$io> is a handle. By default, output goes to C.
166             #pod
167             #pod =cut
168              
169             __END__