File Coverage

blib/lib/App/Run.pm
Criterion Covered Total %
statement 145 164 88.4
branch 35 48 72.9
condition 19 51 37.2
subroutine 29 34 85.2
pod 11 11 100.0
total 239 308 77.6


line stmt bran cond sub pod time code
1             package App::Run;
2             {
3             $App::Run::VERSION = '0.03';
4             }
5             #ABSTRACT: Create simple (command line) applications
6              
7 5     5   107284 use strict;
  5         12  
  5         190  
8 5     5   27 use warnings;
  5         11  
  5         145  
9 5     5   80 use v5.10;
  5         17  
  5         248  
10              
11 5     5   26 use Carp;
  5         7  
  5         478  
12 5     5   4754 use Try::Tiny;
  5         8523  
  5         317  
13 5     5   4221 use Clone qw(clone);
  5         18281  
  5         361  
14 5     5   43 use Scalar::Util qw(reftype blessed);
  5         8  
  5         509  
15 5     5   4685 use Log::Contextual::WarnLogger;
  5         5835  
  5         265  
16 5         73 use Log::Contextual qw(:log :Dlog with_logger set_logger), -default_logger =>
17 5     5   4541 Log::Contextual::WarnLogger->new({ env_prefix => 'APP_RUN' });
  5         612775  
18 5     5   340681 use Log::Log4perl qw();
  5         15  
  5         112  
19 5     5   27 use File::Basename qw();
  5         8  
  5         99  
20 5     5   5192 use Config::Any;
  5         56781  
  5         4681  
