proxy object for the PSGI writer
John Napiorkowski [Tue, 2 Dec 2014 15:16:04 +0000 (09:16 -0600)]
Changes
lib/Catalyst.pm
lib/Catalyst/Response.pm
lib/Catalyst/Response/Writer.pm [new file with mode: 0644]
t/utf_incoming.t

diff --git a/Changes b/Changes
index 619653c..066fc25 100644 (file)
--- a/Changes
+++ b/Changes
   - lots of UTF8 changes.  Again we think this is now more correct but please test.
   - Allow $c->res->redirect($url) to accept $url as an object that does ->as_string
     which I think will ease a common case (and common bug) and added documentation.
-  - UTF-8 is now the default encoding (there used to be none...).  You can disable
+  - !!! UTF-8 is now the default encoding (there used to be none...).  You can disable
     this if you need to with MyApp->config(encoding => undef) if it causes you trouble.
   - Calling $c->res->write($data) now encodes $data based on the configured encoding
     (UTF-8 is default).
+  - $c->res->writer_fh now returns Catalyst::Response::Writer which is a decorator
+    over the PSGI writer and provides and additional methd 'write_encoded' that just
+    does the right thing for encoding your responses.  This is probably the method
+    you want to use.
 
 5.90077 - 2014-11-18
   - We store the PSGI $env in Catalyst::Engine for backcompat reasons.  Changed
index 141099c..9153f82 100644 (file)
@@ -86,8 +86,10 @@ has response => (
     lazy => 1,
 );
 sub _build_response_constructor_args {
-    my $self = shift;
-    { _log => $self->log };
+    return +{
+      _log => $_[0]->log,
+      encoding => $_[0]->encoding,
+    };
 }
 
 has namespace => (is => 'rw');
index bf4ef44..82677a7 100644 (file)
@@ -5,6 +5,7 @@ use HTTP::Headers;
 use Moose::Util::TypeConstraints;
 use namespace::autoclean;
 use Scalar::Util 'blessed';
+use Catalyst::Response::Writer;
 
 with 'MooseX::Emulate::Class::Accessor::Fast';
 
@@ -52,7 +53,17 @@ has write_fh => (
   builder=>'_build_write_fh',
 );
 
