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