File Coverage

blib/lib/Catalyst/Controller/AutoAssets/Handler.pm
Criterion Covered Total %
statement 164 209 78.4
branch 39 70 55.7
condition 11 38 28.9
subroutine 43 53 81.1
pod 0 26 0.0
total 257 396 64.9


line stmt bran cond sub pod time code
1             package Catalyst::Controller::AutoAssets::Handler;
2 3     3   2047 use strict;
  3         4  
  3         114  
3 3     3   14 use warnings;
  3         5  
  3         95  
4              
5             # VERSION
6              
7 3     3   13 use Moose::Role;
  3         5  
  3         23  
8 3     3   9284 use namespace::autoclean;
  3         7  
  3         27  
9              
10             requires qw(
11             asset_request
12             write_built_file
13             );
14              
15 3     3   269 use Cwd;
  3         4  
  3         246  
16 3     3   15 use Path::Class 0.32 qw( dir file );
  3         95  
  3         177  
17 3     3   23 use Fcntl qw( :DEFAULT :flock );
  3         7  
  3         1491  
18 3     3   20 use Carp;
  3         6  
  3         205  
19 3     3   17 use File::stat qw(stat);
  3         5  
  3         34  
20 3     3   225 use Catalyst::Utils;
  3         6  
  3         82  
21 3     3   14 use Time::HiRes qw(gettimeofday tv_interval);
  3         4  
  3         33  
22 3     3   339 use Storable qw(store retrieve);
  3         5  
  3         239  
23 3     3   20 use Try::Tiny;
  3         3  
  3         160  
24 3     3   1768 use Data::Dumper::Concise 'Dumper';
  3         17080  
  3         10074  
