Split out into new function
[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::MOP;
11 use String::RewritePrefix;
12 use List::MoreUtils qw/ any /;
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 };
141
142         if ($@) {
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;
150             Catalyst::Exception->throw(
151                 message => qq/Couldn't create tmpdir '$tmpdir', "$@"/ );
152         }
153     }
154
155     return $tmpdir->stringify;
156 }
157
158 =head2 dist_indicator_file_list
159
160 Returns a list of files which can be tested to check if you're inside a checkout
161
162 =cut
163
164 sub dist_indicator_file_list {
165     qw/ Makefile.PL Build.PL dist.init /;
166 }
167
168 =head2 home($class)
169
170 Returns home directory for given class.
171
172 Note that the class must be loaded for the home directory to be found using this function.
173
174 =cut
175
176 sub home {
177     my $class = shift;
178
179     # make an $INC{ $key } style string from the class name
180     (my $file = "$class.pm") =~ s{::}{/}g;
181
182     if ( my $inc_entry = $INC{$file} ) {
183         {
184             # look for an uninstalled Catalyst app
185
186             # find the @INC entry in which $file was found
187             (my $path = $inc_entry) =~ s/$file$//;
188             my $home = find_home_unloaded_in_checkout($path);
189             return $home if $home;
190         }
191
192         {
193             # look for an installed Catalyst app
194
195             # trim the .pm off the thing ( Foo/Bar.pm -> Foo/Bar/ )
196             ( my $path = $inc_entry) =~ s/\.pm$//;
197             my $home = dir($path)->absolute->cleanup;
198
199             # return if if it's a valid directory
200             return $home->stringify if -d $home;
201         }
202     }
203
204     # we found nothing
205     return 0;
206 }
207
208 sub find_home_unloaded_in_checkout {
209     my ($path) = @_;
210     $path ||= cwd() if !defined $path || !length $path;
211     my $home = dir($path)->absolute->cleanup;
212
213     # pop off /lib and /blib if they're there
214     $home = $home->parent while $home =~ /b?lib$/;
215
216     # only return the dir if it has a Makefile.PL or Build.PL or dist.ini
217     if (any { $_ } map { -f $home->file($_) } dist_indicator_file_list()) {
218
219         # clean up relative path:
220         # MyApp/script/.. -> MyApp
221
222         my $dir;
223         my @dir_list = $home->dir_list();
224         while (($dir = pop(@dir_list)) && $dir eq '..') {
225             $home = dir($home)->parent->parent;
226         }
227
228         return $home->stringify;
229     }
230
231 }
232
233 =head2 prefix($class, $name);
234
235 Returns a prefixed action.
236
237     MyApp::Controller::Foo::Bar, yada becomes foo/bar/yada
238
239 =cut
240
241 sub prefix {
242     my ( $class, $name ) = @_;
243     my $prefix = &class2prefix($class);
244     $name = "$prefix/$name" if $prefix;
245     return $name;
246 }
247
248 =head2 request($uri)
249
250 Returns an L<HTTP::Request> object for a uri.
251
252 =cut
253
254 sub request {
255     my $request = shift;
256     unless ( ref $request ) {
257         if ( $request =~ m/^http/i ) {
258             $request = URI->new($request);
259         }
260         else {
261             $request = URI->new( 'http://localhost' . $request );
262         }
263     }
264     unless ( ref $request eq 'HTTP::Request' ) {
265         $request = HTTP::Request->new( 'GET', $request );
266     }
267     return $request;
268 }
269
270 =head2 ensure_class_loaded($class_name, \%opts)
271
272 Loads the class unless it already has been loaded.
273
274 If $opts{ignore_loaded} is true always tries the require whether the package
275 already exists or not. Only pass this if you're either (a) sure you know the
276 file exists on disk or (b) have code to catch the file not found exception
277 that will result if it doesn't.
278
279 =cut
280
281 sub ensure_class_loaded {
282     my $class = shift;
283     my $opts  = shift;
284
285     croak "Malformed class Name $class"
286         if $class =~ m/(?:\b\:\b|\:{3,})/;
287
288     croak "Malformed class Name $class"
289         if $class =~ m/[^\w:]/;
290
291     croak "ensure_class_loaded should be given a classname, not a filename ($class)"
292         if $class =~ m/\.pm$/;
293
294     # $opts->{ignore_loaded} can be set to true, and this causes the class to be required, even
295     # if it already has symbol table entries. This is to support things like Schema::Loader, which
296     # part-generate classes in memory, but then also load some of their contents from disk.
297     return if !$opts->{ ignore_loaded }
298         && Class::MOP::is_class_loaded($class); # if a symbol entry exists we don't load again
299
300     # this hack is so we don't overwrite $@ if the load did not generate an error
301     my $error;
302     {
303         local $@;
304         my $file = $class . '.pm';
305         $file =~ s{::}{/}g;
306         eval { CORE::require($file) };
307         $error = $@;
308     }
309
310     die $error if $error;
311
312     warn "require $class was successful but the package is not defined."
313         unless Class::MOP::is_class_loaded($class);
314
315     return 1;
316 }
317
318 =head2 merge_hashes($hashref, $hashref)
319
320 Base code to recursively merge two hashes together with right-hand precedence.
321
322 =cut
323
324 sub merge_hashes {
325     my ( $lefthash, $righthash ) = @_;
326
327     return $lefthash unless defined $righthash;
328
329     my %merged = %$lefthash;
330     for my $key ( keys %$righthash ) {
331         my $right_ref = ( ref $righthash->{ $key } || '' ) eq 'HASH';
332         my $left_ref  = ( ( exists $lefthash->{ $key } && ref $lefthash->{ $key } ) || '' ) eq 'HASH';
333         if( $right_ref and $left_ref ) {
334             $merged{ $key } = merge_hashes(
335                 $lefthash->{ $key }, $righthash->{ $key }
336             );
337         }
338         else {
339             $merged{ $key } = $righthash->{ $key };
340         }
341     }
342
343     return \%merged;
344 }
345
346 =head2 env_value($class, $key)
347
348 Checks for and returns an environment value. For instance, if $key is
349 'home', then this method will check for and return the first value it finds,
350 looking at $ENV{MYAPP_HOME} and $ENV{CATALYST_HOME}.
351
352 =cut
353
354 sub env_value {
355     my ( $class, $key ) = @_;
356
357     $key = uc($key);
358     my @prefixes = ( class2env($class), 'CATALYST' );
359
360     for my $prefix (@prefixes) {
361         if ( defined( my $value = $ENV{"${prefix}_${key}"} ) ) {
362             return $value;
363         }
364     }
365
366     return;
367 }
368
369 =head2 term_width
370
371 Try to guess terminal width to use with formatting of debug output
372
373 All you need to get this work, is:
374
375 1) Install Term::Size::Any, or
376
377 2) Export $COLUMNS from your shell.
378
379 (Warning to bash users: 'echo $COLUMNS' may be showing you the bash
380 variable, not $ENV{COLUMNS}. 'export COLUMNS=$COLUMNS' and you should see
381 that 'env' now lists COLUMNS.)
382
383 As last resort, default value of 80 chars will be used.
384
385 =cut
386
387 my $_term_width;
388
389 sub term_width {
390     return $_term_width if $_term_width;
391
392     my $width = eval '
393         use Term::Size::Any;
394         my ($columns, $rows) = Term::Size::Any::chars;
395         return $columns;
396     ';
397
398     if ($@) {
399         $width = $ENV{COLUMNS}
400             if exists($ENV{COLUMNS})
401             && $ENV{COLUMNS} =~ m/^\d+$/;
402     }
403
404     $width = 80 unless ($width && $width >= 80);
405     return $_term_width = $width;
406 }
407
408
409 =head2 resolve_namespace
410
411 Method which adds the namespace for plugins and actions.
412
413   __PACKAGE__->setup(qw(MyPlugin));
414
415   # will load Catalyst::Plugin::MyPlugin
416
417 =cut
418
419
420 sub resolve_namespace {
421     my $appnamespace = shift;
422     my $namespace = shift;
423     my @classes = @_;
424     return String::RewritePrefix->rewrite({
425         q[]  => qq[${namespace}::],
426         q[+] => q[],
427         (defined $appnamespace
428             ? (q[~] => qq[${appnamespace}::])
429             : ()
430         ),
431     }, @classes);
432 }
433
434
435 =head1 AUTHORS
436
437 Catalyst Contributors, see Catalyst.pm
438
439 =head1 COPYRIGHT
440
441 This library is free software. You can redistribute it and/or modify it under
442 the same terms as Perl itself.
443
444 =cut
445
446 1;