From: John Napiorkowski Date: Thu, 6 Feb 2014 15:49:29 +0000 (-0600) Subject: sketch for httpmethods _ new psgi utils / tests X-Git-Tag: 5.90060~5 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=catagits%2FCatalyst-Runtime.git;a=commitdiff_plain;h=9c7b676881de2255b45fdab5bfb71f58a5e6d236 sketch for httpmethods _ new psgi utils / tests --- diff --git a/Changes b/Changes index aa47ccf..68673ef 100644 --- a/Changes +++ b/Changes @@ -3,13 +3,14 @@ - Announcing the repo is now open for development of Perl Catalyst 'Runner' - http://questhub.io/realm/perl/explore/latest/tag/runner -5.90059_005 - TBA +5.90059_006 - TBA - MyApp->setup now returns $app to allow class method chaining. + - New Util helper functional localize $env to make it easier to mount PSIG + applications under controllers and actions. See Catalyst::Utils/PSGI Helpers. 5.90059_005 - 2014-01-28 - - Specify newested versions of some middleware in attempt to solve test errors - reported while installing. - + - Specify newest versions of some middleware in attempt to solve test errors + reported while installing. 5.90059_004 - 2014-01-27 - Make sure IO handle objects do 'getline' before sending them to the response callback, to properly support the PSGI specification. diff --git a/lib/Catalyst/Utils.pm b/lib/Catalyst/Utils.pm index 81ab727..3f594e6 100644 --- a/lib/Catalyst/Utils.pm +++ b/lib/Catalyst/Utils.pm @@ -486,6 +486,131 @@ sub apply_registered_middleware { return $new_psgi; } +=head1 PSGI Helpers + +Utility functions to make it easier to work with PSGI applications under Catalyst + +=head2 env_at_path_prefix + +Localize C<$env> under the current controller path prefix: + + package MyApp::Controller::User; + + use Catalyst::Utils; + + use base 'Catalyst::Controller'; + + sub name :Local { + my ($self, $c) = @_; + my $env = $c->Catalyst::Utils::env_at_path_prefix; + } + +Assuming you have a requst like GET /user/name: + +In the example case C<$env> will have PATH_INFO of '/name' instead of +'/user/name' and SCRIPT_NAME will now be '/user'. + +=cut + +sub env_at_path_prefix { + my $ctx = shift; + my $path_prefix = $ctx->controller->path_prefix; + my $env = $ctx->request->env; + my $path_info = $env->{PATH_INFO}; + my $script_name = ($env->{SCRIPT_NAME} || ''); + + $path_info =~ s/(^\/\Q$path_prefix\E)//; + $script_name = "$script_name$1"; + + return +{ + %$env, + PATH_INFO => $path_info, + SCRIPT_NAME => $script_name }; +} + +=head2 env_at_action + +Localize C<$env> under the current controller path prefix: + + package MyApp::Controller::User; + + use Catalyst::Utils; + + use base 'Catalyst::Controller'; + + sub name :Local { + my ($self, $c) = @_; + my $env = $c->Catalyst::Utils::env_at_action; + } + +Assuming you have a requst like GET /user/name: + +In the example case C<$env> will have PATH_INFO of '/' instead of +'/user/name' and SCRIPT_NAME will now be '/user/name'. + +This is probably a common case where you want to mount a PSGI application +under an action but let the Args fall through to the PSGI app. + +=cut + +sub env_at_action { + my $ctx = shift; + my $argpath = join '/', @{$ctx->request->arguments}; + my $path = '/' . $ctx->request->path; + + $path =~ s/\/?\Q$argpath\E\/?$//; + + my $env = $ctx->request->env; + my $path_info = $env->{PATH_INFO}; + my $script_name = ($env->{SCRIPT_NAME} || ''); + + $path_info =~ s/(^\Q$path\E)//; + $script_name = "$script_name$1"; + + return +{ + %$env, + PATH_INFO => $path_info, + SCRIPT_NAME => $script_name }; +} + +=head2 env_at_request_uri + +Localize C<$env> under the current controller path prefix: + + package MyApp::Controller::User; + + use Catalyst::Utils; + + use base 'Catalyst::Controller'; + + sub name :Local Args(1) { + my ($self, $c, $id) = @_; + my $env = $c->Catalyst::Utils::env_at_request_uri + } + +Assuming you have a requst like GET /user/name/hello: + +In the example case C<$env> will have PATH_INFO of '/' instead of +'/user/name' and SCRIPT_NAME will now be '/user/name/hello'. + +=cut + +sub env_at_request_uri { + my $ctx = shift; + my $path = '/' . $ctx->request->path; + my $env = $ctx->request->env; + my $path_info = $env->{PATH_INFO}; + my $script_name = ($env->{SCRIPT_NAME} || ''); + + $path_info =~ s/(^\Q$path\E)//; + $script_name = "$script_name$1"; + + return +{ + %$env, + PATH_INFO => $path_info, + SCRIPT_NAME => $script_name }; +} + =head1 AUTHORS Catalyst Contributors, see Catalyst.pm diff --git a/t/http_method.t b/t/http_method.t new file mode 100644 index 0000000..506e564 --- /dev/null +++ b/t/http_method.t @@ -0,0 +1,94 @@ +use warnings; +use strict; + +# Test case to check that we now send scalar and filehandle like +# bodys directly to the PSGI engine, rather than call $writer->write +# or unroll the filehandle ourselves. + +{ + package MyApp::Controller::User; + + use base 'Catalyst::Controller'; + use JSON::MaybeXS; + + my %user = ( + name => 'John', + age => 44, + ); + + + sub get_user :Chained(/) PathPrefix CaptureArgs(0) + { + pop->stash(user=>\%user); + } + + sub show :GET Chained(get_user) PathPart('') Args(0) { + my ($self, $c) = @_; + my $user = $c->stash->{user}; + $c->res->format( + 'application/json' => sub { encode_json $user }, + 'text/html' => sub { "

