package Catalyst::DispatchType::Chained;
-use MRO::Compat;
-use mro 'c3';
use Moose;
extends 'Catalyst::DispatchType';
use Text::SimpleTable;
use Catalyst::ActionChain;
+use Catalyst::Utils;
use URI;
+use Scalar::Util ();
has _endpoints => (
is => 'rw',
=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
return unless $self->_endpoints;
+ my $avail_width = Catalyst::Utils::term_width() - 9;
+ my $col1_width = ($avail_width * .50) < 35 ? 35 : int($avail_width * .50);
+ my $col2_width = $avail_width - $col1_width;
my $paths = Text::SimpleTable->new(
- [ 35, 'Path Spec' ], [ 36, 'Private' ]
- );
+ [ $col1_width, 'Path Spec' ], [ $col2_width, 'Private' ],
+ );
+
+ my $has_unattached_actions;
+ my $unattached_actions = Text::SimpleTable->new(
+ [ $col1_width, 'Private' ], [ $col2_width, 'Missing parent' ],
+ );
ENDPOINT: foreach my $endpoint (
sort { $a->reverse cmp $b->reverse }
if (my $cap = $curr->attributes->{CaptureArgs}) {
unshift(@parts, (("*") x $cap->[0]));
}
- if (my $pp = $curr->attributes->{PartPath}) {
+ if (my $pp = $curr->attributes->{PathPart}) {
unshift(@parts, $pp->[0])
if (defined $pp->[0] && length $pp->[0]);
}
$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] || $endpoint)->reverse, $parent);
+ next ENDPOINT;
+ }
my @rows;
foreach my $p (@parents) {
my $name = "/${p}";
push(@rows, [ '', $name ]);
}
push(@rows, [ '', (@rows ? "=> " : '')."/${endpoint}" ]);
- $rows[0][0] = join('/', '', @parts);
+ $rows[0][0] = join('/', '', @parts) || '/';
$paths->row(@$_) for @rows;
}
$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 )
my @parts = split('/', $path);
my ($chain, $captures, $parts) = $self->recurse_match($c, '/', \@parts);
- push @{$request->args}, @$parts if $parts && @$parts;
+
+ if ($parts && @$parts) {
+ for my $arg (@$parts) {
+ $arg =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
+ push @{$request->args}, $arg;
+ }
+ }
return 0 unless $chain;
my ($actions, $captures, $action_parts) = $self->recurse_match(
$c, '/'.$action->reverse, \@parts
);
- if ($actions && (!$best_action || $#$action_parts < $#{$best_action->{parts}})){
+ # No best action currently
+ # OR The action has less parts
+ # OR The action has equal parts but less captured data (ergo more defined)
+ if ($actions &&
+ (!$best_action ||
+ $#$action_parts < $#{$best_action->{parts}} ||
+ ($#$action_parts == $#{$best_action->{parts}} &&
+ $#$captures < $#{$best_action->{captures}}))){
$best_action = {
actions => [ $action, @$actions ],
captures=> [ @captures, @$captures ],
# No best action currently
# OR This one matches with fewer parts left than the current best action,
# And therefore is a better match
- # OR No parts and this expects 0
+ # OR No parts and this expects 0
# The current best action might also be Args(0),
# but we couldn't chose between then anyway so we'll take the last seen
return 0 unless @chained_attr;
- if (@chained_attr > 2) {
+ if (@chained_attr > 1) {
Catalyst::Exception->throw(
"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 = '/'
- }
+ Catalyst::Exception->throw(
+ "Actions cannot chain to themselves registering /${action}"
+ ) if ($chained_to eq '/' . $action);
- $action->attributes->{Chained} = [ $parent ];
-
- my $children = ($self->_children_of->{$parent} ||= {});
+ my $children = ($self->_children_of->{ $chained_to } ||= {});
my @path_part = @{ $action->attributes->{PathPart} || [] };
);
}
- $action->attributes->{PartPath} = [ $part ];
+ $action->attributes->{PathPart} = [ $part ];
unshift(@{ $children->{$part} ||= [] }, $action);
$self->_actions->{'/'.$action->reverse} = $action;
+ if (exists $action->attributes->{Args}) {
+ my $args = $action->attributes->{Args}->[0];
+ if (defined($args) and not (
+ Scalar::Util::looks_like_number($args) and
+ int($args) == $args
+ )) {
+ require Data::Dumper;
+ local $Data::Dumper::Terse = 1;
+ local $Data::Dumper::Indent = 0;
+ $args = Data::Dumper::Dumper($args);
+ Catalyst::Exception->throw(
+ "Invalid Args($args) for action " . $action->reverse() .
+ " (use 'Args' or 'Args(<number>)'"
+ );
+ }
+ }
+
unless ($action->attributes->{CaptureArgs}) {
unshift(@{ $self->_endpoints }, $action);
}
unshift(@parts, splice(@captures, -$cap->[0]));
}
}
- if (my $pp = $curr->attributes->{PartPath}) {
+ if (my $pp = $curr->attributes->{PathPart}) {
unshift(@parts, $pp->[0])
if (defined($pp->[0]) && length($pp->[0]));
}
return undef if @captures; # fail for too many captures
return join('/', '', @parts);
-
+
+}
+
+=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;
'-----------------------+------------------------------'
...
-Here's a more detailed specification of the attributes belonging to
+Here's a more detailed specification of the attributes belonging to
C<:Chained>:
=head2 Attributes
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
you C<detach> out of a chain, the rest of the chain will not get called
after the C<detach>.
-=head1 AUTHOR
+=head1 AUTHORS
-Matt S Trout <mst@shadowcatsystems.co.uk>
+Catalyst Contributors, see Catalyst.pm
=head1 COPYRIGHT
-This program is free software, you can redistribute it and/or modify it under
+This library is free software. You can redistribute it and/or modify it under
the same terms as Perl itself.
=cut