new action class to handle deserializing multi-part HTTP request data
Brian Phillips [Wed, 21 Sep 2011 12:24:42 +0000 (07:24 -0500)]
lib/Catalyst/Action/DeserializeMultiPart.pm [new file with mode: 0644]
lib/Catalyst/Action/REST.pm
lib/Catalyst/Controller/REST.pm
t/catalyst-action-deserialize-multipart.t [new file with mode: 0644]
t/lib/Test/Catalyst/Action/REST/Controller/DeserializeMultiPart.pm [new file with mode: 0644]

diff --git a/lib/Catalyst/Action/DeserializeMultiPart.pm b/lib/Catalyst/Action/DeserializeMultiPart.pm
new file mode 100644 (file)
index 0000000..8a882c3
--- /dev/null
@@ -0,0 +1,103 @@
+package Catalyst::Action::DeserializeMultiPart;
+
+use Moose;
+use namespace::autoclean;
+
+extends 'Catalyst::Action::Deserialize';
+use HTTP::Body;
+
+our $VERSION = '0.91';
+$VERSION = eval $VERSION;
+
+our $NO_HTTP_BODY_TYPES_INITIALIZATION;
+$HTTP::Body::TYPES->{'multipart/mixed'} = 'HTTP::Body::MultiPart' unless $NO_HTTP_BODY_TYPES_INITIALIZATION;
+
+override execute => sub {
+    my $self = shift;
+    my ( $controller, $c ) = @_;
+    if($c->request->content_type =~ m{^multipart/}i && !defined($c->request->body)){
+        my $REST_part = $self->attributes->{DeserializePart} || [];
+        my($REST_body) = $c->request->upload($REST_part->[0] || 'REST');
+        if($REST_body){
+            $c->request->_body->body( $REST_body->fh );
+            $c->request->content_type( $REST_body->type );
+        }
+    }
+    super;
+};
+
+__PACKAGE__->meta->make_immutable;
+
+1;
+
+=head1 NAME
+
+Catalyst::Action::DeserializeMultiPart - Deserialize Data in a Multi-Part Request
+
+=head1 SYNOPSIS
+
+    package Foo::Controller::Bar;
+
+    __PACKAGE__->config(
+        # see Catalyst::Action::Deserialize for standard config
+    );
+
+    sub begin :ActionClass('DeserializeMultiPart') DeserializePart('REST') {}
+
+=head1 DESCRIPTION
+
+This action will deserialize multi-part HTTP POST, PUT, OPTIONS and DELETE
+requests.  It is a simple extension of L<Catalyst::Action::Deserialize>
+with the exception that rather than using the entire request body (which
+may contain multiple sections), it will look for a single part in the request
+body named according to the C<DeserializePart> attribute on that action
+(defaulting to C<REST>).  If a part is found under that name, it then
+proceeds to deserialize the request as normal based on the content-type
+of that individual part.  If no such part is found, the request would
+be processed as if no data was sent.
+
+This module's code will only come into play if the following conditions are met:
+
+=over 4
+
+=item * The C<Content-type> of the request is C<multipart/*>
+
+=item * The request body (as returned by C<$c->request->body> is not defined
+
+=item * There is a part of the request body (as returned by C<$c->request->upload($DeserializePart)>) available
+
+=back
+
+=head1 CONFIGURING HTTP::Body
+
+By default, L<HTTP::Body> parses C<multipart/*> requests as an
+L<HTTP::Body::OctetStream>.  L<HTTP::Body::OctetStream> does not separate
+out the individual parts of the request body.  In order to make use of
+the individual parts, L<HTTP::Body> must be told which content types
+to map to L<HTTP::Body::MultiPart>.  This module makes the assumption
+that you would like to have all C<multipart/mixed> requests parsed by
+L<HTTP::Body::MultiPart> module.  This is done by a package variable
+inside L<HTTP::Body>: C<$HTTP::Body::Types> (a HASH ref).  Feel free to
+add other content-types to this hash if needed or if you would prefer
+that C<multipart/mixed> NOT be added to this hash, simply delete it
+after loading this module.
+
+    # in your controller
+    use Catalyst::Action::DeserializeMultiPart;
+
+    delete $HTTP::Body::Types->{'multipart/mixed'};
+    $HTTP::Body::Types->{'multipart/my-crazy-content-type'} = 'HTTP::Body::MultiPart';
+
+=head1 SEE ALSO
+
+This is a simple sub-class of L<Catalyst::Action::Deserialize>.
+
+=head1 AUTHORS
+
+See L<Catalyst::Action::REST> for authors.
+
+=head1 LICENSE
+
+You may distribute this code under the same terms as Perl itself.
+
+=cut
index 110c61d..fcc1cce 100644 (file)
@@ -214,6 +214,8 @@ Daisuke Maki E<lt>daisuke@endeworks.jpE<gt>
 
 Hans Dieter Pearcey
 
+Brian Phillips E<lt>bphillips@cpan.orgE</gt>
+
 Dave Rolsky E<lt>autarch@urth.orgE<gt>
 
 Luke Saunders
index 3bd6daa..defe764 100644 (file)
@@ -594,6 +594,10 @@ action classes:
 
   sub serialize : ActionClass('Serialize') {}
 
+If you need to deserialize multipart requests (i.e. REST data in
+one part and file uploads in others) you can do so by using the
+L<Catalyst::Action::DeserializeMultiPart> action class.
+
 =back
 
 =head1 A MILD WARNING
diff --git a/t/catalyst-action-deserialize-multipart.t b/t/catalyst-action-deserialize-multipart.t
new file mode 100644 (file)
index 0000000..4d449ca
--- /dev/null
@@ -0,0 +1,21 @@
+use strict;
+use warnings;
+use Test::More;
+use YAML::Syck;
+use FindBin;
+
+use lib ("$FindBin::Bin/lib", "$FindBin::Bin/../lib", "$FindBin::Bin/broken");
+use Test::Rest;
+
+my $t = Test::Rest->new('content_type' => 'multipart/mixed; boundary=----------------------------0b922a55b662');
+
+use_ok 'Catalyst::Test', 'Test::Catalyst::Action::REST';
+my $url = '/deserializemultipart/test';
+
+my $req = $t->put( url => $url, data => qq(------------------------------0b922a55b662\r\nContent-Disposition: form-data; name="REST"; filename="-"\r\nContent-Type: text/x-yaml\r\n\r\n---\r\nkitty: LouLou\r\n------------------------------0b922a55b662\r\nContent-Disposition: form-data; name="other"; filename="foo.txt"\r\nContent-Type: application/octet-stream\r\n\r\nanother part\r\n------------------------------0b922a55b662--\r\n));
+my $res = request($req);
+
+ok( $res->is_success, 'PUT Deserialize request succeeded' );
+is( $res->content, "LouLou|12", "Request returned deserialized data");
+
+done_testing;
diff --git a/t/lib/Test/Catalyst/Action/REST/Controller/DeserializeMultiPart.pm b/t/lib/Test/Catalyst/Action/REST/Controller/DeserializeMultiPart.pm
new file mode 100644 (file)
index 0000000..66c5101
--- /dev/null
@@ -0,0 +1,22 @@
+package Test::Catalyst::Action::REST::Controller::DeserializeMultiPart;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller' }
+
+__PACKAGE__->config(
+    'stash_key' => 'rest',
+    'map'       => {
+        'text/x-yaml'        => 'YAML',
+        'text/x-data-dumper' => [ 'Data::Serializer', 'Data::Dumper' ],
+        'text/broken'        => 'Broken',
+    },
+);
+
+sub test :Local ActionClass('DeserializeMultiPart') DeserializePart('REST') {
+    my ( $self, $c ) = @_;
+    $DB::single=1;
+    $c->res->output($c->req->data->{'kitty'} . '|' . $c->req->uploads->{other}->size);
+}
+
+1;