File Coverage

blib/lib/App/Skeletor.pm
Criterion Covered Total %
statement 27 97 27.8
branch 0 16 0.0
condition 0 7 0.0
subroutine 9 14 64.2
pod 0 4 0.0
total 36 138 26.0


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