File Coverage

blib/lib/App/Spec/Run.pm
Criterion Covered Total %
statement 222 244 90.9
branch 61 80 76.2
condition 29 44 65.9
subroutine 25 28 89.2
pod 17 17 100.0
total 354 413 85.7


line stmt bran cond sub pod time code
1             # ABSTRACT: App::Spec framework to run your app
2 4     4   1036 use strict;
  4         6  
  4         90  
3 4     4   18 use warnings;
  4         4  
  4         93  
4             package App::Spec::Run;
5 4     4   60 use 5.010;
  4         11  
6             our $VERSION = '0.012'; # VERSION
7              
8 4     4   1322 use App::Spec::Run::Validator;
  4         11  
  4         133  
9 4     4   1463 use App::Spec::Run::Response;
  4         12  
  4         112  
10 4     4   2359 use Getopt::Long qw/ :config pass_through bundling /;
  4         34111  
  4         18  
11 4     4   670 use Ref::Util qw/ is_arrayref /;
  4         7  
  4         148  
12 4     4   20 use Moo;
  4         6  
  4         23  
13              
14             has spec => ( is => 'ro' );
15             has options => ( is => 'rw' );
16             has parameters => ( is => 'rw', default => sub { +{} } );
17             has commands => ( is => 'rw' );
18             has argv => ( is => 'rw' );
19             has argv_orig => ( is => 'rw' );
20             #has runmode => ( is => 'rw', default => 'normal' );
21             has validation_errors => ( is => 'rw' );
22             has op => ( is => 'rw' );
23             has cmd => ( is => 'rw' );
24             has response => ( is => 'rw', default => sub { App::Spec::Run::Response->new } );
25             has subscribers => ( is => 'rw', default => sub { +{} } );
26              
27             my %EVENTS = (
28             print_output => 1,
29             global_options => 1,
30             );
31              
32             sub process {
33 30     30 1 29993 my ($self) = @_;
34              
35 30   50     211 my $plugins = $self->spec->plugins || [];
36 30         100 for my $plugin (@$plugins) {
37 84         384 $plugin->init_run($self);
38             }
39 30         73 my @callbacks;
40 30         85 my $subscriber_events = $self->subscribers;
41 30         71 for my $key (qw/ global_options print_output /) {
42 60         120 my $subscribers = $subscriber_events->{ $key };
43 60         109 for my $sub (@$subscribers) {
44 56         99 my $plugin = $sub->{plugin};
45 56         98 my $method = $sub->{method};
46             my $callback = sub {
47 4     4   15 $plugin->$method( run => $self, @_);
48 56         275 };
49 56         158 push @callbacks, $callback;
50             }
51 60         239 $self->response->add_callbacks($key => \@callbacks);
52             }
53              
54 30         114 my $argv = $self->argv;
55 30 100       74 unless ($argv) {
56 29         59 $argv = \@ARGV;
57 29         81 $self->argv($argv);
58 29         111 $self->argv_orig([ @$argv ]);
59             }
60              
61 30         77 my $completion_parameter = $ENV{PERL5_APPSPECRUN_COMPLETION_PARAMETER};
62              
63 30         70 my %option_specs;
64             my %param_specs;
65 30 50       117 unless ($self->op) {
66 30         151 $self->process_input(
67             option_specs => \%option_specs,
68             param_specs => \%param_specs,
69             );
70             }
71              
72 30 100       146 unless ($self->response->halted) {
73 27         827 my $opt = App::Spec::Run::Validator->new({
74             options => $self->options,
75             option_specs => \%option_specs,
76             parameters => $self->parameters,
77             param_specs => \%param_specs,
78             });
79 27         4166 my %errs;
80 27         170 my $ok = $opt->process( $self, \%errs );
81 27 100       125 unless ($ok) {
82 14         50 $self->validation_errors(\%errs);
83             # if we are in completion mode, some errors might be ok
84 14 100       47 if (not $completion_parameter) {
85 9         38 $self->error_output;
86             }
87             }
88             }
89              
90 30 100       475 unless ($self->response->halted) {
91              
92 18         46 my $op = $self->op;
93              
94 18 100       62 if ($completion_parameter) {
95 5         22 $self->completion_output(
96             param_specs => \%param_specs,
97             option_specs => \%option_specs,
98             completion_parameter => $completion_parameter,
99             );
100             }
101             else {
102 13         54 $self->run_op($op);
103             }
104             }
105              
106             }
107              
108             sub run {
109 0     0 1 0 my ($self) = @_;
110              
111 0         0 $self->process;
112              
113             # $self->event_processed;
114 0         0 $self->finish;
115              
116             }
117              
118             sub run_op {
119 17     17 1 52 my ($self, $op, $args) = @_;
120 17         123 $self->cmd->$op($self, $args);
121             }
122              
123             sub out {
124 22     22 1 500 my ($self, $text) = @_;
125 22 100 100     153 $text .= "\n" if (not ref $text and $text !~ m/\n\z/);
126 22         123 $self->response->add_output($text);
127             }
128              
129             sub err {
130 28     28 1 69 my ($self, $text) = @_;
131 28 100 66     189 $text .= "\n" if (not ref $text and $text !~ m/\n\z/);
132 28         120 $self->response->add_error($text);
133             }
134              
135             sub halt {
136 13     13 1 43 my ($self, $exit) = @_;
137 13         40 $self->response->halted(1);
138 13   50     83 $self->response->exit($exit || 0);
139             }
140              
141             sub finish {
142 0     0 1 0 my ($self) = @_;
143 0         0 my $res = $self->response;
144 0         0 $res->print_output;
145 0         0 $res->finished(1);
146 0 0       0 if (my $exit = $res->exit) {
147 0         0 exit $exit;
148             }
149             }
150              
151             sub completion_output {
152 5     5 1 18 my ($self, %args) = @_;
153 5         10 my $completion_parameter = $args{completion_parameter};
154 5         9 my $param_specs = $args{param_specs};
155 5         10 my $option_specs = $args{option_specs};
156 5 100       47 my $shell = $ENV{PERL5_APPSPECRUN_SHELL} or return;
157 4         8 my $param = $param_specs->{ $completion_parameter };
158 4   33     11 $param ||= $option_specs->{ $completion_parameter };
159 4         7 my $unique = $param->{unique};
160 4 50       27 my $completion = $param->completion or return;
161 4         5 my $op;
162 4 100       10 if (ref $completion) {
163 2 50       7 $op = $completion->{op} or return;
164             }
165             else {
166 2 50       9 my $possible_values = $param->values or return;
167 2 50       5 $op = $possible_values->{op} or return;
168             }
169 4         12 my $args = {
170             runmode => "completion",
171             parameter => $completion_parameter,
172             };
173 4         17 my $result = $self->run_op($op, $args);
174              
175 4         207 my $string = '';
176 4         6 my %seen;
177 4 100       8 if ($unique) {
178 2         6 my $params = $self->parameters;
179 2         4 my $value = $params->{ $completion_parameter };
180 2 50       7 $value = [$value] unless is_arrayref $value;
181             # cmd param1 param2 results in
182             # @ARGV = ["param1", "param2"]
183             # cmd param1 param2 results in
184             # @ARGV = ["param1", "param2", ""]
185             # so we know that there is a new value to be completed
186 2         4 my $last = pop @$value;
187 2         6 @seen{ @$value } = (1) x @$value;
188             }
189 4         14 for my $item (@$result) {
190 9 100       22 if (ref $item eq 'HASH') {
191 2         4 my $name = $item->{name};
192 2 50 33     5 $unique and $seen{ $name }++ and next;
193 2         3 my $desc = $item->{description};
194 2         7 $string .= "$name\t$desc\n";
195             }
196             else {
197 7 100 100     23 $unique and $seen{ $item }++ and next;
198 6         15 $string .= "$item\n";
199             }
200             }
201              
202 4         12 $self->out($string);
203 4         118 return;
204             }
205              
206             sub error_output {
207 9     9 1 24 my ($self) = @_;
208 9         26 my $errs = $self->validation_errors;
209 9         15 my @error_output;
210 9         34 for my $key (sort keys %$errs) {
211 9         20 my $errors = $errs->{ $key };
212 9 50 66     43 if ($key eq "parameters" or $key eq "options") {
213 9         30 for my $name (sort keys %$errors) {
214 12         20 my $error = $errors->{ $name };
215 12         52 $key =~ s/s$//;
216 12         61 push @error_output, "Error: $key '$name': $error";
217             }
218             }
219             else {
220 0         0 require Data::Dumper;
221 0         0 my $err = Data::Dumper->Dump([$errs], ['errs']);
222 0         0 push @error_output, $err;
223             }
224             }
225 9         50 my $help = $self->spec->usage(
226             commands => $self->commands,
227             highlights => $errs,
228             colored => $self->colorize_code('err'),
229             );
230 9         50 $self->err($help);
231 9         23 for my $msg (@error_output) {
232 12         42 $msg = $self->colored('err', [qw/ error /], $msg);
233 12         48 $self->err("$msg\n");
234             }
235 9         32 $self->halt(1);
236             }
237              
238             sub colorize_code {
239 16     16 1 44 my ($self, $out) = @_;
240             $self->colorize($out)
241             ? sub {
242 0     0   0 my $colored = $self->colored($out, $_[0], $_[1]);
243 0 0       0 unless (defined wantarray) {
244 0         0 $_[1] = $colored;
245             }
246 0         0 return $colored;
247             }
248 83     83   153 : sub { $_[1] },
249 16 50       67 }
250              
251             sub colorize {
252 31     31 1 59 my ($self, $out) = @_;
253 31   100     90 $out ||= 'out';
254 31 50 100     137 if (($ENV{PERL5_APPSPECRUN_COLOR} // '') eq 'always') {
255 0         0 return 1;
256             }
257 31 100 100     98 if (($ENV{PERL5_APPSPECRUN_COLOR} // '') eq 'never') {
258 29         196 return 0;
259             }
260 2 50 33     26 if ($out eq 'out' and -t STDOUT or $out eq 'err' and -t STDERR) {
      33        
      33        
261 0         0 return 1;
262             }
263 2         18 return 0;
264             }
265              
266             sub process_parameters {
267 61     61 1 178 my ($self, %args) = @_;
268 61         106 my $param_list = $args{parameter_list};
269 61         117 my $parameters = $self->parameters;
270 61         100 my $param_specs = $args{param_specs};
271 61         167 for my $p (@$param_list) {
272 42         112 my $name = $p->name;
273 42         88 my $type = $p->type;
274 42         75 my $multiple = $p->multiple;
275 42         88 my $required = $p->required;
276 42         59 my $value;
277 42 100       84 if ($multiple) {
278 10         20 $value = [@{ $self->argv }];
  10         21  
279 10         20 @{ $self->argv } = ();
  10         20  
280             }
281             else {
282 32         38 $value = shift @{ $self->argv };
  32         79  
283             }
284 42         96 $parameters->{ $name } = $value;
285 42         148 $param_specs->{ $name } = $p;
286             }
287             }
288              
289             sub process_input {
290 30     30 1 141 my ($self, %args) = @_;
291 30         51 my %options;
292 30         151 $self->options(\%options);
293 30         60 my @cmds;
294 30         88 my $spec = $self->spec;
295 30         66 my $option_specs = $args{option_specs};
296 30         59 my $param_specs = $args{param_specs};
297 30         83 my $global_options = $spec->options;
298 30         89 my $global_parameters = $spec->parameters;
299 30         181 my @getopt = $spec->make_getopt($global_options, \%options, $option_specs);
300 30         197 GetOptions(@getopt);
301 30         12420 $self->event_globaloptions;
302 30         62 my $op = $self->op;
303              
304 30         129 $self->process_parameters(
305             parameter_list => $global_parameters,
306             param_specs => $param_specs,
307             );
308              
309              
310              
311 30         59 my $commands = $spec->subcommands;
312 30         107 my $opclass = $self->spec->class;
313 30         59 my $cmd_spec;
314 30         63 my $subcommand_required = 1;
315 30         91 while (keys %$commands) {
316 35         50 my $cmd = shift @{ $self->argv };
  35         99  
317 35 100       108 if (not defined $cmd) {
318 2 100 66     16 if (not $op or $subcommand_required) {
319 1         5 $self->err($spec->usage(
320             commands => \@cmds,
321             colored => $self->colorize_code('err'),
322             highlights => {
323             subcommands => 1,
324             },
325             ));
326 1         6 $self->err( $self->colorize_error("Missing subcommand(s)") );
327 1         4 $self->halt(1);
328             }
329 2         6 last;
330             }
331 33 100       112 $cmd_spec = $commands->{ $cmd } or do {
332 2         9 $self->err($spec->usage(
333             commands => \@cmds,
334             colored => $self->colorize_code('err'),
335             highlights => {
336             subcommands => 1,
337             },
338             ));
339 2         16 $self->err( $self->colorize_error("Unknown subcommand '$cmd'") );
340 2         12 $self->halt(1);
341 2         5 last;
342             };
343 31   100     129 $subcommand_required = $cmd_spec->{subcommand_required} // 1;
344 31         85 my $cmd_options = $cmd_spec->options;
345 31         122 my @getopt = $spec->make_getopt($cmd_options, \%options, $option_specs);
346 31         134 GetOptions(@getopt);
347 31         5024 push @cmds, $cmd;
348 31   100     195 $commands = $cmd_spec->subcommands || {};
349 31 100       135 $op = '::' . $cmd_spec->op if $cmd_spec->op;
350 31 100       118 $opclass = $cmd_spec->class if $cmd_spec->class;
351              
352 31         117 $self->process_parameters(
353             parameter_list => $cmd_spec->parameters,
354             param_specs => $param_specs,
355             );
356             }
357              
358 30 100       145 unless ($self->response->halted) {
359 27 100       66 unless ($op) {
360 1 50       4 if ($spec->has_subcommands) {
361 0         0 $self->err( "Missing op for commands (@cmds)\n" );
362 0         0 my $help = $spec->usage(
363             commands => \@cmds,
364             colored => $self->colorize_code('err'),
365             );
366 0         0 $self->err( $help );
367 0         0 $self->halt(1);
368             }
369             else {
370 1         3 $op = "::execute";
371             }
372             }
373 27         92 $self->commands(\@cmds);
374 27         82 $self->options(\%options);
375 27 100       121 if ($op =~ m/^::/) {
376 26         80 $op = $opclass . $op;
377             }
378 27         98 $self->op($op);
379 27         120 return $op;
380             }
381              
382 3         10 return;
383             }
384              
385             sub colorize_error {
386 3     3 1 9 my ($self, $msg) = @_;
387 3         12 $msg = $self->colored('err', [qw/ error /], $msg) . "\n";
388             }
389              
390             sub colored {
391 15     15 1 48 my ($self, $out, $colors, $msg) = @_;
392 15 50       28 $colors = [ map { $_ eq 'error' ? qw/ bold red / : $_ } @$colors ];
  15         65  
393 15         1534 require Term::ANSIColor;
394 15 50       14425 $self->colorize($out)
395             and $msg = Term::ANSIColor::colored($colors, $msg);
396 15         43 return $msg;
397             }
398              
399             sub subscribe {
400 56     56 1 170 my ($self, %args) = @_;
401              
402 56         165 for my $event (sort keys %args) {
403 56 50       168 next unless exists $EVENTS{ $event };
404 56         86 my $info = $args{ $event };
405 56         79 push @{ $self->subscribers->{ $event } }, $info;
  56         263  
406             }
407              
408             }
409              
410             sub event_globaloptions {
411 30     30 1 72 my ($self) = @_;
412              
413 30         110 my $subscribers = $self->subscribers->{global_options};
414 30         84 for my $sub (@$subscribers) {
415 30         67 my $plugin = $sub->{plugin};
416 30         66 my $method = $sub->{method};
417 30         147 $plugin->$method( run => $self);
418             }
419             }
420              
421             #sub event_processed {
422             # my ($self) = @_;
423             # my $plugins = $self->spec->plugins_by_type->{GlobalOptions};
424             # for my $plugin (@$plugins) {
425             # next unless $plugin->can("event_processed");
426             # $plugin->event_processed(
427             # run => $self,
428             # );
429             # }
430             #}
431              
432             1;
433              
434             __END__