add tests which prove the current behavior of not decoding chained args but decoding...
[catagits/Catalyst-Runtime.git] / lib / Catalyst / DispatchType / Chained.pm
CommitLineData
5882c86e 1package Catalyst::DispatchType::Chained;
141459fa 2
3c0186f2 3use Moose;
e8b9f2a9 4extends 'Catalyst::DispatchType';
5
141459fa 6use Text::SimpleTable;
7use Catalyst::ActionChain;
39fc2ce1 8use Catalyst::Utils;
141459fa 9use URI;
10
be5cb4e4 11has _endpoints => (
12 is => 'rw',
13 isa => 'ArrayRef',
14 required => 1,
15 default => sub{ [] },
16 );
17
18has _actions => (
19 is => 'rw',
20 isa => 'HashRef',
21 required => 1,
22 default => sub{ {} },
23 );
24
25has _children_of => (
26 is => 'rw',
27 isa => 'HashRef',
28 required => 1,
29 default => sub{ {} },
30 );
31
0fc2d522 32no Moose;
33
792b40ac 34# please don't perltidy this. hairy code within.
35
141459fa 36=head1 NAME
37
5882c86e 38Catalyst::DispatchType::Chained - Path Part DispatchType
141459fa 39
40=head1 SYNOPSIS
41
26dc649a 42Path part matching, allowing several actions to sequentially take care of processing a request:
43
05a90578 44 # root action - captures one argument after it
45 sub foo_setup : Chained('/') PathPart('foo') CaptureArgs(1) {
46 my ( $self, $c, $foo_arg ) = @_;
47 ...
48 }
49
50 # child action endpoint - takes one argument
51 sub bar : Chained('foo_setup') Args(1) {
52 my ( $self, $c, $bar_arg ) = @_;
53 ...
54 }
141459fa 55
56=head1 DESCRIPTION
57
26dc649a 58Dispatch type managing default behaviour. For more information on
59dispatch types, see:
60
61=over 4
62
b9b89145 63=item * L<Catalyst::Manual::Intro> for how they affect application authors
26dc649a 64
65=item * L<Catalyst::DispatchType> for implementation information.
66
67=back
05a90578 68
141459fa 69=head1 METHODS
70
71=head2 $self->list($c)
72
73Debug output for Path Part dispatch points
74
141459fa 75=cut
76
792b40ac 77sub list {
78 my ( $self, $c ) = @_;
79
be5cb4e4 80 return unless $self->_endpoints;
792b40ac 81
39fc2ce1 82 my $column_width = Catalyst::Utils::term_width() - 35 - 9;
792b40ac 83 my $paths = Text::SimpleTable->new(
3b9c0812 84 [ 35, 'Path Spec' ], [ $column_width, 'Private' ],
39fc2ce1 85 );
792b40ac 86
007a7ca0 87 my $has_unattached_actions;
88 my $unattached_actions = Text::SimpleTable->new(
9c9a725d 89 [ 35, 'Private' ], [ $column_width, 'Missing parent' ],
007a7ca0 90 );
91
792b40ac 92 ENDPOINT: foreach my $endpoint (
93 sort { $a->reverse cmp $b->reverse }
be5cb4e4 94 @{ $self->_endpoints }
792b40ac 95 ) {
96 my $args = $endpoint->attributes->{Args}->[0];
97 my @parts = (defined($args) ? (("*") x $args) : '...');
d34667c3 98 my @parents = ();
792b40ac 99 my $parent = "DUMMY";
100 my $curr = $endpoint;
101 while ($curr) {
1c34f703 102 if (my $cap = $curr->attributes->{CaptureArgs}) {
792b40ac 103 unshift(@parts, (("*") x $cap->[0]));
104 }
105 if (my $pp = $curr->attributes->{PartPath}) {
106 unshift(@parts, $pp->[0])
107 if (defined $pp->[0] && length $pp->[0]);
108 }
5882c86e 109 $parent = $curr->attributes->{Chained}->[0];
be5cb4e4 110 $curr = $self->_actions->{$parent};
d34667c3 111 unshift(@parents, $curr) if $curr;
792b40ac 112 }
007a7ca0 113 if ($parent ne '/') {
114 $has_unattached_actions = 1;
59d5a638 115 $unattached_actions->row('/' . ($parents[0] || $endpoint)->reverse, $parent);
007a7ca0 116 next ENDPOINT;
117 }
d34667c3 118 my @rows;
119 foreach my $p (@parents) {
120 my $name = "/${p}";
1c34f703 121 if (my $cap = $p->attributes->{CaptureArgs}) {
d34667c3 122 $name .= ' ('.$cap->[0].')';
123 }
124 unless ($p eq $parents[0]) {
125 $name = "-> ${name}";
126 }
127 push(@rows, [ '', $name ]);
128 }
129 push(@rows, [ '', (@rows ? "=> " : '')."/${endpoint}" ]);
f4624073 130 $rows[0][0] = join('/', '', @parts) || '/';
d34667c3 131 $paths->row(@$_) for @rows;
792b40ac 132 }
133
1cf0345b 134 $c->log->debug( "Loaded Chained actions:\n" . $paths->draw . "\n" );
007a7ca0 135 $c->log->debug( "Unattached Chained actions:\n", $unattached_actions->draw . "\n" )
136 if $has_unattached_actions;
792b40ac 137}
141459fa 138
139=head2 $self->match( $c, $path )
140
05a90578 141Calls C<recurse_match> to see if a chain matches the C<$path>.
141459fa 142
143=cut
144
145sub match {
146 my ( $self, $c, $path ) = @_;
147
e5ecd5bc 148 my $request = $c->request;
149 return 0 if @{$request->args};
141459fa 150
151 my @parts = split('/', $path);
152
6365b527 153 my ($chain, $captures, $parts) = $self->recurse_match($c, '/', \@parts);
e5ecd5bc 154 push @{$request->args}, @$parts if $parts && @$parts;
141459fa 155
156 return 0 unless $chain;
157
158 my $action = Catalyst::ActionChain->from_chain($chain);
159
e5ecd5bc 160 $request->action("/${action}");
161 $request->match("/${action}");
162 $request->captures($captures);
141459fa 163 $c->action($action);
164 $c->namespace( $action->namespace );
165
166 return 1;
167}
168
169=head2 $self->recurse_match( $c, $parent, \@path_parts )
170
05a90578 171Recursive search for a matching chain.
141459fa 172
173=cut
174
175sub recurse_match {
176 my ( $self, $c, $parent, $path_parts ) = @_;
be5cb4e4 177 my $children = $self->_children_of->{$parent};
141459fa 178 return () unless $children;
6b495723 179 my $best_action;
141459fa 180 my @captures;
1b04b972 181 TRY: foreach my $try_part (sort { length($b) <=> length($a) }
cdc97b63 182 keys %$children) {
1b04b972 183 # $b then $a to try longest part first
141459fa 184 my @parts = @$path_parts;
185 if (length $try_part) { # test and strip PathPart
186 next TRY unless
187 ($try_part eq join('/', # assemble equal number of parts
188 splice( # and strip them off @parts as well
792b40ac 189 @parts, 0, scalar(@{[split('/', $try_part)]})
190 ))); # @{[]} to avoid split to @_
141459fa 191 }
192 my @try_actions = @{$children->{$try_part}};
193 TRY_ACTION: foreach my $action (@try_actions) {
1c34f703 194 if (my $capture_attr = $action->attributes->{CaptureArgs}) {
f505df49 195
196 # Short-circuit if not enough remaining parts
197 next TRY_ACTION unless @parts >= $capture_attr->[0];
198
141459fa 199 my @captures;
200 my @parts = @parts; # localise
7a7ac23c 201
1c34f703 202 # strip CaptureArgs into list
7a7ac23c 203 push(@captures, splice(@parts, 0, $capture_attr->[0]));
204
141459fa 205 # try the remaining parts against children of this action
6365b527 206 my ($actions, $captures, $action_parts) = $self->recurse_match(
141459fa 207 $c, '/'.$action->reverse, \@parts
208 );
2f381252 209 # No best action currently
210 # OR The action has less parts
211 # OR The action has equal parts but less captured data (ergo more defined)
212 if ($actions &&
213 (!$best_action ||
214 $#$action_parts < $#{$best_action->{parts}} ||
215 ($#$action_parts == $#{$best_action->{parts}} &&
216 $#$captures < $#{$best_action->{captures}}))){
6b495723 217 $best_action = {
218 actions => [ $action, @$actions ],
219 captures=> [ @captures, @$captures ],
220 parts => $action_parts
221 };
222 }
223 }
224 else {
7a7ac23c 225 {
226 local $c->req->{arguments} = [ @{$c->req->args}, @parts ];
227 next TRY_ACTION unless $action->match($c);
228 }
953c176d 229 my $args_attr = $action->attributes->{Args}->[0];
230
231 # No best action currently
232 # OR This one matches with fewer parts left than the current best action,
233 # And therefore is a better match
ac5c933b 234 # OR No parts and this expects 0
953c176d 235 # The current best action might also be Args(0),
236 # but we couldn't chose between then anyway so we'll take the last seen
237
238 if (!$best_action ||
239 @parts < @{$best_action->{parts}} ||
a8194217 240 (!@parts && $args_attr eq 0)){
6b495723 241 $best_action = {
242 actions => [ $action ],
243 captures=> [],
244 parts => \@parts
6b495723 245 }
953c176d 246 }
141459fa 247 }
248 }
249 }
953c176d 250 return @$best_action{qw/actions captures parts/} if $best_action;
141459fa 251 return ();
252}
253
254=head2 $self->register( $c, $action )
255
05a90578 256Calls register_path for every Path attribute for the given $action.
141459fa 257
258=cut
259
260sub register {
261 my ( $self, $c, $action ) = @_;
262
1dc8af44 263 my @chained_attr = @{ $action->attributes->{Chained} || [] };
141459fa 264
1dc8af44 265 return 0 unless @chained_attr;
141459fa 266
2f381252 267 if (@chained_attr > 1) {
141459fa 268 Catalyst::Exception->throw(
5882c86e 269 "Multiple Chained attributes not supported registering ${action}"
141459fa 270 );
271 }
13c6b4cc 272 my $chained_to = $chained_attr[0];
141459fa 273
13c6b4cc 274 Catalyst::Exception->throw(
275 "Actions cannot chain to themselves registering /${action}"
276 ) if ($chained_to eq '/' . $action);
277
278 my $children = ($self->_children_of->{ $chained_to } ||= {});
141459fa 279
280 my @path_part = @{ $action->attributes->{PathPart} || [] };
281
09461385 282 my $part = $action->name;
141459fa 283
09461385 284 if (@path_part == 1 && defined $path_part[0]) {
285 $part = $path_part[0];
141459fa 286 } elsif (@path_part > 1) {
287 Catalyst::Exception->throw(
f3414019 288 "Multiple PathPart attributes not supported registering " . $action->reverse()
141459fa 289 );
290 }
291
8a6a6581 292 if ($part =~ m(^/)) {
293 Catalyst::Exception->throw(
f3414019 294 "Absolute parameters to PathPart not allowed registering " . $action->reverse()
8a6a6581 295 );
296 }
297
792b40ac 298 $action->attributes->{PartPath} = [ $part ];
299
141459fa 300 unshift(@{ $children->{$part} ||= [] }, $action);
301
be5cb4e4 302 $self->_actions->{'/'.$action->reverse} = $action;
792b40ac 303
1c34f703 304 unless ($action->attributes->{CaptureArgs}) {
be5cb4e4 305 unshift(@{ $self->_endpoints }, $action);
792b40ac 306 }
307
308 return 1;
141459fa 309}
310
311=head2 $self->uri_for_action($action, $captures)
312
05a90578 313Get the URI part for the action, using C<$captures> to fill
314the capturing parts.
141459fa 315
316=cut
317
318sub uri_for_action {
319 my ( $self, $action, $captures ) = @_;
320
5882c86e 321 return undef unless ($action->attributes->{Chained}
8b13f357 322 && !$action->attributes->{CaptureArgs});
792b40ac 323
324 my @parts = ();
325 my @captures = @$captures;
326 my $parent = "DUMMY";
327 my $curr = $action;
328 while ($curr) {
1c34f703 329 if (my $cap = $curr->attributes->{CaptureArgs}) {
792b40ac 330 return undef unless @captures >= $cap->[0]; # not enough captures
8b13f357 331 if ($cap->[0]) {
6ab73369 332 unshift(@parts,
333 map { s/([^A-Za-z0-9\-_.!~*'()])/$URI::Escape::escapes{$1}/go; $_; }
334 splice(@captures, -$cap->[0]));
8b13f357 335 }
792b40ac 336 }
337 if (my $pp = $curr->attributes->{PartPath}) {
338 unshift(@parts, $pp->[0])
8b13f357 339 if (defined($pp->[0]) && length($pp->[0]));
792b40ac 340 }
5882c86e 341 $parent = $curr->attributes->{Chained}->[0];
be5cb4e4 342 $curr = $self->_actions->{$parent};
141459fa 343 }
792b40ac 344
345 return undef unless $parent eq '/'; # fail for dangling action
346
347 return undef if @captures; # fail for too many captures
348
349 return join('/', '', @parts);
59d5a638 350
141459fa 351}
352
ae0e35ee 353=head2 $c->expand_action($action)
354
355Return a list of actions that represents a chained action. See
356L<Catalyst::Dispatcher> for more info. You probably want to
357use the expand_action it provides rather than this directly.
358
359=cut
360
52f71256 361sub expand_action {
362 my ($self, $action) = @_;
363
364 return unless $action->attributes && $action->attributes->{Chained};
365
366 my @chain;
367 my $curr = $action;
368
369 while ($curr) {
370 push @chain, $curr;
371 my $parent = $curr->attributes->{Chained}->[0];
372 $curr = $self->_actions->{$parent};
373 }
374
375 return Catalyst::ActionChain->from_chain([reverse @chain]);
376}
377
e5ecd5bc 378__PACKAGE__->meta->make_immutable;
379
05a90578 380=head1 USAGE
381
382=head2 Introduction
383
384The C<Chained> attribute allows you to chain public path parts together
67869327 385by their private names. A chain part's path can be specified with
386C<PathPart> and can be declared to expect an arbitrary number of
387arguments. The endpoint of the chain specifies how many arguments it
388gets through the C<Args> attribute. C<:Args(0)> would be none at all,
389C<:Args> without an integer would be unlimited. The path parts that
390aren't endpoints are using C<CaptureArgs> to specify how many parameters
391they expect to receive. As an example setup:
05a90578 392
393 package MyApp::Controller::Greeting;
394 use base qw/ Catalyst::Controller /;
395
396 # this is the beginning of our chain
397 sub hello : PathPart('hello') Chained('/') CaptureArgs(1) {
398 my ( $self, $c, $integer ) = @_;
399 $c->stash->{ message } = "Hello ";
400 $c->stash->{ arg_sum } = $integer;
401 }
402
403 # this is our endpoint, because it has no :CaptureArgs
404 sub world : PathPart('world') Chained('hello') Args(1) {
405 my ( $self, $c, $integer ) = @_;
406 $c->stash->{ message } .= "World!";
407 $c->stash->{ arg_sum } += $integer;
408
409 $c->response->body( join "<br/>\n" =>
410 $c->stash->{ message }, $c->stash->{ arg_sum } );
411 }
412
413The debug output provides a separate table for chained actions, showing
67869327 414the whole chain as it would match and the actions it contains. Here's an
415example of the startup output with our actions above:
05a90578 416
417 ...
418 [debug] Loaded Path Part actions:
419 .-----------------------+------------------------------.
420 | Path Spec | Private |
421 +-----------------------+------------------------------+
422 | /hello/*/world/* | /greeting/hello (1) |
423 | | => /greeting/world |
424 '-----------------------+------------------------------'
425 ...
426
67869327 427As you can see, Catalyst only deals with chains as whole paths and
428builds one for each endpoint, which are the actions with C<:Chained> but
429without C<:CaptureArgs>.
05a90578 430
431Let's assume this application gets a request at the path
67869327 432C</hello/23/world/12>. What happens then? First, Catalyst will dispatch
433to the C<hello> action and pass the value C<23> as an argument to it
434after the context. It does so because we have previously used
435C<:CaptureArgs(1)> to declare that it has one path part after itself as
436its argument. We told Catalyst that this is the beginning of the chain
437by specifying C<:Chained('/')>. Also note that instead of saying
438C<:PathPart('hello')> we could also just have said C<:PathPart>, as it
439defaults to the name of the action.
05a90578 440
441After C<hello> has run, Catalyst goes on to dispatch to the C<world>
67869327 442action. This is the last action to be called: Catalyst knows this is an
443endpoint because we did not specify a C<:CaptureArgs>
444attribute. Nevertheless we specify that this action expects an argument,
445but at this point we're using C<:Args(1)> to do that. We could also have
446said C<:Args> or left it out altogether, which would mean this action
447would get all arguments that are there. This action's C<:Chained>
448attribute says C<hello> and tells Catalyst that the C<hello> action in
449the current controller is its parent.
05a90578 450
451With this we have built a chain consisting of two public path parts.
67869327 452C<hello> captures one part of the path as its argument, and also
453specifies the path root as its parent. So this part is
454C</hello/$arg>. The next part is the endpoint C<world>, expecting one
455argument. It sums up to the path part C<world/$arg>. This leads to a
456complete chain of C</hello/$arg/world/$arg> which is matched against the
457requested paths.
458
459This example application would, if run and called by e.g.
460C</hello/23/world/12>, set the stash value C<message> to "Hello" and the
461value C<arg_sum> to "23". The C<world> action would then append "World!"
462to C<message> and add C<12> to the stash's C<arg_sum> value. For the
463sake of simplicity no view is shown. Instead we just put the values of
464the stash into our body. So the output would look like:
05a90578 465
466 Hello World!
467 35
468
67869327 469And our test server would have given us this debugging output for the
05a90578 470request:
471
472 ...
473 [debug] "GET" request for "hello/23/world/12" from "127.0.0.1"
474 [debug] Path is "/greeting/world"
475 [debug] Arguments are "12"
476 [info] Request took 0.164113s (6.093/s)
477 .------------------------------------------+-----------.
478 | Action | Time |
479 +------------------------------------------+-----------+
480 | /greeting/hello | 0.000029s |
481 | /greeting/world | 0.000024s |
482 '------------------------------------------+-----------'
483 ...
484
67869327 485What would be common uses of this dispatch technique? It gives the
486possibility to split up logic that contains steps that each depend on
487each other. An example would be, for example, a wiki path like
05a90578 488C</wiki/FooBarPage/rev/23/view>. This chain can be easily built with
489these actions:
490
491 sub wiki : PathPart('wiki') Chained('/') CaptureArgs(1) {
492 my ( $self, $c, $page_name ) = @_;
493 # load the page named $page_name and put the object
494 # into the stash
495 }
496
497 sub rev : PathPart('rev') Chained('wiki') CaptureArgs(1) {
498 my ( $self, $c, $revision_id ) = @_;
67869327 499 # use the page object in the stash to get at its
05a90578 500 # revision with number $revision_id
501 }
502
503 sub view : PathPart Chained('rev') Args(0) {
504 my ( $self, $c ) = @_;
67869327 505 # display the revision in our stash. Another option
05a90578 506 # would be to forward a compatible object to the action
507 # that displays the default wiki pages, unless we want
508 # a different interface here, for example restore
509 # functionality.
510 }
511
67869327 512It would now be possible to add other endpoints, for example C<restore>
513to restore this specific revision as the current state.
05a90578 514
67869327 515You don't have to put all the chained actions in one controller. The
516specification of the parent through C<:Chained> also takes an absolute
517action path as its argument. Just specify it with a leading C</>.
05a90578 518
519If you want, for example, to have actions for the public paths
67869327 520C</foo/12/edit> and C</foo/12>, just specify two actions with
05a90578 521C<:PathPart('foo')> and C<:Chained('/')>. The handler for the former
67869327 522path needs a C<:CaptureArgs(1)> attribute and a endpoint with
05a90578 523C<:PathPart('edit')> and C<:Chained('foo')>. For the latter path give
524the action just a C<:Args(1)> to mark it as endpoint. This sums up to
525this debugging output:
526
527 ...
528 [debug] Loaded Path Part actions:
529 .-----------------------+------------------------------.
530 | Path Spec | Private |
531 +-----------------------+------------------------------+
532 | /foo/* | /controller/foo_view |
533 | /foo/*/edit | /controller/foo_load (1) |
534 | | => /controller/edit |
535 '-----------------------+------------------------------'
536 ...
537
ac5c933b 538Here's a more detailed specification of the attributes belonging to
05a90578 539C<:Chained>:
540
541=head2 Attributes
542
543=over 8
544
545=item PathPart
546
547Sets the name of this part of the chain. If it is specified without
548arguments, it takes the name of the action as default. So basically
549C<sub foo :PathPart> and C<sub foo :PathPart('foo')> are identical.
550This can also contain slashes to bind to a deeper level. An action
551with C<sub bar :PathPart('foo/bar') :Chained('/')> would bind to
552C</foo/bar/...>. If you don't specify C<:PathPart> it has the same
553effect as using C<:PathPart>, it would default to the action name.
554
d21a2b27 555=item PathPrefix
556
557Sets PathPart to the path_prefix of the current controller.
558
05a90578 559=item Chained
560
561Has to be specified for every child in the chain. Possible values are
d21a2b27 562absolute and relative private action paths or a single slash C</> to
563tell Catalyst that this is the root of a chain. The attribute
564C<:Chained> without arguments also defaults to the C</> behavior.
565Relative action paths may use C<../> to refer to actions in parent
566controllers.
05a90578 567
67869327 568Because you can specify an absolute path to the parent action, it
569doesn't matter to Catalyst where that parent is located. So, if your
570design requests it, you can redispatch a chain through any controller or
571namespace you want.
05a90578 572
573Another interesting possibility gives C<:Chained('.')>, which chains
67869327 574itself to an action with the path of the current controller's namespace.
05a90578 575For example:
576
577 # in MyApp::Controller::Foo
578 sub bar : Chained CaptureArgs(1) { ... }
579
580 # in MyApp::Controller::Foo::Bar
581 sub baz : Chained('.') Args(1) { ... }
582
583This builds up a chain like C</bar/*/baz/*>. The specification of C<.>
67869327 584as the argument to Chained here chains the C<baz> action to an action
585with the path of the current controller namespace, namely
586C</foo/bar>. That action chains directly to C</>, so the C</bar/*/baz/*>
587chain comes out as the end product.
05a90578 588
d21a2b27 589=item ChainedParent
590
591Chains an action to another action with the same name in the parent
592controller. For Example:
593
594 # in MyApp::Controller::Foo
595 sub bar : Chained CaptureArgs(1) { ... }
596
597 # in MyApp::Controller::Foo::Moo
598 sub bar : ChainedParent Args(1) { ... }
599
600This builds a chain like C</bar/*/bar/*>.
601
05a90578 602=item CaptureArgs
603
67869327 604Must be specified for every part of the chain that is not an
05a90578 605endpoint. With this attribute Catalyst knows how many of the following
67869327 606parts of the path (separated by C</>) this action wants to capture as
607its arguments. If it doesn't expect any, just specify
608C<:CaptureArgs(0)>. The captures get passed to the action's C<@_> right
609after the context, but you can also find them as array references in
05a90578 610C<$c-E<gt>request-E<gt>captures-E<gt>[$level]>. The C<$level> is the
611level of the action in the chain that captured the parts of the path.
612
67869327 613An action that is part of a chain (that is, one that has a C<:Chained>
614attribute) but has no C<:CaptureArgs> attribute is treated by Catalyst
615as a chain end.
05a90578 616
617=item Args
618
619By default, endpoints receive the rest of the arguments in the path. You
620can tell Catalyst through C<:Args> explicitly how many arguments your
621endpoint expects, just like you can with C<:CaptureArgs>. Note that this
67869327 622also affects whether this chain is invoked on a request. A chain with an
05a90578 623endpoint specifying one argument will only match if exactly one argument
624exists in the path.
625
626You can specify an exact number of arguments like C<:Args(3)>, including
627C<0>. If you just say C<:Args> without any arguments, it is the same as
67869327 628leaving it out altogether: The chain is matched regardless of the number
05a90578 629of path parts after the endpoint.
630
67869327 631Just as with C<:CaptureArgs>, the arguments get passed to the action in
05a90578 632C<@_> after the context object. They can also be reached through
633C<$c-E<gt>request-E<gt>arguments>.
634
635=back
636
67869327 637=head2 Auto actions, dispatching and forwarding
05a90578 638
639Note that the list of C<auto> actions called depends on the private path
67869327 640of the endpoint of the chain, not on the chained actions way. The
641C<auto> actions will be run before the chain dispatching begins. In
642every other aspect, C<auto> actions behave as documented.
05a90578 643
644The C<forward>ing to other actions does just what you would expect. But if
645you C<detach> out of a chain, the rest of the chain will not get called
67869327 646after the C<detach>.
05a90578 647
2f381252 648=head1 AUTHORS
141459fa 649
2f381252 650Catalyst Contributors, see Catalyst.pm
141459fa 651
652=head1 COPYRIGHT
653
654This program is free software, you can redistribute it and/or modify it under
655the same terms as Perl itself.
656
657=cut
658
6591;