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