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