doc tweaks to Chained.pm
[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->{Args});
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             unshift(@parts, splice(@captures, -$cap->[0]));
257         }
258         if (my $pp = $curr->attributes->{PartPath}) {
259             unshift(@parts, $pp->[0])
260                 if (defined $pp->[0] && length $pp->[0]);
261         }
262         $parent = $curr->attributes->{Chained}->[0];
263         $curr = $self->{actions}{$parent};
264     }
265
266     return undef unless $parent eq '/'; # fail for dangling action
267
268     return undef if @captures; # fail for too many captures
269
270     return join('/', '', @parts);
271    
272 }
273
274 =head1 USAGE
275
276 =head2 Introduction
277
278 The C<Chained> attribute allows you to chain public path parts together
279 by their private names. A chain part's path can be specified with
280 C<PathPart> and can be declared to expect an arbitrary number of
281 arguments. The endpoint of the chain specifies how many arguments it
282 gets through the C<Args> attribute. C<:Args(0)> would be none at all,
283 C<:Args> without an integer would be unlimited. The path parts that
284 aren't endpoints are using C<CaptureArgs> to specify how many parameters
285 they expect to receive. As an example setup:
286
287   package MyApp::Controller::Greeting;
288   use base qw/ Catalyst::Controller /;
289
290   #   this is the beginning of our chain
291   sub hello : PathPart('hello') Chained('/') CaptureArgs(1) {
292       my ( $self, $c, $integer ) = @_;
293       $c->stash->{ message } = "Hello ";
294       $c->stash->{ arg_sum } = $integer;
295   }
296
297   #   this is our endpoint, because it has no :CaptureArgs
298   sub world : PathPart('world') Chained('hello') Args(1) {
299       my ( $self, $c, $integer ) = @_;
300       $c->stash->{ message } .= "World!";
301       $c->stash->{ arg_sum } += $integer;
302
303       $c->response->body( join "<br/>\n" =>
304           $c->stash->{ message }, $c->stash->{ arg_sum } );
305   }
306
307 The debug output provides a separate table for chained actions, showing
308 the whole chain as it would match and the actions it contains. Here's an
309 example of the startup output with our actions above:
310
311   ...
312   [debug] Loaded Path Part actions:
313   .-----------------------+------------------------------.
314   | Path Spec             | Private                      |
315   +-----------------------+------------------------------+
316   | /hello/*/world/*      | /greeting/hello (1)          |
317   |                       | => /greeting/world           |
318   '-----------------------+------------------------------'
319   ...
320
321 As you can see, Catalyst only deals with chains as whole paths and
322 builds one for each endpoint, which are the actions with C<:Chained> but
323 without C<:CaptureArgs>.
324
325 Let's assume this application gets a request at the path
326 C</hello/23/world/12>. What happens then? First, Catalyst will dispatch
327 to the C<hello> action and pass the value C<23> as an argument to it
328 after the context. It does so because we have previously used
329 C<:CaptureArgs(1)> to declare that it has one path part after itself as
330 its argument. We told Catalyst that this is the beginning of the chain
331 by specifying C<:Chained('/')>. Also note that instead of saying
332 C<:PathPart('hello')> we could also just have said C<:PathPart>, as it
333 defaults to the name of the action.
334
335 After C<hello> has run, Catalyst goes on to dispatch to the C<world>
336 action. This is the last action to be called: Catalyst knows this is an
337 endpoint because we did not specify a C<:CaptureArgs>
338 attribute. Nevertheless we specify that this action expects an argument,
339 but at this point we're using C<:Args(1)> to do that. We could also have
340 said C<:Args> or left it out altogether, which would mean this action
341 would get all arguments that are there. This action's C<:Chained>
342 attribute says C<hello> and tells Catalyst that the C<hello> action in
343 the current controller is its parent.
344
345 With this we have built a chain consisting of two public path parts.
346 C<hello> captures one part of the path as its argument, and also
347 specifies the path root as its parent. So this part is
348 C</hello/$arg>. The next part is the endpoint C<world>, expecting one
349 argument. It sums up to the path part C<world/$arg>. This leads to a
350 complete chain of C</hello/$arg/world/$arg> which is matched against the
351 requested paths.
352
353 This example application would, if run and called by e.g.
354 C</hello/23/world/12>, set the stash value C<message> to "Hello" and the
355 value C<arg_sum> to "23". The C<world> action would then append "World!"
356 to C<message> and add C<12> to the stash's C<arg_sum> value.  For the
357 sake of simplicity no view is shown. Instead we just put the values of
358 the stash into our body. So the output would look like:
359
360   Hello World!
361   35
362
363 And our test server would have given us this debugging output for the
364 request:
365
366   ...
367   [debug] "GET" request for "hello/23/world/12" from "127.0.0.1"
368   [debug] Path is "/greeting/world"
369   [debug] Arguments are "12"
370   [info] Request took 0.164113s (6.093/s)
371   .------------------------------------------+-----------.
372   | Action                                   | Time      |
373   +------------------------------------------+-----------+
374   | /greeting/hello                          | 0.000029s |
375   | /greeting/world                          | 0.000024s |
376   '------------------------------------------+-----------'
377   ...
378
379 What would be common uses of this dispatch technique? It gives the
380 possibility to split up logic that contains steps that each depend on
381 each other. An example would be, for example, a wiki path like
382 C</wiki/FooBarPage/rev/23/view>. This chain can be easily built with
383 these actions:
384
385   sub wiki : PathPart('wiki') Chained('/') CaptureArgs(1) {
386       my ( $self, $c, $page_name ) = @_;
387       #  load the page named $page_name and put the object
388       #  into the stash
389   }
390
391   sub rev : PathPart('rev') Chained('wiki') CaptureArgs(1) {
392       my ( $self, $c, $revision_id ) = @_;
393       #  use the page object in the stash to get at its
394       #  revision with number $revision_id
395   }
396
397   sub view : PathPart Chained('rev') Args(0) {
398       my ( $self, $c ) = @_;
399       #  display the revision in our stash. Another option
400       #  would be to forward a compatible object to the action
401       #  that displays the default wiki pages, unless we want
402       #  a different interface here, for example restore
403       #  functionality.
404   }
405
406 It would now be possible to add other endpoints, for example C<restore>
407 to restore this specific revision as the current state.
408
409 You don't have to put all the chained actions in one controller. The
410 specification of the parent through C<:Chained> also takes an absolute
411 action path as its argument. Just specify it with a leading C</>.
412
413 If you want, for example, to have actions for the public paths
414 C</foo/12/edit> and C</foo/12>, just specify two actions with
415 C<:PathPart('foo')> and C<:Chained('/')>. The handler for the former
416 path needs a C<:CaptureArgs(1)> attribute and a endpoint with
417 C<:PathPart('edit')> and C<:Chained('foo')>. For the latter path give
418 the action just a C<:Args(1)> to mark it as endpoint. This sums up to
419 this debugging output:
420
421   ...
422   [debug] Loaded Path Part actions:
423   .-----------------------+------------------------------.
424   | Path Spec             | Private                      |
425   +-----------------------+------------------------------+
426   | /foo/*                | /controller/foo_view         |
427   | /foo/*/edit           | /controller/foo_load (1)     |
428   |                       | => /controller/edit          |
429   '-----------------------+------------------------------'
430   ...
431
432 Here's a more detailed specification of the attributes belonging to 
433 C<:Chained>:
434
435 =head2 Attributes
436
437 =over 8
438
439 =item PathPart
440
441 Sets the name of this part of the chain. If it is specified without
442 arguments, it takes the name of the action as default. So basically
443 C<sub foo :PathPart> and C<sub foo :PathPart('foo')> are identical.
444 This can also contain slashes to bind to a deeper level. An action
445 with C<sub bar :PathPart('foo/bar') :Chained('/')> would bind to
446 C</foo/bar/...>. If you don't specify C<:PathPart> it has the same
447 effect as using C<:PathPart>, it would default to the action name.
448
449 =item Chained
450
451 Has to be specified for every child in the chain. Possible values are
452 absolute and relative private action paths, with the relatives pointing
453 to the current controller, or a single slash C</> to tell Catalyst that
454 this is the root of a chain. The attribute C<:Chained> without aguments
455 also defaults to the C</> behavior.
456
457 Because you can specify an absolute path to the parent action, it
458 doesn't matter to Catalyst where that parent is located. So, if your
459 design requests it, you can redispatch a chain through any controller or
460 namespace you want.
461
462 Another interesting possibility gives C<:Chained('.')>, which chains
463 itself to an action with the path of the current controller's namespace.
464 For example:
465
466   #   in MyApp::Controller::Foo
467   sub bar : Chained CaptureArgs(1) { ... }
468
469   #   in MyApp::Controller::Foo::Bar
470   sub baz : Chained('.') Args(1) { ... }
471
472 This builds up a chain like C</bar/*/baz/*>. The specification of C<.>
473 as the argument to Chained here chains the C<baz> action to an action
474 with the path of the current controller namespace, namely
475 C</foo/bar>. That action chains directly to C</>, so the C</bar/*/baz/*>
476 chain comes out as the end product.
477
478 =item CaptureArgs
479
480 Must be specified for every part of the chain that is not an
481 endpoint. With this attribute Catalyst knows how many of the following
482 parts of the path (separated by C</>) this action wants to capture as
483 its arguments. If it doesn't expect any, just specify
484 C<:CaptureArgs(0)>.  The captures get passed to the action's C<@_> right
485 after the context, but you can also find them as array references in
486 C<$c-E<gt>request-E<gt>captures-E<gt>[$level]>. The C<$level> is the
487 level of the action in the chain that captured the parts of the path.
488
489 An action that is part of a chain (that is, one that has a C<:Chained>
490 attribute) but has no C<:CaptureArgs> attribute is treated by Catalyst
491 as a chain end.
492
493 =item Args
494
495 By default, endpoints receive the rest of the arguments in the path. You
496 can tell Catalyst through C<:Args> explicitly how many arguments your
497 endpoint expects, just like you can with C<:CaptureArgs>. Note that this
498 also affects whether this chain is invoked on a request. A chain with an
499 endpoint specifying one argument will only match if exactly one argument
500 exists in the path.
501
502 You can specify an exact number of arguments like C<:Args(3)>, including
503 C<0>. If you just say C<:Args> without any arguments, it is the same as
504 leaving it out altogether: The chain is matched regardless of the number
505 of path parts after the endpoint.
506
507 Just as with C<:CaptureArgs>, the arguments get passed to the action in
508 C<@_> after the context object. They can also be reached through
509 C<$c-E<gt>request-E<gt>arguments>.
510
511 =back
512
513 =head2 Auto actions, dispatching and forwarding
514
515 Note that the list of C<auto> actions called depends on the private path
516 of the endpoint of the chain, not on the chained actions way. The
517 C<auto> actions will be run before the chain dispatching begins. In
518 every other aspect, C<auto> actions behave as documented.
519
520 The C<forward>ing to other actions does just what you would expect. But if
521 you C<detach> out of a chain, the rest of the chain will not get called
522 after the C<detach>.
523
524 =head1 AUTHOR
525
526 Matt S Trout <mst@shadowcatsystems.co.uk>
527
528 =head1 COPYRIGHT
529
530 This program is free software, you can redistribute it and/or modify it under
531 the same terms as Perl itself.
532
533 =cut
534
535 1;