File Coverage

blib/lib/Distribution/Cooker.pm
Criterion Covered Total %
statement 146 209 69.8
branch 16 42 38.1
condition 10 26 38.4
subroutine 34 42 80.9
pod 25 25 100.0
total 231 344 67.1


line stmt bran cond sub pod time code
1 8     8   2084821 use v5.26;
  8         30  
2 8     8   3919 use utf8;
  8         2518  
  8         72  
3              
4             package Distribution::Cooker;
5 8     8   4180 use experimental qw(signatures);
  8         17182  
  8         44  
6              
7             our $VERSION = '2.004';
8              
9 8     8   1798 use Carp qw(croak carp);
  8         21  
  8         489  
10 8     8   56 use Cwd;
  8         14  
  8         570  
11 8     8   6607 use Config::IniFiles;
  8         281699  
  8         332  
12 8     8   68 use File::Find;
  8         15  
  8         538  
13 8     8   61 use File::Basename qw(dirname);
  8         10  
  8         441  
14 8     8   48 use File::Path qw(make_path);
  8         13  
  8         414  
15 8     8   4180 use File::Spec::Functions qw(catfile abs2rel);
  8         6599  
  8         640  
16 8     8   3926 use IO::Interactive qw(is_interactive);
  8         8491  
  8         47  
17 8     8   4306 use Mojo::File;
  8         1647378  
  8         536  
18 8     8   5110 use Mojo::Template;
  8         69683  
  8         63  
19 8     8   483 use Mojo::Util qw(decode encode trim dumper);
  8         18  
  8         26209  
20              
21             __PACKAGE__->run( @ARGV ) unless caller;
22              
23             =encoding utf8
24              
25             =head1 NAME
26              
27             Distribution::Cooker - Create a Perl module directory from your own templates
28              
29             =head1 SYNOPSIS
30              
31             # The dist_cooker is a wrapper for the module
32             % dist_cooker Foo::Bar "This module does that" repo_slug
33              
34             # The dist_cooker can prompt for what's missing
35             % dist_cooker Foo::Bar
36             Description> This module does that
37             Repo name> foo-bar
38              
39             # the script just passes @ARGV to the module
40             use Distribution::Cooker;
41             Distribution::Cooker->run( @ARGV );
42              
43             # if you don't like something, subclass and override
44             package Local::Distribution::Cooker {
45             use parent qw(Distribution::Cooker);
46             sub config_file_path { ... }
47             }
48              
49             =head1 DESCRIPTION
50              
51             This module takes a directory of templates and processes them with
52             L. It's specifically tooled toward Perl modules, and
53             the templates are given a set of variables.
54              
55             The templates have special values for C, C, and
56             C since the default L values get confused when
57             there's Perl code outside them.
58              
59             Tags use « (U+00AB) and » (U+00BB), and whole lines use ϕ (U+03D5):
60              
61             This is the « $module » module
62              
63             ϕ This is a line of Perl code
64              
65             My own templates are at L.
66              
67             =head2 Process methods
68              
69             =over 4
70              
71             =item * cook
72              
73             Take the templates and cook them. This version uses L,
74             but you can make a subclass to override it. See the notes about
75             L.
76              
77             I assume my own favorite values, and haven't made these
78             customizable yet.
79              
80             =over 4
81              
82             =item * Your distribution template directory is F<~/.templates/modules>
83              
84             =item * Your module template name is F, which will be moved into place later
85              
86             =back
87              
88             When C processes the templates, it provides definitions for
89             these template variables listed for C.
90              
91             While processing the templates, F ignores F<.git>, F<.svn>, and
92             F directories.
93              
94             =cut
95              
96 0     0 1 0 sub cook ( $self ) {
  0         0  
  0         0  
97 0         0 my $dir = lc $self->dist;
98              
99 0         0 my $cwd = Cwd::getcwd;
100              
101 0         0 make_path( $dir );
102 0 0       0 croak "<$dir> does not exist" unless -d $dir;
103 0 0       0 chdir $dir or croak "chdir $dir: $!";
104              
105 0         0 my $files = $self->template_files;
106              
107 0         0 my $old = catfile( 'lib', $self->module_template_basename );
108 0         0 my $new = catfile( 'lib', $self->module_path );
109              
110 0         0 my $vars = $self->template_vars;
111              
112             my $mt = Mojo::Template->new
113             ->line_start( $self->{line_start} )
114             ->tag_start( $self->{tag_start} )
115             ->tag_end( $self->{tag_end} )
116 0         0 ->vars(1);
117 0         0 foreach my $file ( $files->@* ) {
118 0         0 my $new_file = abs2rel( $file, $self->template_dir );
119              
120 0 0       0 if( -d $file ) {
121 0         0 make_path( $new_file );
122 0         0 next;
123             }
124              
125 0         0 my $contents = decode( 'UTF-8', Mojo::File->new( $file )->slurp );
126 0         0 my $rendered = $mt->vars(1)->render( $contents, $vars );
127 0         0 Mojo::File->new( $new_file )->spew( encode( 'UTF-8', $rendered ) );
128             }
129              
130 0         0 make_path dirname($new);
131 0 0       0 rename $old => $new
132             or croak "Could not rename [$old] to [$new]: $!";
133             }
134              
135             =item * init
136              
137             Initialize the object. There's nothing fancy here, but if you need
138             something more powerful you can create a subclass and run some info here.
139              
140             This step happens right after object create and configuration handling
141             and before the C step. By default, this does nothing.
142              
143             =cut
144              
145 3     3 1 1409 sub init { 1 }
146              
147             =item * new
148              
149             Creates the bare object with the name and email of the module author,
150             looking for values in this order, with any combination for author and
151             email:
152              
153             * take values from the env: DIST_COOKER_AUTHOR and DIST_COOKER_EMAIL
154             * look at git config for C and C
155             * use default values from the method C and C
156              
157             This looks for F<~/.dist_cooker.ini> to read the INI config and add that
158             information to the object.
159              
160             Override C to use a different name.
161              
162              
163             =cut
164              
165 8     8 1 214964 sub new ( $class ) { bless $class->get_config, $class }
  8         29  
  8         17  
  8         77  
