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