update distar url
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Utils.pm
CommitLineData
f05af9ba 1package Catalyst::Utils;
2
3use strict;
37a3ac5c 4use File::Spec;
d837e1a7 5use HTTP::Request;
812a28c9 6use Path::Class;
d837e1a7 7use URI;
5e5bd6df 8use Carp qw/croak/;
a8946dc8 9use Cwd;
e7399d8b 10use Class::Load 'is_class_loaded';
17b3d800 11use String::RewritePrefix;
3086ccde 12use Class::Load ();
0db6e9d5 13use namespace::clean;
ec4d7259 14use Devel::InnerPackage;
cbe627b9 15use Moose::Util;
0db6e9d5 16
f05af9ba 17=head1 NAME
18
19Catalyst::Utils - The Catalyst Utils
20
21=head1 SYNOPSIS
22
23See L<Catalyst>.
24
25=head1 DESCRIPTION
26
b0ad47c1 27Catalyst Utilities.
39fc2ce1 28
f05af9ba 29=head1 METHODS
30
b5ecfcf0 31=head2 appprefix($class)
41ca9ba7 32
85d9fce6 33 MyApp::Foo becomes myapp_foo
41ca9ba7 34
35=cut
36
37sub appprefix {
38 my $class = shift;
0ef447d8 39 $class =~ s/::/_/g;
41ca9ba7 40 $class = lc($class);
41 return $class;
42}
43
b5ecfcf0 44=head2 class2appclass($class);
84cf74e7 45
0ef447d8 46 MyApp::Controller::Foo::Bar becomes MyApp
47 My::App::Controller::Foo::Bar becomes My::App
2d90477f 48
84cf74e7 49=cut
50
51sub class2appclass {
52 my $class = shift || '';
53 my $appname = '';
0ef447d8 54 if ( $class =~ /^(.+?)::([MVC]|Model|View|Controller)::.+$/ ) {
84cf74e7 55 $appname = $1;
56 }
57 return $appname;
58}
59
b5ecfcf0 60=head2 class2classprefix($class);
2930d610 61
0ef447d8 62 MyApp::Controller::Foo::Bar becomes MyApp::Controller
63 My::App::Controller::Foo::Bar becomes My::App::Controller
2d90477f 64
2930d610 65=cut
66
67sub class2classprefix {
68 my $class = shift || '';
69 my $prefix;
0ef447d8 70 if ( $class =~ /^(.+?::([MVC]|Model|View|Controller))::.+$/ ) {
2930d610 71 $prefix = $1;
72 }
73 return $prefix;
74}
75
b5ecfcf0 76=head2 class2classsuffix($class);
84cf74e7 77
0ef447d8 78 MyApp::Controller::Foo::Bar becomes Controller::Foo::Bar
2d90477f 79
84cf74e7 80=cut
81
82sub class2classsuffix {
83 my $class = shift || '';
84 my $prefix = class2appclass($class) || '';
0ef447d8 85 $class =~ s/$prefix\:://;
84cf74e7 86 return $class;
87}
88
b5ecfcf0 89=head2 class2env($class);
3ad654e0 90
26e73131 91Returns the environment name for class.
3ad654e0 92
93 MyApp becomes MYAPP
94 My::App becomes MY_APP
95
96=cut
97
98sub class2env {
99 my $class = shift || '';
0ef447d8 100 $class =~ s/::/_/g;
3ad654e0 101 return uc($class);
102}
103
b5ecfcf0 104=head2 class2prefix( $class, $case );
f05af9ba 105
e2cc89a9 106Returns the uri prefix for a class. If case is false the prefix is converted to lowercase.
f05af9ba 107
0ef447d8 108 My::App::Controller::Foo::Bar becomes foo/bar
2d90477f 109
f05af9ba 110=cut
111
112sub class2prefix {
113 my $class = shift || '';
e494bd6b 114 my $case = shift || 0;
f05af9ba 115 my $prefix;
0ef447d8 116 if ( $class =~ /^.+?::([MVC]|Model|View|Controller)::(.+)$/ ) {
e494bd6b 117 $prefix = $case ? $2 : lc $2;
0ef447d8 118 $prefix =~ s{::}{/}g;
f05af9ba 119 }
120 return $prefix;
121}
122
b5ecfcf0 123=head2 class2tempdir( $class [, $create ] );
37a3ac5c 124
e2cc89a9 125Returns a tempdir for a class. If create is true it will try to create the path.
37a3ac5c 126
127 My::App becomes /tmp/my/app
7d7519a4 128 My::App::Controller::Foo::Bar becomes /tmp/my/app/c/foo/bar
37a3ac5c 129
130=cut
131
132sub class2tempdir {
133 my $class = shift || '';
134 my $create = shift || 0;
4be535b1 135 my @parts = split '::', lc $class;
37a3ac5c 136
137 my $tmpdir = dir( File::Spec->tmpdir, @parts )->cleanup;
138
4be535b1 139 if ( $create && !-e $tmpdir ) {
37a3ac5c 140
ab61f021 141 eval { $tmpdir->mkpath; 1 }
142 or do {
41a8bf1f 143 # don't load Catalyst::Exception as a BEGIN in Utils,
144 # because Utils often gets loaded before MyApp.pm, and if
145 # Catalyst::Exception is loaded before MyApp.pm, it does
146 # not honor setting
147 # $Catalyst::Exception::CATALYST_EXCEPTION_CLASS in
148 # MyApp.pm
149 require Catalyst::Exception;
37a3ac5c 150 Catalyst::Exception->throw(
4be535b1 151 message => qq/Couldn't create tmpdir '$tmpdir', "$@"/ );
37a3ac5c 152 }
153 }
154
155 return $tmpdir->stringify;
156}
157
a8946dc8 158=head2 home($class)
159
160Returns home directory for given class.
161
0f519d62 162=head2 dist_indicator_file_list
163
a8946dc8 164Returns a list of files which can be tested to check if you're inside
e01b6093 165a CPAN distribution which is not yet installed.
166
167These are:
168
169=over
170
171=item Makefile.PL
172
173=item Build.PL
174
175=item dist.ini
176
df221478 177=item L<cpanfile>
178
e01b6093 179=back
0f519d62 180
181=cut
182
183sub dist_indicator_file_list {
df221478 184 qw{Makefile.PL Build.PL dist.ini cpanfile};
0f519d62 185}
186
812a28c9 187sub home {
51f412bd 188 my $class = shift;
189
190 # make an $INC{ $key } style string from the class name
191 (my $file = "$class.pm") =~ s{::}{/}g;
192
193 if ( my $inc_entry = $INC{$file} ) {
51452916 194 {
51f412bd 195 # look for an uninstalled Catalyst app
196
197 # find the @INC entry in which $file was found
198 (my $path = $inc_entry) =~ s/$file$//;
a8946dc8 199 $path ||= cwd() if !defined $path || !length $path;
200 my $home = dir($path)->absolute->cleanup;
201
202 # pop off /lib and /blib if they're there
203 $home = $home->parent while $home =~ /b?lib$/;
204
205 # only return the dir if it has a Makefile.PL or Build.PL or dist.ini
206 if (grep { -f $home->file($_) } dist_indicator_file_list()) {
207 # clean up relative path:
208 # MyApp/script/.. -> MyApp
209
210 my $dir;
211 my @dir_list = $home->dir_list();
212 while (($dir = pop(@dir_list)) && $dir eq '..') {
213 $home = dir($home)->parent->parent;
214 }
215
216 return $home->stringify;
217 }
51452916 218 }
4be535b1 219
51f412bd 220 {
221 # look for an installed Catalyst app
222
223 # trim the .pm off the thing ( Foo/Bar.pm -> Foo/Bar/ )
224 ( my $path = $inc_entry) =~ s/\.pm$//;
225 my $home = dir($path)->absolute->cleanup;
226
bd85860b 227 # return if it's a valid directory
51f412bd 228 return $home->stringify if -d $home;
62459712 229 }
812a28c9 230 }
51f412bd 231
232 # we found nothing
a8946dc8 233 return 0;
03fb1bee 234}
235
b5ecfcf0 236=head2 prefix($class, $name);
812a28c9 237
238Returns a prefixed action.
239
0ef447d8 240 MyApp::Controller::Foo::Bar, yada becomes foo/bar/yada
812a28c9 241
242=cut
243
244sub prefix {
245 my ( $class, $name ) = @_;
246 my $prefix = &class2prefix($class);
247 $name = "$prefix/$name" if $prefix;
248 return $name;
249}
250
b5ecfcf0 251=head2 request($uri)
4d60aa90 252
e2cc89a9 253Returns an L<HTTP::Request> object for a uri.
4d60aa90 254
255=cut
256
257sub request {
258 my $request = shift;
259 unless ( ref $request ) {
a88c7ec8 260 if ( $request =~ m/^http/i ) {
f4c0f6f7 261 $request = URI->new($request);
4d60aa90 262 }
263 else {
f4c0f6f7 264 $request = URI->new( 'http://localhost' . $request );
4d60aa90 265 }
266 }
267 unless ( ref $request eq 'HTTP::Request' ) {
268 $request = HTTP::Request->new( 'GET', $request );
269 }
4d60aa90 270 return $request;
271}
272
dd91afb5 273=head2 ensure_class_loaded($class_name, \%opts)
d9183506 274
275Loads the class unless it already has been loaded.
276
dd91afb5 277If $opts{ignore_loaded} is true always tries the require whether the package
278already exists or not. Only pass this if you're either (a) sure you know the
279file exists on disk or (b) have code to catch the file not found exception
280that will result if it doesn't.
281
d9183506 282=cut
283
284sub ensure_class_loaded {
285 my $class = shift;
d06051f7 286 my $opts = shift;
d9183506 287
5e5bd6df 288 croak "Malformed class Name $class"
289 if $class =~ m/(?:\b\:\b|\:{3,})/;
290
59ede84e 291 croak "Malformed class Name $class"
292 if $class =~ m/[^\w:]/;
293
294 croak "ensure_class_loaded should be given a classname, not a filename ($class)"
295 if $class =~ m/\.pm$/;
296
f55d1491 297 # $opts->{ignore_loaded} can be set to true, and this causes the class to be required, even
298 # if it already has symbol table entries. This is to support things like Schema::Loader, which
299 # part-generate classes in memory, but then also load some of their contents from disk.
d06051f7 300 return if !$opts->{ ignore_loaded }
e7399d8b 301 && is_class_loaded($class); # if a symbol entry exists we don't load again
fbedfd6b 302
d9183506 303 # this hack is so we don't overwrite $@ if the load did not generate an error
304 my $error;
305 {
306 local $@;
7a1958eb 307 my $file = $class . '.pm';
308 $file =~ s{::}{/}g;
309 eval { CORE::require($file) };
d9183506 310 $error = $@;
311 }
6bfff75e 312
d9183506 313 die $error if $error;
fbedfd6b 314
f55d1491 315 warn "require $class was successful but the package is not defined."
e7399d8b 316 unless is_class_loaded($class);
6bfff75e 317
318 return 1;
d9183506 319}
320
358e1592 321=head2 merge_hashes($hashref, $hashref)
322
323Base code to recursively merge two hashes together with right-hand precedence.
324
325=cut
326
327sub merge_hashes {
328 my ( $lefthash, $righthash ) = @_;
329
330 return $lefthash unless defined $righthash;
b0ad47c1 331
358e1592 332 my %merged = %$lefthash;
0ef447d8 333 for my $key ( keys %$righthash ) {
334 my $right_ref = ( ref $righthash->{ $key } || '' ) eq 'HASH';
335 my $left_ref = ( ( exists $lefthash->{ $key } && ref $lefthash->{ $key } ) || '' ) eq 'HASH';
336 if( $right_ref and $left_ref ) {
358e1592 337 $merged{ $key } = merge_hashes(
338 $lefthash->{ $key }, $righthash->{ $key }
0ef447d8 339 );
358e1592 340 }
341 else {
342 $merged{ $key } = $righthash->{ $key };
0ef447d8 343 }
358e1592 344 }
b0ad47c1 345
358e1592 346 return \%merged;
347}
348
cb69249e 349=head2 env_value($class, $key)
350
351Checks for and returns an environment value. For instance, if $key is
352'home', then this method will check for and return the first value it finds,
353looking at $ENV{MYAPP_HOME} and $ENV{CATALYST_HOME}.
354
355=cut
356
357sub env_value {
358 my ( $class, $key ) = @_;
359
360 $key = uc($key);
361 my @prefixes = ( class2env($class), 'CATALYST' );
362
363 for my $prefix (@prefixes) {
364 if ( defined( my $value = $ENV{"${prefix}_${key}"} ) ) {
365 return $value;
366 }
367 }
368
369 return;
370}
d9183506 371
39fc2ce1 372=head2 term_width
373
374Try to guess terminal width to use with formatting of debug output
375
376All you need to get this work, is:
377
3781) Install Term::Size::Any, or
379
b0ad47c1 3802) Export $COLUMNS from your shell.
39fc2ce1 381
382(Warning to bash users: 'echo $COLUMNS' may be showing you the bash
b0ad47c1 383variable, not $ENV{COLUMNS}. 'export COLUMNS=$COLUMNS' and you should see
39fc2ce1 384that 'env' now lists COLUMNS.)
385
386As last resort, default value of 80 chars will be used.
387
ad9e8de9 388Calling C<term_width> with a true value will cause it to be recalculated; you
389can use this to cause it to get recalculated when your terminal is resized like
390this
391
392 $SIG{WINCH} = sub { Catalyst::Utils::term_width(1) };
393
39fc2ce1 394=cut
395
396my $_term_width;
8ace712d 397my $_use_term_size_any;
39fc2ce1 398
399sub term_width {
ad9e8de9 400 my $force_reset = shift;
401
402 undef $_term_width if $force_reset;
403
39fc2ce1 404 return $_term_width if $_term_width;
405
0cf6f36c 406 if ($ENV{COLUMNS} && $ENV{COLUMNS} =~ /\A\d+\z/) {
407 return $_term_width = $ENV{COLUMNS};
408 }
409
410 if (!-t STDOUT && !-t STDERR) {
411 return $_term_width = 80;
412 }
413
8ace712d 414 if (!defined $_use_term_size_any) {
415 eval {
416 require Term::Size::Any;
417 Term::Size::Any->import();
418 $_use_term_size_any = 1;
419 1;
420 } or do {
421 if ( $@ =~ m[Can't locate Term/Size/Any\.pm] ) {
422 warn "Term::Size::Any is not installed, can't autodetect terminal column width\n";
423 }
424 else {
425 warn "There was an error trying to detect your terminal size: $@\n";
426 }
427 $_use_term_size_any = 0;
428 };
429 }
430
ab61f021 431 my $width;
8ace712d 432
433 if ($_use_term_size_any) {
0cf6f36c 434 $width = Term::Size::Any::chars(*STDERR) || Term::Size::Any::chars(*STDOUT);
8ace712d 435 }
436
8ace712d 437 if (!$width || $width < 80) {
438 $width = 80;
b4ed26ff 439 }
39fc2ce1 440
39fc2ce1 441 return $_term_width = $width;
442}
443
17b3d800 444
445=head2 resolve_namespace
446
447Method which adds the namespace for plugins and actions.
448
449 __PACKAGE__->setup(qw(MyPlugin));
196932de 450
17b3d800 451 # will load Catalyst::Plugin::MyPlugin
452
453=cut
454
455
456sub resolve_namespace {
5d8129e9 457 my $appnamespace = shift;
17b3d800 458 my $namespace = shift;
459 my @classes = @_;
196932de 460 return String::RewritePrefix->rewrite({
461 q[] => qq[${namespace}::],
462 q[+] => q[],
463 (defined $appnamespace
464 ? (q[~] => qq[${appnamespace}::])
465 : ()
466 ),
467 }, @classes);
17b3d800 468}
469
3086ccde 470=head2 build_middleware (@args)
471
472Internal application that converts a single middleware definition (see
473L<Catalyst/psgi_middleware>) into an actual instance of middleware.
474
475=cut
476
477sub build_middleware {
478 my ($class, $namespace, @init_args) = @_;
479
480 if(
481 $namespace =~s/^\+// ||
482 $namespace =~/^Plack::Middleware/ ||
483 $namespace =~/^$class/
484 ) { ## the string is a full namespace
485 return Class::Load::try_load_class($namespace) ?
486 $namespace->new(@init_args) :
487 die "Can't load class $namespace";
488 } else { ## the string is a partial namespace
9b5bca00 489 if(Class::Load::try_load_class($class .'::Middleware::'. $namespace)) { ## Load Middleware from Project namespace
490 my $ns = $class .'::Middleware::'. $namespace;
491 return $ns->new(@init_args);
318213cd 492 } elsif(Class::Load::try_load_class("Plack::Middleware::$namespace")) { ## Act like Plack::Builder
493 return "Plack::Middleware::$namespace"->new(@init_args);
d9c6a83f 494 } else {
495 die "Can't load middleware via '$namespace'. It's not ".$class."::Middleware::".$namespace." or Plack::Middleware::$namespace";
3086ccde 496 }
497 }
498
499 return; ## be sure we can count on a proper return when valid
500}
501
502=head2 apply_registered_middleware ($psgi)
503
504Given a $psgi reference, wrap all the L<Catalyst/registered_middlewares>
505around it and return the wrapped version.
506
507This exists to deal with the fact Catalyst registered middleware can be
508either an object with a wrap method or a coderef.
509
510=cut
511
512sub apply_registered_middleware {
513 my ($class, $psgi) = @_;
514 my $new_psgi = $psgi;
515 foreach my $middleware ($class->registered_middlewares) {
516 $new_psgi = Scalar::Util::blessed $middleware ?
517 $middleware->wrap($new_psgi) :
518 $middleware->($new_psgi);
519 }
520 return $new_psgi;
521}
17b3d800 522
ec4d7259 523=head2 inject_component
6adc45cf 524
ec4d7259 525Used to add components at runtime:
526
527 into The Catalyst package to inject into (e.g. My::App)
528 component The component package to inject
38c68f3d 529 traits (Optional) ArrayRef of L<Moose::Role>s that the component should consume.
ec4d7259 530 as An optional moniker to use as the package name for the derived component
531
532For example:
533
534 Catalyst::Utils::inject_component( into => My::App, component => Other::App::Controller::Apple )
535
536 The above will create 'My::App::Controller::Other::App::Controller::Apple'
537
538 Catalyst::Utils::inject_component( into => My::App, component => Other::App::Controller::Apple, as => Apple )
539
540 The above will create 'My::App::Controller::Apple'
541
542 Catalyst::Utils::inject_component( into => $myapp, component => 'MyRootV', as => 'Controller::Root' );
543
544Will inject Controller, Model, and View components into your Catalyst application
545at setup (run)time. It does this by creating a new package on-the-fly, having that
546package extend the given component, and then having Catalyst setup the new component
547(via $app->setup_component).
548
549B<NOTE:> This is basically a core version of L<CatalystX::InjectComponent>. If you were using that
550you can now use this safely instead. Going forward changes required to make this work will be
551synchronized with the core method.
552
cbe627b9 553B<NOTE:> The 'traits' option is unique to the L<Catalyst::Utils> version of this feature.
554
555B<NOTE:> These injected components really need to be a L<Catalyst::Component> and a L<Moose>
556based class.
557
ec4d7259 558=cut
559
560sub inject_component {
561 my %given = @_;
562 my ($into, $component, $as) = @given{qw/into component as/};
563
564 croak "No Catalyst (package) given" unless $into;
565 croak "No component (package) given" unless $component;
566
567 Class::Load::load_class($component);
568
569 $as ||= $component;
570 unless ( $as =~ m/^(?:Controller|Model|View)::/ || $given{skip_mvc_renaming} ) {
571 my $category;
572 for (qw/ Controller Model View /) {
573 if ( $component->isa( "Catalyst::$_" ) ) {
574 $category = $_;
575 last;
576 }
577 }
578 croak "Don't know what kind of component \"$component\" is" unless $category;
579 $as = "${category}::$as";
580 }
581 my $component_package = join '::', $into, $as;
582
583 unless ( Class::Load::is_class_loaded $component_package ) {
584 eval "package $component_package; use base qw/$component/; 1;" or
585 croak "Unable to build component package for \"$component_package\": $@";
cbe627b9 586 Moose::Util::apply_all_roles($component_package, @{$given{traits}}) if $given{traits};
ec4d7259 587 (my $file = "$component_package.pm") =~ s{::}{/}g;
88e5a8b0 588 $INC{$file} ||= 1;
ec4d7259 589 }
590
591 my $_setup_component = sub {
592 my $into = shift;
593 my $component_package = shift;
e39312ba 594 $into->components->{$component_package} = $into->delayed_setup_component( $component_package );
ec4d7259 595 };
596
597 $_setup_component->( $into, $component_package );
ec4d7259 598}
6adc45cf 599
9c7b6768 600=head1 PSGI Helpers
601
602Utility functions to make it easier to work with PSGI applications under Catalyst
603
604=head2 env_at_path_prefix
605
606Localize C<$env> under the current controller path prefix:
607
608 package MyApp::Controller::User;
609
610 use Catalyst::Utils;
611
612 use base 'Catalyst::Controller';
613
614 sub name :Local {
615 my ($self, $c) = @_;
616 my $env = $c->Catalyst::Utils::env_at_path_prefix;
617 }
618
efa8265f 619Assuming you have a request like GET /user/name:
9c7b6768 620
621In the example case C<$env> will have PATH_INFO of '/name' instead of
622'/user/name' and SCRIPT_NAME will now be '/user'.
623
624=cut
625
626sub env_at_path_prefix {
627 my $ctx = shift;
628 my $path_prefix = $ctx->controller->path_prefix;
629 my $env = $ctx->request->env;
630 my $path_info = $env->{PATH_INFO};
631 my $script_name = ($env->{SCRIPT_NAME} || '');
632
633 $path_info =~ s/(^\/\Q$path_prefix\E)//;
634 $script_name = "$script_name$1";
635
636 return +{
637 %$env,
638 PATH_INFO => $path_info,
639 SCRIPT_NAME => $script_name };
640}
641
642=head2 env_at_action
643
4477b313 644Localize C<$env> under the current action namespace.
9c7b6768 645
646 package MyApp::Controller::User;
647
648 use Catalyst::Utils;
649
650 use base 'Catalyst::Controller';
651
652 sub name :Local {
653 my ($self, $c) = @_;
654 my $env = $c->Catalyst::Utils::env_at_action;
655 }
656
4477b313 657Assuming you have a request like GET /user/name:
9c7b6768 658
659In the example case C<$env> will have PATH_INFO of '/' instead of
660'/user/name' and SCRIPT_NAME will now be '/user/name'.
661
efa8265f 662Alternatively, assuming you have a request like GET /user/name/foo:
4477b313 663
664In this example case C<$env> will have PATH_INFO of '/foo' instead of
665'/user/name/foo' and SCRIPT_NAME will now be '/user/name'.
666
9c7b6768 667This is probably a common case where you want to mount a PSGI application
668under an action but let the Args fall through to the PSGI app.
669
670=cut
671
672sub env_at_action {
673 my $ctx = shift;
674 my $argpath = join '/', @{$ctx->request->arguments};
675 my $path = '/' . $ctx->request->path;
676
677 $path =~ s/\/?\Q$argpath\E\/?$//;
678
679 my $env = $ctx->request->env;
680 my $path_info = $env->{PATH_INFO};
681 my $script_name = ($env->{SCRIPT_NAME} || '');
682
683 $path_info =~ s/(^\Q$path\E)//;
684 $script_name = "$script_name$1";
685
686 return +{
687 %$env,
688 PATH_INFO => $path_info,
689 SCRIPT_NAME => $script_name };
690}
691
692=head2 env_at_request_uri
693
4477b313 694Localize C<$env> under the current request URI:
9c7b6768 695
696 package MyApp::Controller::User;
697
698 use Catalyst::Utils;
699
700 use base 'Catalyst::Controller';
701
702 sub name :Local Args(1) {
703 my ($self, $c, $id) = @_;
704 my $env = $c->Catalyst::Utils::env_at_request_uri
705 }
706
efa8265f 707Assuming you have a request like GET /user/name/hello:
9c7b6768 708
709In the example case C<$env> will have PATH_INFO of '/' instead of
710'/user/name' and SCRIPT_NAME will now be '/user/name/hello'.
711
712=cut
713
714sub env_at_request_uri {
715 my $ctx = shift;
716 my $path = '/' . $ctx->request->path;
717 my $env = $ctx->request->env;
718 my $path_info = $env->{PATH_INFO};
719 my $script_name = ($env->{SCRIPT_NAME} || '');
720
721 $path_info =~ s/(^\Q$path\E)//;
722 $script_name = "$script_name$1";
723
724 return +{
725 %$env,
726 PATH_INFO => $path_info,
727 SCRIPT_NAME => $script_name };
728}
729
2f381252 730=head1 AUTHORS
f05af9ba 731
2f381252 732Catalyst Contributors, see Catalyst.pm
f05af9ba 733
734=head1 COPYRIGHT
735
536bee89 736This library is free software. You can redistribute it and/or modify it under
f05af9ba 737the same terms as Perl itself.
738
739=cut
740
7411;