tests now pass!
[catagits/Catalyst-Devel.git] / lib / Catalyst / Helper.pm
1 package Catalyst::Helper;
2
3 use strict;
4 use warnings;
5 use base 'Class::Accessor::Fast';
6 use Config;
7 use File::Spec;
8 use File::Path;
9 use FindBin;
10 use IO::File;
11 use POSIX 'strftime';
12 use Template;
13 use Catalyst::Devel;
14 use Catalyst::Utils;
15 use Catalyst::Exception;
16 use Path::Class qw/dir file/;
17 use File::ShareDir qw/dist_dir/;
18 use Moose;
19 use aliased 'Path::Class::Dir';
20
21
22 my %cache;
23
24 =head1 NAME
25
26 Catalyst::Helper - Bootstrap a Catalyst application
27
28 =head1 SYNOPSIS
29
30   catalyst.pl <myappname>
31
32 =cut
33
34
35
36 sub get_sharedir_file {
37     my ($self, @filename) = @_;
38     my $dist_dir;
39     if (-d "inc/.author") { # Can't use sharedir if we're in a checkout
40                             # this feels horrible, better ideas?
41         $dist_dir = 'share';
42     }
43     else {
44         $dist_dir = dist_dir('Catalyst-Devel');
45     }
46     my $file = file( $dist_dir, @filename);
47     my $contents = $file->slurp;
48     return $contents;
49 }
50
51 # Do not touch this method, *EVER*, it is needed for back compat.
52 sub get_file {
53     my ( $self, $class, $file ) = @_;
54     unless ( $cache{$class} ) {
55         local $/;
56         $cache{$class} = eval "package $class; <DATA>";
57     }
58     my $data = $cache{$class};
59     my @files = split /^__(.+)__\r?\n/m, $data;
60     shift @files;
61     while (@files) {
62         my ( $name, $content ) = splice @files, 0, 2;
63         return $content if $name eq $file;
64     }
65     return 0;
66 }
67
68
69 sub mk_app {
70     my ( $self, $name ) = @_;
71
72     # Needs to be here for PAR
73     require Catalyst;
74
75     if ( $name =~ /[^\w:]/ || $name =~ /^\d/ || $name =~ /\b:\b|:{3,}/) {
76         warn "Error: Invalid application name.\n";
77         return 0;
78     }
79     $self->{name            } = $name;
80     $self->{dir             } = $name;
81     $self->{dir             } =~ s/\:\:/-/g;
82     $self->{script          } = File::Spec->catdir( $self->{dir}, 'script' );
83     $self->{appprefix       } = Catalyst::Utils::appprefix($name);
84     $self->{appenv          } = Catalyst::Utils::class2env($name);
85     $self->{startperl       } = -r '/usr/bin/env'
86                                 ? '#!/usr/bin/env perl'
87                                 : "#!$Config{perlpath} -w";
88     $self->{scriptgen       } = $Catalyst::Devel::CATALYST_SCRIPT_GEN || 4;
89     $self->{catalyst_version} = $Catalyst::VERSION;
90     $self->{author          } = $self->{author} = $ENV{'AUTHOR'}
91       || eval { @{ [ getpwuid($<) ] }[6] }
92       || 'Catalyst developer';
93
94     my $gen_scripts  = ( $self->{makefile} ) ? 0 : 1;
95     my $gen_makefile = ( $self->{scripts} )  ? 0 : 1;
96     my $gen_app = ( $self->{scripts} || $self->{makefile} ) ? 0 : 1;
97
98     if ($gen_app) {
99         $self->_mk_dirs;
100         $self->_mk_config;
101         $self->_mk_appclass;
102         $self->_mk_rootclass;
103         $self->_mk_readme;
104         $self->_mk_changes;
105         $self->_mk_apptest;
106         $self->_mk_images;
107         $self->_mk_favicon;
108     }
109     if ($gen_makefile) {
110         $self->_mk_makefile;
111     }
112     if ($gen_scripts) {
113         $self->_mk_cgi;
114         $self->_mk_fastcgi;
115         $self->_mk_server;
116         $self->_mk_test;
117         $self->_mk_create;
118         $self->_mk_information;
119     }
120     return $self->{dir};
121 }
122
123 sub mk_component {
124     my $self = shift;
125     my $app  = shift;
126     $self->{app} = $app;
127     $self->{author} = $self->{author} = $ENV{'AUTHOR'}
128       || eval { @{ [ getpwuid($<) ] }[6] }
129       || 'A clever guy';
130     $self->{base} ||= File::Spec->catdir( $FindBin::Bin, '..' );
131     unless ( $_[0] =~ /^(?:model|view|controller)$/i ) {
132         my $helper = shift;
133         my @args   = @_;
134         my $class  = "Catalyst::Helper::$helper";
135         eval "require $class";
136
137         if ($@) {
138             Catalyst::Exception->throw(
139                 message => qq/Couldn't load helper "$class", "$@"/ );
140         }
141
142         if ( $class->can('mk_stuff') ) {
143             return 1 unless $class->mk_stuff( $self, @args );
144         }
145     }
146     else {
147         my $type   = shift;
148         my $name   = shift || "Missing name for model/view/controller";
149         my $helper = shift;
150         my @args   = @_;
151        return 0 if $name =~ /[^\w\:]/;
152         $type              = lc $type;
153         $self->{long_type} = ucfirst $type;
154         $type              = 'M' if $type =~ /model/i;
155         $type              = 'V' if $type =~ /view/i;
156         $type              = 'C' if $type =~ /controller/i;
157         my $appdir = File::Spec->catdir( split /\:\:/, $app );
158         my $test_path =
159           File::Spec->catdir( $FindBin::Bin, '..', 'lib', $appdir, 'C' );
160         $type = $self->{long_type} unless -d $test_path;
161         $self->{type}  = $type;
162         $self->{name}  = $name;
163         $self->{class} = "$app\::$type\::$name";
164
165         # Class
166         my $path =
167           File::Spec->catdir( $FindBin::Bin, '..', 'lib', $appdir, $type );
168         my $file = $name;
169         if ( $name =~ /\:/ ) {
170             my @path = split /\:\:/, $name;
171             $file = pop @path;
172             $path = File::Spec->catdir( $path, @path );
173         }
174         $self->mk_dir($path);
175         $file = File::Spec->catfile( $path, "$file.pm" );
176         $self->{file} = $file;
177
178         # Test
179         $self->{test_dir} = File::Spec->catdir( $FindBin::Bin, '..', 't' );
180         $self->{test}     = $self->next_test;
181
182         # Helper
183         if ($helper) {
184             my $comp  = $self->{long_type};
185             my $class = "Catalyst::Helper::$comp\::$helper";
186             eval "require $class";
187
188             if ($@) {
189                 Catalyst::Exception->throw(
190                     message => qq/Couldn't load helper "$class", "$@"/ );
191             }
192
193             if ( $class->can('mk_compclass') ) {
194                 return 1 unless $class->mk_compclass( $self, @args );
195             }
196             else { return 1 unless $self->_mk_compclass }
197
198             if ( $class->can('mk_comptest') ) {
199                 $class->mk_comptest( $self, @args );
200             }
201             else { $self->_mk_comptest }
202         }
203
204         # Fallback
205         else {
206             return 1 unless $self->_mk_compclass;
207             $self->_mk_comptest;
208         }
209     }
210     return 1;
211 }
212
213 sub mk_dir {
214     my ( $self, $dir ) = @_;
215     if ( -d $dir ) {
216         print qq/ exists "$dir"\n/;
217         return 0;
218     }
219     if ( mkpath [$dir] ) {
220         print qq/created "$dir"\n/;
221         return 1;
222     }
223
224     Catalyst::Exception->throw( message => qq/Couldn't create "$dir", "$!"/ );
225 }
226
227 sub mk_file {
228     my ( $self, $file, $content ) = @_;
229     if ( -e $file && -s _ ) {
230         print qq/ exists "$file"\n/;
231         return 0
232           unless ( $self->{'.newfiles'}
233             || $self->{scripts}
234             || $self->{makefile} );
235         if ( $self->{'.newfiles'} ) {
236             if ( my $f = IO::File->new("< $file") ) {
237                 my $oldcontent = join( '', (<$f>) );
238                 return 0 if $content eq $oldcontent;
239             }
240             $file .= '.new';
241         }
242     }
243     if ( my $f = IO::File->new("> $file") ) {
244         binmode $f;
245         print $f $content;
246         print qq/created "$file"\n/;
247         return 1;
248     }
249
250     Catalyst::Exception->throw( message => qq/Couldn't create "$file", "$!"/ );
251 }
252
253 sub next_test {
254     my ( $self, $tname ) = @_;
255     if ($tname) { $tname = "$tname.t" }
256     else {
257         my $name   = $self->{name};
258         my $prefix = $name;
259         $prefix =~ s/::/-/g;
260         $prefix         = $prefix;
261         $tname          = $prefix . '.t';
262         $self->{prefix} = $prefix;
263         $prefix         = lc $prefix;
264         $prefix =~ s/-/\//g;
265         $self->{uri} = "/$prefix";
266     }
267     my $dir  = $self->{test_dir};
268     my $type = lc $self->{type};
269     $self->mk_dir($dir);
270     return File::Spec->catfile( $dir, "$type\_$tname" );
271 }
272
273 # Do not touch this method, *EVER*, it is needed for back compat.
274 ## addendum: we had to split this method so we could have backwards
275 ## compatability.  otherwise, we'd have no way to pass stuff from __DATA__ 
276
277 sub render_file {
278     my ( $self, $file, $path, $vars ) = @_;
279     my $template = $self->get_file( ( caller(0) )[0], $file );
280     $self->render_file_contents($template, $path, $vars);
281 }
282
283 sub render_sharedir_file {
284     my ( $self, $file, $path, $vars ) = @_;
285     my $template = $self->get_sharedir_file( $file );
286     $self->render_file_contents($template, $path, $vars);
287 }
288
289 sub render_file_contents {
290     my ( $self, $template, $path, $vars ) = @_;
291     $vars ||= {};
292     my $t = Template->new;
293     return 0 unless $template;
294     my $output;
295     $t->process( \$template, { %{$self}, %$vars }, \$output )
296       || Catalyst::Exception->throw(
297         message => qq/Couldn't process "$template", / . $t->error() );
298     $self->mk_file( $path, $output );
299 }
300
301 sub _mk_information {
302     my $self = shift;
303     print qq/Change to application directory and Run "perl Makefile.PL" to make sure your install is complete\n/;
304 }
305
306 sub _mk_dirs {
307     my $self = shift;
308     $self->mk_dir( $self->{dir} );
309     $self->mk_dir( $self->{script} );
310     $self->{lib} = File::Spec->catdir( $self->{dir}, 'lib' );
311     $self->mk_dir( $self->{lib} );
312     $self->{root} = File::Spec->catdir( $self->{dir}, 'root' );
313     $self->mk_dir( $self->{root} );
314     $self->{static} = File::Spec->catdir( $self->{root}, 'static' );
315     $self->mk_dir( $self->{static} );
316     $self->{images} = File::Spec->catdir( $self->{static}, 'images' );
317     $self->mk_dir( $self->{images} );
318     $self->{t} = File::Spec->catdir( $self->{dir}, 't' );
319     $self->mk_dir( $self->{t} );
320
321     $self->{class} = File::Spec->catdir( split( /\:\:/, $self->{name} ) );
322     $self->{mod} = File::Spec->catdir( $self->{lib}, $self->{class} );
323     $self->mk_dir( $self->{mod} );
324
325     if ( $self->{short} ) {
326         $self->{m} = File::Spec->catdir( $self->{mod}, 'M' );
327         $self->mk_dir( $self->{m} );
328         $self->{v} = File::Spec->catdir( $self->{mod}, 'V' );
329         $self->mk_dir( $self->{v} );
330         $self->{c} = File::Spec->catdir( $self->{mod}, 'C' );
331         $self->mk_dir( $self->{c} );
332     }
333     else {
334         $self->{m} = File::Spec->catdir( $self->{mod}, 'Model' );
335         $self->mk_dir( $self->{m} );
336         $self->{v} = File::Spec->catdir( $self->{mod}, 'View' );
337         $self->mk_dir( $self->{v} );
338         $self->{c} = File::Spec->catdir( $self->{mod}, 'Controller' );
339         $self->mk_dir( $self->{c} );
340     }
341     my $name = $self->{name};
342     $self->{rootname} =
343       $self->{short} ? "$name\::C::Root" : "$name\::Controller::Root";
344     $self->{base} = File::Spec->rel2abs( $self->{dir} );
345 }
346
347 sub _mk_appclass {
348     my $self = shift;
349     my $mod  = $self->{mod};
350     $self->render_file( 'appclass.tt', "$mod.pm" );
351 }
352
353 sub _mk_rootclass {
354     my $self = shift;
355     $self->render_file( 'rootclass.tt',
356         File::Spec->catfile( $self->{c}, "Root.pm" ) );
357 }
358
359 sub _mk_makefile {
360     my $self = shift;
361     $self->{path} = File::Spec->catfile( 'lib', split( '::', $self->{name} ) );
362     $self->{path} .= '.pm';
363     my $dir = $self->{dir};
364     $self->render_file( 'makefile.tt', "$dir\/Makefile.PL" );
365
366     if ( $self->{makefile} ) {
367
368         # deprecate the old Build.PL file when regenerating Makefile.PL
369         $self->_deprecate_file(
370             File::Spec->catdir( $self->{dir}, 'Build.PL' ) );
371     }
372 }
373
374 sub _mk_config {
375     my $self      = shift;
376     my $dir       = $self->{dir};
377     my $appprefix = $self->{appprefix};
378     $self->render_file( 'config.tt',
379         File::Spec->catfile( $dir, "$appprefix.conf" ) );
380 }
381
382 sub _mk_readme {
383     my $self = shift;
384     my $dir  = $self->{dir};
385     $self->render_file( 'readme.tt', "$dir\/README" );
386 }
387
388 sub _mk_changes {
389     my $self = shift;
390     my $dir  = $self->{dir};
391     my $time = strftime('%Y-%m-%d %H:%M:%S', localtime time);
392     $self->render_file( 'changes.tt', "$dir\/Changes", { time => $time } );
393 }
394
395 sub _mk_apptest {
396     my $self = shift;
397     my $t    = $self->{t};
398     $self->render_file( 'apptest.tt',         "$t\/01app.t" );
399     $self->render_file( 'podtest.tt',         "$t\/02pod.t" );
400     $self->render_file( 'podcoveragetest.tt', "$t\/03podcoverage.t" );
401 }
402
403 sub _mk_cgi {
404     my $self      = shift;
405     my $script    = $self->{script};
406     my $appprefix = $self->{appprefix};
407     $self->render_file( 'cgi.tt', "$script\/$appprefix\_cgi.pl" );
408     chmod 0700, "$script/$appprefix\_cgi.pl";
409 }
410
411 sub _mk_fastcgi {
412     my $self      = shift;
413     my $script    = $self->{script};
414     my $appprefix = $self->{appprefix};
415     $self->render_file( 'fastcgi.tt', "$script\/$appprefix\_fastcgi.pl" );
416     chmod 0700, "$script/$appprefix\_fastcgi.pl";
417 }
418
419 sub _mk_server {
420     my $self      = shift;
421     my $script    = $self->{script};
422     my $appprefix = $self->{appprefix};
423     $self->render_file( 'server.tt', "$script\/$appprefix\_server.pl" );
424     chmod 0700, "$script/$appprefix\_server.pl";
425 }
426
427 sub _mk_test {
428     my $self      = shift;
429     my $script    = $self->{script};
430     my $appprefix = $self->{appprefix};
431     $self->render_file( 'test.tt', "$script/$appprefix\_test.pl" );
432     chmod 0700, "$script/$appprefix\_test.pl";
433 }
434
435 sub _mk_create {
436     my $self      = shift;
437     my $script    = $self->{script};
438     my $appprefix = $self->{appprefix};
439     $self->render_file( 'create.tt', "$script\/$appprefix\_create.pl" );
440     chmod 0700, "$script/$appprefix\_create.pl";
441 }
442
443 sub _mk_compclass {
444     my $self = shift;
445     my $file = $self->{file};
446     return $self->render_file( 'compclass.tt', "$file" );
447 }
448
449 sub _mk_comptest {
450     my $self = shift;
451     my $test = $self->{test};
452     $self->render_file( 'comptest.tt', "$test" );
453 }
454
455 sub _mk_images {
456     my $self   = shift;
457     my $images = $self->{images};
458     my @images =
459       qw/catalyst_logo btn_120x50_built btn_120x50_built_shadow
460       btn_120x50_powered btn_120x50_powered_shadow btn_88x31_built
461       btn_88x31_built_shadow btn_88x31_powered btn_88x31_powered_shadow/;
462     for my $name (@images) {
463         my $image = $self->get_sharedir_file("root", "$name.png");
464         $self->mk_file( File::Spec->catfile( $images, "$name.png" ), $image );
465     }
466 }
467
468 sub _mk_favicon {
469     my $self    = shift;
470     my $root    = $self->{root};
471     my $favicon = $self->get_sharedir_file( 'root', 'favicon.ico' );
472     my $dest = File::Spec->catfile( $root, "favicon.ico" );
473     $self->mk_file( $dest, $favicon );
474
475 }
476
477 sub _deprecate_file {
478     my ( $self, $file ) = @_;
479     if ( -e $file ) {
480         my $oldcontent;
481         if ( my $f = IO::File->new("< $file") ) {
482             $oldcontent = join( '', (<$f>) );
483         }
484         my $newfile = $file . '.deprecated';
485         if ( my $f = IO::File->new("> $newfile") ) {
486             binmode $f;
487             print $f $oldcontent;
488             print qq/created "$newfile"\n/;
489             unlink $file;
490             print qq/removed "$file"\n/;
491             return 1;
492         }
493         Catalyst::Exception->throw(
494             message => qq/Couldn't create "$file", "$!"/ );
495     }
496 }
497
498
499 ## this is so you don't have to do make install after every change to test
500 sub _find_share_dir {
501   my ($self, $args) = @_;
502   my $share_name = $self->name;
503   if ($share_name =~ s!^/(.*?)/!!) {
504     my $dist = $1;
505     $args->{share_base_dir} = eval {
506         Dir->new(File::ShareDir::dist_dir($dist))
507            ->subdir('share');
508     };
509     if ($@) {
510         # not installed
511         my $file = __FILE__;
512         my $dir = Dir->new(dirname($file));
513         my $share_base;
514         while ($dir->parent) {
515             if (-d $dir->subdir('share') && -d $dir->subdir('share')->subdir('root')) {
516                 $share_base = $dir->subdir('share')->subdir('root');
517                 last;
518             }
519             $dir = $dir->parent;
520         }
521         confess "could not find sharebase by recursion. ended up at $dir, from $file"
522           unless $share_base;
523         $args->{share_base_dir} = $share_base; 
524     }
525   }
526   my $base = $args->{share_base_dir}->subdir($share_name);
527   confess "No such share base directory ${base}"
528     unless -d $base;
529   $self->share_dir($base);
530 };
531
532
533
534 =head1 DESCRIPTION
535
536 This module is used by B<catalyst.pl> to create a set of scripts for a
537 new catalyst application. The scripts each contain documentation and
538 will output help on how to use them if called incorrectly or in some
539 cases, with no arguments.
540
541 It also provides some useful methods for a Helper module to call when
542 creating a component. See L</METHODS>.
543
544 =head1 SCRIPTS
545
546 =head2 _create.pl
547
548 Used to create new components for a catalyst application at the
549 development stage.
550
551 =head2 _server.pl
552
553 The catalyst test server, starts an HTTPD which outputs debugging to
554 the terminal.
555
556 =head2 _test.pl
557
558 A script for running tests from the command-line.
559
560 =head2 _cgi.pl
561
562 Run your application as a CGI.
563
564 =head2 _fastcgi.pl
565
566 Run the application as a fastcgi app. Either by hand, or call this
567 from FastCgiServer in your http server config.
568
569 =head1 HELPERS
570
571 The L</_create.pl> script creates application components using Helper
572 modules. The Catalyst team provides a good number of Helper modules
573 for you to use. You can also add your own.
574
575 Helpers are classes that provide two methods.
576
577     * mk_compclass - creates the Component class
578     * mk_comptest  - creates the Component test
579
580 So when you call C<scripts/myapp_create.pl view MyView TT>, create
581 will try to execute Catalyst::Helper::View::TT->mk_compclass and
582 Catalyst::Helper::View::TT->mk_comptest.
583
584 See L<Catalyst::Helper::View::TT> and
585 L<Catalyst::Helper::Model::DBIC::Schema> for examples.
586
587 All helper classes should be under one of the following namespaces.
588
589     Catalyst::Helper::Model::
590     Catalyst::Helper::View::
591     Catalyst::Helper::Controller::
592
593 =head2 COMMON HELPERS
594
595 =over
596
597 =item *
598
599 L<Catalyst::Helper::Model::DBIC::Schema> - DBIx::Class models
600
601 =item *
602
603 L<Catalyst::Helper::View::TT> - Template Toolkit view
604
605 =item *
606
607 L<Catalyst::Helper::Model::LDAP>
608
609 =item *
610
611 L<Catalyst::Helper::Model::Adaptor> - wrap any class into a Catalyst model
612
613 =back
614
615 =head3 NOTE
616
617 The helpers will read author name from /etc/passwd by default. + To override, please export the AUTHOR variable.
618
619 =head1 METHODS
620
621 =head2 mk_compclass
622
623 This method in your Helper module is called with C<$helper>
624 which is a L<Catalyst::Helper> object, and whichever other arguments
625 the user added to the command-line. You can use the $helper to call methods
626 described below.
627
628 If the Helper module does not contain a C<mk_compclass> method, it
629 will fall back to calling L</render_file>, with an argument of
630 C<compclass>.
631
632 =head2 mk_comptest
633
634 This method in your Helper module is called with C<$helper>
635 which is a L<Catalyst::Helper> object, and whichever other arguments
636 the user added to the command-line. You can use the $helper to call methods
637 described below.
638
639 If the Helper module does not contain a C<mk_compclass> method, it
640 will fall back to calling L</render_file>, with an argument of
641 C<comptest>.
642
643 =head2 mk_stuff
644
645 This method is called if the user does not supply any of the usual
646 component types C<view>, C<controller>, C<model>. It is passed the
647 C<$helper> object (an instance of L<Catalyst::Helper>), and any other
648 arguments the user typed.
649
650 There is no fallback for this method.
651
652 =head1 INTERNAL METHODS
653
654 These are the methods that the Helper classes can call on the
655 <$helper> object passed to them.
656
657 =head2 render_file ($file, $path, $vars)
658
659 Render and create a file from a template in DATA using Template
660 Toolkit. $file is the relevent chunk of the __DATA__ section, $path is
661 the path to the file and $vars is the hashref as expected by
662 L<Template Toolkit|Template>.
663
664 =head2 get_file ($class, $file)
665
666 Fetch file contents from the DATA section. This is used internally by
667 L</render_file>.  $class is the name of the class to get the DATA
668 section from.  __PACKAGE__ or ( caller(0) )[0] might be sensible
669 values for this.
670
671 =head2 mk_app
672
673 Create the main application skeleton. This is called by L<catalyst.pl>.
674
675 =head2 mk_component ($app)
676
677 This method is called by L<create.pl> to make new components
678 for your application.
679
680 =head3 mk_dir ($path)
681
682 Surprisingly, this function makes a directory.
683
684 =head2 mk_file ($file, $content)
685
686 Writes content to a file. Called by L</render_file>.
687
688 =head2 next_test ($test_name)
689
690 Calculates the name of the next numbered test file and returns it.
691 Don't give the number or the .t suffix for the test name.
692
693 =head1 NOTE
694
695 The helpers will read author name from /etc/passwd by default.
696 To override, please export the AUTHOR variable.
697
698 =head1 SEE ALSO
699
700 L<Catalyst::Manual>, L<Catalyst::Test>, L<Catalyst::Request>,
701 L<Catalyst::Response>, L<Catalyst>
702
703 =head1 AUTHORS
704
705 Catalyst Contributors, see Catalyst.pm
706
707 =head1 LICENSE
708
709 This library is free software. You can redistribute it and/or modify
710 it under the same terms as Perl itself.
711
712 =begin pod_to_ignore
713
714 =cut
715
716 1;
717