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