File Coverage

blib/lib/Module/Cooker.pm
Criterion Covered Total %
statement 209 222 94.1
branch 63 90 70.0
condition 6 9 66.6
subroutine 38 43 88.3
pod 10 10 100.0
total 326 374 87.1


line stmt bran cond sub pod time code
1             package Module::Cooker;
2              
3             our $VERSION = 'v0.1.4';
4              
5             #use 5.008_008;
6              
7 6     6   28863 use strict;
  6         15  
  6         239  
8 6     6   35 use warnings FATAL => 'all';
  6         11  
  6         269  
9              
10 6     6   4541 use Data::Dumper;
  6         32202  
  6         468  
11              
12 6     6   53 use Carp;
  6         11  
  6         346  
13 6     6   37 use Cwd ();
  6         10  
  6         101  
14 6     6   1005 use Try::Tiny;
  6         1822  
  6         396  
15              
16 6     6   5660 use version 0.77;
  6         14643  
  6         46  
17              
18 6     6   17511 use ExtUtils::Manifest qw( mkmanifest );
  6         72396  
  6         583  
19 6     6   7790 use Storable (qw( dclone ));
  6         24839  
  6         518  
20              
21 6     6   59 use File::Path qw( make_path );
  6         13  
  6         372  
22 6     6   5938 use File::Spec::Functions qw( catdir catfile );
  6         5317  
  6         472  
23 6     6   4947 use File::Which;
  6         10865  
  6         363  
24              
25 6     6   5540 use POSIX qw( strftime );
  6         65100  
  6         49  
26              
27 6     6   22743 use Template;
  6         181712  
  6         1297  
