Index: t/live_component_controller_action_visit.t =================================================================== --- t/live_component_controller_action_visit.t (revision 0) +++ t/live_component_controller_action_visit.t (revision 0) @@ -0,0 +1,289 @@ +#!perl + +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../lib"; + +our $iters; + +BEGIN { $iters = $ENV{CAT_BENCH_ITERS} || 1; } + +use Test::More tests => 54 * $iters; +use Catalyst::Test 'TestApp'; + +if ( $ENV{CAT_BENCHMARK} ) { + require Benchmark; + Benchmark::timethis( $iters, \&run_tests ); +} +else { + for ( 1 .. $iters ) { + run_tests(); + } +} + +sub run_tests { + { + # Test visit to global private action + ok( my $response = request('http://localhost/action/visit/global'), + 'Request' ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content_type, 'text/plain', 'Response Content-Type' ); + is( $response->header('X-Catalyst-Action'), + 'action/visit/global', 'Main Class Action' ); + } + + { + my @expected = qw[ + TestApp::Controller::Action::Visit->one + TestApp::Controller::Action::Visit->two + TestApp::Controller::Action::Visit->three + TestApp::Controller::Action::Visit->four + TestApp::Controller::Action::Visit->five + TestApp::View::Dump::Request->process + TestApp->end + TestApp->end + TestApp->end + TestApp->end + TestApp->end + ]; + + @expected = map { /Action/ ? (_begin($_), $_) : ($_) } @expected; + my $expected = join( ", ", @expected ); + + # Test visit to chain of actions. + ok( my $response = request('http://localhost/action/visit/one'), + 'Request' ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content_type, 'text/plain', 'Response Content-Type' ); + is( $response->header('X-Catalyst-Action'), + 'action/visit/one', 'Test Action' ); + is( + $response->header('X-Test-Class'), + 'TestApp::Controller::Action::Visit', + 'Test Class' + ); + is( $response->header('X-Catalyst-Executed'), + $expected, 'Executed actions' ); + like( + $response->content, + qr/^bless\( .* 'Catalyst::Request' \)$/s, + 'Content is a serialized Catalyst::Request' + ); + } + { + my @expected = qw[ + TestApp::Controller::Action::Visit->visit_die + TestApp::Controller::Action::Visit->args + TestApp->end + TestApp->end + ]; + + @expected = map { /Action/ ? (_begin($_), $_) : ($_) } @expected; + my $expected = join( ", ", @expected ); + + ok( my $response = request('http://localhost/action/visit/visit_die'), + 'Request' ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content_type, 'text/plain', 'Response Content-Type' ); + is( $response->header('X-Catalyst-Action'), + 'action/visit/visit_die', 'Test Action' + ); + is( + $response->header('X-Test-Class'), + 'TestApp::Controller::Action::Visit', + 'Test Class' + ); + is( $response->header('X-Catalyst-Executed'), + $expected, 'Executed actions' ); + is( $response->content, "visit() doesn't die", "Visit does not die" ); + } + { + ok( + my $response = request('http://localhost/action/visit/model'), + 'Request with args' + ); + is( $response->content, + q[FATAL ERROR: Couldn't visit("Model::Foo"): Action cannot _DISPATCH. Did you try to visit() a non-controller action?] + ); + } + { + ok( + my $response = request('http://localhost/action/visit/view'), + 'Request with args' + ); + is( $response->content, + q[FATAL ERROR: Couldn't visit("View::Dump"): Action cannot _DISPATCH. Did you try to visit() a non-controller action?] + ); + } + { + ok( + my $response = + request('http://localhost/action/visit/with_args/old'), + 'Request with args' + ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content, 'old', 'visit() with args (old)' ); + } + + { + ok( + my $response = request( + 'http://localhost/action/visit/with_method_and_args/new'), + 'Request with args and method' + ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content, 'new', 'visit() with args (new)' ); + } + + # test visit with embedded args + { + ok( + my $response = + request('http://localhost/action/visit/args_embed_relative'), + 'Request' + ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content, 'ok', 'visit() with args_embed_relative' ); + } + + { + ok( + my $response = + request('http://localhost/action/visit/args_embed_absolute'), + 'Request' + ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content, 'ok', 'visit() with args_embed_absolute' ); + } + { + my @expected = qw[ + TestApp::Controller::Action::TestRelative->relative_visit + TestApp::Controller::Action::Visit->one + TestApp::Controller::Action::Visit->two + TestApp::Controller::Action::Visit->three + TestApp::Controller::Action::Visit->four + TestApp::Controller::Action::Visit->five + TestApp::View::Dump::Request->process + TestApp->end + TestApp->end + TestApp->end + TestApp->end + TestApp->end + TestApp->end + ]; + + @expected = map { /Action/ ? (_begin($_), $_) : ($_) } @expected; + my $expected = join( ", ", @expected ); + + # Test visit to chain of actions. + ok( my $response = request('http://localhost/action/relative/relative_visit'), + 'Request' ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content_type, 'text/plain', 'Response Content-Type' ); + is( $response->header('X-Catalyst-Action'), + 'action/relative/relative_visit', 'Test Action' ); + is( + $response->header('X-Test-Class'), + 'TestApp::Controller::Action::Visit', + 'Test Class' + ); + is( $response->header('X-Catalyst-Executed'), + $expected, 'Executed actions' ); + like( + $response->content, + qr/^bless\( .* 'Catalyst::Request' \)$/s, + 'Content is a serialized Catalyst::Request' + ); + } + { + my @expected = qw[ + TestApp::Controller::Action::TestRelative->relative_visit_two + TestApp::Controller::Action::Visit->one + TestApp::Controller::Action::Visit->two + TestApp::Controller::Action::Visit->three + TestApp::Controller::Action::Visit->four + TestApp::Controller::Action::Visit->five + TestApp::View::Dump::Request->process + TestApp->end + TestApp->end + TestApp->end + TestApp->end + TestApp->end + TestApp->end + ]; + + @expected = map { /Action/ ? (_begin($_), $_) : ($_) } @expected; + my $expected = join( ", ", @expected ); + + # Test visit to chain of actions. + ok( + my $response = + request('http://localhost/action/relative/relative_visit_two'), + 'Request' + ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content_type, 'text/plain', 'Response Content-Type' ); + is( + $response->header('X-Catalyst-Action'), + 'action/relative/relative_visit_two', + 'Test Action' + ); + is( + $response->header('X-Test-Class'), + 'TestApp::Controller::Action::Visit', + 'Test Class' + ); + is( $response->header('X-Catalyst-Executed'), + $expected, 'Executed actions' ); + like( + $response->content, + qr/^bless\( .* 'Catalyst::Request' \)$/s, + 'Content is a serialized Catalyst::Request' + ); + } + + # test class visit -- MUST FAIL! + { + ok( + my $response = request( + 'http://localhost/action/visit/class_visit_test_action'), + 'Request' + ); + ok( !$response->is_success, 'Response Fails' ); + is( $response->content, + q[FATAL ERROR: Couldn't visit("TestApp"): Action has no namespace: cannot visit() to a plain method or component, must be a :Action or some sort.], + "Cannot visit app namespace" + ); + } + + { + my @expected = qw[ + TestApp::Controller::Action::Visit->begin + TestApp::Controller::Action::Visit->visit_chained + TestApp::Controller::Action::Chained->begin + TestApp::Controller::Action::Chained->foo + TestApp::Controller::Action::Chained::Foo->spoon + TestApp::Controller::Action::Chained->end + TestApp->end + ]; + + my $expected = join( ", ", @expected ); + + ok( my $response = request('http://localhost/action/visit/visit_chained'), 'visit to chained + subcontroller endpoint' ); + is( $response->header('X-Catalyst-Executed'), + $expected, 'Executed actions' ); + is( $response->content, '; 1', 'Content OK' ); + } + +} + + + +sub _begin { + local $_ = shift; + s/->(.*)$/->begin/; + return $_; +} + Index: t/lib/TestApp/Controller/Action/Go.pm =================================================================== --- t/lib/TestApp/Controller/Action/Go.pm (revision 9055) +++ t/lib/TestApp/Controller/Action/Go.pm (working copy) @@ -25,7 +25,7 @@ sub five : Local { my ( $self, $c ) = @_; - $c->go('View::Dump::Request'); + $c->forward('View::Dump::Request'); } sub inheritance : Local { @@ -66,6 +66,18 @@ $c->go('/action/chained/foo/spoon',[1]); } +sub view : Local { + my ( $self, $c, $val ) = @_; + eval { $c->go('View::Dump') }; + $c->res->body( $@ ? $@ : "go() did not die" ); +} + +sub model : Local { + my ( $self, $c, $val ) = @_; + eval { $c->go('Model::Foo') }; + $c->res->body( $@ ? $@ : "go() did not die" ); +} + sub args_embed_relative : Local { my ( $self, $c ) = @_; $c->go('embed/ok'); Index: t/lib/TestApp/Controller/Action/TestRelative.pm =================================================================== --- t/lib/TestApp/Controller/Action/TestRelative.pm (revision 9055) +++ t/lib/TestApp/Controller/Action/TestRelative.pm (working copy) @@ -26,4 +26,15 @@ my ( $self, $c ) = @_; $c->go( 'TestApp::Controller::Action::Go', 'one' ); } + +sub relative_visit : Local { + my ( $self, $c ) = @_; + $c->visit('/action/visit/one'); +} + +sub relative_visit_two : Local { + my ( $self, $c ) = @_; + $c->visit( 'TestApp::Controller::Action::Visit', 'one' ); +} + 1; Index: t/lib/TestApp/Controller/Action/Visit.pm =================================================================== --- t/lib/TestApp/Controller/Action/Visit.pm (revision 0) +++ t/lib/TestApp/Controller/Action/Visit.pm (revision 0) @@ -0,0 +1,101 @@ +package TestApp::Controller::Action::Visit; + +use strict; +use base 'TestApp::Controller::Action'; + +sub one : Local { + my ( $self, $c ) = @_; + $c->visit('two'); +} + +sub two : Private { + my ( $self, $c ) = @_; + $c->visit('three'); +} + +sub three : Local { + my ( $self, $c ) = @_; + $c->visit( $self, 'four' ); +} + +sub four : Private { + my ( $self, $c ) = @_; + $c->visit('/action/visit/five'); +} + +sub five : Local { + my ( $self, $c ) = @_; + $c->forward('View::Dump::Request'); +} + +sub inheritance : Local { + my ( $self, $c ) = @_; + $c->visit('/action/inheritance/a/b/default'); +} + +sub global : Local { + my ( $self, $c ) = @_; + $c->visit('/global_action'); +} + +sub with_args : Local { + my ( $self, $c, $arg ) = @_; + $c->visit( 'args', [$arg] ); +} + +sub with_method_and_args : Local { + my ( $self, $c, $arg ) = @_; + $c->visit( qw/TestApp::Controller::Action::Visit args/, [$arg] ); +} + +sub args : Local { + my ( $self, $c, $val ) = @_; + die "passed argument does not match args" unless $val eq $c->req->args->[0]; + $c->res->body($val); +} + +sub visit_die : Local { + my ( $self, $c, $val ) = @_; + eval { $c->visit( 'args', [qq/new/] ) }; + $c->res->body( $@ ? $@ : "visit() doesn't die" ); +} + +sub visit_chained : Local { + my ( $self, $c, $val ) = @_; + $c->visit('/action/chained/foo/spoon',[1]); +} + +sub view : Local { + my ( $self, $c, $val ) = @_; + eval { $c->visit('View::Dump') }; + $c->res->body( $@ ? $@ : "visit() did not die" ); +} + +sub model : Local { + my ( $self, $c, $val ) = @_; + eval { $c->visit('Model::Foo') }; + $c->res->body( $@ ? $@ : "visit() did not die" ); +} + +sub args_embed_relative : Local { + my ( $self, $c ) = @_; + $c->visit('embed/ok'); +} + +sub args_embed_absolute : Local { + my ( $self, $c ) = @_; + $c->visit('/action/visit/embed/ok'); +} + +sub embed : Local { + my ( $self, $c, $ok ) = @_; + $ok ||= 'not ok'; + $c->res->body($ok); +} + +sub class_visit_test_action : Local { + my ( $self, $c ) = @_; + $c->visit(qw/TestApp class_visit_test_method/); +} + +1; Index: t/lib/TestApp/View/Dump.pm =================================================================== --- t/lib/TestApp/View/Dump.pm (revision 9055) +++ t/lib/TestApp/View/Dump.pm (working copy) @@ -1,7 +1,7 @@ package TestApp::View::Dump; use strict; -use base 'Catalyst::Base'; +use base 'Catalyst::View'; use Data::Dumper (); use Scalar::Util qw(weaken); Index: t/lib/TestApp.pm =================================================================== --- t/lib/TestApp.pm (revision 9055) +++ t/lib/TestApp.pm (working copy) @@ -77,6 +77,11 @@ $c->response->headers->header( 'X-Class-Go-Test-Method' => 1 ); } +sub class_visit_test_method :Private { + my ( $self, $c ) = @_; + $c->response->headers->header( 'X-Class-Visit-Test-Method' => 1 ); +} + sub loop_test : Local { my ( $self, $c ) = @_; Index: t/live_component_controller_action_go.t =================================================================== --- t/live_component_controller_action_go.t (revision 0) +++ t/live_component_controller_action_go.t (revision 0) @@ -0,0 +1,277 @@ +#!perl + +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../lib"; + +our $iters; + +BEGIN { $iters = $ENV{CAT_BENCH_ITERS} || 1; } + +use Test::More tests => 54 * $iters; +use Catalyst; +use Catalyst::Test 'TestApp'; + +if ( $ENV{CAT_BENCHMARK} ) { + require Benchmark; + Benchmark::timethis( $iters, \&run_tests ); +} +else { + for ( 1 .. $iters ) { + run_tests(); + } +} + +sub run_tests { + { + # Test go to global private action + ok( my $response = request('http://localhost/action/go/global'), + 'Request' ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content_type, 'text/plain', 'Response Content-Type' ); + is( $response->header('X-Catalyst-Action'), + 'action/go/global', 'Main Class Action' ); + } + + { + my @expected = qw[ + TestApp::Controller::Action::Go->one + TestApp::Controller::Action::Go->two + TestApp::Controller::Action::Go->three + TestApp::Controller::Action::Go->four + TestApp::Controller::Action::Go->five + TestApp::View::Dump::Request->process + TestApp->end + ]; + + @expected = map { /Action/ ? (_begin($_), $_) : ($_) } @expected; + my $expected = join( ", ", @expected ); + + # Test go to chain of actions. + ok( my $response = request('http://localhost/action/go/one'), + 'Request' ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content_type, 'text/plain', 'Response Content-Type' ); + is( $response->header('X-Catalyst-Action'), + 'action/go/one', 'Test Action' ); + is( + $response->header('X-Test-Class'), + 'TestApp::Controller::Action::Go', + 'Test Class' + ); + is( $response->header('X-Catalyst-Executed'), + $expected, 'Executed actions' ); + like( + $response->content, + qr/^bless\( .* 'Catalyst::Request' \)$/s, + 'Content is a serialized Catalyst::Request' + ); + } + + { + my @expected = qw[ + TestApp::Controller::Action::Go->go_die + TestApp::Controller::Action::Go->args + TestApp->end + ]; + + @expected = map { /Action/ ? (_begin($_), $_) : ($_) } @expected; + my $expected = join( ", ", @expected ); + + ok( my $response = request('http://localhost/action/go/go_die'), + 'Request' ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content_type, 'text/plain', 'Response Content-Type' ); + is( $response->header('X-Catalyst-Action'), + 'action/go/go_die', 'Test Action' + ); + is( + $response->header('X-Test-Class'), + 'TestApp::Controller::Action::Go', + 'Test Class' + ); + is( $response->header('X-Catalyst-Executed'), + $expected, 'Executed actions' ); + is( $response->content, $Catalyst::GO, "Go died as expected" ); + } + { + ok( + my $response = request('http://localhost/action/go/model'), + 'Request with args' + ); + is( $response->content, + q[FATAL ERROR: Couldn't go("Model::Foo"): Action cannot _DISPATCH. Did you try to go() a non-controller action?], + q[go('Model::...') test] + ); + } + { + ok( + my $response = request('http://localhost/action/go/view'), + 'Request with args' + ); + is( $response->content, + q[FATAL ERROR: Couldn't go("View::Dump"): Action cannot _DISPATCH. Did you try to go() a non-controller action?], + q[go('View::...') test] + ); + } + { + ok( + my $response = + request('http://localhost/action/go/with_args/old'), + 'Request with args' + ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content, 'old', 'go() with args (old)' ); + } + + { + ok( + my $response = request( + 'http://localhost/action/go/with_method_and_args/new'), + 'Request with args and method' + ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content, 'new', 'go() with args (new)' ); + } + + # test go with embedded args + { + ok( + my $response = + request('http://localhost/action/go/args_embed_relative'), + 'Request' + ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content, 'ok', 'go() with args_embed_relative' ); + } + + { + ok( + my $response = + request('http://localhost/action/go/args_embed_absolute'), + 'Request' + ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content, 'ok', 'go() with args_embed_absolute' ); + } + { + my @expected = qw[ + TestApp::Controller::Action::TestRelative->relative_go + TestApp::Controller::Action::Go->one + TestApp::Controller::Action::Go->two + TestApp::Controller::Action::Go->three + TestApp::Controller::Action::Go->four + TestApp::Controller::Action::Go->five + TestApp::View::Dump::Request->process + TestApp->end + ]; + + @expected = map { /Action/ ? (_begin($_), $_) : ($_) } @expected; + my $expected = join( ", ", @expected ); + + # Test go to chain of actions. + ok( my $response = request('http://localhost/action/relative/relative_go'), + 'Request' ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content_type, 'text/plain', 'Response Content-Type' ); + is( $response->header('X-Catalyst-Action'), + 'action/relative/relative_go', 'Test Action' ); + is( + $response->header('X-Test-Class'), + 'TestApp::Controller::Action::Go', + 'Test Class' + ); + is( $response->header('X-Catalyst-Executed'), + $expected, 'Executed actions' ); + like( + $response->content, + qr/^bless\( .* 'Catalyst::Request' \)$/s, + 'Content is a serialized Catalyst::Request' + ); + } + { + my @expected = qw[ + TestApp::Controller::Action::TestRelative->relative_go_two + TestApp::Controller::Action::Go->one + TestApp::Controller::Action::Go->two + TestApp::Controller::Action::Go->three + TestApp::Controller::Action::Go->four + TestApp::Controller::Action::Go->five + TestApp::View::Dump::Request->process + TestApp->end + ]; + + @expected = map { /Action/ ? (_begin($_), $_) : ($_) } @expected; + my $expected = join( ", ", @expected ); + + # Test go to chain of actions. + ok( + my $response = + request('http://localhost/action/relative/relative_go_two'), + 'Request' + ); + ok( $response->is_success, 'Response Successful 2xx' ); + is( $response->content_type, 'text/plain', 'Response Content-Type' ); + is( + $response->header('X-Catalyst-Action'), + 'action/relative/relative_go_two', + 'Test Action' + ); + is( + $response->header('X-Test-Class'), + 'TestApp::Controller::Action::Go', + 'Test Class' + ); + is( $response->header('X-Catalyst-Executed'), + $expected, 'Executed actions' ); + like( + $response->content, + qr/^bless\( .* 'Catalyst::Request' \)$/s, + 'Content is a serialized Catalyst::Request' + ); + } + + # test class go -- MUST FAIL! + { + ok( + my $response = request( + 'http://localhost/action/go/class_go_test_action'), + 'Request' + ); + ok( !$response->is_success, 'Response Fails' ); + is( $response->content, + q(FATAL ERROR: Couldn't go("TestApp"): Action has no namespace: cannot go() to a plain method or component, must be a :Action or some sort.), + 'Error message' + ); + } + + { + my @expected = qw[ + TestApp::Controller::Action::Go->begin + TestApp::Controller::Action::Go->go_chained + TestApp::Controller::Action::Chained->begin + TestApp::Controller::Action::Chained->foo + TestApp::Controller::Action::Chained::Foo->spoon + TestApp::Controller::Action::Chained->end + ]; + + my $expected = join( ", ", @expected ); + + ok( my $response = request('http://localhost/action/go/go_chained'), 'go to chained + subcontroller endpoint' ); + is( $response->header('X-Catalyst-Executed'), + $expected, 'Executed actions' ); + is( $response->content, '; 1', 'Content OK' ); + } + +} + + + +sub _begin { + local $_ = shift; + s/->(.*)$/->begin/; + return $_; +} + Index: lib/Catalyst.pm =================================================================== --- lib/Catalyst.pm (revision 9055) +++ lib/Catalyst.pm (working copy) @@ -49,6 +49,7 @@ our $START = time; our $RECURSION = 1000; our $DETACH = "catalyst_detach\n"; +our $GO = "catalyst_go\n"; __PACKAGE__->mk_classdata($_) for qw/components arguments dispatcher engine log dispatcher_class @@ -327,6 +328,40 @@ sub detach { my $c = shift; $c->dispatcher->detach( $c, @_ ) } +=head2 $c->visit( $action [, \@arguments ] ) + +=head2 $c->visit( $class, $method, [, \@arguments ] ) + +Almost the same as C, but does a full dispatch, instead of just +calling the new C<$action> / C<$class-E$method>. This means that C, +C and the method you go to are called, just like a new request. + +C<$c-Estash> is kept unchanged. + +In effect, C allows you to "wrap" another action, just as it +would have been called by dispatching from a URL, while the analogous +C allows you to transfer control to another action as if it had +been reached directly from a URL. + +=cut + +sub visit { my $c = shift; $c->dispatcher->visit( $c, @_ ) } + +=head2 $c->go( $action [, \@arguments ] ) + +=head2 $c->go( $class, $method, [, \@arguments ] ) + +Almost the same as C, but does a full dispatch like C, +instead of just calling the new C<$action> / +C<$class-E$method>. This means that C, C and the +method you visit are called, just like a new request. + +C<$c-Estash> is kept unchanged. + +=cut + +sub go { my $c = shift; $c->dispatcher->go( $c, @_ ) } + =head2 $c->response =head2 $c->res @@ -1339,6 +1374,9 @@ if ( !ref($error) and $error eq $DETACH ) { die $DETACH if($c->depth > 1); } + elsif ( !ref($error) and $error eq $GO ) { + die $GO if($c->depth > 0); + } else { unless ( ref $error ) { no warnings 'uninitialized'; @@ -2554,6 +2592,8 @@ willert: Sebastian Willert +batman: Jan Henning Thorsen + =head1 LICENSE This library is free software, you can redistribute it and/or modify it under Index: lib/Catalyst/DispatchType/Chained.pm =================================================================== --- lib/Catalyst/DispatchType/Chained.pm (revision 9055) +++ lib/Catalyst/DispatchType/Chained.pm (working copy) @@ -294,6 +294,31 @@ } +=head2 $c->expand_action($action) + +Return a list of actions that represents a chained action. See +L 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]); +} + =head1 USAGE =head2 Introduction Index: lib/Catalyst/Dispatcher.pm =================================================================== --- lib/Catalyst/Dispatcher.pm (revision 9055) +++ lib/Catalyst/Dispatcher.pm (working copy) @@ -151,9 +151,14 @@ my $action; - # go to a string path ("/foo/bar/gorch") - # or action object which stringifies to that - $action = $self->_invoke_as_path( $c, "$command", \@args ); + if (Scalar::Util::blessed($command) && $command->isa('Catalyst::Action')) { + $action = $command; + } + else { + # go to a string path ("/foo/bar/gorch") + # or action object which stringifies to that + $action = $self->_invoke_as_path( $c, "$command", \@args ); + } # go to a component ( "MyApp::*::Foo" or $c->component("...") # - a path or an object) @@ -165,6 +170,67 @@ return $action, \@args; } +=head2 $self->visit( $c, $command [, \@arguments ] ) + +Documented in L + +=cut + +sub visit { + my $self = shift; + $self->_do_visit('visit', @_); +} + +sub _do_visit { + my $self = shift; + my $opname = shift; + my ( $c, $command ) = @_; + my ( $action, $args ) = $self->_command2action(@_); + my $error = qq/Couldn't $opname("$command"): /; + + if (!$action) { + $error .= qq/Couldn't $opname to command "$command": / + .qq/Invalid action or component./; + } + elsif (!defined $action->namespace) { + $error .= qq/Action has no namespace: cannot $opname() to a plain / + .qq/method or component, must be a :Action or some sort./ + } + elsif (!$action->class->can('_DISPATCH')) { + $error .= qq/Action cannot _DISPATCH. / + .qq/Did you try to $opname() a non-controller action?/; + } + else { + $error = q(); + } + + if($error) { + $c->error($error); + $c->log->debug($error) if $c->debug; + return 0; + } + + $action = $self->expand_action($action); + + local $c->request->{arguments} = $args; + local $c->{namespace} = $action->{'namespace'}; + local $c->{action} = $action; + + $self->dispatch($c); +} + +=head2 $self->go( $c, $command [, \@arguments ] ) + +Documented in L + +=cut + +sub go { + my $self = shift; + $self->_do_visit('go', @_); + die $Catalyst::GO; +} + =head2 $self->forward( $c, $command [, \@arguments ] ) Documented in L @@ -392,6 +458,25 @@ return undef; } +=head2 expand_action + +expand an action into a full representation of the dispatch. +mostly useful for chained, other actions will just return a +single action. + +=cut + +sub expand_action { + my ($self, $action) = @_; + + foreach my $dispatch_type (@{ $self->dispatch_types }) { + my $expanded = $dispatch_type->expand_action($action); + return $expanded if $expanded; + } + + return $action; +} + =head2 $self->register( $c, $action ) Make sure all required dispatch types for this action are loaded, then Index: lib/Catalyst/DispatchType.pm =================================================================== --- lib/Catalyst/DispatchType.pm (revision 9055) +++ lib/Catalyst/DispatchType.pm (working copy) @@ -46,6 +46,15 @@ sub register { } +=head2 $self->expand_action + +Default fallback, returns nothing. See L for more info +about expand_action. + +=cut + +sub expand_action { } + =head2 $self->uri_for_action( $action, \@captures ) abstract method, to be implemented by dispatchtypes. Takes a