File Coverage

lib/App/Sandy/CLI/App.pm
Criterion Covered Total %
statement 12 112 10.7
branch 0 42 0.0
condition 0 9 0.0
subroutine 4 30 13.3
pod 8 8 100.0
total 24 201 11.9


line stmt bran cond sub pod time code
1             package App::Sandy::CLI::App;
2             # ABSTRACT: App::Sandy::CLI subclass for command line application interface.
3              
4 1     1   527 use App::Sandy::Base 'class';
  1         3  
  1         6  
5 1     1   9 use Path::Class 'file';
  1         2  
  1         54  
6 1     1   6 use Pod::Usage;
  1         2  
  1         111  
7 1     1   7 use Try::Tiny;
  1         2  
  1         2203  
8              
9             extends 'App::Sandy::CLI';
10              
11             with 'App::Sandy::Role::ParseArgv';
12              
13             our $VERSION = '0.22'; # VERSION
14              
15             has 'command_stack' => (
16             traits => ['Array'],
17             is => 'ro',
18             isa => 'ArrayRef[HashRef]',
19             default => sub { [] },
20             handles => {
21             add_command => 'push',
22             get_command => 'get',
23             map_command => 'map',
24             has_no_command => 'is_empty'
25             }
26             );
27              
28             has 'app_path' => (
29             is => 'ro',
30             isa => 'Str',
31             builder => '_build_app_path'
32             );
33              
34             sub _build_app_path {
35             # Determine dynamic the app path that inherit from this class
36             # If no one is inheriting, return this class path
37 0     0     my $class = (caller(1))[3];
38 0           $class =~ s/::new//;
39 0           my $command_pm = file(split /::/ => "$class.pm");
40 0           return $INC{$command_pm};
41             }
42              
43             override 'opt_spec' => sub {
44             super
45             };
46              
47             sub command_map_bultin {
48 0     0 1   help => \&help_command,
49             man => \&man_command
50             }
51              
52       0 1   sub command_map {
53             # It needs to be override
54             }
55              
56             sub error {
57 0     0 1   my ($self, $error_msg) = @_;
58 0           my $sender = $self->_whois;
59 0           chomp $error_msg;
60 0           die "$sender: $error_msg\n";
61             }
62              
63             sub _whois {
64 0     0     my $self = shift;
65 0           my $sender = $self->progname;
66 0     0     my @commands = $self->map_command(sub { $_->{name} });
  0            
67 0 0         $sender .= " @commands" unless $self->has_no_command;
68 0           return $sender;
69             }
70              
71             sub _try_msg {
72 0     0     my $self = shift;
73 0           return sprintf "Try '%s --help' for more information" => $self->_whois;
74             }
75              
76             sub _help_text {
77 0     0     my ($self, $path) = @_;
78 0   0       $path ||= $self->app_path;
79 0           pod2usage(-input => $path, -verbose => 99, -sections => ['SYNOPSIS'], -exitval => 0);
80             }
81              
82             sub _man_text {
83 0     0     my ($self, $path) = @_;
84 0   0       $path ||= $self->app_path;
85 0           pod2usage(-input => $path, -verbose => 2, -exitval => 0);
86             }
87              
88             sub help_command {
89 0     0 1   my ($self, $argv) = @_;
90 0           my %command_map = $self->command_map;
91 0           $self->_dispatcher(\%command_map, $argv);
92 0 0         $self->error("Too many arguments: '@$argv'\n" . $self->_try_msg) if @$argv;
93 0           my $path;
94 0 0         $path = $self->get_command(-1)->{path} unless $self->has_no_command;
95 0           return $self->_help_text($path);
96             }
97              
98             sub man_command {
99 0     0 1   my ($self, $argv) = @_;
100 0           my %command_map = $self->command_map;
101 0           $self->_dispatcher(\%command_map, $argv);
102 0 0         $self->error("Too many arguments: '@$argv'\n" . $self->_try_msg) if @$argv;
103 0           my $path;
104 0 0         $path = $self->get_command(-1)->{path} unless $self->has_no_command;
105 0           return $self->_man_text($path);
106             }
107              
108             sub run_no_command {
109 0     0 1   my ($self, $argv) = @_;
110 0           my ($opts, $args);
111              
112             try {
113 0     0     ($opts, $args) = $self->with_parser($argv, $self->opt_spec);
114             } catch {
115 0     0     $self->error("$_" . $self->_try_msg);
116 0           };
117              
118 0 0         $self->_help_text if $opts->{help};
119 0 0         $self->_man_text if $opts->{man};
120             }
121              
122             sub run_command {
123 0     0 1   my ($self, $argv) = @_;
124 0           my %command_map = $self->command_map;
125 0           $self->_dispatcher(\%command_map, $argv);
126              
127 0           my $command = $self->get_command(-1);
128 0           my $o = $command->{class}->new;
129              
130             # $args has at least $argv if no opt has been passed
131 0           my ($opts, $args) = (undef, $argv);
132              
133 0 0         if ($o->can('opt_spec')) {
134             try {
135 0     0     ($opts, $args) = $self->with_parser($argv, $o->opt_spec);
136             } catch {
137 0     0     $self->error("$_" . $self->_try_msg);
138 0           };
139             }
140              
141 0 0         $self->_help_text($command->{path}) if $opts->{help};
142 0 0         $self->_man_text($command->{path}) if $opts->{man};
143              
144             # Deep copy the arguments, just in case the user
145             # manages to mess with
146 0           my %opts_copy = %$opts;
147 0           my @args_copy = @$args;
148              
149 0 0         if ($o->can('validate_args')) {
150             try {
151 0     0     $o->validate_args(\@args_copy);
152             } catch {
153 0     0     $self->error("$_" . $self->_try_msg);
154 0           };
155             }
156              
157 0 0         if ($o->can('validate_opts')) {
158             try {
159 0     0     $o->validate_opts(\%opts_copy);
160             } catch {
161 0     0     $self->error("$_" . $self->_try_msg);
162 0           };
163             }
164              
165             try {
166 0     0     $o->execute($opts, $args);
167             } catch {
168 0     0     $self->error($_);
169 0           };
170             }
171              
172             sub _command_loading {
173 0     0     my ($self, $command_class) = @_;
174 0           my $command_pm = file(split /::/ => "$command_class.pm");
175              
176 0           eval { require $command_pm };
  0            
177 0 0         die $@ if $@;
178              
179 0 0         my $command_class_path = $INC{ $command_pm }
180             or die "$command_class not found in \%INC";
181              
182 0           return $command_class_path;
183             }
184              
185             sub _dispatcher {
186 0     0     my ($self, $command_map, $argv) = @_;
187              
188 0 0 0       if (@$argv && exists $command_map->{$argv->[0]}) {
189 0           my $command_name = shift @$argv;
190 0           my $command_class = $command_map->{$command_name};
191 0           my $command_class_path = $self->_command_loading($command_class);
192              
193 0           $self->add_command({
194             'name' => $command_name,
195             'class' => $command_class,
196             'path' => $command_class_path
197             });
198              
199 0 0         unless ($command_class->can('execute')) {
200 0           die "Not defined method 'execute' for $command_class";
201             }
202              
203 0 0         if ($command_class->can('subcommand_map')) {
204 0           my %command_map = $command_class->subcommand_map;
205 0           return $self->_dispatcher(\%command_map, $argv);
206             }
207             }
208             }
209              
210             sub run {
211 0     0 1   my $self = shift;
212 0           my @argv = @{ $self->argv };
  0            
213 0           my %command_map = $self->command_map;
214 0           my %command_map_bultin = $self->command_map_bultin;
215              
216 0 0         $self->_help_text unless scalar @argv;
217              
218 0 0         if ($command_map_bultin{$argv[0]}) {
    0          
    0          
219 0           my $command_name = shift @argv;
220 0           my $command_method = $command_map_bultin{$command_name};
221 0           $self->$command_method(\@argv);
222             } elsif ($command_map{$argv[0]}) {
223 0           $self->run_command(\@argv);
224             } elsif ($argv[0] =~ /^-/) {
225 0           $self->run_no_command(\@argv);
226             } else {
227 0           $self->error("Unknown command '$argv[0]'\n" . $self->_try_msg);
228             }
229             }
230              
231             __END__
232              
233             =pod
234              
235             =encoding UTF-8
236              
237             =head1 NAME
238              
239             App::Sandy::CLI::App - App::Sandy::CLI subclass for command line application interface.
240              
241             =head1 VERSION
242              
243             version 0.22
244              
245             =head1 SYNOPSIS
246              
247             extends 'App::Sandy::CLI::App';
248              
249             =head1 DESCRIPTION
250              
251             This is the base interface to application class.
252             Classes need to override command_map method to
253             provide command arguments
254              
255             =head1 METHODS
256              
257             =head2 command_stack
258              
259             This method returns a stach with commands and subcommands
260              
261             =head2 app_path
262              
263             This method returns the application class path
264              
265             =head2 command_map_bultin
266              
267             This method retuns a hash with bultin command
268              
269             =head2 command_map
270              
271             This method needs to be override by child to provide
272             command arguments. It is expected to return a hash
273              
274             =head2 error
275              
276             This method prints a formatted error message
277              
278             =head2 help_command
279              
280             This method calls help message to the command up
281             in the C<command_stack>. If no command was passed,
282             it calls help message to the app itself. Help messages
283             are in pod format inside the app, command classes
284              
285             =head2 man_command
286              
287             This method calls man message to the command up
288             in the C<command_stack>. If no command was passed,
289             it calls man message to the app itself. Man messages
290             are in pod format inside the app, command classes
291              
292             =head2 run_no_command
293              
294             This method runs app options, those defined in
295             C<opt_spec> method
296              
297             =head2 run_command
298              
299             This method checkes C<command_stack> and executes the
300             command up mathods C<validate_args>, C<validate_opts>
301             and C<execute>
302              
303             =head2 run
304              
305             This method checks the arguments passed to the
306             application and call the appropriate methods
307             C<run_no_command>, C<run_command> or
308             C<help_command>/C<man_command>
309              
310             =head1 SEE ALSO
311              
312             =over 4
313              
314             =item *
315              
316             L<App::Sandy::CLI::Command>
317              
318             =back
319              
320             =head1 AUTHORS
321              
322             =over 4
323              
324             =item *
325              
326             Thiago L. A. Miller <tmiller@mochsl.org.br>
327              
328             =item *
329              
330             J. Leonel Buzzo <lbuzzo@mochsl.org.br>
331              
332             =item *
333              
334             Felipe R. C. dos Santos <fsantos@mochsl.org.br>
335              
336             =item *
337              
338             Helena B. Conceição <hconceicao@mochsl.org.br>
339              
340             =item *
341              
342             Gabriela Guardia <gguardia@mochsl.org.br>
343              
344             =item *
345              
346             Fernanda Orpinelli <forpinelli@mochsl.org.br>
347              
348             =item *
349              
350             Pedro A. F. Galante <pgalante@mochsl.org.br>
351              
352             =back
353              
354             =head1 COPYRIGHT AND LICENSE
355              
356             This software is Copyright (c) 2018 by Teaching and Research Institute from Sírio-Libanês Hospital.
357              
358             This is free software, licensed under:
359              
360             The GNU General Public License, Version 3, June 2007
361              
362             =cut