Initial commit of Moosified Catalyst parts.
[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 }
953c176d 173 my $args_attr = $action->attributes->{Args}->[0];
174
175 # No best action currently
176 # OR This one matches with fewer parts left than the current best action,
177 # And therefore is a better match
178 # OR No parts and this expects 0
179 # The current best action might also be Args(0),
180 # but we couldn't chose between then anyway so we'll take the last seen
181
182 if (!$best_action ||
183 @parts < @{$best_action->{parts}} ||
a8194217 184 (!@parts && $args_attr eq 0)){
6b495723 185 $best_action = {
186 actions => [ $action ],
187 captures=> [],
188 parts => \@parts
6b495723 189 }
953c176d 190 }
141459fa 191 }
192 }
193 }
953c176d 194 return @$best_action{qw/actions captures parts/} if $best_action;
141459fa 195 return ();
196}
197
198=head2 $self->register( $c, $action )
199
05a90578 200Calls register_path for every Path attribute for the given $action.
141459fa 201
202=cut
203
204sub register {
205 my ( $self, $c, $action ) = @_;
206
1dc8af44 207 my @chained_attr = @{ $action->attributes->{Chained} || [] };
141459fa 208
1dc8af44 209 return 0 unless @chained_attr;
141459fa 210
1dc8af44 211 if (@chained_attr > 2) {
141459fa 212 Catalyst::Exception->throw(
5882c86e 213 "Multiple Chained attributes not supported registering ${action}"
141459fa 214 );
215 }
216
1dc8af44 217 my $parent = $chained_attr[0];
141459fa 218
219 if (defined($parent) && length($parent)) {
1dc8af44 220 if ($parent eq '.') {
221 $parent = '/'.$action->namespace;
222 } elsif ($parent !~ m/^\//) {
7f64ae17 223 if ($action->namespace) {
224 $parent = '/'.join('/', $action->namespace, $parent);
225 } else {
226 $parent = '/'.$parent; # special case namespace '' (root)
227 }
141459fa 228 }
229 } else {
1dc8af44 230 $parent = '/'
141459fa 231 }
232
5882c86e 233 $action->attributes->{Chained} = [ $parent ];
792b40ac 234
141459fa 235 my $children = ($self->{children_of}{$parent} ||= {});
236
237 my @path_part = @{ $action->attributes->{PathPart} || [] };
238
09461385 239 my $part = $action->name;
141459fa 240
09461385 241 if (@path_part == 1 && defined $path_part[0]) {
242 $part = $path_part[0];
141459fa 243 } elsif (@path_part > 1) {
244 Catalyst::Exception->throw(
245 "Multiple PathPart attributes not supported registering ${action}"
246 );
247 }
248
8a6a6581 249 if ($part =~ m(^/)) {
250 Catalyst::Exception->throw(
251 "Absolute parameters to PathPart not allowed registering ${action}"
252 );
253 }
254
792b40ac 255 $action->attributes->{PartPath} = [ $part ];
256
141459fa 257 unshift(@{ $children->{$part} ||= [] }, $action);
258
792b40ac 259 ($self->{actions} ||= {})->{'/'.$action->reverse} = $action;
260
1c34f703 261 unless ($action->attributes->{CaptureArgs}) {
792b40ac 262 unshift(@{ $self->{endpoints} ||= [] }, $action);
263 }
264
265 return 1;
141459fa 266}
267
268=head2 $self->uri_for_action($action, $captures)
269
05a90578 270Get the URI part for the action, using C<$captures> to fill
271the capturing parts.
141459fa 272
273=cut
274
275sub uri_for_action {
276 my ( $self, $action, $captures ) = @_;
277
5882c86e 278 return undef unless ($action->attributes->{Chained}
8b13f357 279 && !$action->attributes->{CaptureArgs});
792b40ac 280
281 my @parts = ();
282 my @captures = @$captures;
283 my $parent = "DUMMY";
284 my $curr = $action;
285 while ($curr) {
1c34f703 286 if (my $cap = $curr->attributes->{CaptureArgs}) {
792b40ac 287 return undef unless @captures >= $cap->[0]; # not enough captures
8b13f357 288 if ($cap->[0]) {
289 unshift(@parts, splice(@captures, -$cap->[0]));
290 }
792b40ac 291 }
292 if (my $pp = $curr->attributes->{PartPath}) {
293 unshift(@parts, $pp->[0])
8b13f357 294 if (defined($pp->[0]) && length($pp->[0]));
792b40ac 295 }
5882c86e 296 $parent = $curr->attributes->{Chained}->[0];
792b40ac 297 $curr = $self->{actions}{$parent};
141459fa 298 }
792b40ac 299
300 return undef unless $parent eq '/'; # fail for dangling action
301
302 return undef if @captures; # fail for too many captures
303
304 return join('/', '', @parts);
305
141459fa 306}
307
05a90578 308=head1 USAGE
309
310=head2 Introduction
311
312The C<Chained> attribute allows you to chain public path parts together
67869327 313by their private names. A chain part's path can be specified with
314C<PathPart> and can be declared to expect an arbitrary number of
315arguments. The endpoint of the chain specifies how many arguments it
316gets through the C<Args> attribute. C<:Args(0)> would be none at all,
317C<:Args> without an integer would be unlimited. The path parts that
318aren't endpoints are using C<CaptureArgs> to specify how many parameters
319they expect to receive. As an example setup:
05a90578 320
321 package MyApp::Controller::Greeting;
322 use base qw/ Catalyst::Controller /;
323
324 # this is the beginning of our chain
325 sub hello : PathPart('hello') Chained('/') CaptureArgs(1) {
326 my ( $self, $c, $integer ) = @_;
327 $c->stash->{ message } = "Hello ";
328 $c->stash->{ arg_sum } = $integer;
329 }
330
331 # this is our endpoint, because it has no :CaptureArgs
332 sub world : PathPart('world') Chained('hello') Args(1) {
333 my ( $self, $c, $integer ) = @_;
334 $c->stash->{ message } .= "World!";
335 $c->stash->{ arg_sum } += $integer;
336
337 $c->response->body( join "<br/>\n" =>
338 $c->stash->{ message }, $c->stash->{ arg_sum } );
339 }
340
341The debug output provides a separate table for chained actions, showing
67869327 342the whole chain as it would match and the actions it contains. Here's an
343example of the startup output with our actions above:
05a90578 344
345 ...
346 [debug] Loaded Path Part actions:
347 .-----------------------+------------------------------.
348 | Path Spec | Private |
349 +-----------------------+------------------------------+
350 | /hello/*/world/* | /greeting/hello (1) |
351 | | => /greeting/world |
352 '-----------------------+------------------------------'
353 ...
354
67869327 355As you can see, Catalyst only deals with chains as whole paths and
356builds one for each endpoint, which are the actions with C<:Chained> but
357without C<:CaptureArgs>.
05a90578 358
359Let's assume this application gets a request at the path
67869327 360C</hello/23/world/12>. What happens then? First, Catalyst will dispatch
361to the C<hello> action and pass the value C<23> as an argument to it
362after the context. It does so because we have previously used
363C<:CaptureArgs(1)> to declare that it has one path part after itself as
364its argument. We told Catalyst that this is the beginning of the chain
365by specifying C<:Chained('/')>. Also note that instead of saying
366C<:PathPart('hello')> we could also just have said C<:PathPart>, as it
367defaults to the name of the action.
05a90578 368
369After C<hello> has run, Catalyst goes on to dispatch to the C<world>
67869327 370action. This is the last action to be called: Catalyst knows this is an
371endpoint because we did not specify a C<:CaptureArgs>
372attribute. Nevertheless we specify that this action expects an argument,
373but at this point we're using C<:Args(1)> to do that. We could also have
374said C<:Args> or left it out altogether, which would mean this action
375would get all arguments that are there. This action's C<:Chained>
376attribute says C<hello> and tells Catalyst that the C<hello> action in
377the current controller is its parent.
05a90578 378
379With this we have built a chain consisting of two public path parts.
67869327 380C<hello> captures one part of the path as its argument, and also
381specifies the path root as its parent. So this part is
382C</hello/$arg>. The next part is the endpoint C<world>, expecting one
383argument. It sums up to the path part C<world/$arg>. This leads to a
384complete chain of C</hello/$arg/world/$arg> which is matched against the
385requested paths.
386
387This example application would, if run and called by e.g.
388C</hello/23/world/12>, set the stash value C<message> to "Hello" and the
389value C<arg_sum> to "23". The C<world> action would then append "World!"
390to C<message> and add C<12> to the stash's C<arg_sum> value. For the
391sake of simplicity no view is shown. Instead we just put the values of
392the stash into our body. So the output would look like:
05a90578 393
394 Hello World!
395 35
396
67869327 397And our test server would have given us this debugging output for the
05a90578 398request:
399
400 ...
401 [debug] "GET" request for "hello/23/world/12" from "127.0.0.1"
402 [debug] Path is "/greeting/world"
403 [debug] Arguments are "12"
404 [info] Request took 0.164113s (6.093/s)
405 .------------------------------------------+-----------.
406 | Action | Time |
407 +------------------------------------------+-----------+
408 | /greeting/hello | 0.000029s |
409 | /greeting/world | 0.000024s |
410 '------------------------------------------+-----------'
411 ...
412
67869327 413What would be common uses of this dispatch technique? It gives the
414possibility to split up logic that contains steps that each depend on
415each other. An example would be, for example, a wiki path like
05a90578 416C</wiki/FooBarPage/rev/23/view>. This chain can be easily built with
417these actions:
418
419 sub wiki : PathPart('wiki') Chained('/') CaptureArgs(1) {
420 my ( $self, $c, $page_name ) = @_;
421 # load the page named $page_name and put the object
422 # into the stash
423 }
424
425 sub rev : PathPart('rev') Chained('wiki') CaptureArgs(1) {
426 my ( $self, $c, $revision_id ) = @_;
67869327 427 # use the page object in the stash to get at its
05a90578 428 # revision with number $revision_id
429 }
430
431 sub view : PathPart Chained('rev') Args(0) {
432 my ( $self, $c ) = @_;
67869327 433 # display the revision in our stash. Another option
05a90578 434 # would be to forward a compatible object to the action
435 # that displays the default wiki pages, unless we want
436 # a different interface here, for example restore
437 # functionality.
438 }
439
67869327 440It would now be possible to add other endpoints, for example C<restore>
441to restore this specific revision as the current state.
05a90578 442
67869327 443You don't have to put all the chained actions in one controller. The
444specification of the parent through C<:Chained> also takes an absolute
445action path as its argument. Just specify it with a leading C</>.
05a90578 446
447If you want, for example, to have actions for the public paths
67869327 448C</foo/12/edit> and C</foo/12>, just specify two actions with
05a90578 449C<:PathPart('foo')> and C<:Chained('/')>. The handler for the former
67869327 450path needs a C<:CaptureArgs(1)> attribute and a endpoint with
05a90578 451C<:PathPart('edit')> and C<:Chained('foo')>. For the latter path give
452the action just a C<:Args(1)> to mark it as endpoint. This sums up to
453this debugging output:
454
455 ...
456 [debug] Loaded Path Part actions:
457 .-----------------------+------------------------------.
458 | Path Spec | Private |
459 +-----------------------+------------------------------+
460 | /foo/* | /controller/foo_view |
461 | /foo/*/edit | /controller/foo_load (1) |
462 | | => /controller/edit |
463 '-----------------------+------------------------------'
464 ...
465
466Here's a more detailed specification of the attributes belonging to
467C<:Chained>:
468
469=head2 Attributes
470
471=over 8
472
473=item PathPart
474
475Sets the name of this part of the chain. If it is specified without
476arguments, it takes the name of the action as default. So basically
477C<sub foo :PathPart> and C<sub foo :PathPart('foo')> are identical.
478This can also contain slashes to bind to a deeper level. An action
479with C<sub bar :PathPart('foo/bar') :Chained('/')> would bind to
480C</foo/bar/...>. If you don't specify C<:PathPart> it has the same
481effect as using C<:PathPart>, it would default to the action name.
482
483=item Chained
484
485Has to be specified for every child in the chain. Possible values are
486absolute and relative private action paths, with the relatives pointing
487to the current controller, or a single slash C</> to tell Catalyst that
83784422 488this is the root of a chain. The attribute C<:Chained> without arguments
67869327 489also defaults to the C</> behavior.
05a90578 490
67869327 491Because you can specify an absolute path to the parent action, it
492doesn't matter to Catalyst where that parent is located. So, if your
493design requests it, you can redispatch a chain through any controller or
494namespace you want.
05a90578 495
496Another interesting possibility gives C<:Chained('.')>, which chains
67869327 497itself to an action with the path of the current controller's namespace.
05a90578 498For example:
499
500 # in MyApp::Controller::Foo
501 sub bar : Chained CaptureArgs(1) { ... }
502
503 # in MyApp::Controller::Foo::Bar
504 sub baz : Chained('.') Args(1) { ... }
505
506This builds up a chain like C</bar/*/baz/*>. The specification of C<.>
67869327 507as the argument to Chained here chains the C<baz> action to an action
508with the path of the current controller namespace, namely
509C</foo/bar>. That action chains directly to C</>, so the C</bar/*/baz/*>
510chain comes out as the end product.
05a90578 511
512=item CaptureArgs
513
67869327 514Must be specified for every part of the chain that is not an
05a90578 515endpoint. With this attribute Catalyst knows how many of the following
67869327 516parts of the path (separated by C</>) this action wants to capture as
517its arguments. If it doesn't expect any, just specify
518C<:CaptureArgs(0)>. The captures get passed to the action's C<@_> right
519after the context, but you can also find them as array references in
05a90578 520C<$c-E<gt>request-E<gt>captures-E<gt>[$level]>. The C<$level> is the
521level of the action in the chain that captured the parts of the path.
522
67869327 523An action that is part of a chain (that is, one that has a C<:Chained>
524attribute) but has no C<:CaptureArgs> attribute is treated by Catalyst
525as a chain end.
05a90578 526
527=item Args
528
529By default, endpoints receive the rest of the arguments in the path. You
530can tell Catalyst through C<:Args> explicitly how many arguments your
531endpoint expects, just like you can with C<:CaptureArgs>. Note that this
67869327 532also affects whether this chain is invoked on a request. A chain with an
05a90578 533endpoint specifying one argument will only match if exactly one argument
534exists in the path.
535
536You can specify an exact number of arguments like C<:Args(3)>, including
537C<0>. If you just say C<:Args> without any arguments, it is the same as
67869327 538leaving it out altogether: The chain is matched regardless of the number
05a90578 539of path parts after the endpoint.
540
67869327 541Just as with C<:CaptureArgs>, the arguments get passed to the action in
05a90578 542C<@_> after the context object. They can also be reached through
543C<$c-E<gt>request-E<gt>arguments>.
544
545=back
546
67869327 547=head2 Auto actions, dispatching and forwarding
05a90578 548
549Note that the list of C<auto> actions called depends on the private path
67869327 550of the endpoint of the chain, not on the chained actions way. The
551C<auto> actions will be run before the chain dispatching begins. In
552every other aspect, C<auto> actions behave as documented.
05a90578 553
554The C<forward>ing to other actions does just what you would expect. But if
555you C<detach> out of a chain, the rest of the chain will not get called
67869327 556after the C<detach>.
05a90578 557
141459fa 558=head1 AUTHOR
559
792b40ac 560Matt S Trout <mst@shadowcatsystems.co.uk>
141459fa 561
562=head1 COPYRIGHT
563
564This program is free software, you can redistribute it and/or modify it under
565the same terms as Perl itself.
566
567=cut
568
5691;