File Coverage

blib/lib/Logwatch/RecordTree.pm
Criterion Covered Total %
statement 143 235 60.8
branch 39 90 43.3
condition 13 25 52.0
subroutine 23 29 79.3
pod 11 14 78.5
total 229 393 58.2


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             #===============================================================================
3             # PODNAME: Logwatch::RecordTree
4             # ABSTRACT: an object to collect and print Logwatch events
5             #
6             # AUTHOR: Reid Augustin (REID)
7             # EMAIL: reid@hellosix.com
8             # CREATED: Thu Mar 12 18:41:04 PDT 2015
9             #===============================================================================
10              
11 1     1   562 use 5.008;
  1         2  
  1         27  
12 1     1   3 use strict;
  1         1  
  1         21  
13 1     1   3 use warnings;
  1         1  
  1         30  
14              
15             package Logwatch::RecordTree;
16 1     1   3 use Moo;
  1         1  
  1         6  
17 1     1   370 use Carp qw( croak );
  1         2  
  1         41  
18 1     1   383 use UNIVERSAL::require;
  1         820  
  1         7  
19 1     1   22 use List::Util qw ( max min sum );
  1         1  
  1         74  
20 1     1   381 use Sort::Key::Natural qw( natsort natkeysort );
  1         24422  
  1         78  
21              
22 1     1   7 use overload '""' => \&sprint;
  1         3  
  1         9  