Hi I'm $user->{name} and my age is $user->{age}

" } + ); + } + + sub post_user :POST Chained(root) PathPart('') Args(0) Consumes(HTMLForm,JSON) + { + my ($self, $c) = @_; + %user = (%user, %{$c->req->body_data}); + $c->res->status(201); + $c->res->location($c->uri_for( $self->action_for('show'))); + } + + $INC{'MyApp/Controller/User.pm'} = '1'; + + package MyApp; + use Catalyst; + + use HTTP::Headers::ActionPack; + + my $cn = HTTP::Headers::ActionPack->new + ->get_content_negotiator; + + sub Catalyst::Response::format + { + my $self = shift; + my %formats = @_; + my @formats = keys %formats; + + my $accept = $self->_context->req->header('Accept') || + $format{default} || + $_[0]; + + $self->headers->header('Vary' => 'Accept'); + $self->headers->header('Accepts' => (join ',', @formats)); + + if(my $which = $cn->choose_media_type(\@formats, $accept)) { + $self->content_type($which); + if(my $possible_body = $formats{$which}->($self)) { + $self->body($possible_body) unless $self->has_body || $self->has_write_fh; + } + } else { + $self->status(406); + $self->body("Method Not Acceptable"); + } + } + + + MyApp->setup; +} + + + +use Devel::Dwarn; +use Test::More; +use HTTP::Request::Common; +use Catalyst::Test 'MyApp'; + +ok my($res, $c) = ctx_request('/'); + + + + +Dwarn(MyApp->dispatcher); + +done_testing(); diff --git a/t/psgi_utils.t b/t/psgi_utils.t new file mode 100644 index 0000000..21f6004 --- /dev/null +++ b/t/psgi_utils.t @@ -0,0 +1,322 @@ +use warnings; +use strict; + +# Make it easier to mount PSGI apps under catalyst + +{ + package MyApp::Controller::User; + + use base 'Catalyst::Controller'; + use Plack::Request; + use Catalyst::Utils; + + my $psgi_app = sub { + my $req = Plack::Request->new(shift); + return [200,[],[$req->path]]; + }; + + sub local_example :Local { + my ($self, $c) = @_; + my $env = $self->get_env($c); + $c->res->from_psgi_response( + $psgi_app->($env)); + } + + sub local_example_args1 :Local Args(1) { + my ($self, $c) = @_; + my $env = $self->get_env($c); + $c->res->from_psgi_response( + $psgi_app->($env)); + } + + sub path_example :Path('path-example') { + my ($self, $c) = @_; + my $env = $self->get_env($c); + $c->res->from_psgi_response( + $psgi_app->($env)); + } + + sub path_example_args1 :Path('path-example-args1') { + my ($self, $c) = @_; + my $env = $self->get_env($c); + $c->res->from_psgi_response( + $psgi_app->($env)); + } + + sub chained :Chained(/) PathPrefix CaptureArgs(0) { } + + sub from_chain :Chained('chained') PathPart('') CaptureArgs(0) {} + + sub end_chain :Chained('from_chain') PathPath(abc-123) Args(1) + { + my ($self, $c) = @_; + my $env = $self->get_env($c); + $c->res->from_psgi_response( + $psgi_app->($env)); + } + + sub get_env { + my ($self, $c) = @_; + if($c->req->query_parameters->{path_prefix}) { + return $c->Catalyst::Utils::env_at_path_prefix; + } elsif($c->req->query_parameters->{env_path}) { + return $c->Catalyst::Utils::env_at_action; + } elsif($c->req->query_parameters->{path}) { + return $c->Catalyst::Utils::env_at_request_uri; + } else { + return $c->req->env; + } + } + + $INC{'MyApp/Controller/User.pm'} = __FILE__; + + package MyApp; + use Catalyst; + MyApp->setup; + +} + +use Test::More; +use Catalyst::Test 'MyApp'; + +# BEGIN [user/local_example] +{ + my ($res, $c) = ctx_request('/user/local_example'); + is $c->action, 'user/local_example'; + is $res->content, '/user/local_example'; + is_deeply $c->req->args, []; +} + +{ + my ($res, $c) = ctx_request('/user/local_example/111/222'); + is $c->action, 'user/local_example'; + is $res->content, '/user/local_example/111/222'; + is_deeply $c->req->args, [111,222]; +} + +{ + my ($res, $c) = ctx_request('/user/local_example?path_prefix=1'); + is $c->action, 'user/local_example'; + is $res->content, '/local_example'; + is_deeply $c->req->args, []; +} + +{ + my ($res, $c) = ctx_request('/user/local_example/111/222?path_prefix=1'); + is $c->action, 'user/local_example'; + is $res->content, '/local_example/111/222'; + is_deeply $c->req->args, [111,222]; +} + +{ + my ($res, $c) = ctx_request('/user/local_example?env_path=1'); + is $c->action, 'user/local_example'; + is $res->content, '/'; + is_deeply $c->req->args, []; +} + +{ + my ($res, $c) = ctx_request('/user/local_example/111/222?env_path=1'); + is $c->action, 'user/local_example'; + is $res->content, '/111/222'; + is_deeply $c->req->args, [111,222]; +} + +{ + my ($res, $c) = ctx_request('/user/local_example?path=1'); + is $c->action, 'user/local_example'; + is $res->content, '/'; + is_deeply $c->req->args, []; +} + +{ + my ($res, $c) = ctx_request('/user/local_example/111/222?path=1'); + is $c->action, 'user/local_example'; + is $res->content, '/'; + is_deeply $c->req->args, [111,222]; +} + +# END [user/local_example] + +# BEGIN [/user/local_example_args1/***/] + +{ + my ($res, $c) = ctx_request('/user/local_example_args1/333'); + is $c->action, 'user/local_example_args1'; + is $res->content, '/user/local_example_args1/333'; + is_deeply $c->req->args, [333]; +} + +{ + my ($res, $c) = ctx_request('/user/local_example_args1/333?path_prefix=1'); + is $c->action, 'user/local_example_args1'; + is $res->content, '/local_example_args1/333'; + is_deeply $c->req->args, [333]; +} + +{ + my ($res, $c) = ctx_request('/user/local_example_args1/333?env_path=1'); + is $c->action, 'user/local_example_args1'; + is $res->content, '/333'; + is_deeply $c->req->args, [333]; +} + +{ + my ($res, $c) = ctx_request('/user/local_example_args1/333?path=1'); + is $c->action, 'user/local_example_args1'; + is $res->content, '/'; + is_deeply $c->req->args, [333]; +} + +# END [/user/local_example_args1/***/] + +# BEGIN [/user/path-example] + +{ + my ($res, $c) = ctx_request('/user/path-example'); + is $c->action, 'user/path_example'; + is $res->content, '/user/path-example'; + is_deeply $c->req->args, []; +} + +{ + my ($res, $c) = ctx_request('/user/path-example?path_prefix=1'); + is $c->action, 'user/path_example'; + is $res->content, '/path-example'; + is_deeply $c->req->args, []; +} + +{ + my ($res, $c) = ctx_request('/user/path-example?env_path=1'); + is $c->action, 'user/path_example'; + is $res->content, '/'; + is_deeply $c->req->args, []; +} + +{ + my ($res, $c) = ctx_request('/user/path-example?path=1'); + is $c->action, 'user/path_example'; + is $res->content, '/'; + is_deeply $c->req->args, []; +} + + +{ + my ($res, $c) = ctx_request('/user/path-example/111/222'); + is $c->action, 'user/path_example'; + is $res->content, '/user/path-example/111/222'; + is_deeply $c->req->args, [111,222]; +} + +{ + my ($res, $c) = ctx_request('/user/path-example/111/222?path_prefix=1'); + is $c->action, 'user/path_example'; + is $res->content, '/path-example/111/222'; + is_deeply $c->req->args, [111,222]; +} + +{ + my ($res, $c) = ctx_request('/user/path-example/111/222?env_path=1'); + is $c->action, 'user/path_example'; + is $res->content, '/111/222'; + is_deeply $c->req->args, [111,222]; +} + +{ + my ($res, $c) = ctx_request('/user/path-example/111/222?path=1'); + is $c->action, 'user/path_example'; + is $res->content, '/'; + is_deeply $c->req->args, [111,222]; +} + +{ + my ($res, $c) = ctx_request('/user/path-example-args1/333'); + is $c->action, 'user/path_example_args1'; + is $res->content, '/user/path-example-args1/333'; + is_deeply $c->req->args, [333]; +} + +{ + my ($res, $c) = ctx_request('/user/path-example-args1/333?path_prefix=1'); + is $c->action, 'user/path_example_args1'; + is $res->content, '/path-example-args1/333'; + is_deeply $c->req->args, [333]; +} + +{ + my ($res, $c) = ctx_request('/user/path-example-args1/333?env_path=1'); + is $c->action, 'user/path_example_args1'; + is $res->content, '/333'; + is_deeply $c->req->args, [333]; +} + +{ + my ($res, $c) = ctx_request('/user/path-example-args1/333?path=1'); + is $c->action, 'user/path_example_args1'; + is $res->content, '/'; + is_deeply $c->req->args, [333]; +} + +# Chaining test /user/end_chain/* +# +# + +{ + my ($res, $c) = ctx_request('/user/end_chain/444'); + is $c->action, 'user/end_chain'; + is $res->content, '/user/end_chain/444'; + is_deeply $c->req->args, [444]; +} + +{ + my ($res, $c) = ctx_request('/user/end_chain/444?path_prefix=1'); + is $c->action, 'user/end_chain'; + is $res->content, '/end_chain/444'; + is_deeply $c->req->args, [444]; +} + +{ + my ($res, $c) = ctx_request('/user/end_chain/444?env_path=1'); + is $c->action, 'user/end_chain'; + is $res->content, '/444'; + is_deeply $c->req->args, [444]; +} + +{ + my ($res, $c) = ctx_request('/user/end_chain/444?path=1'); + is $c->action, 'user/end_chain'; + is $res->content, '/'; + is_deeply $c->req->args, [444]; +} + + +done_testing(); + +__END__ + + +use Plack::App::URLMap; +use HTTP::Request::Common; +use HTTP::Message::PSGI; + +my $urlmap = Plack::App::URLMap->new; + +my $app1 = sub { + my $env = shift; + return [200, [], [ + "REQUEST_URI: $env->{REQUEST_URI}, FROM: $env->{MAP_TO}, PATH_INFO: $env->{PATH_INFO}, SCRIPT_NAME $env->{SCRIPT_NAME}"]]; +}; + +$urlmap->map("/" => sub { my $env = shift; $env->{MAP_TO} = '/'; $app1->($env)}); +$urlmap->map("/foo" => sub { my $env = shift; $env->{MAP_TO} = '/foo'; $app1->($env)}); +$urlmap->map("/bar/baz" => sub { my $env = shift; $env->{MAP_TO} = '/foo/bar'; $app1->($env)}); + +my $app = $urlmap->to_app; + +warn $app->(req_to_psgi(GET '/'))->[2]->[0]; +warn $app->(req_to_psgi(GET '/111'))->[2]->[0]; +warn $app->(req_to_psgi(GET '/foo'))->[2]->[0]; +warn $app->(req_to_psgi(GET '/foo/222'))->[2]->[0]; +warn $app->(req_to_psgi(GET '/bar/baz'))->[2]->[0]; +warn $app->(req_to_psgi(GET '/bar/baz/333'))->[2]->[0]; +