-sub _build_write_fh { shift ->_writer }
+sub _build_write_fh {
+  my $writer = $_[0]->_writer; # We need to get the finalize headers side effect...
+  my $requires_encoding = $_[0]->content_type =~ m/^text|xml$|javascript$/;
+  my %fields = (
+    _writer => $writer,
+    _encoding => $_[0]->encoding,
+    _requires_encoding => $requires_encoding,
+  );
+
+  return bless \%fields, 'Catalyst::Response::Writer';
+}
 
 sub DEMOLISH {
   my $self = shift;
@@ -83,6 +94,8 @@ has _context => (
   clearer => '_clear_context',
 );
 
+has encoding => (is=>'ro');
+
 before [qw(status headers content_encoding content_length content_type header)] => sub {
   my $self = shift;
 
@@ -411,9 +424,22 @@ encoding is UTF-8).
 
 =head2 $res->write_fh
 
-Returns a PSGI $writer object that has two methods, write and close.  You can
-close over this object for asynchronous and nonblocking applications.  For
-example (assuming you are using a supporting server, like L<Twiggy>
+Returns an instance of L<Catalyst::Response::Writer>, which is a lightweight
+decorator over the PSGI C<$writer> object (see L<PSGI.pod\Delayed-Response-and-Streaming-Body>).
+
+In addition to proxying the C<write> and C<close> method from the underlying PSGI
+writer, this proxy object knows any application wide encoding, and provides a method
+C<write_encoded> that will properly encode your written lines based upon your
+encoding settings.  By default in L<Catalyst> responses are UTF-8 encoded and this
+is the encoding used if you respond via C<write_encoded>.  If you want to handle
+encoding yourself, you can use the C<write> method directly.
+
+Encoding only applies to content types for which it matters.  Currently the following
+content types are assumed to need encoding: text (including HTML), xml and javascript.
+
+We provide access to this object so that you can properly close over it for use in
+asynchronous and nonblocking applications.  For example (assuming you are using a supporting
+server, like L<Twiggy>:
 
     package AsyncExample::Controller::Root;
 
diff --git a/lib/Catalyst/Response/Writer.pm b/lib/Catalyst/Response/Writer.pm
new file mode 100644 (file)
index 0000000..55cbdd1
--- /dev/null
@@ -0,0 +1,65 @@
+package Catalyst::Response::Writer;
+
+sub write { shift->{_writer}->write(@_) }
+sub close { shift->{_writer}->close }
+
+sub write_encoded {
+  my ($self, $line) = @_;
+  if((my $enc = $self->{_encoding}) && $self->{_requires_encoding}) {
+    # Not going to worry about CHECK arg since Unicode always croaks I think - jnap
+    $line = $enc->encode($line);
+  }
+
+  $self->write($line);
+}
+
+=head1 TITLE 
+
+Catalyst::Response::Writer - Proxy over the PSGI Writer
+
+=head1 SYNOPSIS
+
+    sub myaction : Path {
+      my ($self, $c) = @_;
+      my $w = $c->response->writer_fh;
+
+      $w->write("hello world");
+      $w->close;
+    }
+
+=head1 DESCRIPTION
+
+This wraps the PSGI writer (see L<PSGI.pod\Delayed-Response-and-Streaming-Body>)
+for more.  We wrap this object so we can provide some additional methods that
+make sense from inside L<Catalyst>
+
+=head1 METHODS
+
+This class does the following methods
+
+=head2 write
+
+=head2 close
+
+These delegate to the underlying L<PSGI> writer object
+
+=head2 write_encoded
+
+If the application defines a response encoding (default is UTF8) and the 
+content type is a type that needs to be encoded (text types like HTML or XML and
+Javascript) we first encode the line you want to write.  This is probably the
+thing you want to always do.  If you use the L<\write> method directly you will
+need to handle your own encoding.
+
+=head1 AUTHORS
+
+Catalyst Contributors, see Catalyst.pm
+
+=head1 COPYRIGHT
+
+This library is free software. You can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut
+
+1;
index 8966d56..fef3553 100644 (file)
@@ -81,20 +81,32 @@ use File::Spec;
     $c->response->content_type('text/html');
 
     my $writer = $c->res->write_fh;
-
-    $writer->write(Encode::encode_utf8('<p>This is stream_write_fh action ♥</p>'));
+    $writer->write_encoded('<p>This is stream_write_fh action ♥</p>');
     $writer->close;
   }
 
+  # Stream a file with utf8 chars directly, you don't need to decode
   sub stream_body_fh :Local {
     my ($self, $c) = @_;
-
     my $path = File::Spec->catfile('t', 'utf8.txt');
     open(my $fh, '<', $path) || die "trouble: $!";
     $c->response->content_type('text/html');
     $c->response->body($fh);
   }
 
+  # If you pull the file contents into a var, NOW you need to specify the
+  # IO encoding on the FH.  Ultimately Plack at the end wants bytes...
+  sub stream_body_fh2 :Local {
+    my ($self, $c) = @_;
+    my $path = File::Spec->catfile('t', 'utf8.txt');
+    open(my $fh, '<:encoding(UTF-8)', $path) || die "trouble: $!";
+    my $contents = do { local $/; <$fh> };
+
+    $c->response->content_type('text/html');
+    $c->response->body($contents);
+  }
+
+
   package MyApp;
   use Catalyst;
 
@@ -242,4 +254,13 @@ use Catalyst::Test 'MyApp';
   is $res->content_charset, 'UTF-8';
 }
 
+{
+  my $res = request "/root/stream_body_fh2";
+
+  is $res->code, 200, 'OK';
+  is decode_utf8($res->content), "<p>This is stream_body_fh action ♥</p>\n", 'correct body';
+  is $res->content_length, 41, 'correct length';
+  is $res->content_charset, 'UTF-8';
+}
+
 done_testing;