23              
24             our $VERSION = '2.046'; # VERSION
25              
26 215 50   215 0 4352 sub check_coderef { die 'Not a CODE ref' if (ref $_[0] ne 'CODE') };
27 92 50   92 0 1890 sub check_hashref { die 'Not a HASH ref' if (ref $_[0] ne 'HASH') };
28 95 50   95 0 2093 sub check_arryref { die 'Not an ARRAY ref' if (ref $_[0] ne 'ARRAY') };
29              
30             has name => ( # name/title for this item
31             is => 'ro',
32             );
33             has sprint_name => ( # callback to print the name
34             is => 'rw',
35             isa => \&check_coderef,
36             default => sub {
37             sub {
38             return $_[0]->name;
39             }
40             },
41             );
42             has sort_key => ( # this overrides ->name in sort_children
43             is => 'rw',
44             );
45             has case_sensitive => (
46             is => 'rw',
47             );
48             has count => ( # count how many times we log this event
49             is => 'rw',
50             default => sub { 0 },
51             trigger => sub {
52             $_[0]->no_count;
53             }
54             );
55             has count_fields => ( # fields to make the count
56             is => 'rw',
57             isa => \&check_arryref,
58             default => sub { [] },
59             );
60             has count_formatted => ( # count and extended fields, after formatting
61             is => 'rw',
62             );
63             has no_count => ( # suppress count field (probably because it's the same as the parent)
64             is => 'rw',
65             );
66             has children => ( # a hash of child Logwtch::RecordTrees
67             is => 'rw',
68             isa => \&check_hashref,
69             default => sub { {} },
70             );
71             has limit => ( # limit number of children printed
72             is => 'rw',
73             default => sub { 0 }, # default to no limit
74             );
75             has indent => ( # how much to indent this level of the tree
76             is => 'rw',
77             );
78             has no_indent => ( # flag to suppress indentation of children
79             is => 'rw',
80             );
81             has curr_indent => ( # total indentation of this level
82             is => 'rw',
83             default => sub { '' },
84             );
85             has post_callback => ( # when array is ready for printing, call this for final adjustments
86             is => 'rw',
87             isa => \&check_coderef,
88             default => sub { sub {} },
89             );
90             has lines => ( # when array is ready for printing, store a ref to it here
91             is => 'rw',
92             isa => \&check_arryref,
93             );
94             has columnize => ( # flag to indicate we should columnize children
95             is => 'rw',
96             );
97             has neat_names => ( # for the neat freaks
98             is => 'rw',
99             );
100             has neat_format => ( # formatter for neatness, set by sprint
101             is => 'rw',
102             default => sub { "%s" }, # not neat
103             );
104             has extra => ( # a little something extra...
105             is => 'rw',
106             );
107              
108             sub child_by_name { # find child by name(s), follow down the tree
109 394     394 1 380 my ($self, @names) = @_;
110              
111 394         264 my $child = $self;
112 394         385 for my $name (@names) {
113 394 100       5134 return if (not exists $child->children->{$name});
114 303         4800 $child = $child->children->{$name};
115             }
116 303         1385 return $child;
117             }
118              
119             sub create_child { # create child, add to children
120 91     91 1 85 my ($self, $name, $type, $opts) = @_;
121              
122 91   100     216 $type ||= __PACKAGE__; # default to this package
123 91   100     191 $opts ||= {};
124 91         119 $opts->{name} = $name;
125 91 50       194 $type->require or croak($@);
126 91         1493 return $self->children->{$name} = $type->new( %{$opts} );
  91         1446  
127             }
128              
129             # adopt items, handle name conflicts
130             sub adopt {
131 0     0 1 0 my ($self, $item) = @_;
132              
133 0         0 my $item_name = $item->name;
134 0         0 my $my_child = $self->child_by_name($item_name);
135 0 0       0 if ($my_child) {
136             # name conflict. my_child must adopt $item's children
137 0         0 my @item_children = values %{$item->children};
  0         0  
138 0 0       0 if (@item_children) {
139 0         0 for my $child (@item_children) {
140 0         0 $my_child->adopt($child);
141             }
142             }
143             else {
144             # no children, so transfer count directly from item to my_child
145 0         0 $my_child->count($my_child->count + $item->count);
146             }
147             }
148             else {
149             # no name conflict, just copy over
150 0         0 $self->children->{$item_name} = $item;
151             }
152 0         0 $self->count($self->count + $item->count);
153             }
154              
155             # log event, add new children if necessaary
156             sub _log_children {
157 197     197   245 my ($self, $name, @children) = @_;
158              
159 197         122 my ($type, $opts);
160 197 100       274 if (ref $name eq 'ARRAY') {
161 42         34 ($name, $type, $opts) = @{$name};
  42         60  
162             }
163              
164 197 50       238 $name = "" if (not defined $name); # supposed to be a list of names or array-refs
165              
166 197         203 my $child = $self->child_by_name($name);
167 197 100       1031 if (not defined $child) {
168 91         137 $child = $self->create_child($name, $type, $opts)
169             }
170              
171 197 100       1499 if (@children) {
172 111         174 return $child->_log_children(@children);
173             }
174              
175 86         174 return $child;
176             }
177              
178             sub _count { # add 1 to count down the path
179 283     283   334 my ($self, $name, @children) = @_;
180              
181 283         3556 $self->count($self->count + 1);
182              
183 283 100       707 $name = $name->[0] if (ref $name);
184 283 100       328 if (defined $name) {
185 197         248 return $self->child_by_name($name)->_count(@children);
186             }
187 86         265 return $self;
188             }
189              
190             sub log_no_count { # log new event without counting, add children if necessary
191 0     0 1 0 my ($self, @args) = @_;
192              
193 0         0 return $self->_log_children(@args);
194             }
195              
196             sub log { # log new event adding to count, add children if necessary
197 86     86 1 570 my ($self, @args) = @_;
198              
199 86         108 $self->_log_children(@args);
200 86         121 return $self->_count(@args);
201             }
202              
203             # return sorted list of child names
204             sub sort_children { # sort children
205 2     2 1 2 my ($self) = @_;
206              
207             # make hash, value is name, key is sort_key or name
208 8 100       41 my %keys = map { (defined($_->sort_key) ? $_->sort_key : $_->name) => $_ }
  2         25  
209 2         1 values %{$self->children};
210              
211             # sort by hash keys, create array of values to get back to names
212 0         0 my @children = $self->case_sensitive
213 8         86 ? map { $keys{$_} } natsort keys %keys
214 2 50   8   19 : map { $keys{$_} } natkeysort { lc $_ } keys %keys;
  8         323  
215              
216             return wantarray
217             ? @children
218 2 50       11 : \@children;
219             }
220              
221             # make neat column of child names
222             sub _neaten_children {
223 1     1   2 my ($self) = @_;
224              
225 1         2 my $max = max(1, map { length $_->sprint_name->($_) } values %{$self->children});
  1         18  
  1         16  
226 0 0       0 my $format = $self->neat_names < 0
227             ? "%-${max}s"
228             : "%${max}s";
229 0         0 map { $_->neat_format($format) } values %{$self->children};
  0         0  
  0         0  
230             }
231              
232             # make neat columns of all the count fields
233             sub _format_child_counts {
234 4     4   16 my ($self, $children, $depth) = @_;
235              
236             # measure each field, save max length for each column
237 4         3 my @maxes;
238 4         3 for my $child (values %{$children}) {
  4         12  
239 15         11 unshift @{$child->count_fields}, $child->count;
  15         180  
240 15         805 my $ii = 0;
241 15         10 for my $field (@{$child->count_fields}) {
  15         206  
242 15   100     94 $maxes[$ii] = max($maxes[$ii] || 0, length $field);
243 15         23 $ii++;
244             }
245             }
246              
247             # string to indent children: total count field width or at least 3
248 4         11 my $min = sum(1, @maxes);
249 4 100       9 $min = 3 if ($min < 3);
250 4         5 my $child_indent = " " x $min;
251              
252             # pad all fields to the max for the column
253 4         3 for my $child (values %{$children}) {
  4         6  
254 15         210 my $ccf = $child->count_fields;
255 15         40 my @padded;
256 15         18 for my $ii (0 .. $#maxes) {
257 15   50     44 $padded[$ii] = sprintf "%*s", $maxes[$ii], $ccf->[$ii] || '';
258             }
259 15         26 $child->count_formatted(join '', @padded);
260 15 100       37 $child->indent($child_indent) if (not defined $child->indent);
261 15         8 shift @{$child->count_fields}; # remove the count field we inserted above
  15         186  
262             }
263             }
264              
265             # compare our count fields to $other's (for suppression when identical)
266             sub _count_fields_differ {
267 1     1   2 my ($self, $other) = @_;
268              
269 1 50       15 return 1 if ($self->count != $other->count);
270 1         8 for my $ii (0 .. max($#{$self->count_fields}, $#{$other->count_fields})) {
  1         14  
  1         16  
271 0 0 0     0 return 1 if (
      0        
272             not defined $self->count_fields->[$ii] or
273             not defined $other->count_fields->[$ii] or
274             $self->count_fields->[$ii] ne $other->count_fields->[$ii]);
275             }
276 1         9 return 0; # match
277             }
278              
279             sub sprint {
280 3     3 1 5 my ($self, $callback, $path, $parent_indent, $depth) = @_;
281              
282 3   100     7 $path ||= [];
283 3   50     9 $parent_indent ||= '';
284 3   100     6 $depth ||= 1;
285              
286 3 100       5 if ($depth == 1) {
287             # top level needs to format its own count field
288 1         5 $self->_format_child_counts({ top => $self }, 0);
289             }
290              
291 3 100       14 my $count = $self->no_count
292             ? ''
293             : $self->count_formatted . ' ';
294              
295 3         37 $self->lines(my $lines = []);
296              
297 3 50       61 if (length($self->name)) {
298 3         3 push @{$lines}, join( '',
  3         46  
299             $count,
300             sprintf $self->neat_format, $self->sprint_name->($self),
301             );
302             }
303             else {
304 0         0 push @{$lines}, ''; # name is blank, so don't add anything here
  0         0  
305             }
306              
307             # format count fields and calculate indent for all children,
308 3         40 $self->_format_child_counts($self->children, $depth);
309              
310 3         18 my $children = $self->sort_children;
311 3         2 my $child_count = @{$children};
  3         4  
312              
313 3 100       8 if ($child_count == 1) { # join single child to this line
314 1         2 my $child = $children->[0];
315             # save the child's flags we're going to alter
316 1         2 my %flags = map { $_ => $child->$_ } qw( no_count no_indent );
  2         10  
317             # suppress count field if child's is same as ours
318 1 50       5 $child->no_count(1) if (not $self->_count_fields_differ($child));
319 1         2 $child->no_indent(1);
320 1         5 $child->curr_indent($parent_indent); # no extra indent since we concat this line
321              
322 1         1 push @{$path}, $child->name;
  1         3  
323 1         6 $child->sprint($callback, $path, $self->curr_indent, $depth + 1);
324 0 0       0 if (length($lines->[0]) + length($child->lines->[0]) <= $self->width) {
325 0         0 $lines->[0] .= ' ' . shift @{$child->lines};
  0         0  
326             }
327 0         0 push @{$lines}, @{$child->lines};
  0         0  
  0         0  
328 0         0 pop @{$path};
  0         0  
329              
330             # restore child's flags
331 0         0 map { $child->$_($flags{$_}) } keys %flags;
  0         0  
332             }
333              
334 2 50       4 if ($child_count > 1) {
335 2   33     11 my $last = $self->limit || $child_count; # if limit is zero, print all
336 2 50       6 $last = $child_count if ($child_count - $last < 3); # if within 3, just print all
337 2         2 $last = $child_count - $last; # convert from limit to the last index
338              
339             # handle neat_children flag
340 2 100       13 $self->_neaten_children if ($self->neat_names);
341              
342 1         1 for my $child (@{$children}) {
  1         3  
343 1         5 $child->curr_indent($parent_indent . $self->indent);
344 1 50       2 if ($child_count <= $last) {
345 0         0 push @{$lines}, "... and $child_count more";
  0         0  
346 0         0 last;
347             }
348 1         1 push @{$path}, $child->name;
  1         3  
349 1         9 $child->sprint($callback, $path, $self->curr_indent, $depth + 1);
350 0           push @{$lines}, @{$child->lines};
  0            
  0            
351 0           pop @{$path};
  0            
352 0           $child_count--;
353             }
354             }
355              
356 0 0         $self->sprint_columns if ($self->columnize);
357              
358             # indent children
359 0 0         if (not $self->no_indent) {
360 0           for my $ii (1 .. $#{$lines}) {
  0            
361 0           $lines->[$ii] = $self->indent . $lines->[$ii];
362             }
363             }
364              
365 0           $self->post_callback->($self, $path);
366              
367 0 0         $callback->($self, $path) if ($callback);
368              
369 0           return join "\n", @{$lines};
  0            
370             }
371              
372             sub width {
373 0     0 1   my ($self, $new) = @_;
374              
375 0 0         if (@_ > 1) {
376 0           $self->{_width} = $new;
377             }
378 0 0         return $self->{_width} if (exists $self->{_width});
379 0           return 80; # default page width
380             }
381              
382             sub col_width {
383 0     0 1   my ($self, $new) = @_;
384              
385 0 0         if (@_ > 1) {
386 0           $self->{_col_width} = $new;
387             }
388              
389 0 0         if (not exists $self->{_col_width}) {
390             # find longest line length, excluding the first line
391 0           my $lines = $self->lines;
392 0           $self->{_col_width} = max( 1, map { length $lines->[$_] } (1 .. $#{$lines}));
  0            
  0            
393             }
394 0           return $self->{_col_width};
395             }
396              
397             sub col_count {
398 0     0 1   my ($self, $new) = @_;
399              
400 0 0         if (@_ > 1) {
401 0           $self->{_col_count} = $new;
402             }
403              
404 0 0         if (not exists $self->{_col_count}) {
405             # calculate from curr_indent, width, and col_width
406 0           my $width = $self->width - length($self->curr_indent);
407 0           $width = max(0, $width); # at least 0
408 0   0       $self->{_col_count} = int ($width / $self->col_width) || 1; # at least 1
409             }
410 0           return $self->{_col_count};
411             }
412              
413             # re-arrange children into columns
414             sub sprint_columns {
415 0     0 1   my ($self, $width, $col_count, $col_width) = @_;
416              
417             # allow caller to override width, col_count, and col_width
418 0 0         $self->width($width) if (defined $width);
419 0 0         $self->col_count($col_count) if (defined $col_count);
420 0 0         $self->col_width($col_width) if (defined $col_width);
421              
422 0           $col_count = $self->col_count;
423 0           $col_width = $self->col_width;
424              
425 0           my $lines = $self->lines;
426 0           my @new_lines = shift @{$lines}; # first line is unchanged
  0            
427              
428 0           my $lines_per_col = int ((@{$lines} + $col_count - 1) / $col_count);
  0            
429              
430 0           for my $ii (0 .. $lines_per_col - 1) {
431 0           my @line;
432 0 0         if ($col_count <= 1) {
433             # single column, just prepend indent
434 0           push @new_lines, join '',
435             $self->indent,
436             $lines->[$ii];
437             }
438             else {
439             # join segments into a line of columns
440 0           for my $jj (0 .. $col_count - 1) {
441 0           my $l = $lines->[$ii + $jj * $lines_per_col];
442 0 0         push @line, sprintf "%-*s", $col_width, $l if (defined $l);
443             }
444 0           push @new_lines, join '',
445             $self->indent,
446             join(' ', @line);
447             }
448             }
449 0           $self->lines(\@new_lines);
450             }
451              
452             1;
453              
454             __END__