File Coverage

lib/App/TimeTracker/Proto.pm
Criterion Covered Total %
statement 132 155 85.1
branch 28 40 70.0
condition 15 29 51.7
subroutine 27 30 90.0
pod 0 5 0.0
total 202 259 77.9


line stmt bran cond sub pod time code
1             package App::TimeTracker::Proto;
2              
3             # ABSTRACT: App::TimeTracker Proto Class
4             our $VERSION = '3.010'; # VERSION
5              
6 6     6   1163885 use strict;
  6         73  
  6         190  
7 6     6   34 use warnings;
  6         11  
  6         173  
8 6     6   112 use 5.010;
  6         23  
9              
10 6     6   2121 use App::TimeTracker::Utils qw(error_message);
  6         14  
  6         335  
11 6     6   2652 use Moose;
  6         2397893  
  6         44  
12 6     6   49912 use MooseX::Types::Path::Class;
  6         2218004  
  6         62  
13 6     6   8533 use File::HomeDir ();
  6         17203  
  6         146  
14 6     6   40 use Path::Class;
  6         15  
  6         457  
15 6     6   3494 use Hash::Merge qw(merge);
  6         49188  
  6         441  
16 6     6   4161 use JSON::XS;
  6         26903  
  6         391  
17 6     6   48 use Carp;
  6         17  
  6         351  
18 6     6   79 use Try::Tiny;
  6         37  
  6         307  
19 6     6   2523 use App::TimeTracker::Data::Task;
  6         25  
  6         263  
20 6     6   2361 use App::TimeTracker::Constants qw(MISSING_PROJECT_HELP_MSG);
  6         17  
  6         9646  
