File Coverage

blib/lib/App/JobLog/Log/Line.pm
Criterion Covered Total %
statement 157 165 95.1
branch 52 74 70.2
condition 10 25 40.0
subroutine 28 28 100.0
pod 21 22 95.4
total 268 314 85.3


line stmt bran cond sub pod time code
1             package App::JobLog::Log::Line;
2             $App::JobLog::Log::Line::VERSION = '1.039';
3             # ABSTRACT: encapsulates one line of log text
4              
5              
6 5     5   4986 use Modern::Perl;
  5         13021  
  5         31  
7 5     5   1627 use Class::Autouse qw{DateTime};
  5         6664  
  5         40  
8 5     5   2092 use autouse 'App::JobLog::Time' => qw(now tz);
  5         731  
  5         40  
9              
10             # represents a single non-comment line in the log
11             # not using Moose to keep CLI snappy
12              
13             # to_string method for convenience
14 5     5   2054 use overload '""' => \&to_string;
  5         1102  
  5         43  
15 5     5   391 use overload 'bool' => sub { 1 };
  5     4358   10  
  5         32  
  4358         22152  
16              
17             # some global variables for use in BNF regex
18             our ( $date, @tags, @description, $is_beginning, $is_note );
19              
20             # log line parser
21             our $re = qr{
22             ^ (?&ts) (?&non_ts) $
23             (?(DEFINE)
24 24472         179216 (? (\d{4}\s++\d++\s++\d++\s++\d++\s++\d++\s++\d++) (?{$date = $^N}) )
25             (? (?¬e) | (?&duration_mark) )
26             (? : (?: (?&done) | (?&event) ) )
27             (? DONE )
28 203         1367 (? (?&event) (?{$is_note = 1}) )
29 20363         145535 (? (?&tags) : (?&descriptions) (?{$is_beginning = 1}) )
30             (? (?:(?&tag)(\s++(?&tag))*+)?)
31 19704         452947 (? ((?:[^\s:\\]|(?&escaped))++) (?{push @tags, $^N}))
32             (? \\.)
33             (? (?: (?&description) (?: ; \s*+ (?&description) )*+ )? )
34 18545         87238 (? ((?:[^;\\]|(?&escaped))++) (?{push @description, $^N}))
35             )
36             }xi;
37              
38              
39             sub new {
40 568     568 1 139087 my ( $class, @args ) = @_;
41 568   33     2428 $class = ref $class || $class;
42 568         1651 my %opts = @args;
43              
44             # validate %opts
45 568         1245 my $self = bless {}, $class;
46 568 100       2165 if ( exists $opts{comment} ) {
    100          
    100          
    50          
47 73         184 $self->{comment} = $opts{comment};
48 73         149 delete $opts{comment};
49 73 50       204 die 'inconsistent arguments: ' . join( ', ', @args ) if keys %opts;
50             }
51             elsif ( exists $opts{done} ) {
52 1         3 my $time = $opts{time};
53 1 50 33     4 die "invalid value for time: $time"
54             if $time && ref $time ne 'DateTime';
55 1   33     47 $self->{time} = $time || now;
56 1         39 $self->{done} = 1;
57 1         4 delete $opts{done};
58 1         3 delete $opts{time};
59 1 50       4 die 'inconsistent arguments: ' . join( ', ', @args ) if keys %opts;
60             }
61             elsif ( exists $opts{time} ) {
62 493         823 my $time = $opts{time};
63 493 50 33     1389 die "invalid value for time: $time"
64             if $time && ref $time ne 'DateTime';
65 493         21073 $self->{time} = $time;
66 493         801 my $tags = $opts{tags};
67 493 50 66     1344 die 'invalid value for tags: ' . $tags
68             if defined $tags && ref $tags ne 'ARRAY';
69 493 100       1125 unless ($tags) {
70 486         782 $tags = [];
71 486         969 $self->{tags_unspecified} = 1;
72             }
73 493         859 $self->{tags} = $tags;
74 493         902 my $description = $opts{description};
75 493 100       1386 if ( my $type = ref $description ) {
    100          
76 1 50       5 die 'invalid type for description: ' . $type
77             unless $type eq 'ARRAY';
78 1         2 $self->{description} = $description;
79             }
80             elsif ( defined $description ) {
81              
82             # normalize whitespace; this is useful for testing
83 401         1629 $description =~ s/^\s++|\s++$//g;
84 401         730 $description =~ s/\s++/ /g;
85            
86 401         944 $description = [$description];
87             }
88             else {
89 91         146 $description = [];
90             }
91 493         990 $self->{description} = $description;
92 493         1276 delete @opts{qw(time tags description)};
93 493 50       1518 die 'inconsistent arguments: ' . join( ', ', @args ) if keys %opts;
94             }
95             elsif ( exists $opts{text} ) {
96 0 0       0 die 'text lines in log must be blank' if $opts{text} =~ /\S/;
97 0         0 $self->{text} = $opts{text} . '';
98 0         0 delete $opts{text};
99 0 0       0 die 'inconsistent arguments: ' . join( ', ', @args ) if keys %opts;
100             }
101 568         2291 return $self;
102             }
103              
104              
105             sub parse {
106 28745     28745 1 1189006 my ( $class, $text ) = @_;
107 28745         107383 my $obj = bless { text => $text }, $class;
108 28745 100       137839 if ( $text =~ /^\s*(?:#\s*+(.*?)\s*)?$/ ) {
109 4271 100       12726 if ( defined $1 ) {
110 4124         11135 $obj->{comment} = $1;
111 4124         7888 delete $obj->{text};
112             }
113 4271         11743 return $obj;
114             }
115 24474         50821 local ( $date, @tags, @description, $is_beginning, $is_note );
116 24474 100       179690 if ( $text =~ $re ) {
117              
118             # must use to_string to obtain text
119 24470         51659 delete $obj->{text};
120 24470         154016 my @time = split /\s++/, $date;
121 24470         97092 $date = DateTime->new(
122             year => $time[0],
123             month => $time[1],
124             day => $time[2],
125             hour => $time[3],
126             minute => $time[4],
127             second => $time[5],
128             time_zone => tz,
129             );
130 24470         10056785 $obj->{time} = $date;
131 24470 100       46891 if ($is_beginning) {
132 20363         32477 my %tags = map { $_ => 1 } @tags;
  19702         73724  
133             $obj->{tags} =
134 20363         56779 [ map { ( my $v = $_ ) =~ s/\\(.)/$1/g; $v } sort keys %tags ];
  19702         38534  
  19702         62934  
135             $obj->{description} = [
136             map {
137 20363         39710 ( my $v = $_ ) =~ s/\\(.)/$1/g;
  18545         85598  
138 18545         199954 $v =~ s/^\s++|\s++$//g;
139 18545         130535 $v =~ s/\s++/ /g;
140 18545         60689 $v
141             } @description
142             ];
143 20363 100       64606 $obj->{note} = 1 if $is_note;
144             }
145             else {
146 4107         8112 $obj->{done} = 1;
147             }
148 24470         118679 return $obj;
149             }
150             else {
151 4         61 $obj->{malformed} = 1;
152             }
153 4         21 return $obj;
154             }
155              
156              
157             sub clone {
158 1085     1085 1 1547 my ($self) = @_;
159 1085         2477 my $clone = bless {}, ref $self;
160 1085 50       2409 if ( $self->is_malformed ) {
    50          
161 0         0 $clone->{malformed} = 1;
162 0         0 $clone->text = $self->text;
163             }
164             elsif ( $self->is_event ) {
165 1085         39066 $clone->time = $self->time->clone;
166 1085 50       3878 if ( $self->is_beginning ) {
167 1085 50       2595 $clone->{note} = 1 if $self->is_note;
168 1085         1476 $clone->tags = [ @{ $self->tags } ];
  1085         2390  
169 1085         1820 $clone->description = [ @{ $self->description } ];
  1085         2460  
170             }
171             }
172             else {
173 0 0       0 $clone->comment = $self->comment if $self->is_comment;
174 0 0       0 $clone->text = $self->text if exists $self->{text};
175             }
176 1085         4835 return $clone;
177             }
178              
179              
180             sub to_string {
181 496     496 1 135970 my ($self) = @_;
182 496 100       1493 return $self->{text} if exists $self->{text};
183 495 100       1226 if ( $self->is_event ) {
    100          
184 412         16366 my $text = $self->time_stamp;
185 412 100       9040 $text .= $self->is_note ? '' : ':';
186 412 100       858 if ( $self->is_beginning ) {
187 410   50     816 $self->tags ||= [];
188 410         558 my %tags = map { $_ => 1 } @{ $self->tags };
  16         52  
  410         775  
189 410         964 $text .= join ' ', map { s/([:\\\s])/\\$1/g; $_ } sort keys %tags;
  16         47  
  16         39  
190 410         707 $text .= ':';
191 410   50     894 $self->description ||= [];
192             $text .= join ';',
193 410         892 map { ( my $d = $_ ) =~ s/([;\\])/\\$1/g; $d }
  410         1305  
194 410         572 @{ $self->description };
  410         817  
195             }
196             else {
197 2         5 $text .= 'DONE';
198             }
199 412         3155 return $text;
200             }
201             elsif ( $self->is_comment ) {
202 82         217 return '# ' . $self->comment;
203             }
204             }
205              
206              
207             sub time_stamp {
208 412     412 1 640 my ( $self, $time ) = @_;
209 412   33     1374 $time ||= $self->time;
210 412         8793 return sprintf '%d %2s %2s %2s %2s %2s', $time->year, $time->month,
211             $time->day, $time->hour,
212             $time->minute, $time->second;
213             }
214              
215             # a bunch of attributes, here for convenience
216              
217              
218             sub text : lvalue {
219 2     2 1 10 $_[0]->{text};
220             }
221              
222              
223             sub tags : lvalue {
224 3630     3630 1 11076 $_[0]->{tags};
225             }
226              
227              
228             sub comment : lvalue {
229 83     83 1 934 $_[0]->{comment};
230             }
231              
232              
233             sub time : lvalue {
234 51322     51322 1 205817 $_[0]->{time};
235             }
236              
237              
238             sub description : lvalue {
239 3202     3202 1 10531 $_[0]->{description};
240             }
241              
242             # a bunch of tests
243              
244              
245 1096     1096 0 4610 sub is_malformed { exists $_[0]->{malformed} }
246              
247              
248 25550     25550 1 106688 sub is_beginning { exists $_[0]->{tags} }
249              
250              
251 221     221 1 1399 sub is_end { $_[0]->{done} }
252              
253              
254 1837     1837 1 7513 sub is_note { $_[0]->{note} }
255              
256              
257 2467     2467 1 10990 sub is_event { $_[0]->{time} }
258              
259              
260 16997 100   16997 1 67349 sub is_endpoint { $_[0]->{time} && !$_[0]->{note} }
261              
262              
263 85     85 1 383 sub is_comment { exists $_[0]->{comment} }
264              
265              
266 189     189 1 964 sub tags_unspecified { $_[0]->{tags_unspecified} }
267              
268              
269             sub is_blank {
270 1   33 1 1 13 !( $_[0]->is_malformed || $_[0]->is_comment || $_[0]->is_event );
271             }
272              
273             # some useful methods
274              
275              
276             sub comment_out {
277 3     3 1 5 my ($self) = @_;
278 3         8 my $text = $self->to_string;
279 3         18 delete $self->{$_} for keys %$self;
280 3         8 $self->{comment} = $text;
281 3         10 return $self;
282             }
283              
284              
285             sub all_tags {
286 436     436 1 2384 my ( $self, @tags ) = @_;
287 436 50       945 return unless $self->tags;
288 436         574 my %tags = map { $_ => 1 } @{ $self->{tags} };
  12         25  
  436         980  
289 436         747 for my $tag (@tags) {
290 7 100       25 return unless $tags{$tag};
291             }
292 435         2066 return 1;
293             }
294              
295              
296             sub exists_tag {
297 2     2 1 7 my ( $self, @tags ) = @_;
298 2 50       5 return unless $self->tags;
299 2         3 my %tags = map { $_ => 1 } @{ $self->{tags} };
  12         24  
  2         6  
300 2         6 for my $tag (@tags) {
301 2 100       11 return 1 if $tags{$tag};
302             }
303 1         5 return;
304             }
305              
306             1;
307              
308             __END__