25              
26             require Digest::SHA1;
27             require MIME::Types;
28             require Module::Runtime;
29              
30             has 'Controller' => (
31             is => 'ro', required => 1,
32             isa => 'Catalyst::Controller::AutoAssets',
33             handles => [qw(type _app action_namespace unknown_asset _build_params _module_version)],
34             );
35              
36             # Directories to include
37             has 'include', is => 'ro', isa => 'Str|ArrayRef[Str]', required => 1;
38              
39             # Optional regex to require files to match to be included
40             has 'include_regex', is => 'ro', isa => 'Maybe[Str]', default => undef;
41              
42             # Optional regex to exclude files
43             has 'exclude_regex', is => 'ro', isa => 'Maybe[Str]', default => undef;
44              
45             # Whether or not to use qr/$regex/i or qr/$regex/
46             has 'regex_ignore_case', is => 'ro', isa => 'Bool', default => 0;
47              
48             # Whether or not to make the current asset available via 307 redirect to the
49             # real, current checksum/fingerprint asset path
50             has 'current_redirect', is => 'ro', isa => 'Bool', default => 1;
51              
52             # What string to use for the 'current' redirect
53             has 'current_alias', is => 'ro', isa => 'Str', default => 'current';
54              
55             # Whether or not to make the current asset available via a static path
56             # with no benefit of caching
57             has 'allow_static_requests', is => 'ro', isa => 'Bool', default => 0;
58              
59             # What string to use for the 'static' path
60             has 'static_alias', is => 'ro', isa => 'Str', default => 'static';
61              
62             # Extra custom response headers for current/static requests
63             has 'current_response_headers', is => 'ro', isa => 'HashRef', default => sub {{}};
64             has 'static_response_headers', is => 'ro', isa => 'HashRef', default => sub {{}};
65              
66             # Whether or not to set 'Etag' response headers and check 'If-None-Match' request headers
67             # Very useful when using 'static' paths
68             has 'use_etags', is => 'ro', isa => 'Bool', default => 0;
69              
70             # Max number of seconds before recalculating the fingerprint (sha1 checksum)
71             # regardless of whether or not the mtime has changed. 0 means infinite/disabled
72             has 'max_fingerprint_calc_age', is => 'ro', isa => 'Int', default => sub {0};
73              
74             # Max number of seconds to wait to obtain a lock (to be thread safe)
75             has 'max_lock_wait', is => 'ro', isa => 'Int', default => 120;
76              
77             has 'cache_control_header', is => 'ro', isa => 'Str',
78             default => sub { 'public, max-age=31536000, s-max-age=31536000' }; # 31536000 = 1 year
79              
80             # Whether or not to use stored state data across restarts to avoid rebuilding.
81             has 'persist_state', is => 'ro', isa => 'Bool', default => sub{0};
82              
83             # Optional shorter checksum
84             has 'sha1_string_length', is => 'ro', isa => 'Int', default => sub{40};
85              
86             # directory to use for relative includes (defaults to the Catalyst home dir);
87             # TODO: coerce from Str
88             has 'include_relative_dir', isa => 'Path::Class::Dir', is => 'ro', lazy => 1, default => sub {
89             my $self = shift;
90             my $home = $self->_app->config->{home};
91             $home = $home && -d $home ? $self->_app->config->{home} : cwd();
92             return dir( $home );
93             };
94              
95              
96              
97             ######################################
98              
99              
100 0     0 0 0 sub BUILD {}
101             before BUILD => sub {
102             my $self = shift;
103            
104             # optionally initialize state data from the copy stored on disk for fast
105             # startup (avoids having to always rebuild after every app restart):
106             $self->_restore_state if($self->persist_state);
107              
108             # init includes
109             $self->includes;
110            
111             Catalyst::Exception->throw("Must include at least one file/directory")
112             unless (scalar @{$self->includes} > 0);
113              
114             # if the user picks something lower than 5 it is probably a mistake (really, anything
115             # lower than 8 is probably not a good idea. But the full 40 is probably way overkill)
116             Catalyst::Exception->throw("sha1_string_length must be between 5 and 40")
117             unless ($self->sha1_string_length >= 5 && $self->sha1_string_length <= 40);
118              
119             # init work_dir:
120             $self->work_dir->mkpath($self->_app->debug);
121             $self->work_dir->resolve;
122            
123             $self->prepare_asset;
124             };
125              
126             # Main code entry point:
127             sub request {
128 16     16 0 260 my ( $self, $c, @args ) = @_;
129 16         23 my $sha1 = $args[0];
130            
131 16 100       68 return $self->current_request($c, @args) if (
132             $self->is_current_request_arg(@args)
133             );
134            
135 12 50 33     390 return $self->static_request($c, @args) if (
136             $self->allow_static_requests
137             && $self->static_alias eq $sha1
138             );
139            
140 12         43 return $self->handle_asset_request($c, @args);
141             }
142              
143             sub is_current_request_arg {
144 5     5 0 9 my ($self, $arg) = @_;
145 5 100       182 return $arg eq $self->current_alias ? 1 : 0;
146             }
147              
148             sub current_request {
149 4     4 0 9 my ( $self, $c, $arg, @args ) = @_;
150 4         131 my %headers = (
151             'Cache-Control' => 'no-cache',
152 4         8 %{$self->current_response_headers}
153             );
154 4         84 $c->response->header( $_ => $headers{$_} ) for (keys %headers);
155 4         926 $c->response->redirect(join('/',$self->asset_path,@args), 307);
156 4         520 return $c->detach;
157             }
158              
159             sub static_request {
160 0     0 0 0 my ( $self, $c, $arg, @args ) = @_;
161 0         0 my %headers = (
162             'Cache-Control' => 'no-cache',
163 0         0 %{$self->static_response_headers}
164             );
165 0         0 $c->response->header( $_ => $headers{$_} ) for (keys %headers);
166             # Simulate a request to the current sha1 checksum:
167 0         0 return $self->handle_asset_request($c, $self->asset_name, @args);
168             }
169              
170              
171             sub handle_asset_request {
172 12     12 0 23 my ( $self, $c, $arg, @args ) = @_;
173            
174 12         43 $self->prepare_asset(@args);
175            
176 12 50 33     384 if($self->use_etags && $self->client_current_etag($c, $arg, @args)) {
177             # Set 304 Not Modified:
178 0         0 $c->response->status(304);
179             }
180             else {
181 12         44 $self->asset_request($c, $arg, @args);
182             }
183 10         185 return $c->detach;
184             }
185              
186             sub client_current_etag {
187 0     0 0 0 my ( $self, $c, $arg, @args ) = @_;
188            
189 0         0 my $etag = $self->etag_value(@args);
190 0         0 $c->response->header( Etag => $etag );
191 0         0 my $client_etag = $c->request->headers->{'if-none-match'};
192 0 0 0     0 return ($client_etag && $client_etag eq $etag) ? 1 : 0;
193             }
194              
195             sub etag_value {
196 0     0 0 0 my $self = shift;
197 0         0 return '"' . join('/',$self->asset_name,@_) . '"';
198             }
199              
200              
201             ############################
202              
203              
204             has 'work_dir', is => 'ro', isa => 'Path::Class::Dir', lazy => 1, default => sub {
205             my $self = shift;
206             my $c = $self->_app;
207            
208             my $tmpdir = Catalyst::Utils::class2tempdir($c)
209             || Catalyst::Exception->throw("Can't determine tempdir for $c");
210            
211             return dir($tmpdir, "AutoAssets", $self->action_namespace($c));
212             };
213              
214             has 'built_file', is => 'ro', isa => 'Path::Class::File', lazy => 1, default => sub {
215             my $self = shift;
216             my $filename = 'built_file';
217             return file($self->work_dir,$filename);
218             };
219              
220             has 'fingerprint_file', is => 'ro', isa => 'Path::Class::File', lazy => 1, default => sub {
221             my $self = shift;
222             return file($self->work_dir,'fingerprint');
223             };
224              
225             has 'lock_file', is => 'ro', isa => 'Path::Class::File', lazy => 1, default => sub {
226             my $self = shift;
227             return file($self->work_dir,'lockfile');
228             };
229              
230             has 'includes', is => 'ro', isa => 'ArrayRef', lazy => 1, default => sub {
231             my $self = shift;
232             my $rel = $self->include_relative_dir;
233             my @list = ref $self->include ? @{$self->include} : $self->include;
234             return [ map {
235             my $inc = file($_);
236             $inc = $rel->file($inc) unless ($inc->is_absolute);
237             $inc = dir($inc) if (-d $inc); #<-- convert to Path::Class::Dir
238             $inc->resolve
239             } @list ];
240             };
241              
242              
243              
244              
245             sub get_include_files {
246 15     15 0 20 my $self = shift;
247            
248 15         27 my @excluded = ();
249 15         27 my @files = ();
250 15         19 for my $inc (@{$self->includes}) {
  15         500  
251 15 100       79 if($inc->is_dir) {
252             $inc->recurse(
253             preorder => 1,
254             depthfirst => 1,
255             callback => sub {
256 41     41   6881 my $child = shift;
257 41 100       84 $self->_valid_include_file($child)
258             ? push @files, $child->absolute
259             : push @excluded, $child->absolute;
260             }
261 12         128 );
262             }
263             else {
264 3 50       17 $self->_valid_include_file($inc)
265             ? push @files, $inc->absolute
266             : push @excluded, $inc->absolute;
267             }
268             }
269            
270             # Some handlers (like Directory) need to know about excluded files
271 15         622 $self->_record_excluded_files(\@excluded);
272            
273             # force consistent ordering of files:
274 15         65 return [sort @files];
275             }
276              
277             # optional hook for excluded files:
278 14     14   18 sub _record_excluded_files {}
279              
280              
281             has '_include_regexp', is => 'ro', isa => 'Maybe[RegexpRef]',
282             lazy => 1, init_arg => undef, default => sub {
283             my $self = shift;
284             my $str = $self->include_regex or return undef;
285             return $self->regex_ignore_case ? qr/$str/i : qr/$str/;
286             };
287             has '_exclude_regexp', is => 'ro', isa => 'Maybe[RegexpRef]',
288             lazy => 1, init_arg => undef, default => sub {
289             my $self = shift;
290             my $str = $self->exclude_regex or return undef;
291             return $self->regex_ignore_case ? qr/$str/i : qr/$str/;
292             };
293              
294             sub _valid_include_file {
295 44     44   46 my ($self, $file) = @_;
296             return (
297 44 100 33     101 $file->is_dir
298             || ($self->include_regex && ! ($file =~ $self->_include_regexp))
299             || ($self->exclude_regex && $file =~ $self->_exclude_regexp)
300             ) ? 0 : 1;
301             }
302              
303             has 'last_fingerprint_calculated', is => 'rw', isa => 'Maybe[Int]', default => sub{undef};
304              
305             has 'built_mtime', is => 'rw', isa => 'Maybe[Str]', default => sub{undef};
306             sub get_built_mtime {
307 32     32 0 47 my $self = shift;
308 32 100       1115 return -f $self->built_file ? $self->built_file->stat->mtime : undef;
309             }
310              
311             # inc_mtimes are the mtime(s) of the include files. For directory assets
312             # this is *only* the mtime of the top directory (see subfile_meta below)
313             has 'inc_mtimes', is => 'rw', isa => 'Maybe[Str]', default => undef;
314             sub get_inc_mtime_concat {
315 26     26 0 29 my $self = shift;
316 26         30 my $list = shift;
317 26         54 return join('-', map { $_->stat->mtime } @$list );
  37         2046  
318             }
319              
320              
321             sub calculate_fingerprint {
322 6     6 0 9 my $self = shift;
323 6         8 my $list = shift;
324             # include both the include (source) and built (output) in the fingerprint:
325 6         231 my $sha1 = $self->file_checksum(@$list,$self->built_file);
326 6 50       250 $self->last_fingerprint_calculated(time) if ($sha1);
327 6         21 return $sha1;
328             }
329              
330             sub current_fingerprint {
331 23     23 0 36 my $self = shift;
332 23 100       854 return undef unless (-f $self->fingerprint_file);
333 20         1445 my $fingerprint = $self->fingerprint_file->slurp(iomode => '<:raw');
334 20         4235 return $fingerprint;
335             }
336              
337             sub save_fingerprint {
338 3     3 0 5 my $self = shift;
339 3 50       10 my $fingerprint = shift or die "Expected fingerprint/checksum argument";
340 3         119 return $self->fingerprint_file->spew(iomode => '>:raw', $fingerprint);
341             }
342              
343             sub calculate_save_fingerprint {
344 3     3 0 6 my $self = shift;
345 3 50       9 my $fingerprint = $self->calculate_fingerprint(@_) or return 0;
346 3         11 return $self->save_fingerprint($fingerprint);
347             }
348              
349             sub fingerprint_calc_current {
350 23     23 0 36 my $self = shift;
351 23 50       754 my $last = $self->last_fingerprint_calculated or return 0;
352 23 50       698 return 1 if ($self->max_fingerprint_calc_age == 0); # <-- 0 means infinite
353 0 0       0 return 1 if (time - $last < $self->max_fingerprint_calc_age);
354 0         0 return 0;
355             }
356              
357             # -----
358             # Quick and dirty state persistence for faster startup
359             has 'persist_state_file', is => 'ro', isa => 'Path::Class::File', lazy => 1, default => sub {
360             my $self = shift;
361             return file($self->work_dir,'state.dat');
362             };
363              
364             has '_persist_attrs', is => 'ro', isa => 'ArrayRef', default => sub{[qw(
365             built_mtime
366             inc_mtimes
367             last_fingerprint_calculated
368             )]};
369              
370             sub _persist_state {
371 3     3   92 my $self = shift;
372 3 50       126 return undef unless ($self->persist_state);
373 0         0 my $data = { map { $_ => $self->$_ } @{$self->_persist_attrs} };
  0         0  
  0         0  
374 0         0 $data->{_module_version} = $self->_module_version;
375 0         0 $data->{_build_params} = $self->_build_params;
376 0         0 store $data, $self->persist_state_file;
377 0         0 return $data;
378             }
379              
380             sub _restore_state {
381 0     0   0 my $self = shift;
382 0 0       0 return 0 unless (-f $self->persist_state_file);
383 0         0 my $data;
384             try {
385 0     0   0 $data = retrieve $self->persist_state_file;
386 0 0       0 if($self->_valid_state_data($data)) {
387 0         0 $self->$_($data->{$_}) for (@{$self->_persist_attrs});
  0         0  
388             }
389             }
390             catch {
391 0     0   0 $self->clear_asset; #<-- make sure no partial state data is used
392 0         0 $self->_app->log->warn(
393             'Failed to restore state from ' . $self->persist_state_file
394             );
395 0         0 };
396 0         0 return $data;
397             }
398              
399             sub _valid_state_data {
400 0     0   0 my ($self, $data) = @_;
401            
402             # Make sure the version and config params hasn't changed
403             return (
404 0 0 0     0 $self->_module_version eq $data->{_module_version}
405             && Dumper($self->_build_params) eq Dumper($data->{_build_params})
406             ) ? 1 : 0;
407             }
408             # -----
409              
410              
411             # force rebuild on next request/prepare_asset
412             sub clear_asset {
413 0     0 0 0 my $self = shift;
414 0         0 $self->inc_mtimes(undef);
415             }
416              
417             sub _build_required {
418 26     26   37 my ($self, $d) = @_;
419             return (
420 26 100 33     843 $self->inc_mtimes && $self->built_mtime &&
421             $d->{inc_mtimes} && $d->{built_mtime} &&
422             $self->inc_mtimes eq $d->{inc_mtimes} &&
423             $self->built_mtime eq $d->{built_mtime} &&
424             $self->fingerprint_calc_current
425             ) ? 0 : 1;
426             }
427              
428              
429             # Gets the data used throughout the prepare_asset process:
430             sub get_prepare_data {
431 14     14 0 22 my $self = shift;
432            
433 14         44 my $files = $self->get_include_files;
434 14         420 my $inc_mtimes = $self->get_inc_mtime_concat($files);
435 14         2038 my $built_mtime = $self->get_built_mtime;
436            
437             return {
438 14         1542 files => $files,
439             inc_mtimes => $inc_mtimes,
440             built_mtime => $built_mtime
441             };
442             }
443              
444 14     14 0 22 sub before_prepare_asset {}
445              
446             sub prepare_asset {
447 26     26 0 41 my $self = shift;
448 26         116 my $start = [gettimeofday];
449              
450             # Optional hook:
451 26         93 $self->before_prepare_asset(@_);
452              
453 26         90 my $opt = $self->get_prepare_data;
454 26 100       79 return 1 unless $self->_build_required($opt);
455              
456             #### -----
457             #### The code above this line happens on every request and is designed
458             #### to be as fast as possible
459             ####
460             #### The code below this line is (comparatively) expensive and only
461             #### happens when a rebuild is needed which should be rare--only when
462             #### content is modified, or on app startup (unless 'persist_state' is set)
463             #### -----
464              
465             ### Do a rebuild:
466              
467             # --- Blocks for up to max_lock_wait seconds waiting to get an exclusive lock
468             # The lock is held until it goes out of scope.
469             # If we fail to get the lock, we just continue anyway in hopes that the second
470             # build won't corrupt the first, which is arguably better than killing the
471             # request.
472 3     3   46 my $lock= try { $self->_get_lock($self->lock_file, $self->max_lock_wait); };
  3         230  
473             # ---
474            
475 3         52 $self->build_asset($opt);
476            
477 3 50       567 $self->_app->log->debug(
478             "Built asset: " . $self->base_path . '/' . $self->asset_name .
479             ' in ' . sprintf("%.3f", tv_interval($start) ) . 's'
480             ) if ($self->_app->debug);
481              
482             # Release the lock and return:
483 3         205 $self->_persist_state;
484 3         54 return 1;
485             }
486              
487             sub build_asset {
488 3     3 0 7 my ($self, $opt) = @_;
489            
490 3   33     19 my $files = $opt->{files} || $self->get_include_files;
491 3   33     11 my $inc_mtimes = $opt->{inc_mtimes} || $self->get_inc_mtime_concat($files);
492 3   33     21 my $built_mtime = $opt->{built_mtime} || $self->get_built_mtime;
493            
494             # Check the fingerprint to see if we can avoid a full rebuild (if mtimes changed
495             # but the actual content hasn't by comparing the fingerprint/checksum):
496 3         142 my $fingerprint = $self->calculate_fingerprint($files);
497 3         14 my $cur_fingerprint = $self->current_fingerprint;
498 3 50 33     176 if($fingerprint && $cur_fingerprint && $cur_fingerprint eq $fingerprint) {
      33        
499             # If the mtimes changed but the fingerprint matches we don't need to regenerate.
500             # This will happen if another process just built the files while we were waiting
501             # for the lock and on the very first time after the application starts up
502 0         0 $self->inc_mtimes($inc_mtimes);
503 0         0 $self->built_mtime($built_mtime);
504 0         0 $self->_persist_state;
505 0         0 return 1;
506             }
507              
508             ### Ok, we really need to do a full rebuild:
509              
510 3 50       135 my $fd = $self->built_file->openw or die $!;
511 3         504 binmode $fd;
512 3         25 $self->write_built_file($fd,$files);
513 3 100       263 $fd->close if ($fd->opened);
514            
515             # Update the fingerprint (global) and cached mtimes (specific to the current process)
516 3         229 $self->inc_mtimes($opt->{inc_mtimes});
517 3         9 $self->built_mtime($self->get_built_mtime);
518             # we're calculating the fingerprint again because the built_file, which was just
519             # regenerated, is included in the checksum data. This could probably be optimized,
520             # however, this only happens on rebuild which rarely happens (should never happen)
521             # in production so an extra second is no big deal in this case.
522 3         24 $self->calculate_save_fingerprint($opt->{files});
523             }
524              
525             sub file_checksum {
526 6     6 0 9 my $self = shift;
527 6 50       22 my $files = ref $_[0] eq 'ARRAY' ? $_[0] : \@_;
528            
529 6         44 my $Sha1 = Digest::SHA1->new;
530 6         13 foreach my $file ( grep { -f $_ } @$files ) {
  22         561  
531 19 50       344 my $fh = $file->openr or die "$! : $file\n";
532 19         2351 binmode $fh;
533 19         818 $Sha1->addfile($fh);
534 19         67 $fh->close;
535             }
536              
537 6         333 return substr $Sha1->hexdigest, 0, $self->sha1_string_length;
538             }
539              
540 8     8 0 378 sub asset_name { (shift)->current_fingerprint }
541              
542             sub base_path {
543 8     8 0 9 my $self = shift;
544 8   50 8   69 my $pfx = try{RapidApp->active_request_context->mount_url} || '';
  8         257  
545 8         129 return join('/',$pfx,$self->action_namespace($self->_app));
546             }
547              
548             # this is just used for some internal optimization to avoid calling stat
549             # duplicate times. It is basically me being lazy, adding an internal extra param
550             # to asset_path() without changing its public API/arg list
551             has '_asset_path_skip_prepare', is => 'rw', isa => 'Bool', default => 0;
552             before asset_path => sub {
553             my $self = shift;
554             $self->prepare_asset(@_) unless ($self->_asset_path_skip_prepare);
555             };
556             sub asset_path {
557 8     8 0 22 my $self = shift;
558 8         24 return $self->base_path . '/' . $self->asset_name;
559             }
560              
561 0     0 0 0 sub html_head_tags { undef }
562              
563             # This locks a file or dies trying, and on success, returns a "lock object"
564             # which will release the lock if it goes out of scope. At the moment, this
565             # "object" is just a file handle.
566             #
567             # This lock is specifically *not* inherited by child processes (thanks to
568             # fcntl(FL_CLOEXEC), and in fact, this design principle gives it
569             # cross-platform compatibility that most lock module sdon't have.
570             #
571             sub _get_lock {
572 3     3   7 my ($self, $fname, $timeout)= @_;
573 3         5 my $fh;
574 3 50 33     24 sysopen($fh, $fname, O_RDWR|O_CREAT|O_EXCL, 0644)
575             or sysopen($fh, $fname, O_RDWR)
576             or croak "Unable to create or open $fname";
577            
578 3     3   80 try { fcntl($fh, F_SETFD, FD_CLOEXEC) }
579 3 50       429 or carp "Failed to set close-on-exec for $fname (see BUGS in Catalyst::Controller::AutoAssets)";
580            
581             # Try to get lock until timeout. We poll because there isn't a sensible
582             # way to wait for the lock. (I don't consider SIGALRM to be very sensible)
583 3         58 my $deadline= Time::HiRes::time() + $timeout;
584 3         4 my $locked= 0;
585 3         5 while (1) {
586 3 50       27 last if flock($fh, LOCK_EX|LOCK_NB);
587 0 0       0 croak "Can't get lock on $fname after $timeout seconds" if Time::HiRes::time() >= $deadline;
588 0         0 Time::HiRes::sleep(0.4);
589             }
590            
591             # Succeeded in getting the lock, so write our pid.
592 3         18 my $data= "$$";
593 3 50       143 syswrite($fh, $data, length($data)) or croak "Failed to write pid to $fname";
594 3 50       92 truncate($fh, length($data)) or croak "Failed to resize $fname";
595            
596 3         18 return $fh;
597             }
598              
599             1;
600              
601             __END__
602              
603             =pod
604              
605             =head1 NAME
606              
607             Catalyst::Controller::AutoAssets::Handler - Handler type Role and default namespace
608              
609             =head1 DESCRIPTION
610              
611             This is the base Role for C<Catalyst::Controller::AutoAssets> Handler classes and is
612             where the majority of the work is done for the AutoAssets module. The Handler class is
613             specified in the 'type' config param and is relative to this namespace. Absolute class
614             names can also be specified with the '+' prefix, so the following are equivalent:
615              
616             type => 'Directory'
617            
618             type => '+Catalyst::Controller::AutoAssets::Handler::Directory'
619              
620             Custom Handler classes can be written and used as long as they consume this Role. For examples
621             of how to write custom Handlers, see the existing Handlers below for reference.
622              
623             =head1 TYPE HANDLERS
624              
625             These are the current built in handler classes:
626              
627             =over
628              
629             =item L<Catalyst::Controller::AutoAssets::Handler::Directory>
630              
631             =item L<Catalyst::Controller::AutoAssets::Handler::CSS>
632              
633             =item L<Catalyst::Controller::AutoAssets::Handler::JS>
634              
635             =item L<Catalyst::Controller::AutoAssets::Handler::ImageSet>
636              
637             =item L<Catalyst::Controller::AutoAssets::Handler::IconSet>
638              
639             =back
640              
641             =head1 AUTHOR
642              
643             Henry Van Styn <vanstyn@cpan.org>
644              
645             =head1 COPYRIGHT AND LICENSE
646              
647             This software is copyright (c) 2013 by IntelliTree Solutions llc.
648              
649             This is free software; you can redistribute it and/or modify it under
650             the same terms as the Perl 5 programming language system itself.
651              
652             =cut
653              
654