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