package Catalyst::DispatchType::Chained;
-use strict;
-use base qw/Catalyst::DispatchType/;
+use Moose;
+extends 'Catalyst::DispatchType';
+
use Text::SimpleTable;
use Catalyst::ActionChain;
+use Catalyst::Utils;
use URI;
+has _endpoints => (
+ is => 'rw',
+ isa => 'ArrayRef',
+ required => 1,
+ default => sub{ [] },
+ );
+
+has _actions => (
+ is => 'rw',
+ isa => 'HashRef',
+ required => 1,
+ default => sub{ {} },
+ );
+
+has _children_of => (
+ is => 'rw',
+ isa => 'HashRef',
+ required => 1,
+ default => sub{ {} },
+ );
+
+no Moose;
+
# please don't perltidy this. hairy code within.
=head1 NAME
=head1 SYNOPSIS
+Path part matching, allowing several actions to sequentially take care of processing a request:
+
# root action - captures one argument after it
sub foo_setup : Chained('/') PathPart('foo') CaptureArgs(1) {
my ( $self, $c, $foo_arg ) = @_;
=head1 DESCRIPTION
-See L</USAGE>.
+Dispatch type managing default behaviour. For more information on
+dispatch types, see:
+
+=over 4
+
+=item * L<Catalyst::Manual::Intro> for how they affect application authors
+
+=item * L<Catalyst::DispatchType> for implementation information.
+
+=back
=head1 METHODS
sub list {
my ( $self, $c ) = @_;
- return unless $self->{endpoints};
+ return unless $self->_endpoints;
+ my $column_width = Catalyst::Utils::term_width() - 35 - 9;
my $paths = Text::SimpleTable->new(
- [ 35, 'Path Spec' ], [ 36, 'Private' ]
- );
+ [ 35, 'Path Spec' ], [ $column_width, 'Private' ],
+ );
+
+ my $has_unattached_actions;
+ my $unattached_actions = Text::SimpleTable->new(
+ [ 35, 'Private' ], [ $column_width, 'Missing parent' ],
+ );
ENDPOINT: foreach my $endpoint (
sort { $a->reverse cmp $b->reverse }
- @{ $self->{endpoints} }
+ @{ $self->_endpoints }
) {
my $args = $endpoint->attributes->{Args}->[0];
my @parts = (defined($args) ? (("*") x $args) : '...');
if (defined $pp->[0] && length $pp->[0]);
}
$parent = $curr->attributes->{Chained}->[0];
- $curr = $self->{actions}{$parent};
+ $curr = $self->_actions->{$parent};
unshift(@parents, $curr) if $curr;
}
- next ENDPOINT unless $parent eq '/'; # skip dangling action
+ if ($parent ne '/') {
+ $has_unattached_actions = 1;
+ $unattached_actions->row('/'.$parents[0]->reverse, $parent);
+ next ENDPOINT;
+ }
my @rows;
foreach my $p (@parents) {
my $name = "/${p}";
}
$c->log->debug( "Loaded Chained actions:\n" . $paths->draw . "\n" );
+ $c->log->debug( "Unattached Chained actions:\n", $unattached_actions->draw . "\n" )
+ if $has_unattached_actions;
}
=head2 $self->match( $c, $path )
sub match {
my ( $self, $c, $path ) = @_;
- return 0 if @{$c->req->args};
+ my $request = $c->request;
+ return 0 if @{$request->args};
my @parts = split('/', $path);
my ($chain, $captures, $parts) = $self->recurse_match($c, '/', \@parts);
- push @{$c->req->args}, @$parts if $parts && @$parts;
+ push @{$request->args}, @$parts if $parts && @$parts;
return 0 unless $chain;
my $action = Catalyst::ActionChain->from_chain($chain);
- $c->req->action("/${action}");
- $c->req->match("/${action}");
- $c->req->captures($captures);
+ $request->action("/${action}");
+ $request->match("/${action}");
+ $request->captures($captures);
$c->action($action);
$c->namespace( $action->namespace );
sub recurse_match {
my ( $self, $c, $parent, $path_parts ) = @_;
- my $children = $self->{children_of}{$parent};
+ my $children = $self->_children_of->{$parent};
return () unless $children;
my $best_action;
my @captures;
"Multiple Chained attributes not supported registering ${action}"
);
}
+ my $chained_to = $chained_attr[0];
- my $parent = $chained_attr[0];
-
- if (defined($parent) && length($parent)) {
- if ($parent eq '.') {
- $parent = '/'.$action->namespace;
- } elsif ($parent !~ m/^\//) {
- if ($action->namespace) {
- $parent = '/'.join('/', $action->namespace, $parent);
- } else {
- $parent = '/'.$parent; # special case namespace '' (root)
- }
- }
- } else {
- $parent = '/'
- }
-
- $action->attributes->{Chained} = [ $parent ];
+ Catalyst::Exception->throw(
+ "Actions cannot chain to themselves registering /${action}"
+ ) if ($chained_to eq '/' . $action);
- my $children = ($self->{children_of}{$parent} ||= {});
+ my $children = ($self->_children_of->{ $chained_to } ||= {});
my @path_part = @{ $action->attributes->{PathPart} || [] };
$part = $path_part[0];
} elsif (@path_part > 1) {
Catalyst::Exception->throw(
- "Multiple PathPart attributes not supported registering ${action}"
+ "Multiple PathPart attributes not supported registering " . $action->reverse()
);
}
if ($part =~ m(^/)) {
Catalyst::Exception->throw(
- "Absolute parameters to PathPart not allowed registering ${action}"
+ "Absolute parameters to PathPart not allowed registering " . $action->reverse()
);
}
unshift(@{ $children->{$part} ||= [] }, $action);
- ($self->{actions} ||= {})->{'/'.$action->reverse} = $action;
+ $self->_actions->{'/'.$action->reverse} = $action;
unless ($action->attributes->{CaptureArgs}) {
- unshift(@{ $self->{endpoints} ||= [] }, $action);
+ unshift(@{ $self->_endpoints }, $action);
}
return 1;
if (defined($pp->[0]) && length($pp->[0]));
}
$parent = $curr->attributes->{Chained}->[0];
- $curr = $self->{actions}{$parent};
+ $curr = $self->_actions->{$parent};
}
return undef unless $parent eq '/'; # fail for dangling action
}
+=head2 $c->expand_action($action)
+
+Return a list of actions that represents a chained action. See
+L<Catalyst::Dispatcher> for more info. You probably want to
+use the expand_action it provides rather than this directly.
+
+=cut
+
+sub expand_action {
+ my ($self, $action) = @_;
+
+ return unless $action->attributes && $action->attributes->{Chained};
+
+ my @chain;
+ my $curr = $action;
+
+ while ($curr) {
+ push @chain, $curr;
+ my $parent = $curr->attributes->{Chained}->[0];
+ $curr = $self->_actions->{$parent};
+ }
+
+ return Catalyst::ActionChain->from_chain([reverse @chain]);
+}
+
+__PACKAGE__->meta->make_immutable;
+
=head1 USAGE
=head2 Introduction
C</foo/bar/...>. If you don't specify C<:PathPart> it has the same
effect as using C<:PathPart>, it would default to the action name.
+=item PathPrefix
+
+Sets PathPart to the path_prefix of the current controller.
+
=item Chained
Has to be specified for every child in the chain. Possible values are
-absolute and relative private action paths, with the relatives pointing
-to the current controller, or a single slash C</> to tell Catalyst that
-this is the root of a chain. The attribute C<:Chained> without arguments
-also defaults to the C</> behavior.
+absolute and relative private action paths or a single slash C</> to
+tell Catalyst that this is the root of a chain. The attribute
+C<:Chained> without arguments also defaults to the C</> behavior.
+Relative action paths may use C<../> to refer to actions in parent
+controllers.
Because you can specify an absolute path to the parent action, it
doesn't matter to Catalyst where that parent is located. So, if your
C</foo/bar>. That action chains directly to C</>, so the C</bar/*/baz/*>
chain comes out as the end product.
+=item ChainedParent
+
+Chains an action to another action with the same name in the parent
+controller. For Example:
+
+ # in MyApp::Controller::Foo
+ sub bar : Chained CaptureArgs(1) { ... }
+
+ # in MyApp::Controller::Foo::Moo
+ sub bar : ChainedParent Args(1) { ... }
+
+This builds a chain like C</bar/*/bar/*>.
+
=item CaptureArgs
Must be specified for every part of the chain that is not an