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