sketch for httpmethods _ new psgi utils / tests
John Napiorkowski [Thu, 6 Feb 2014 15:49:29 +0000 (09:49 -0600)]
Changes
lib/Catalyst/Utils.pm
t/http_method.t [new file with mode: 0644]
t/psgi_utils.t [new file with mode: 0644]

diff --git a/Changes b/Changes
index aa47ccf..68673ef 100644 (file)
--- 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.
index 81ab727..3f594e6 100644 (file)
@@ -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 (file)
index 0000000..506e564
--- /dev/null
@@ -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 { "<p>Hi I'm $user->{name} and my age is $user->{age}</p>" }
+      );
+    }
+
+    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 (file)
index 0000000..21f6004
--- /dev/null
@@ -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];
+