File Coverage

blib/lib/App/Skeletor.pm
Criterion Covered Total %
statement 21 84 25.0
branch 0 14 0.0
condition 0 5 0.0
subroutine 7 12 58.3
pod 0 4 0.0
total 28 119 23.5


line stmt bran cond sub pod time code
1 1     1   707 use strict;
  1         2  
  1         24  
2 1     1   5 use warnings;
  1         1  
  1         39  
3              
4             package App::Skeletor;
5              
6 1     1   864 use Getopt::Long::Descriptive;
  1         45272  
  1         7  
7 1     1   1075 use File::Share 'dist_dir';
  1         8132  
  1         58  
8 1     1   7 use Module::Runtime 'use_module';
  1         2  
  1         10  
9 1     1   1219 use Path::Tiny;
  1         10066  
  1         71  
10 1     1   820 use Template::Tiny;
  1         1267  
  1         1108  
11              
12             our $VERSION = '0.003';
13              
14             sub getopt_spec {
15             return (
16 0     0 0   'skeletor %o',
17             ['template|t=s', 'Namespace of the project templates', { required=>1 }],
18             ['as|p=s', 'Target namespace of the new project', { required=>1 }],
19             ['directory|d=s', 'Where to build the new project (default: cwd)', {default=>Path::Tiny->cwd}],
20             ['author|a=s', 'Primary author for the project', { required=>1 }],
21             ['year|y=i', 'Copyright year (default: current year)', {default=>(localtime)[5]+1900}],
22             ['overwrite|o', 'overwrite existing files' ],
23             );
24             }
25             sub path_to_share {
26 0     0 0   my $project_template = shift;
27 0           my $tmp;
28 0 0         unless(eval { use_module $project_template }) {
  0            
29             # cant use, assume not loaded.
30 0           $tmp = Path::Tiny->tempdir;
31 0           `curl -L https://cpanmin.us | perl - --verbose --metacpan -l $tmp $project_template`;
32 0           eval "use lib '$tmp/lib/perl5'";
33 0           use_module $project_template;
34             }
35 0           $project_template=~s/::/-/g;
36 0           my $ret = path(dist_dir($project_template), 'skel');
37 0           return ($ret, $tmp);
38             }
39              
40             sub template_as_name {
41 0     0 0   my $name_proto = shift;
42 0           $name_proto=~s/::/-/g;
43 0           return $name_proto;
44             }
45              
46             sub run {
47 0     0 0   my ($class, @args) = @_;
48 0           local @ARGV = @args;
49              
50 0           my ($desc ,@spec) = getopt_spec;
51 0           my ($opt, $usage) = describe_options($desc, @spec, {getopt_conf=>['pass_through']});
52 0           my ($path_to_share, $tmp) = path_to_share($opt->template);
53              
54 0 0         if($opt->template->can('extra_getopt_spec')) {
55 0           my @new_spec = (@spec, $opt->template->extra_getopt_spec);
56 0           local @ARGV = @args;
57 0           ($opt, $usage) = describe_options($desc, @new_spec);
58             }
59              
60             my %template_var_names = (
61 0           (map { $_->{name} => $opt->${\$_->{name}} } @{$usage->{options}}),
  0            
  0            
62             name => template_as_name($opt->as),
63             namespace => $opt->as,
64 0           project_fullpath => do {my $path = path(split('::', $opt->as)); "$path" },
  0            
65             name_lowercase => lc(template_as_name($opt->as)),
66             name_lc => lc(template_as_name($opt->as)),
67             name_lowercase_underscore => do {
68 0           my $val = lc(template_as_name($opt->as));
69 0           $val=~s/-/_/g; $val;
  0            
70             },
71 0           name_lc_underscore => do {
72 0           my $val = lc(template_as_name($opt->as));
73 0           $val=~s/-/_/g; $val;
  0            
74             },
75             );
76              
77 0           my $tt = Template::Tiny->new(TRIM => 1);
78              
79             $path_to_share->visit(sub {
80 0     0     my ($path, $stuff) = @_;
81 0 0         return if $path=~m/\.DS_Store/g;
82 0           my $expanded_path = $path;
83 0           my $target_path = path($opt->directory, $expanded_path->relative($path_to_share));
84 0           my (@vars) = ($target_path=~m/__(?:(?![__]_).)+__/g);
85 0           foreach my $var(@vars) {
86 0           my ($key) = ($var=~m/^__(\w+)__$/);
87 0   0       my $subst = $template_var_names{$key} || die "$key not a defined variable";
88 0           $target_path=~s/${var}/$subst/g;
89             }
90              
91 0           $target_path = path($target_path);
92              
93 0 0 0       if(-e $target_path && !$opt->overwrite) {
94 0           print "$target_path exists, skipping (set --overwrite to rebuild)\n";
95 0           return;
96             }
97            
98 0 0         if($expanded_path->is_file) {
    0          
99 0           $expanded_path->parent->mkpath;
100 0 0         if("$path"=~/\.ttt$/) {
101 0           my $data = $expanded_path->slurp;
102 0           $tt->process(\$data, \%template_var_names, \my $out);
103 0           my ($new_target_path) = ("$target_path" =~m/^(.+)\.ttt$/);
104 0           path($new_target_path)->touchpath;
105 0           my $fh = path($new_target_path)->openw;
106 0           print $fh $out;
107              
108             } else {
109 0           $expanded_path->copy($target_path);
110             }
111             } elsif($path->is_dir) {
112 0           $target_path->mkpath;
113             } else {
114 0           print "Don't know want $path is!";
115             }
116 0           }, {recurse=>1});
117             }
118              
119             caller(1) ? 1 : run(@ARGV);
120              
121             =head1 NAME
122              
123             App::Skeletor - Bootstrap a new project from a shared template
124              
125             =head1 SYNOPSIS
126              
127             From the commandline:
128              
129             skeletor --template Skeltor::Template::Example \
130             --as Local::MyApp \
131             --directory ~/new_projects \
132             --author 'John Napiorkowski ' \
133             --year 2015
134              
135             Bootstrap from URL hosted version:
136              
137             curl -L bit.ly/app-skeletor | perl - \
138             --template Skeletor::Template::Example \
139             --as Local::MyApp \
140             --author 'test author'
141              
142             (Assumes you have `curl` installed, as it is on many modern unix-like systems).
143              
144             =head1 DESCRIPTION
145              
146             When initially setting up a project (like a website build using L or
147             an application that uses L) there is often a number of boilerplate
148             files and directories you need to create before beginning the true work of
149             application building. Additionally, during general development certain types
150             of repeated tasks may occur which would benefit from automation, such as adding
151             new controllers to L or new tables in L. For these types
152             of activities you may find having a code generator speeds up some of the grunt
153             work and promotes uniformity of design. L is such a code generator.
154              
155             The core design is simple. You install L and any of the code
156             patterns on CPAN that you wish to derive projects from (typically using the
157             L namespace, but you can use any namespace, and project
158             patterns can be attached to any arbitirary CPAN module). You then can use the
159             'skeletor' commandline application to generate code into a target directory,
160             using expansion variables to customize how the directories and files are created.
161              
162             For example if you wish to build a new project called C which is
163             based off the L project, you'd install that distribution
164             (via L or whichever tool you prefer) and then type something like the
165             following:
166              
167             skeletor --template Skeltor::Template::Example \
168             --as Local::MyApp \
169             --directory ~/new_projects \
170             --author 'John Napiorkowski ' \
171             --year 2015
172              
173             This would create a new project which consists of directories and files that have been
174             generated and customized based on the commandline options given.
175              
176             Alternatively you may use the URL hosted version of L which will always
177             track the most current release. This allows you to use the tool without installing it
178             first, making it useful for bootstrapping new development environments:
179              
180             curl -L bit.ly/app-skeletor | perl - \
181             --template Skeletor::Template::Example \
182             --as Local::MyApp \
183             --author 'test author'
184              
185             This assumes a working internet connection as well as some version of Perl installed
186             and the C commandline tool installed. In general this should be true for most
187             Unix and Unixlike systems. However running an application directly off the internet
188             this way may violate your companies security policies (and some so common sense) so
189             use this option with caution.
190              
191             B C and C are optional, and default to the current working directory
192             and current year respectively. Some project templates may define additional configuration
193             options, you should review the documentation.
194              
195             B Template distributions may define custom options for the commandline tool. You
196             should review its documentation to make sure you are using it properly.
197              
198             B If you specify a template that is not currently installed, L will
199             download it and install it to a temporary area for one time use. When the application
200             exits, the temporary install is cleaned up.
201              
202             =head1 COMPARISON WITH SIMILAR TOOLS
203              
204             Other similar boilerplate code generators exist on CPAN. For example L has a
205             commandline tool for creating a simple L project. L, L
206             also have dedicated project builders. L differs from those
207             approaches in that it is detached from a particular project domain and thus can
208             be more generically useful. This should give the community the chance for people
209             to suggest their favorite approach to bootstrapping a project without forcing people
210             to accept default options they don't like (current approach tends to be one size fits
211             no one).
212              
213             When comparing L to similar generic code builders like L
214             minting profiles, the main different is that L is dependency manager
215             agnostic (doesn't require L). I think its also a lot more simple than
216             a minting profile.
217              
218             L is probably more comparable with tools like L which
219             at this time are more mature tools. If L has tool many rough edges you
220             may wish to take a look. At this point the main comparison is that I think the way
221             a project skelton is created and organized is significantly easier to understand (famous
222             last words I know :) ). Also L can be run directly from the URL hosted
223             version, if you are not afraid of that!
224              
225             =head1 ARGUMENTS
226              
227             The following configuration options are available, which are used as template
228             variables and directory/file path expansions.
229              
230             =head2 template
231              
232             This is the namespace of the distribution containing the templates for generating
233             a new project. For example, L.
234              
235             If the distribution is not already installed into your @INC, we will download it
236             and install it into a temporary directory. After generating files the temporary
237             install is deleted. Obviously you need a working internet connection for this
238             feature to work.
239              
240             =head2 namespace
241              
242             =head2 as
243              
244             The new project Perl namespace, as you might use it in a 'package' declaration.
245             For example "Local::MyApp". Use this to declare the base package for your new
246             project.
247              
248             =head2 name
249              
250             Derived from L. We substitute '::' for '-' to create a project
251             'name' that is normalized to the CPAN specification. For example 'Local-MyApp'
252              
253             =head2 name_lowercase
254              
255             =head2 name_lc
256              
257             Same as L but using lowercased characters via 'lc'. For example 'local-myapp'.
258              
259             =head2 name_lowercase_underscore
260              
261             =head2 name_lc_underscore
262              
263             Same as L but using lowercase characters via 'lc' and substituting all
264             '-' characters with '_'. For example 'local_myapp'.
265              
266             =head2 project_fullpath
267              
268             Given a L like "Local::My::App":
269              
270             When used as an expansion for a directory expands to a nest of
271             directories such as "Local/My/App". Directories will be created as needed.
272              
273             When used as an expansion for a filename, expands directories as needed and
274             creates a terminal file as needed such as "Local/My/App". Extensions are
275             preserved, for example "${namespace_fullpath}.pm" becomes "Local/My/App.pm".
276              
277             When used as a variable in a template, resolves to a L object that
278             points to the directory+filename as already described.
279              
280             =head2 author
281              
282             Used in templates, set to the project author.
283              
284             =head2 year
285              
286             Year information for setting project copyright, etc. Default is current year.
287              
288             =head1 BUILDING A TEMPLATE
289              
290             An L template is just a CPAN module under any namespace you like
291             (athough Skeletor::Template::* is not a terrible place to put one to make it
292             easier for people to find) with a share/skel directory which should contain
293             asset files (files copied to a new project without alteration), project templates
294             (files that are copied to a new project but are first processed thru L
295             to customize them) and directories. Directory names may also contain expansion
296             variables in order to customize directory layout.
297              
298             There is a reasonable complex example on CPAN under the namespace
299             L which you may refer to as a somewhat complex
300             template that includes all the mentioned types of data. You may find reviewing
301             the example to be a faster way to understand how to make your own project templates.
302              
303             Here is a very simple template with explanation to get you started. The example
304             namespace given is mythical and does not exist on CPAN. In this example a path
305             ending in '/' indicates a directory.
306              
307             Local-Skeltor-Template-MyTemplate/
308             Makefile.PL
309             lib/
310             Local/
311             Skeletor/
312             Template/
313             MyTemplate.pm
314             share/
315             skel/
316             __name__/
317             dist.ini.ttt
318             lib/
319             __project_fullpath__.pm.ttt
320             __project_fullpath__/
321             Web.pm.ttt
322             t/
323             basic.t.ttt
324             share/
325             image.jpg
326             docs.txt
327              
328             So first of all you should note that the template is just a normal CPAN module that
329             declares its installation process and has a file (in this case under
330             'lib/Local/Skeletor/Template/MyTemplate.pm') that should be used to describe what
331             the skeleton does. Also note that you may include skeleton template files under
332             any CPAN module you wish, it doesn't need to be stand alone.
333              
334             The main work happens under 'share/skel/' which is the root directory that
335             L uses when finding a template pattern. The way it works is
336             that we traverse the filesystem recursively and copy directories and files from
337             the project template share/skel/ to the target directory, performing any
338             template expansions as needed. Template variable are defined above. We
339             expand directories and files by matching a template variable in the path
340             using a similar approach as we do variable interpolation in a string. for
341             example a directory called "__name__" would expand to the project name variable
342             (which is derived from the L commandline option.
343              
344             In the case where you need to combine a template variable with other characters
345             you may do so as in the example "__project_fullpath__.pm.ttt".
346              
347             Any file ending in '.ttt' is considered a template and is processed via L
348             expanding variables as described in the previous section. We trucate the '.ttt' as
349             part of the conversion process so a file template "myapp.pm.ttt" becomes 'myapp.pm'
350             in the build directory.
351              
352             =head1 CUSTOMIZING TEMPLATE VARIABLES
353              
354             When you create you template distribution the bulk of you code will go under
355             C. However you may use distribution module (the file for example
356             in C) to customize aspects of the
357             build process. The following methods may be defined in your distribution module.
358              
359             =head2 extra_getopt_spec
360              
361             This method is called in class context and should return an array of options as
362             L describes for C<@opt_spec> (the second of the three
363             arguments one passes to 'describe_options'. You may use this to add custom
364             template and file expansion variables to your template.
365              
366             =head1 AUTHOR
367            
368             John Napiorkowski L
369            
370             =head1 COPYRIGHT & LICENSE
371            
372             Copyright 2015, John Napiorkowski L
373            
374             This library is free software; you can redistribute it and/or modify it under
375             the same terms as Perl itself.
376              
377             =cut