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 11 11 100.0
total 327 375 87.2


line stmt bran cond sub pod time code
1             package Module::Cooker;
2              
3             our $VERSION = 'v0.1_6';
4              
5             #use 5.008_008;
6              
7 6     6   31531 use strict;
  6         17  
  6         298  
8 6     6   36 use warnings FATAL => 'all';
  6         14  
  6         315  
9              
10 6     6   5275 use Data::Dumper;
  6         40040  
  6         512  
11              
12 6     6   57 use Carp;
  6         13  
  6         396  
13 6     6   33 use Cwd ();
  6         12  
  6         109  
14 6     6   12814 use Try::Tiny;
  6         9606  
  6         431  
15              
16 6     6   5944 use version 0.77;
  6         16110  
  6         50  
17              
18 6     6   15645 use ExtUtils::Manifest qw( mkmanifest );
  6         108227  
  6         641  
19 6     6   17574 use Storable (qw( dclone ));
  6         38601  
  6         1291  
20              
21 6     6   70 use File::Path 2.07 qw( make_path );
  6         194  
  6         457  
22 6     6   7984 use File::Spec::Functions qw( catdir catfile );
  6         8217  
  6         519  
23 6     6   6981 use File::Which;
  6         12244  
  6         471  
24              
25 6     6   7650 use POSIX qw( strftime );
  6         63932  
  6         53  
