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