File Coverage

blib/lib/Applify.pm
Criterion Covered Total %
statement 231 250 92.4
branch 113 140 80.7
condition 21 30 70.0
subroutine 39 40 97.5
pod 9 9 100.0
total 413 469 88.0


line stmt bran cond sub pod time code
1             package Applify;
2 13     13   23422 use strict;
  13         31  
  13         333  
3 12     12   63 use warnings;
  12         23  
  12         322  
4 12     12   67 use File::Basename ();
  12         24  
  12         411  
5              
6 12 50   12   662 use constant SUB_NAME_IS_AVAILABLE => $INC{'App/FatPacker/Trace.pm'}
  12 50       8303  
  0         0  
  0         0  
7             ? 0 # this will be true when running under "fatpack"
8 12     12   60 : eval 'use Sub::Name; 1' ? 1 : 0;
  12         21  
9              
10             our $VERSION = '0.13';
11             our $PERLDOC = 'perldoc';
12             my $ANON = 1;
13              
14             sub app {
15 28     28 1 2687 my $self = shift;
16 28   66     129 my $code = $self->{app} ||= shift;
17 28         69 my $parser = $self->_option_parser;
18 28         1322 my (%options, @options_spec, $application_class, $app);
19              
20 28         52 for my $option (@{$self->{options}}) {
  28         111  
21 105         214 my $switch = $self->_attr_to_option($option->{name});
22 105         208 push @options_spec, $self->_calculate_option_spec($option);
23 105 50       284 $options{$switch} = $option->{default} if exists $option->{default};
24 105 100       246 $options{$switch} = [@{$options{$switch}}] if ref($options{$switch}) eq 'ARRAY';
  24         61  
25             }
26              
27 28 50       99 unless ($parser->getoptions(\%options, @options_spec, $self->_default_options)) {
28 0         0 $self->_exit(1);
29             }
30              
31 28 50       14917 if ($options{help}) {
    50          
    50          
32 0         0 $self->print_help;
33 0         0 $self->_exit('help');
34             }
35             elsif ($options{man}) {
36 0         0 system $PERLDOC => $self->documentation;
37 0         0 $self->_exit($? >> 8);
38             }
39             elsif ($options{version}) {
40 0         0 $self->print_version;
41 0         0 $self->_exit('version');
42             }
43              
44 28   66     141 $application_class = $self->{application_class} ||= $self->_generate_application_class($code);
45             $app = $application_class->new(
46 28         105 {map { my $k = $self->_option_to_attr($_); $k => $self->_upgrade($k, $options{$_}) } keys %options});
  105         250  
  105         205  
47              
48 28 50       281 return $app if defined wantarray; # $app = do $script_file;
49 0         0 $self->_exit($app->run(@ARGV));
50             }
51              
52             sub documentation {
53 41 100   41 1 1469 return $_[0]->{documentation} if @_ == 1;
54 4 100       19 $_[0]->{documentation} = $_[1] or die 'Usage: documentation $file|$module_name;';
55 3         10 return $_[0];
56             }
57              
58             sub extends {
59 1     1 1 3 my $self = shift;
60 1         4 $self->{extends} = [@_];
61 1         14 return $self;
62             }
63              
64             sub import {
65 11     11   94 my ($class, %args) = @_;
66 11         42 my @caller = caller;
67 11         48 my $self = $class->new({caller => \@caller});
68 11         34 my $ns = $caller[0] . '::';
69 11         21 my %export;
70              
71 11         57 strict->import;
72 11         117 warnings->import;
73              
74 11         47 $self->{skip_subs} = {app => 1, option => 1, version => 1, documentation => 1, extends => 1,};
75              
76 12     12   90 no strict 'refs';
  12         28  
  12         1285  
77 11         560 for my $name (keys %$ns) {
78 2137         4349 $self->{'skip_subs'}{$name} = 1;
79             }
80              
81 11         109 for my $k (qw(app extends option version documentation)) {
82 55   66     215 my $name = $args{$k} // $k;
83 55 50       129 next unless $name;
84 55 50       200 $export{$k} = $name =~ /::/ ? $name : "$caller[0]\::$name";
85             }
86              
87 12     12   76 no warnings 'redefine'; # need to allow redefine when loading a new app
  12         29  
  12         14519  
88 11     11   55 *{$export{app}} = sub (&) { $self->app(@_) };
  11         57  
  11         163  
89 11     19   41 *{$export{option}} = sub { $self->option(@_) };
  11         41  
  19         77  
90 11     1   31 *{$export{version}} = sub { $self->version(@_) };
  11         39  
  1         2  
91 11     1   38 *{$export{documentation}} = sub { $self->documentation(@_) };
  11         37  
  1         3  
92 11     1   52 *{$export{extends}} = sub { $self->extends(@_) };
  11         858  
  1         10  
93             }
94              
95             sub new {
96 11     11 1 25 my ($class, $args) = @_;
97 11         25 my $self = bless $args, $class;
98              
99 11   50     125 $self->{options} ||= [];
100 11 50       43 $self->{caller} or die 'Usage: $self->new({ caller => [...], ... })';
101              
102 11         30 return $self;
103             }
104              
105             sub option {
106 28     28 1 4502 my $self = shift;
107 28 100       95 my $type = shift or die 'Usage: option $type => ...';
108 27 100       80 my $name = shift or die 'Usage: option $type => $name => ...';
109 26 100       109 my $documentation = shift or die 'Usage: option $type => $name => $documentation, ...';
110 25         43 my ($default, %args);
111              
112 25 100       68 if (@_ % 2) {
113 6         14 $default = shift;
114 6         20 %args = @_;
115             }
116             else {
117 19         41 %args = @_;
118             }
119              
120 25 100 66     82 if ($args{alias} and !ref $args{alias}) {
121 1         2 $args{alias} = [$args{alias}];
122             }
123              
124 25         39 push @{$self->{options}}, {default => $default, %args, type => $type, name => $name, documentation => $documentation};
  25         111  
125              
126 25         151 return $self;
127             }
128              
129 4     4 1 36 sub options { $_[0]->{options} }
130              
131             sub print_help {
132 3     3 1 887 my $self = shift;
133 3         6 my @options = @{$self->{options}};
  3         9  
134 3         7 my $width = 0;
135              
136 3         10 push @options, {name => ''};
137 3         9 push @options, {name => 'help', documentation => 'Print this help text'};
138 3 100       8 push @options, {name => 'man', documentation => 'Display manual for this application'} if $self->documentation;
139 3 100       11 push @options, {name => 'version', documentation => 'Print application name and version'} if $self->version;
140 3         7 push @options, {name => ''};
141              
142 3         11 $self->_print_synopsis;
143              
144             OPTION:
145 3         8 for my $option (@options) {
146 24         34 my $length = length $option->{name};
147 24 100       48 $width = $length if $width < $length;
148             }
149              
150 3         8 print "Usage:\n";
151              
152             OPTION:
153 3         7 for my $option (@options) {
154 24 100       55 my $name = $self->_attr_to_option($option->{name}) or do { print "\n"; next OPTION };
  6         13  
  6         16  
155              
156             printf(
157             " %s %2s%-${width}s %s\n",
158             $option->{required} ? '*' : ' ',
159             length($name) > 1 ? '--' : '-',
160             $name, $option->{documentation},
161 18 100       112 );
    100          
162             }
163              
164 3         16 return $self;
165             }
166              
167             sub print_version {
168 3     3 1 818 my $self = shift;
169 3 100       8 my $version = $self->version or die 'Cannot print version without version()';
170              
171 2 100       11 unless ($version =~ m!^\d!) {
172 1 50       47 eval "require $version; 1" or die "Could not load $version: $@";
173 1         12 $version = $version->VERSION;
174             }
175              
176 2         86 printf "%s version %s\n", File::Basename::basename($0), $version;
177             }
178              
179             sub version {
180 42 100   42 1 2128 return $_[0]->{version} if @_ == 1;
181 5 100       27 $_[0]->{version} = $_[1] or die 'Usage: version $module_name|$num;';
182 4         33 return $_[0];
183             }
184              
185             sub _attr_to_option {
186 245 100   245   562 local $_ = $_[1] or return;
187 239         499 s!_!-!g;
188 239         436 $_;
189             }
190              
191             sub _calculate_option_spec {
192 115     115   199 my ($self, $option) = @_;
193 115         220 my $spec = $self->_attr_to_option($option->{name});
194              
195 115 100       285 if (ref $option->{alias} eq 'ARRAY') {
196 2         4 $spec .= join '|', '', @{$option->{alias}};
  2         6  
197             }
198              
199 115 100       664 if ($option->{type} =~ /^(?:bool|flag)/i) { $spec .= '!' }
  4 100       11  
    100          
    100          
    100          
    100          
    50          
200 1         4 elsif ($option->{type} =~ /^inc/) { $spec .= '+' }
201 49         85 elsif ($option->{type} =~ /^str/) { $spec .= '=s' }
202 1         4 elsif ($option->{type} =~ /^int/i) { $spec .= '=i' }
203 3         10 elsif ($option->{type} =~ /^num/i) { $spec .= '=f' }
204 47         75 elsif ($option->{type} =~ /^file/) { $spec .= '=s' } # TODO
205 10         23 elsif ($option->{type} =~ /^dir/) { $spec .= '=s' } # TODO
206 0         0 else { die 'Usage: option {bool|flag|inc|str|int|num|file|dir} ...' }
207              
208 115 100       288 if (my $n_of = $option->{n_of}) {
209 26 100       66 $spec .= $n_of eq '@' ? $n_of : "{$n_of}";
210             $option->{default}
211 26 50 66     121 and ref $option->{default} ne 'ARRAY'
212             and die 'Usage option ... default => [Need to be an array ref]';
213 26   100     91 $option->{default} ||= [];
214             }
215              
216 115         269 return $spec;
217             }
218              
219             sub _default_options {
220 30     30   1735 my $self = shift;
221 30         49 my @default;
222              
223 30         62 push @default, 'help';
224 30 100       90 push @default, 'man' if $self->documentation;
225 30 100       87 push @default, 'version' if $self->version;
226              
227 30         122 return @default;
228             }
229              
230             sub _exit {
231 0     0   0 my ($self, $reason) = @_;
232 0 0       0 exit 0 unless ($reason =~ /^\d+$/); # may change without warning...
233 0         0 exit $reason;
234             }
235              
236             sub _generate_application_class {
237 12     12   41 my ($self, $code) = @_;
238 12         40 my $application_class = $self->{caller}[1];
239 12   100     68 my $extends = $self->{extends} || [];
240 12         28 my ($meta, @required);
241              
242 12         100 $application_class =~ s!\W!_!g;
243 12         67 $application_class = join '::', ref($self), "__ANON__${ANON}__", $application_class;
244 12         28 $ANON++;
245              
246 11 50   11   92 eval qq[
  11         27  
  11         778  
  12         958  
247             package $application_class;
248             use base qw(@$extends);
249             1;
250             ] or die "Failed to generate applicatin class: $@";
251              
252             {
253 12     12   93 no strict 'refs';
  12         24  
  12         9224  
  12         26  
254 28     28   103 _sub("$application_class\::new" => sub { my $class = shift; bless shift, $class })
  28         85  
255 12 50       136 unless grep { $_->can('new') } @$extends;
  1         28  
256 12     9   71 _sub("$application_class\::_script" => sub {$self});
  9         77  
257             _sub(
258             "$application_class\::run" => sub {
259 1     1   2362 my ($app, @extra) = @_;
260              
261 1 50       2 if (@required = grep { not defined $app->{$_} } @required) {
  1         9  
262 1         3 my $required = join ', ', map { '--' . $self->_attr_to_option($_) } @required;
  1         3  
263 1         4 $app->_script->print_help;
264 1         7 die "Required attribute missing: $required\n";
265             }
266              
267 0         0 return $app->$code(@extra);
268             }
269 12         81 );
270              
271 12         42 for ('app', $self->{caller}[0]) {
272 24         42 my $ns = \%{"$_\::"};
  24         85  
273              
274 24         689 for my $name (keys %$ns) {
275 2518 100       5464 $self->{skip_subs}{$name} and next;
276 106 100       171 my $code = eval { ref $ns->{$name} eq 'CODE' ? $ns->{$name} : *{$ns->{$name}}{CODE} } or next;
  106 100       261  
  103         453  
277 7         33 my $fqn = join '::', $application_class, $name;
278 7         27 _sub($fqn => $code);
279 7         32 delete $ns->{$name}; # may be a bit too destructive?
280             }
281             }
282              
283 12 50 33     178 $meta = $application_class->meta if $application_class->isa('Moose::Object') and $application_class->can('meta');
284              
285 12         29 for my $option (@{$self->{options}}) {
  12         45  
286 22         37 my $name = $option->{name};
287 22         44 my $fqn = join '::', $application_class, $name;
288 22 50       44 if ($meta) {
289 0         0 $meta->add_attribute($name => {is => 'rw', default => $option->{default}});
290             }
291             else {
292 22 100   56   64 _sub($fqn => sub { @_ == 2 and $_[0]->{$name} = $_[1]; $_[0]->{$name} });
  56         1642  
  56         251  
293             }
294 22 100       68 push @required, $name if $option->{required};
295             }
296             }
297              
298 12         65 return $application_class;
299             }
300              
301             sub _load_class {
302 53 100   53   230 my $class = shift or return undef;
303 26 100       139 return $class if $class->can('new');
304 1 50       55 return eval "require $class; 1" ? $class : "";
305             }
306              
307             sub _option_parser {
308 30   66 30   183 $_[0]->{_option_parser} ||= do {
309 11         7053 require Getopt::Long;
310 11         94672 Getopt::Long::Parser->new(config => [qw(no_auto_help no_auto_version pass_through)]);
311             };
312             }
313              
314             sub _option_to_attr {
315 105 50   105   221 local $_ = $_[1] or return;
316 105         217 s!-!_!g;
317 105         189 $_;
318             }
319              
320             sub _print_synopsis {
321 3     3   4 my $self = shift;
322 3 100       12 my $documentation = $self->documentation or return;
323 2         5 my $print;
324              
325 2 50       38 unless (-e $documentation) {
326 0 0       0 eval "use $documentation; 1" or die "Could not load $documentation: $@";
327 0         0 $documentation =~ s!::!/!g;
328 0         0 $documentation = $INC{"$documentation.pm"};
329             }
330              
331 2 50       48 open my $FH, '<', $documentation or die "Failed to read synopsis from $documentation: $@";
332              
333 2         26 while (<$FH>) {
334 99 100 100     223 last if $print and /^=(?:cut|head1)/;
335 97 100       159 print if $print;
336 97 100       266 $print = 1 if /^=head1 SYNOPSIS/;
337             }
338             }
339              
340             sub _sub {
341 65     65   128 my ($fqn, $code) = @_;
342 12     12   90 no strict 'refs';
  12         29  
  12         2285  
343 65 100       301 return if *$fqn{CODE};
344 64         178 *$fqn = SUB_NAME_IS_AVAILABLE ? Sub::Name::subname($fqn, $code) : $code;
345             }
346              
347             sub _upgrade {
348 105     105   192 my ($self, $name, $input) = @_;
349 105 100       267 return $input unless defined $input;
350              
351 53         72 my ($option) = grep { $_->{name} eq $name } @{$self->{options}};
  289         513  
  53         100  
352 53 100       135 return $input unless my $class = _load_class($option->{isa});
353 25 100       87 return ref $input eq 'ARRAY' ? [map { $class->new($_) } @$input] : $class->new($input);
  5         22  
354             }
355              
356             1;
357              
358             =encoding utf8
359              
360             =head1 NAME
361              
362             Applify - Write object oriented scripts with ease
363              
364             =head1 VERSION
365              
366             0.13
367              
368             =head1 DESCRIPTION
369              
370             This module should keep all the noise away and let you write scripts
371             very easily. These scripts can even be unittested even though they
372             are define directly in the script file and not in a module.
373              
374             =head1 SYNOPSIS
375              
376             #!/usr/bin/perl
377             use Applify;
378              
379             option file => input_file => 'File to read from';
380             option dir => output_dir => 'Directory to write files to';
381             option flag => dry_run => 'Use --no-dry-run to actually do something', 1;
382              
383             documentation __FILE__;
384             version 1.23;
385              
386             sub generate_exit_value {
387             return int rand 100;
388             }
389              
390             app {
391             my($self, @extra) = @_;
392             my $exit_value = 0;
393              
394             print "Extra arguments: @extra\n" if(@extra);
395             print "Will read from: ", $self->input_file, "\n";
396             print "Will write files to: ", $self->output_dir, "\n";
397              
398             if($self->dry_run) {
399             die 'Will not run script';
400             }
401              
402             return $self->generate_exit_value;
403             };
404              
405             =head1 APPLICATION CLASS
406              
407             This module will generate an application class, which C<$self> inside the
408             L block refere to. This class will have:
409              
410             =over 4
411              
412             =item * new()
413              
414             An object constructor. This method will not be auto generated if any of
415             the classes given to L has the method C.
416              
417             =item * run()
418              
419             This method is basically the code block given to L.
420              
421             =item * Other methods
422              
423             Other methods defined in the script file will be accesible from C<$self>
424             inside C.
425              
426             =item * _script()
427              
428             This is an accessor which return the L object which
429             is refered to as C<$self> in this documentation.
430              
431             NOTE: This accessor starts with an underscore to prevent conflicts
432             with L.
433              
434             =item * Other accessors
435              
436             Any L (application switch) will be available as an accessor on the
437             application object.
438              
439             =back
440              
441             =head1 EXPORTED FUNCTIONS
442              
443             =head2 option
444              
445             option $type => $name => $documentation;
446             option $type => $name => $documentation, $default;
447             option $type => $name => $documentation, $default, @args;
448             option $type => $name => $documentation, @args;
449              
450             This function is used to define options which can be given to this
451             application. See L for example code. This function can also be
452             called as a method on C<$self>.
453              
454             =over 4
455              
456             =item * $type
457              
458             Used to define value types for this input.
459              
460             =over 4
461              
462             =item bool, flag
463              
464             =item inc
465              
466             =item str
467              
468             =item int
469              
470             =item num
471              
472             =item file (TODO)
473              
474             =item dir (TODO)
475              
476             =back
477              
478             =item * $name
479              
480             The name of an application switch. This name will also be used as
481             accessor name inside the application. Example:
482              
483             # define an application switch:
484             option file => some_file => '...';
485              
486             # call the application from command line:
487             > myapp.pl --some-file /foo/bar
488              
489             # run the application code:
490             app {
491             my $self = shift;
492             print $self->some_file # prints "/foo/bar"
493             return 0;
494             };
495              
496             =item * C<$documentation>
497              
498             Used as description text when printing the usage text.
499              
500             =item * C<@args>
501              
502             =over 4
503              
504             =item * C
505              
506             The script will not start if a required field is omitted.
507              
508             =item * C
509              
510             Allow the option to hold a list of values. Examples: "@", "4", "1,3".
511             See L for details.
512              
513             =item * C
514              
515             Specify the class an option should be instantiated as. Example:
516              
517             option file => output => "output file", isa => "Mojo::File";
518              
519             The C attribute will then later return an object of L,
520             instead of just a plain string.
521              
522             =item * Other
523              
524             Any other L attribute argument may/will be supported in
525             future release.
526              
527             =back
528              
529             =back
530              
531             =head2 documentation
532              
533             documentation __FILE__; # current file
534             documentation '/path/to/file';
535             documentation 'Some::Module';
536              
537             Specifies where to retrieve documentaion from when giving the C<--man>
538             switch to your script.
539              
540             =head2 version
541              
542             version 'Some::Module';
543             version $num;
544              
545             Specifies where to retrieve the version number from when giving the
546             C<--version> switch to your script.
547              
548             =head2 extends
549              
550             extends @classes;
551              
552             Specify which classes this application should inherit from. These
553             classes can be L based.
554              
555             =head2 app
556              
557             app CODE;
558              
559             This function will define the code block which is called when the application
560             is started. See L for example code. This function can also be
561             called as a method on C<$self>.
562              
563             IMPORTANT: This function must be the last function called in the script file
564             for unittests to work. Reason for this is that this function runs the
565             application in void context (started from command line), but returns the
566             application object in list/scalar context (from L).
567              
568             =head1 ATTRIBUTES
569              
570             =head2 options
571              
572             $array_ref = $self->options;
573              
574             Holds the application options given to L.
575              
576             =head1 METHODS
577              
578             =head2 new
579              
580             $self = $class->new({ options => $array_ref, ... });
581              
582             Object constructor. Creates a new object representing the script meta
583             information.
584              
585             =head2 print_help
586              
587             Will print L to selected filehandle (STDOUT by default) in
588             a normalized matter. Example:
589              
590             Usage:
591             --foo Foo does this and that
592             * --bar Bar does something else
593              
594             --help Print this help text
595             --man Display manual for this application
596             --version Print application name and version
597              
598             =head2 print_version
599              
600             Will print L to selected filehandle (STDOUT by default) in
601             a normalized matter. Example:
602              
603             some-script.pl version 1.23
604              
605             =head2 import
606              
607             Will export the functions listed under L. The functions
608             will act on a L object created by this method.
609              
610             =head1 COPYRIGHT & LICENSE
611              
612             This library is free software. You can redistribute it and/or modify
613             it under the same terms as Perl itself.
614              
615             =head1 AUTHORS
616              
617             Jan Henning Thorsen - C
618              
619             Roy Storey
620              
621             =cut