File Coverage

blib/lib/Logwatch/RecordTree.pm
Criterion Covered Total %
statement 153 247 61.9
branch 39 90 43.3
condition 17 30 56.6
subroutine 26 32 81.2
pod 12 16 75.0
total 247 415 59.5


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