26              
27 6     6   19902 use Template;
  6         184550  
  6         1432  
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 12009 my $class = shift;
57 23         62 my %incoming = @_;
58              
59 23         60 my $self = bless _validate_incoming( \%incoming ), $class;
60              
61             # be lazy and automatically generate accessors.
62             # Perl Monk GrandFather should appreciate this. :)
63 12         22 foreach my $attribute ( keys( %{$self} ) ) {
  12         83  
64 108 50       217 next if $attribute =~ /^_/;
65 108 100       473 next if $self->can($attribute);
66              
67 6     6   74 no strict 'refs';
  6         15  
  6         19296  
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         357 *{ __PACKAGE__ . "::$attribute" } = sub {
73 29     29   6580 my $self = shift;
74              
75 29 100       994 croak "Can't set read-only attribute: $attribute" if @_;
76              
77 22         118 return $self->{$attribute};
78 42         161 };
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         48 $self->{_basename_dir} = $self->_basename_dir;
86              
87 12         36 $self->{_made_dist_dir} = 0;
88 12         30 $self->{_templates} = {};
89 12         29 $self->{_template_dirs} = [];
90              
91 12         48 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   30 my $incoming = shift;
99              
100 23         39 my $args = {};
101 23         35 for ( keys( %{$defaults} ) ) {
  23         96  
102 207   100     994 $args->{$_} = delete( $incoming->{$_} ) || $defaults->{$_};
103             }
104 0         0 croak 'Unknown parameter(s): ' . join( ', ', keys( %{$incoming} ) )
  23         73  
105 23 50       47 if keys( %{$incoming} );
106              
107 23 50       72 croak "Parameter 'package' must be supplied"
108             unless $args->{package};
109 23 100       1776 croak "Invalid package name: $args->{package}"
110             unless $args->{package} =~ /\A$module_name_rx\z/o;
111              
112 12 50       173 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         29 for (@boolean_params) {
117 12         57 my $orig = $args->{$_};
118 12   50     70 $args->{$_} = !!$args->{$_} || 0;
119 12 50       63 croak "Boolean param $_ must be '0' or '1': $orig ne $args->{$_}"
120             unless $args->{$_} eq $orig;
121             }
122              
123             try {
124 12     12   787 version->parse( $args->{version} );
125             }
126             catch {
127 0     0   0 croak $_;
128 12         95 };
129              
130 12 50 50     313 croak "Param 'extravars' must be a hashref"
131             unless ( ref( $args->{extravars} ) || '' ) eq 'HASH';
132              
133 12 50 50     60 croak "Param 'localdirs' must be an arrayref"
134             unless ( ref( $args->{localdirs} ) || '' ) eq 'ARRAY';
135              
136 12         40 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   35 my $self = shift;
143              
144 27         150 my @parts = split( /::/, $self->{package} );
145 27         41 pop(@parts); # remove basename
146              
147 27         57 unshift( @parts, 'lib' );
148              
149 27         115 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   475 my $package = __PACKAGE__;
160              
161 13         54 $package =~ s/::/\//g;
162 13         46 my $packpath = $INC{ join( '.', $package, 'pm' ) };
163 13         54 $packpath =~ s/\.pm$//;
164              
165 13         5761 my $realpath = Cwd::realpath($packpath);
166              
167 13         86 return $realpath;
168             }
169              
170             # create the dist dir in the cwd
171             sub _make_dist_dir {
172 1     1   4 my $self = shift;
173              
174             # croak if a fatal error occurs, better to die here than later
175             try {
176 1 50   1   33 make_path( $self->dist_name ) or die $!;
177 1         5 $self->{_made_dist_dir} = 1;
178             }
179             catch {
180 0     0   0 die "Can not make distribution dir: $_";
181 1         23 };
182              
183 1         17 return;
184             }
185              
186             # builds a hash that will be passed to Template
187             sub _package_info {
188 13     13   19 my $self = shift;
189              
190 13         49 my $module_path = catfile( $self->_lib_path, $self->module_name );
191              
192 13         169 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         135 return $package;
205             }
206              
207             # builds a hash that will be passed to Template
208             sub _author_info {
209 13     13   24 my $self = shift;
210              
211 13         61 my $author = {
212             name => $self->{author},
213             email => $self->{email},
214             };
215              
216 13         52 return $author;
217             }
218              
219             sub _include_path {
220 0     0   0 my $self = shift;
221              
222 0 0       0 return $self->{_include_path} if $self->{_include_path};
223             }
224              
225             sub _process_template {
226 13     13   23 my $self = shift;
227 13         116 my %args = @_;
228              
229             # Template will automatically create missing dirs, but doing this
230             # allows for bailing out if the main dist dir already exists.
231             # having the test here ensures catching such a condition at a
232             # common point that is less likely to be skipped over.
233 13 100       45 if ( !$self->{_made_dist_dir} ) {
234 2         9 my $direxists = !!( -d $self->dist_name );
235 2 100       7 die "Distribution directory already exists: " . $self->dist_name
236             if -d $self->dist_name;
237              
238             # dist dir does not exist. this also sets _made_dist_dir
239 1         9 $self->_make_dist_dir;
240             }
241              
242 12 50       35 die "Template name missing!" unless $args{template};
243              
244 12         15 my $outfile;
245 12 100       41 if ( $args{template} =~ /^Module\.pm$/ ) { # gets speical treatment
246 1         5 $outfile = catfile( $self->_lib_path, $self->module_name );
247             } else {
248 11         18 $outfile = $args{template};
249             }
250              
251             # need to add logic to add paths for INCLUDE directives to INCLIDE_PATH
252              
253             # this is a seperate stucture to all for a future method to let
254             # users specify additional config options similar to how
255             # extravars work.
256 12         36 my $tt_config = {
257             TRIM => 0,
258             PRE_CHOMP => 0,
259             POST_CHOMP => 0,
260 12         26 INCLUDE_PATH => \@{ $self->profile_dirs },
261             OUTPUT_PATH => $self->dist_name,
262             };
263 12         126 my $t = Template->new($tt_config);
264              
265 12         181607 my $vars = $self->template_data;
266              
267 12 50       64 $t->process( $args{template}, $vars, $outfile ) || die $t->error . "\n";
268              
269 12         19811 return;
270             }
271              
272             sub _gather_profile {
273 6     6   10 my $self = shift;
274 6         32 my %args = @_;
275              
276 6         10 my $dir = $args{abs_path};
277 6         10 my $subdir = $args{subdir_path};
278              
279 6 50       170 die "Can't find dir: $dir\n" unless -d $dir;
280              
281 6 50       308 opendir( my $dh, $dir ) or die "can't opendir $dir: $!";
282 6         188 my @files = readdir($dh);
283 6         65 closedir $dh;
284              
285 6         16 my $std_dir = $self->std_profiles_dir;
286 6 50       84 my $src_type = ( $dir =~ /^(?:\Q$std_dir\E)/ ) ? 'standard' : 'local';
287              
288 6         15 for my $fname (@files) {
289 40 100       125 next if $fname =~ m{^\.{1,2}\z};
290              
291 28         299 my $fpath = File::Spec->catfile( $dir, $fname );
292              
293             # $fpath = readlink($fpath) if -l $fpath;
294              
295             # don't follow symlinks for now.
296             # use nolinks param to control this later if desired.
297 28 50       791 next if -l $fpath;
298              
299 28 100       825 if ( -d $fpath ) {
300 4 50       17 if ( $self->{nosubs} ) {
301 0         0 warn "Skipping profile sub-directory: $fpath\n";
302 0         0 next;
303             }
304              
305 4 50       14 my $subpath = $subdir ? catdir( $subdir, $fname ) : $fname;
306              
307             try {
308 4     4   131 push( @{ $self->{_template_dirs} }, $subpath );
  4         18  
309              
310             # trust perl's deep recursion detection
311 4         18 $self->_gather_profile(
312             abs_path => $fpath,
313             subdir_path => $subpath
314             );
315             }
316             catch {
317 0     0   0 die $_;
318 4         36 };
319              
320 4         61 next;
321             }
322              
323 24 50       1046 next unless -f $fpath;
324              
325 24 100       133 my $template = $subdir ? catfile( $subdir, $fname ) : $fname;
326             # $self->{_templates}{$template} = catfile( $dir, $subdir )
327 24 50       208 $self->{_templates}{$template} = $src_type
328             unless $self->{_templates}{$template};
329             }
330              
331 6         45 return;
332             }
333              
334             # future use? considering an option to pass Template through perltidy
335             sub _perltidy_cmd {
336 0     0   0 my $tidy = which('perltidy');
337              
338 0         0 return $tidy;
339             }
340              
341             # override the default accessor generation to ensure a copy is made
342             sub extravars {
343 4     4 1 1456 my $self = shift;
344              
345 4 100       131 croak "Can't set read-only attribute: extravars" if @_;
346              
347 3         8 my $tmp = $self->{extravars};
348 3         207 my $extravars = dclone($tmp);
349              
350 3 50       20 return wantarray ? %{$extravars} : $extravars;
  0         0  
351             }
352              
353             # override the default accessor generation to ensure a copy is made
354             sub localdirs {
355 19     19 1 1057 my $self = shift;
356              
357 19 100       183 croak "Can't set read-only attribute: localdirs" if @_;
358              
359 18         29 my @localdirs = @{ $self->{localdirs} };
  18         53  
360              
361 18 100       79 return wantarray ? @localdirs : \@localdirs;
362             }
363              
364             # return a list of dirs that actually contain the requested profile
365             sub profile_dirs {
366 16     16 1 3180 my $self = shift;
367              
368 16 100       241 croak "Can't set read-only method: profile_dirs" if @_;
369              
370 15         57 my @searchdirs = $self->localdirs;
371 15         49 push( @searchdirs, catdir( $self->std_profiles_dir ) );
372              
373 15         32 my @profile_dirs;
374 15         36 for (@searchdirs) {
375 15         52 my $profile_dir = catdir( $_, $self->profile );
376 15 50       967 push( @profile_dirs, $profile_dir ) if -d $profile_dir;
377             }
378              
379 15 50       109 return wantarray ? @profile_dirs : \@profile_dirs;
380             }
381              
382             sub basename_dir {
383 23     23 1 1211 my $self = shift;
384              
385 23 100       164 croak "Can't set read-only method: basename_dir" if @_;
386              
387 22         151 return $self->{_basename_dir};
388             }
389              
390             # builds path to where standard templates located
391             sub std_profiles_dir {
392 21     21 1 38 my $self = shift;
393              
394             # my $dir = catdir( $self->basename_dir, 'profiles', $self->{profile} );
395 21         87 my $dir = catdir( $self->basename_dir, 'profiles' );
396              
397 21 50       1113 -d $dir ? return $dir : return;
398             }
399              
400             # builds list of final attribute values
401             sub summary {
402 2     2 1 1145 my $self = shift;
403              
404 2 100       130 croak "Can't set read-only method: summary" if @_;
405              
406 1         3 my $tmp = {};
407 1         2 for ( keys( %{$self} ) ) {
  1         7  
408 13 100       31 next if /^_/; # we only want the attributes, not internals
409 9         28 $tmp->{$_} = $self->{$_};
410             }
411              
412 1         130 my $summary = dclone($tmp);
413              
414             # sorry, Will, but i think this is handy. :)
415 1 50       11 return wantarray ? %{$summary} : $summary;
  0         0  
416             }
417              
418             # simple transform: i.e. Foo::Bar -> Foo-Bar
419             sub dist_name {
420 35     35 1 1217 my $self = shift;
421              
422 35 100       202 croak "Can't set read-only method: dist_name" if @_;
423              
424 34         61 my $dname = $self->{package};
425 34         123 $dname =~ s/::/-/g;
426              
427 34         562 return $dname;
428             }
429              
430             # generates main module name. i.e. Foo::Bar -> Bar.pm
431             sub module_name {
432 29     29 1 1325 my $self = shift;
433              
434 29 100       185 croak "Can't set read-only method: module_name" if @_;
435              
436 28         102 my @parts = split( /::/, $self->{package} );
437              
438 28         5088 return join( '.', pop(@parts), 'pm' );
439             }
440              
441             sub template_data {
442 14     14 1 1120 my $self = shift;
443              
444 14 100       179 croak "Can't set read-only method: template_data" if @_;
445              
446 13         89 my $tmp = {
447             author => $self->_author_info,
448             package => $self->_package_info,
449             modcooker => {
450             version => $VERSION,
451             perlver => $],
452             },
453             extra => $self->{extravars},
454             };
455              
456 13         791 my $tdata = dclone($tmp);
457              
458 13 50       155 return wantarray ? %{$tdata} : $tdata;
  0         0  
459             }
460              
461             # the ultimate goal of this module
462             sub cook {
463 2     2 1 34021 my $self = shift;
464              
465             # clear our template list
466 2         12 $self->{_templates} = {};
467 2         16 $self->{_template_dirs} = [];
468              
469 2         8 for ( @{ $self->profile_dirs } ) {
  2         15  
470 2         300 my $dir = Cwd::realpath($_);
471 2         16 $self->_gather_profile( abs_path => $dir, subdir_path => undef );
472             }
473              
474             #warn Dumper($self->{_templates});
475 2         5 foreach ( keys( %{ $self->{_templates} } ) ) {
  2         12  
476 13         1239 $self->_process_template( template => $_ );
477             }
478              
479 1 50       62 if ( !-f catfile( $self->dist_name, 'MANIFEST' ) ) {
480 1         4 chdir $self->dist_name;
481 1         19 mkmanifest();
482 1         3670 chdir '..';
483             }
484              
485             }
486              
487             1; # End of Module::Cooker
488             __END__