21              
22             has 'home' => (
23             is => 'ro',
24             isa => 'Path::Class::Dir',
25             lazy_build => 1,
26             );
27              
28             sub _build_home {
29 7     7   2375 my ( $self, $home ) = @_;
30              
31             $home ||=
32 7   33     98 Path::Class::Dir->new( $ENV{TRACKER_HOME} || (File::HomeDir->my_home, '.TimeTracker' ));
      66        
33 7 50       333 unless (-d $home) {
34 0         0 $home->mkpath;
35 0         0 $self->_write_config_file_locations( {} );
36 0         0 my $fh = $self->global_config_file->openw;
37 0         0 print $fh $self->json_decoder->encode( {} );
38 0         0 close $fh;
39             }
40 7         520 return $home;
41             }
42              
43             has 'global_config_file' => (
44             is => 'ro',
45             isa => 'Path::Class::File',
46             lazy_build => 1,
47             );
48              
49             sub _build_global_config_file {
50 17     17   37 my $self = shift;
51 17         485 return $self->home->file('tracker.json');
52             }
53              
54             has 'config_file_locations' => (
55             is => 'ro',
56             isa => 'HashRef',
57             lazy_build => 1,
58             );
59              
60             sub _build_config_file_locations {
61 17     17   39 my $self = shift;
62 17         502 my $file = $self->home->file('projects.json');
63 17 50 33     1599 if ( -e $file && -s $file ) {
64 17         1662 my $decoded_json;
65             try {
66 17     17   1372 $decoded_json = decode_json( $file->slurp );
67             }
68             catch {
69 0     0   0 error_message( "Could not json decode '%s'.\nError: '%s'", $file, $_ );
70 0         0 exit 1;
71 17         182 };
72 17         5176 return $decoded_json;
73             }
74             else {
75 0         0 return {};
76             }
77             }
78              
79             has 'project' => ( is => 'rw', isa => 'Str', predicate => 'has_project' );
80              
81             has 'json_decoder' => ( is => 'ro', isa => 'JSON::XS', lazy_build => 1 );
82              
83             sub _build_json_decoder {
84 17     17   40 my $self = shift;
85 17         735 return JSON::XS->new->utf8->pretty->canonical->relaxed;
86             }
87              
88             sub run {
89 1     1 0 10598 my $self = shift;
90              
91             try {
92 1     1   89 my $config = $self->load_config;
93 1         7 my $class = $self->setup_class($config);
94 1 50       41 $class->name->new_with_options( {
95             home => $self->home,
96             config => $config,
97             ( $self->has_project
98             ? ( _current_project => $self->project )
99             : ()
100             ),
101             } )->run;
102             }
103             catch {
104 0     0   0 my $e = $_;
105 0 0 0     0 if ( blessed $e && $e->can('message') ) {
106 0         0 warn $e->message, "\n";
107             }
108             else {
109 0         0 warn "$e\n";
110             }
111             }
112 1         13 }
113              
114             sub setup_class {
115 27     27 0 27867 my ( $self, $config, $command ) = @_;
116              
117             # unique plugins
118 27   100     203 $config->{plugins} ||= [];
119 27         64 my %plugins_unique = map { $_ => 1 } @{ $config->{plugins} };
  0         0  
  27         109  
120 27         131 $config->{plugins} = [ keys %plugins_unique ];
121              
122             my $class = Moose::Meta::Class->create_anon_class(
123             superclasses => ['App::TimeTracker'],
124             roles => [
125 27         360 map { 'App::TimeTracker::Command::' . $_ } 'Core',
126 27         116 @{ $config->{plugins} }
  27         82  
127             ],
128             );
129              
130 27         294756 my %commands;
131 27         188 foreach my $method ( $class->get_all_method_names ) {
132 2079 100       41722 next unless $method =~ /^cmd_/;
133 378         827 $method =~ s/^cmd_//;
134 378         841 $commands{$method} = 1;
135             }
136              
137 27         203 my $load_attribs_for_command;
138 27 100       123 foreach my $cmd ( $command ? $command : @ARGV) {
139 26 50       125 if ( defined $commands{$cmd} ) {
140 26         134 $load_attribs_for_command = '_load_attribs_' . $cmd;
141              
142 26 50 66     329 if ($cmd eq 'start' && !$self->has_project) {
143 0         0 error_message( MISSING_PROJECT_HELP_MSG );
144 0         0 exit;
145             }
146              
147 26         68 last;
148             }
149             }
150 27 100 100     184 if ( $load_attribs_for_command
151             && $class->has_method($load_attribs_for_command) )
152             {
153 24         797 $class->name->$load_attribs_for_command($class, $config);
154             }
155 27         130256 $class->make_immutable();
156 27         12145 return $class;
157             }
158              
159             sub load_config {
160 20     20 0 68873 my ($self, $dir, $project) = @_;
161 20   66     135 $dir ||= Path::Class::Dir->new->absolute;
162 20         1473 my $config = {};
163 20         50 my @used_config_files;
164 20         847 my $cfl = $self->config_file_locations;
165              
166 20         75 my $projects = $self->slurp_projects;
167 20         249 my $opt_parser = Getopt::Long::Parser->new(
168             config => [qw( no_auto_help pass_through )] );
169 20         2542 $opt_parser->getoptions( "project=s" => \$project );
170              
171 20 100       5202 if ( defined $project ) {
172 8         303 $self->project($project);
173 8 100       34 if ( my $project_config = $projects->{$project} ) {
174 7         46 $dir = Path::Class::Dir->new($project_config);
175             }
176             }
177 20 50       399 if ($dir) {
178 20         128 my $try = 0;
179 20         82 $dir = $dir->absolute;
180 20         718 WALKUP: while ( $try++ < 30 ) {
181 91         4829 my $config_file = $dir->file('.tracker.json');
182 91         6105 my $this_config;
183 91 100       292 if ( -e $config_file ) {
184 22         986 push( @used_config_files, $config_file->stringify );
185 22         697 $this_config = $self->slurp_config($config_file);
186 22         512 $config = merge( $config, $this_config );
187              
188 22         3208 my @path = $config_file->parent->dir_list;
189 22 100       355 my $project = exists $this_config->{project} ? $this_config->{project} : $path[-1];
190 22         76 $cfl->{$project} = $config_file->stringify;
191 22 100       1557 $self->project($project)
192             unless $self->has_project;
193             }
194 91 100       3050 last WALKUP if $dir->parent eq $dir;
195              
196 71 50       7495 if ( my $parent = $this_config->{'parent'} ) {
197 0 0       0 if ( $projects->{$parent} ) {
198 0         0 $dir = Path::Class::file( $projects->{$parent} )->parent;
199             }
200             else {
201 0         0 $dir = $dir->parent;
202 0         0 say
203             "Cannot find project >$parent< that's specified as a parent in $config_file";
204             }
205             }
206             else {
207 71         177 $dir = $dir->parent;
208             }
209             }
210             }
211 20         1979 $self->_write_config_file_locations($cfl);
212              
213 20 100       888 if ( -e $self->global_config_file ) {
214 15         1337 push( @used_config_files, $self->global_config_file->stringify );
215 15         952 $config = merge( $config,
216             $self->slurp_config( $self->global_config_file ) );
217             }
218 20         2832 $config->{_used_config_files} = \@used_config_files;
219              
220 20         249 return $config;
221             }
222              
223             sub _write_config_file_locations {
224 20     20   53 my ( $self, $cfl ) = @_;
225 20         695 my $fh = $self->home->file('projects.json')->openw;
226 20   33     6199 print $fh $self->json_decoder->encode( $cfl
227             || $self->config_file_locations );
228 20         1964 close $fh;
229             }
230              
231             sub slurp_config {
232 37     37 0 94 my ( $self, $file ) = @_;
233             try {
234 37     37   2798 my $content = $file->slurp();
235 37         9367 return $self->json_decoder->decode($content);
236             }
237             catch {
238 0     0   0 error_message( "Cannot parse config file $file:\n%s", $_ );
239 0         0 exit;
240 37         283 };
241             }
242              
243             sub slurp_projects {
244 24     24 0 3587 my $self = shift;
245 24         817 my $file = $self->home->file('projects.json');
246 24 50 33     1928 unless ( -e $file && -s $file ) {
247 0         0 error_message("Cannot find projects.json\n");
248 0         0 exit;
249             }
250 24         2183 my $projects = decode_json( $file->slurp );
251 24         5213 return $projects;
252             }
253              
254             1;
255              
256             __END__
257              
258             =pod
259              
260             =encoding UTF-8
261              
262             =head1 NAME
263              
264             App::TimeTracker::Proto - App::TimeTracker Proto Class
265              
266             =head1 VERSION
267              
268             version 3.010
269              
270             =head1 DESCRIPTION
271              
272             Ugly internal stuff, see L<YAPC::Europe 2011 talk...|https://domm.plix.at/talks/2011_riga_app_timetracker/>
273              
274             =head1 AUTHOR
275              
276             Thomas Klausner <domm@plix.at>
277              
278             =head1 COPYRIGHT AND LICENSE
279              
280             This software is copyright (c) 2011 - 2021 by Thomas Klausner.
281              
282             This is free software; you can redistribute it and/or modify it under
283             the same terms as the Perl 5 programming language system itself.
284              
285             =cut