21              
22             our $CALLPKG;
23              
24             sub import {
25 5     5   49 my $pkg = shift;
26 5         16 $CALLPKG = caller(0);
27              
28 5     5   46 no strict 'refs';
  5         13  
  5         9926  
29 5 100 66     257 if (@_ and $_[0] eq 'script') {
30 1         1 my $app;
31             my $run = sub {
32 1     1   2 my $opts = shift;
33 1         2 *{"${CALLPKG}::OPTS"} = \$opts;
  1         6  
34 1         14 @ARGV = @_;
35             # set_logger $app->logger;
36 1         5 };
37             # foreach my $name (qw(log_error)) {
38             # *{"${CALLPKG}::$name"} = \*{ $name };
39             # }
40 1         4 $app = App::Run->new($run);
41 1         3 $app->run_with_args(@ARGV);
42             }
43             # TODO: require_version
44             }
45              
46              
47             sub new {
48 6     6 1 7557 my $self = bless { }, shift;
49              
50 6         25 $self->app( shift );
51 6         16 $self->{options} = { @_ };
52              
53 6         89 $self;
54             }
55              
56              
57             sub parse_options {
58 2     2 1 5 my $self = shift;
59              
60 2         11 local @ARGV = @_;
61              
62 2         2946 require Getopt::Long;
63 2         25693 my $parser = Getopt::Long::Parser->new(
64             config => [ "no_ignore_case", "pass_through" ],
65             );
66              
67 2         190 my $options = $self->{options};
68              
69 2         37 my ($help,$version);
70             $parser->getoptions(
71             "h|?|help" => \$help,
72             "v|version" => \$version,
73 0     0   0 "q|quiet" => sub { $options->{loglevel} = 'ERROR' },
74 0     0   0 "c|config=s" => sub { $options->{config} = $_[1] },
75 2         27 );
76              
77 2 50       893 if ($help) {
78 0         0 require Pod::Usage;
79 0         0 Pod::Usage::pod2usage(0);
80             }
81              
82 2 50       9 if ($version) {
83 0   0     0 say $self->name." ".($self->version // '(unknown version)');
84 0         0 exit;
85             }
86              
87 2         4 my @args;
88 2         10 while (defined(my $arg = shift @ARGV)) {
89 7 100       28 if ($arg =~ /^([^=]+)=(.+)/) {
90 3         13 my @path = split /\./, $1;
91 3         6 my $hash = $options;
92 3         11 while( @path > 1 ) {
93 0         0 my $e = shift @path;
94 0   0     0 $hash->{$e} //= { };
95 0         0 $hash = $hash->{$e};
96             }
97 3 50 50     41 $hash->{ $path[0] } = $2
98             if (reftype($hash)||'') eq 'HASH';
99             } else {
100 4         17 push @args, $arg;
101             }
102             }
103              
104 2 100       10 $options->{config} = ''
105             unless exists $options->{config};
106              
107 2         33 return @args;
108             }
109              
110              
111             sub load_config {
112 2     2 1 21 my ($self, $from) = @_;
113              
114             # TODO: log this by using a default logger
115              
116 2         3 my ($config,$configfile);
117             try {
118 2 100   2   117 if ($from) {
119             # Config::Any interface sucks
120 1         16 $config = Config::Any->load_files( {
121             files => [$from], use_ext => 1,
122             flatten_to_hash => 1,
123             } );
124             } else {
125 1         6 $config = Config::Any->load_stems( {
126             stems => [$self->name],
127             use_ext => 1,
128             flatten_to_hash => 1,
129             } );
130             }
131 2         31037 ($configfile,$config) = %$config;
132             } catch {
133 0   0 0   0 $_ //= ''; # where's our error message?!
134 0   0     0 croak sprintf("failed to load config file %s: $_",
135             ($from || $self->name.".*"));
136 2         38 };
137              
138 2 100       69 if ($config) {
139 1         6 while (my ($key,$value) = each %$config) {
140 2   33     21 $self->{options}->{$key} //= $value;
141             }
142             }
143             }
144              
145              
146             sub init {
147 1     1 1 2 my $self = shift;
148              
149             # TODO: also set options with this method?
150              
151 1         6 $self->enable_logger;
152              
153 1         596 my $app = $self->app;
154 1 50 33     12 if (blessed $app and $app->can('init')) {
155 0     0   0 with_logger $self->logger, sub { $app->init( $self->{options} ) };
  0         0  
156             }
157             }
158              
159              
160             sub run {
161 5     5 1 2756 my $self = shift;
162              
163 5         92 my $options = clone($self->{options});
164 5         15 my $config = delete $options->{config};
165              
166             # called only the first time
167 5 100       20 if ( defined $config ) {
168 1         5 $self->load_config( $config );
169 1         6 $self->init;
170             }
171              
172             # override options
173 5 100 100     55 if (@_ and (reftype($_[0])//'') eq 'HASH') {
      100        
174             # TODO: use Data::Iterator to merge options (?)
175 2         3 my $curopt = shift;
176 2         8 while(my ($k,$v) = each %$curopt) {
177 2         7 $options->{$k} = $v;
178             }
179             }
180              
181 5         15 my @args = @_;
182 5     0   38 log_trace { "run with args: ",join(',',@args) };
  0         0  
183              
184 5 100       389 $self->enable_logger unless $self->logger;
185              
186 5         1171 my $app = $self->app;
187              
188             with_logger $self->logger, sub {
189             # Dlog_trace { "run $_" } $cmd, @args;
190             try {
191 5 50       199 return( (reftype $app eq 'CODE')
192             ? $app->( $options, @args )
193             : $app->run( $options, @args ) );
194             } catch {
195 0         0 log_error { $_ };
  0         0  
196 0         0 return undef;
197             }
198 5     5   15 };
  5         129  
199             }
200              
201              
202             sub run_with_args {
203 2     2 1 31 my $self = shift;
204 2         8 $self->run( $self->parse_options( @_ ) );
205             }
206              
207              
208             sub name {
209 1     1 1 3 my $self = shift;
210 1         4 my $app = $self->app;
211              
212 1 50 0     6 $self->{name} //= $app->name
      33        
213             if blessed $app and $app->can('name');
214              
215 1 50       45 ($self->{name}) = File::Basename::fileparse($0)
216             unless defined $self->{name};
217              
218 1         14 return $self->{name};
219             }
220              
221              
222             sub version {
223 3     3 1 29 my $self = shift;
224              
225 3         6 my $pkg = blessed $self->app;
226 3 100       10 if (!$pkg) {
    50          
227 1         2 $pkg = $CALLPKG;
228             } elsif( $self->app->can('VERSION') ) {
229 2         6 return $self->app->VERSION;
230             }
231              
232 5     5   41 no strict 'refs';
  5         9  
  5         2434  
233 1         2 return ${"${pkg}::VERSION"};
  1         7  
234             }
235              
236              
237             sub app {
238 20     20 1 32 my $self = shift;
239              
240 20 100       64 if (@_) {
241 6         10 my $app = shift;
242 6 50 50     77 croak 'app must be code reference or object with ->run'
      33        
      66        
243             unless (reftype($app) // '') eq 'CODE'
244             or (blessed $app and $app->can('run'));
245 6         39 $self->{app} = $app;
246             }
247              
248 20         70 return $self->{app};
249             }
250              
251              
252             sub logger {
253 16     16 1 28 my $self = shift;
254 16 100       122 return $self->{log4perl} unless @_;
255              
256 3 50 33     35 if (blessed($_[0]) and $_[0]->isa('Log::Log4perl::Logger')) {
257 0         0 return ($self->{log4perl} = $_[0]);
258             }
259              
260 3 50 0     16 croak "logger configuration must be an array reference"
      33        
261             unless !$_[0] or (reftype($_[0]) || '') eq 'ARRAY';
262              
263 3 50       31 my @config = $_[0] ? @{$_[0]} : ({
  0         0  
264             class => 'Log::Log4perl::Appender::Screen',
265             threshold => 'WARN'
266             });
267              
268 3         38 my $log = Log::Log4perl->get_logger( __PACKAGE__ );
269 3         1098 foreach my $c (@config) {
270 3         47 my $app = Log::Log4perl::Appender->new( $c->{class}, %$c );
271 3   50     4205 my $layout = Log::Log4perl::Layout::PatternLayout->new(
272             $c->{layout} || "%d{yyyy-mm-ddTHH::mm} %p{1} %c: %m{chomp}%n" );
273 3         1859 $app->layout( $layout);
274 3 50       55 $app->threshold( $c->{threshold} ) if exists $c->{threshold};
275 3         72 $log->add_appender($app);
276             }
277              
278 3         1926 $log->trace( "new logger initialized" );
279              
280 3         39 return ($self->{log4perl} = $log);
281             }
282              
283              
284             sub enable_logger {
285 3     3 1 9 my $self = shift;
286 3         8 my $options = $self->{options};
287              
288 3         16 $self->logger( $options->{logger} );
289 3   50     16 $self->logger->level( $options->{loglevel} || 'WARN' );
290             }
291              
292             1;
293              
294             __END__