166              
167             =item * pre_run
168              
169             Runs right before C does its work.
170              
171             run() calls this method immediately after it creates the object and
172             after it initializes it. By default, this does nothing.
173              
174              
175             =cut
176              
177 3     3 1 2106 sub pre_run { 1 }
178              
179             =item * post_run
180              
181             C calls this method right after it processes the template files.
182             By default, this does nothing.
183              
184             =cut
185              
186 3     3 1 11 sub post_run { 1 }
187              
188             =item * report
189              
190             =cut
191              
192 2     2 1 4 sub report ( $self ) {
  2         4  
  2         36  
193 2 50       666 open my $fh, '>', 'cooker_report.txt' or return;
194              
195 2         12 print { $fh } "$0 " . localtime() . "\n";
  2         113  
196              
197 2         11 print { $fh } dumper( $self->template_vars ), "\n";
  2         32  
198             }
199              
200             =item * run( [ MODULE_NAME, [ DESCRIPTION ] ] )
201              
202             The C method kicks off everything, and gives you a chance to
203             do things between steps/.
204              
205             * create the object
206             * run init (by default, does nothing)
207             * run pre_run (by default, does nothing)
208             * collects information and prompts interactively for what it needs
209             * cooks the templates (~/.templates/modules by default)
210             * run post_run (by default, does nothing)
211             * create cooker_report.txt (it's in .gitignore)
212              
213             If you don't specify the module name, it prompts you. If you don't
214             specify a description, it prompts you.
215              
216             =cut
217              
218 3     3 1 12727 sub run ( $class, $module, @args ) {
  2         11  
  2         24  
  2         6  
  2         21  
219 2         4 my( $description, $repo_name ) = @args;
220              
221 2         10 my $self = $class->new;
222 2         45 $self->init;
223              
224 2         17 $self->pre_run;
225              
226 2   33     24 $self->module( $module || prompt( "Module name" ) );
227 2 50       7 croak( "No module specified!\n" ) unless $self->module;
228 2 50       8 croak( "Illegal module name [$module]\n" )
229             unless $self->module =~ m/ \A [A-Za-z0-9_]+ ( :: [A-Za-z0-9_]+ )* \z /x;
230 2   50     44 $self->description( $description || prompt( "Description" ) || "An undescribed module" );
231              
232 2   66     29 $self->repo_name( $repo_name || prompt( "Repo name" ) );
233              
234 2         9 $self->dist( $self->module_to_distname( $self->module ) );
235              
236 2         25 $self->cook;
237              
238 2         26 $self->post_run;
239              
240 2         15 $self->report;
241              
242 2         826 $self;
243             }
244              
245              
246             =back
247              
248             =head2 Informative methods
249              
250             These provide information the processing needs to do its work.
251              
252             =over 4
253              
254             =item * config_file_name
255              
256             Return the filename (the basename) of the config file. The default is
257             F<.dist_cooker.ini>.
258              
259             =cut
260              
261 8     8 1 69 sub config_file_name { '.dist_cooker.ini' }
262              
263             =item * default_author_email
264              
265             =item * default_author_name
266              
267             Returns the last resort values for author name or email. These are
268             C and C.
269              
270             =item * default_github_user
271              
272             Returns C, which should be easy to find for global
273             search and replace.
274              
275             =cut
276              
277 0     0 1 0 sub default_author_email ( $class ) { 'serpico@example.com' }
  0         0  
  0         0  
  0         0  
278 0     0 1 0 sub default_author_name ( $class ) { 'Frank Serpico' }
  0         0  
  0         0  
  0         0  
279 8     8 1 50 sub default_github_user ( $class ) { $ENV{'GITHUB_USER'} }
  8         75  
  8         30  
  8         112  
280              
281             =item * description( [ DESCRIPTION ] )
282              
283             Returns the description of the module. With an argument, it sets
284             the value.
285              
286             The default name is C. You can override
287             this in a subclass.
288              
289             =cut
290              
291 3     3 1 28 sub description ( $class, @args ) {
  3         8  
  3         13  
  3         5  
292 3 100       38 $class->{description} = $args[0] if defined $args[0];
293 3 50       56 $class->{description} || 'TODO: describe this module'
294             }
295              
296             =item * template_dir
297              
298             Returns the path for the distribution templates. The default is
299             F<$ENV{HOME}/.templates/modules>. If that path is a symlink, this
300             returns that target of that link.
301              
302             =cut
303              
304             sub template_dir {
305 0     0 1 0 my $path = catfile( $ENV{HOME}, '.templates', 'modules' );
306 0 0       0 $path = readlink($path) if -l $path;
307              
308 0 0       0 croak "Couldn't find templates at $path!\n" unless -d $path;
309              
310 0         0 $path;
311             }
312              
313             =item * default_config
314              
315             Returns a hash reference of the config values.
316              
317             * author_name
318             * email
319             * github_user
320             * line_start
321             * tag_end
322             * tag_start
323              
324             This looks for values in this order, and in any combination:
325              
326             * take values from the env: DIST_COOKER_AUTHOR and DIST_COOKER_EMAIL
327             * look at git config for C and C
328             * look in the env for DIST_COOKER_GITHUB_USER
329             * use default values from the method C,
330             C, or C
331              
332             =cut
333              
334             sub _git_user_name {
335 8     8   73454 my $name = `git config user.name`;
336 8         154 $name =~ s/\R//g;
337 8 50       165 trim( $name ) if length $name;
338 8         459 $name;
339             }
340              
341             sub _git_user_email {
342 8     8   62728 my $email = `git config user.email`;
343 8         166 $email =~ s/\R//g;
344 8 50       435 trim( $email ) if defined $email;
345 8         562 $email;
346             }
347              
348 8     8 1 12 sub default_config ( $class ) {
  8         13  
  8         10  
349             my( $author, $email, $github_user ) = (
350             $ENV{DIST_COOKER_AUTHOR} // _git_user_name() // $class->default_author_name,
351             $ENV{DIST_COOKER_EMAIL} // _git_user_email() // $class->default_author_email,
352 8   33     75 $ENV{GITHUB_ACCOUNT_NAME} // $class->default_github_user,
      33        
      33        
      33        
      33        
353             );
354              
355             {
356 8         231 author_name => $author,
357             email => $email,
358             github_user => $github_user,
359             line_start => 'ϕ',
360             tag_end => '»',
361             tag_start => '«',
362             }
363              
364             }
365              
366             =item * dist( [ DIST_NAME ] )
367              
368             Return the dist name. With an argument, set the module name.
369              
370             =cut
371              
372 8     8 1 1291 sub dist ( $self, @args ) {
  8         18  
  8         22  
  8         17  
373 8 100       44 $self->{dist} = $args[0] if defined $args[0];
374 8         72 $self->{dist};
375             }
376              
377             =item * module( [ MODULE_NAME ] )
378              
379             Return the module name. With an argument, set the module name.
380              
381             =cut
382              
383 14     14 1 1320 sub module ( $self, @args ) {
  14         21  
  14         29  
  14         20  
384 14 100       51 $self->{module} = $args[0] if defined $args[0];
385 14         165 $self->{module};
386             }
387              
388             =item * module_path()
389              
390             Return the module path under F. You must have set C
391             already.
392              
393             =cut
394              
395 2     2 1 10 sub module_path ( $self ) {
  2         25  
  2         11  
396 2         17 my @parts = split /::/, $self->{module};
397 2 50       21 return unless @parts;
398 2         8 $parts[-1] .= '.pm';
399 2         185 my $path = catfile( @parts );
400             }
401              
402             =item * module_to_distname( MODULE_NAME )
403              
404             Take a module name, such as C, and turn it into a
405             distribution name, such as C.
406              
407             =cut
408              
409 3     3 1 7735 sub module_to_distname ( $self, $module ) { $module =~ s/::/-/gr }
  3         8  
  3         13  
  3         11  
  3         83  
410              
411             =item * module_template_basename
412              
413             Returns the name of the template file that is the module. The default
414             name is F. This file is moved to the right place under F
415             in the cooked templates.
416              
417             =cut
418              
419 0     0 1 0 sub module_template_basename ( $class ) { 'Foo.pm' }
  0         0  
  0         0  
  0         0  
420              
421             =item * repo_name
422              
423             Returns the repo_name for the project. This defaults to the module
424             name all lowercased with C<::> replaced with C<->. You can override
425             this in a subclass.
426              
427             =cut
428              
429 3     3 1 22 sub repo_name ( $class, @args ) {
  3         5  
  3         7  
  3         7  
430 3 100       22 $class->{repo_name} = $args[0] if defined $args[0];
431 3   33     43 $class->{repo_name} // $class->module =~ s/::/-/gr
432             }
433              
434             =item * template_files
435              
436             Return the list of templates to process. These are all the files in
437             the C excluding F<.git>, F<.svn>, F,
438             and C<.infra>.
439              
440             =cut
441              
442 0     0 1 0 sub template_files ( $self ) {
  0         0  
  0         0  
443 0         0 my @files;
444             my $wanted = sub {
445 0 0   0   0 if( /\A(\.git|\.svn|CVS|\.infra)\b/ ) {
446 0         0 $File::Find::prune = 1;
447 0         0 return;
448             }
449 0         0 push @files, $File::Find::name;
450 0         0 };
451              
452 0         0 find( $wanted, $self->template_dir );
453              
454 0         0 return \@files;
455             }
456              
457             =item * template_vars
458              
459             Returns a hash reference of values to fill in the templates. This hash
460             is passed to the L renderer.
461              
462             =over 4
463              
464             =item author_name => the name of the module author
465              
466             =item cooker_version => version of Distribution::Cooker
467              
468             =item cwd => the current working directory of the new module
469              
470             =item description => the module description
471              
472             =item dir => path to module file
473              
474             =item dist => dist name (Foo-Bar)
475              
476             =item email => author email
477              
478             =item github_user => the GitHub account name
479              
480             =item module => the package name (Foo::Bar)
481              
482             =item module_path => module path under lib/ (Foo/Bar.pm)
483              
484             =item repo_name => lowercase module with hyphens (foo-bar)
485              
486             =item template_path => the source of the template files
487              
488             =item year => the current year
489              
490             =back
491              
492             =cut
493              
494 2     2 1 6 sub template_vars ( $self ) {
  2         5  
  2         6  
495             state $hash = {
496             author_name => $self->{author_name},
497             cooker_version => $VERSION,
498             cwd => cwd(),
499             description => $self->description,
500             dir => catfile( 'lib', dirname( $self->module_path ) ),
501             dist => $self->dist,
502             email => $self->{email},
503             github_user => $self->{github_user},
504 2         5372 module => $self->module,
505             module_path => $self->module_path,
506             repo_name => $self->repo_name,
507             template_path => $self->template_dir,
508             year => ( localtime )[5] + 1900,
509             };
510              
511 2         117 $hash;
512             }
513              
514             =back
515              
516             =head2 Utility methods
517              
518             =over 4
519              
520             =item * config_file_path
521              
522             Returns the path to the config file. By default, this is the value of
523             C under the home directory.
524              
525             =cut
526              
527 8     8 1 12 sub config_file_path ( $class ) {
  8         12  
  8         11  
528 8         33 catfile( $ENV{HOME}, $class->config_file_name )
529             }
530              
531             =item * get_config
532              
533             Returns a hash reference of the config values. These are the values
534             that apply across runs.
535              
536             First, this populates a hash with C, then replaces
537             values from the config file (C).
538              
539             This version uses L
540              
541             [author]
542             name=...
543             email=...
544              
545             [templates]
546             line_start=...
547             tag_end=...
548             tag_start=...
549              
550             =cut
551              
552 8     8 1 33 sub get_config ( $class ) {
  8         41  
  8         15  
553 8         30 my $file = $class->config_file_path;
554              
555 8         28 my $hash = $class->default_config;
556              
557 8         294 my @table = (
558             [ qw( author_name author name ) ],
559             [ qw( author_email author email ) ],
560             [ qw( github_user author github ) ],
561             [ qw( line_start templates line_start ) ],
562             [ qw( tag_end templates tag_end ) ],
563             [ qw( tag_start templates tag_start ) ],
564             );
565              
566 8 50       257 if( -e $file ) {
567 0         0 require Config::IniFiles;
568 0         0 my $config = Config::IniFiles->new( -file => $file );
569              
570 0         0 foreach my $row ( @table ) {
571 0         0 my( $config_name, $section, $field ) = @$row;
572 0 0       0 $hash->{$config_name} = $config->val( $section, $field )
573             if $config->exists( $section, $field );
574             }
575             }
576              
577 8         137 $hash;
578             }
579              
580             =item * prompt( MESSAGE )
581              
582             Show the user MESSAGE, grap a line from STDIN, and return it. If the
583             session is not interactive, this returns nothing.
584              
585             Most things that prompt should have a default value in the case that
586             C cannot work.
587              
588             =cut
589              
590 0     0 1   sub prompt ( @args ) {
  0            
  0            
591 0 0         return unless is_interactive();
592              
593 0           print join "\n", @args;
594 0           print "> ";
595              
596 0           chomp( my $line = );
597 0           $line;
598             }
599              
600             =back
601              
602             =head1 TO DO
603              
604             Right now, C uses the defaults that I like, but
605             that should come from a configuration file.
606              
607             =head1 SEE ALSO
608              
609             Other modules, such as C, do a similar job but don't
610             give you as much flexibility with your templates.
611              
612             =head1 SOURCE AVAILABILITY
613              
614             This module is in Github:
615              
616             http://github.com/briandfoy/distribution-cooker/
617              
618             =head1 AUTHOR
619              
620             brian d foy, C<< >>
621              
622             =head1 COPYRIGHT AND LICENSE
623              
624             Copyright © 2008-2025, brian d foy . All rights reserved.
625              
626             You may redistribute this under the same terms as Perl itself.
627              
628             =cut
629              
630             1;