28              
29             my $profile_name_rx = qr/[A-Z_a-z][A-Z_a-z0-9.-]*/;
30              
31             # the following regex is ripped from Module::Runtime
32             # suggested by Perl Monk tobyink (http://www.perlmonks.org/?node_id=757127)
33             my $module_name_rx = qr/[A-Z_a-z][0-9A-Z_a-z]*(?:::[0-9A-Z_a-z]+)*/;
34              
35             my $defaults = {
36             minperl => '',
37             author => 'A. Uthor',
38             email => 'author@example.com',
39             profile => 'default',
40             package => 'My::MC::Module',
41             version => 'v0.1_1',
42             extravars => {},
43             localdirs => [],
44             nosubs => 0,
45              
46             # nolinks => 1, # future use?
47             };
48              
49             my @boolean_params = (
50             'nosubs',
51              
52             # 'nolinks', # future use?
53             );
54              
55             sub new {
56 23     23 1 11707 my $class = shift;
57 23         69 my %incoming = @_;
58              
59 23         61 my $self = bless _validate_incoming( \%incoming ), $class;
60              
61             # be lazy and automatically generate accessors.
62             # Perl Monk GrandFather should appreciate this. :)
63 12         21 foreach my $attribute ( keys( %{$self} ) ) {
  12         101  
64 108 50       225 next if $attribute =~ /^_/;
65 108 100       512 next if $self->can($attribute);
66              
67 6     6   65 no strict 'refs';
  6         12  
  6         24618  
68              
69             # auto-generated accessors should go in this package, not a
70             # sub-class. the sub-class can always override like
71             # normal if need be.
72 42         156 *{ __PACKAGE__ . "::$attribute" } = sub {
73 29     29   8133 my $self = shift;
74              
75 29 100       845 croak "Can't set read-only attribute: $attribute" if @_;
76              
77 22         150 return $self->{$attribute};
78 42         170 };
79             }
80              
81             # this needs to be set at the time of instance creation because
82             # if a subsequent chdir occurs the method won't be able to
83             # resolve a relative path in %INC. this is specifically needed
84             # for the test suite to work properly in a tmp dir.
85 12         51 $self->{_basename_dir} = $self->_basename_dir;
86              
87 12         33 $self->{_made_dist_dir} = 0;
88 12         34 $self->{_templates} = {};
89 12         28 $self->{_template_dirs} = [];
90              
91 12         41 return $self;
92             }
93              
94             # NOTE: email addresses are not validated since it might be desirable to
95             # use an anti-SPAM pattern. i.e. "author at example dot com". suggestions
96             # as to how to add some form of minimal checking are welcome.
97             sub _validate_incoming {
98 23     23   35 my $incoming = shift;
99              
100 23         36 my $args = {};
101 23         34 for ( keys( %{$defaults} ) ) {
  23         99  
102 207   100     1022 $args->{$_} = delete( $incoming->{$_} ) || $defaults->{$_};
103             }
104 0         0 croak 'Unknown parameter(s): ' . join( ', ', keys( %{$incoming} ) )
  23         79  
105 23 50       47 if keys( %{$incoming} );
106              
107 23 50       77 croak "Parameter 'package' must be supplied"
108             unless $args->{package};
109 23 100       1879 croak "Invalid package name: $args->{package}"
110             unless $args->{package} =~ /\A$module_name_rx\z/o;
111              
112 12 50       151 croak "Illegal profile name: $args->{profile}"
113             unless $args->{profile} =~ /\A$profile_name_rx\z/o;
114              
115             # ensure that boolean params have boolean values
116 12         30 for (@boolean_params) {
117 12         27 my $orig = $args->{$_};
118 12   50     67 $args->{$_} = !!$args->{$_} || 0;
119 12 50       60 croak "Boolean param $_ must be '0' or '1': $orig ne $args->{$_}"
120             unless $args->{$_} eq $orig;
121             }
122              
123             try {
124 12     12   621 version->parse( $args->{version} );
125             }
126             catch {
127 0     0   0 croak $_;
128 12         97 };
129              
130 12 50 50     459 croak "Param 'extravars' must be a hashref"
131             unless ( ref( $args->{extravars} ) || '' ) eq 'HASH';
132              
133 12 50 50     59 croak "Param 'localdirs' must be an arrayref"
134             unless ( ref( $args->{localdirs} ) || '' ) eq 'ARRAY';
135              
136 12         43 return $args;
137             }
138              
139             # used to build path to where the main package module will be placed
140             # in the distribution dir.
141             sub _lib_path {
142 27     27   34 my $self = shift;
143              
144 27         111 my @parts = split( /::/, $self->{package} );
145 27         38 pop(@parts); # remove basename
146              
147 27         57 unshift( @parts, 'lib' );
148              
149 27         112 return join( '/', @parts );
150             }
151              
152             # used to find the location of THIS module. assumes that all support
153             # dirs will be under a directory named after this module (without
154             # the '.pm')
155             # NOTE! this is a class method that doesn't check the 'cached' value.
156             # YOU WILL BE SURPRISED if there has been an intervening chdir operation!
157             # see the public 'basename_dir' method for normal use.
158             sub _basename_dir {
159 13     13   353 my $package = __PACKAGE__;
160              
161 13         51 $package =~ s/::/\//g;
162 13         45 my $packpath = $INC{ join( '.', $package, 'pm' ) };
163 13         53 $packpath =~ s/\.pm$//;
164              
165 13         1797 my $realpath = Cwd::realpath($packpath);
166              
167 13         88 return $realpath;
168             }
169              
170             # create the dist dir in the cwd
171             sub _make_dist_dir {
172 1     1   3 my $self = shift;
173              
174             # croak if a fatal error occurs, better to die here than later
175             try {
176 1 50   1   230 make_path( $self->dist_name ) or die $!;
177 1         8 $self->{_made_dist_dir} = 1;
178             }
179             catch {
180 0     0   0 die "Can not make distribution dir: $_";
181 1         20 };
182              
183 1         925 return;
184             }
185              
186             # builds a hash that will be passed to Template
187             sub _package_info {
188 13     13   23 my $self = shift;
189              
190 13         42 my $module_path = catfile( $self->_lib_path, $self->module_name );
191              
192 13         86 my $package = {
193             name => $self->{package},
194             dist_name => $self->dist_name,
195             libpath => $self->_lib_path,
196             module => $self->module_name,
197             modulepath => $module_path,
198             version => $self->{version},
199             minperl => $self->{minperl},
200             timestamp => strftime( '%Y-%m-%d %T', localtime() ),
201             year => strftime( '%Y', localtime() ),
202             };
203              
204 13         127 return $package;
205             }
206              
207             # builds a hash that will be passed to Template
208             sub _author_info {
209 13     13   20 my $self = shift;
210              
211 13         76 my $author = {
212             name => $self->{author},
213             email => $self->{email},
214             };
215              
216 13         51 return $author;
217             }
218              
219             # builds path to where standard templates for a given profile are located
220             sub _profile_dir {
221 6     6   10 my $self = shift;
222              
223 6         14 my $dir = catdir( $self->basename_dir, $self->{profile} );
224              
225 6 50       151 -d $dir ? return $dir : return;
226             }
227              
228             sub _include_path {
229 0     0   0 my $self = shift;
230              
231 0 0       0 return $self->{_include_path} if $self->{_include_path};
232             }
233              
234             sub _process_template {
235 13     13   24 my $self = shift;
236 13         46 my %args = @_;
237              
238             # Template will automatically create missing dirs, but doing this
239             # allows for bailing out if the main dist dir already exists.
240             # having the test here ensures catching such a condition at a
241             # common point that is less likely to be skipped over.
242 13 100       54 if ( !$self->{_made_dist_dir} ) {
243 2         60 my $direxists = !!( -d $self->dist_name );
244 2 100       8 die "Distribution directory already exists: " . $self->dist_name
245             if -d $self->dist_name;
246              
247             # dist dir does not exist. this also sets _made_dist_dir
248 1         6 $self->_make_dist_dir;
249             }
250              
251 12 50       42 die "Template name missing!" unless $args{template};
252              
253 12         35 my $outfile;
254 12 100       53 if ( $args{template} =~ /^Module\.pm$/ ) { # gets speical treatment
255 1         5 $outfile = catfile( $self->_lib_path, $self->module_name );
256             } else {
257 11         25 $outfile = $args{template};
258             }
259              
260             # need to add logic to add paths for INCLUDE directives to INCLIDE_PATH
261              
262             # this is a seperate stucture to all for a future method to let
263             # users specify additional config options similar to how
264             # extravars work.
265 12         71 my $tt_config = {
266             TRIM => 0,
267             PRE_CHOMP => 0,
268             POST_CHOMP => 0,
269 12         26 INCLUDE_PATH => \@{ $self->profile_dirs },
270             OUTPUT_PATH => $self->dist_name,
271             };
272 12         131 my $t = Template->new($tt_config);
273              
274 12         286892 my $vars = $self->template_data;
275              
276 12 50       60 $t->process( $args{template}, $vars, $outfile ) || die $t->error . "\n";
277              
278 12         131637 return;
279             }
280              
281             sub _gather_profile {
282 6     6   12 my $self = shift;
283 6         35 my %args = @_;
284              
285 6         11 my $dir = $args{abs_path};
286 6         9 my $subdir = $args{subdir_path};
287              
288 6 50       161 die "Can't find dir: $dir\n" unless -d $dir;
289              
290 6 50       264 opendir( my $dh, $dir ) or die "can't opendir $dir: $!";
291 6         157 my @files = readdir($dh);
292 6         62 closedir $dh;
293              
294 6         24 my $std_dir = $self->_profile_dir;
295 6 50       81 my $src_type = ( $dir =~ /^(?:\Q$std_dir\E)/ ) ? 'standard' : 'local';
296              
297 6         15 for my $fname (@files) {
298 40 100       150 next if $fname =~ m{^\.{1,2}\z};
299              
300 28         2003 my $fpath = File::Spec->catfile( $dir, $fname );
301              
302             # $fpath = readlink($fpath) if -l $fpath;
303              
304             # don't follow symlinks for now.
305             # use nolinks param to control this later if desired.
306 28 50       768 next if -l $fpath;
307              
308 28 100       16774 if ( -d $fpath ) {
309 4 50       14 if ( $self->{nosubs} ) {
310 0         0 warn "Skipping profile sub-directory: $fpath\n";
311 0         0 next;
312             }
313              
314 4 50       12 my $subpath = $subdir ? catdir( $subdir, $fname ) : $fname;
315              
316             try {
317 4     4   119 push( @{ $self->{_template_dirs} }, $subpath );
  4         10  
318              
319             # trust perl's deep recursion detection
320 4         15 $self->_gather_profile(
321             abs_path => $fpath,
322             subdir_path => $subpath
323             );
324             }
325             catch {
326 0     0   0 die $_;
327 4         39 };
328              
329 4         57 next;
330             }
331              
332 24 50       1365 next unless -f $fpath;
333              
334 24 100       123 my $template = $subdir ? catfile( $subdir, $fname ) : $fname;
335             # $self->{_templates}{$template} = catfile( $dir, $subdir )
336 24 50       147 $self->{_templates}{$template} = $src_type
337             unless $self->{_templates}{$template};
338             }
339              
340 6         55 return;
341             }
342              
343             # future use? considering an option to pass Template through perltidy
344             sub _perltidy_cmd {
345 0     0   0 my $tidy = which('perltidy');
346              
347 0         0 return $tidy;
348             }
349              
350             # override the default accessor generation to ensure a copy is made
351             sub extravars {
352 4     4 1 1828 my $self = shift;
353              
354 4 100       125 croak "Can't set read-only attribute: extravars" if @_;
355              
356 3         5 my $tmp = $self->{extravars};
357 3         147 my $extravars = dclone($tmp);
358              
359 3 50       14 return wantarray ? %{$extravars} : $extravars;
  0         0  
360             }
361              
362             # override the default accessor generation to ensure a copy is made
363             sub localdirs {
364 19     19 1 1416 my $self = shift;
365              
366 19 100       179 croak "Can't set read-only attribute: localdirs" if @_;
367              
368 18         30 my @localdirs = @{ $self->{localdirs} };
  18         52  
369              
370 18 100       70 return wantarray ? @localdirs : \@localdirs;
371             }
372              
373             # return a list of dirs that actually contain the requested profile
374             sub profile_dirs {
375 16     16 1 1508 my $self = shift;
376              
377 16 100       224 croak "Can't set read-only method: profile_dirs" if @_;
378              
379 15         59 my @searchdirs = $self->localdirs;
380 15         45 push( @searchdirs, $self->basename_dir );
381              
382 15         24 my @profile_dirs;
383 15         185 for (@searchdirs) {
384 15         58 my $profile_dir = catdir( $_, $self->profile );
385 15 50       496 push( @profile_dirs, $profile_dir ) if -d $profile_dir;
386             }
387              
388 15 50       92 return wantarray ? @profile_dirs : \@profile_dirs;
389             }
390              
391             sub basename_dir {
392 23     23 1 1263 my $self = shift;
393              
394 23 100       171 croak "Can't set read-only method: basename_dir" if @_;
395              
396 22         85 return $self->{_basename_dir};
397             }
398              
399             # builds list of final attribute values
400             sub summary {
401 2     2 1 1206 my $self = shift;
402              
403 2 100       129 croak "Can't set read-only method: summary" if @_;
404              
405 1         3 my $tmp = {};
406 1         3 for ( keys( %{$self} ) ) {
  1         8  
407 13 100       35 next if /^_/; # we only want the attributes, not internals
408 9         33 $tmp->{$_} = $self->{$_};
409             }
410              
411 1         165 my $summary = dclone($tmp);
412              
413             # sorry, Will, but i think this is handy. :)
414 1 50       13 return wantarray ? %{$summary} : $summary;
  0         0  
415             }
416              
417             # simple transform: i.e. Foo::Bar -> Foo-Bar
418             sub dist_name {
419 35     35 1 1205 my $self = shift;
420              
421 35 100       228 croak "Can't set read-only method: dist_name" if @_;
422              
423 34         71 my $dname = $self->{package};
424 34         243 $dname =~ s/::/-/g;
425              
426 34         1073 return $dname;
427             }
428              
429             # generates main module name. i.e. Foo::Bar -> Bar.pm
430             sub module_name {
431 29     29 1 1396 my $self = shift;
432              
433 29 100       228 croak "Can't set read-only method: module_name" if @_;
434              
435 28         100 my @parts = split( /::/, $self->{package} );
436              
437 28         2084 return join( '.', pop(@parts), 'pm' );
438             }
439              
440             sub template_data {
441 14     14 1 1323 my $self = shift;
442              
443 14 100       172 croak "Can't set read-only method: template_data" if @_;
444              
445 13         41 my $tmp = {
446             author => $self->_author_info,
447             package => $self->_package_info,
448             modcooker => {
449             version => $VERSION,
450             perlver => $],
451             },
452             extra => $self->{extravars},
453             };
454              
455 13         793 my $tdata = dclone($tmp);
456              
457 13 50       101 return wantarray ? %{$tdata} : $tdata;
  0         0  
458             }
459              
460             # the ultimate goal of this module
461             sub cook {
462 2     2 1 32432 my $self = shift;
463              
464             # clear our template list
465 2         11 $self->{_templates} = {};
466 2         10 $self->{_template_dirs} = [];
467              
468 2         9 for ( @{ $self->profile_dirs } ) {
  2         14  
469 2         248 my $dir = Cwd::realpath($_);
470 2         15 $self->_gather_profile( abs_path => $dir, subdir_path => undef );
471             }
472              
473             #warn Dumper($self->{_templates});
474 2         7 foreach ( keys( %{ $self->{_templates} } ) ) {
  2         25  
475 13         954 $self->_process_template( template => $_ );
476             }
477              
478 1 50       60 if ( !-f catfile( $self->dist_name, 'MANIFEST' ) ) {
479 1         4 chdir $self->dist_name;
480 1         16 mkmanifest();
481 1         3563 chdir '..';
482             }
483              
484             }
485              
486             1; # End of Module::Cooker
487             __END__