=head1 Curl::Transport

This module shows:

=over

=item buildtime version check

Required features will be missing if libcurl was too old at Net::Curl
compilation.

=item basic inheritance

Use Net::Curl::* as base for your modules.

=item exception handling

Most methods die() with a dualvar exception on error. You can compare them
numerically, or display as part of a message.

=back

=head2 Motivation

recv() and send() methods use non-blocking transfer, this may be very annoying
in simple scripts. This wrapper implements blocking send() wrapper, and two
recv() wrappers called read() and readline().

=head2 MODULE CODE

=cut
package Curl::Transport;

use strict;
use warnings;
use Net::Curl::Easy qw(/^CURLE_/);
use base qw(Net::Curl::Easy);

BEGIN {
	if ( Net::Curl::LIBCURL_VERSION_NUM() < 0x071202 ) {
		my $ver = Net::Curl::LIBCURL_VERSION();
		die "curl $ver does not support send() and recv()";
	}
	# alternatively you can write:
	if ( not Net::Curl::Easy->can( "send" )
			or not Net::Curl::Easy->can( "recv" ) ) {
		die "Net::Curl is missing send() and recv()\n"
	}
}

use constant {
	B_URI => 0,
	B_SOCKET => 1,
	B_VEC => 2,
	B_READBUF => 3,
};


# new( URL ) -- get new object
sub new
{
	my $class = shift;
	my $uri = shift;

	# use an array as our object base
	my $base = [ $uri, undef, undef, '' ];

	my $self = $class->SUPER::new( $base );

	$self->setopt( Net::Curl::Easy::CURLOPT_URL, $uri );
	$self->setopt( Net::Curl::Easy::CURLOPT_CONNECT_ONLY, 1 );

	# will die if fails
	$self->perform();

	$self->[ B_SOCKET ] = $self->getinfo(
		Net::Curl::Easy::CURLINFO_LASTSOCKET
	);

	# prepare select vector
	my $vec = '';
	vec( $vec, $self->[ B_SOCKET ], 1 ) = 1;
	$self->[ B_VEC ] = $vec;

	return $self;
}

# send( DATA ) -- send some data, wait for socket availability
# if it cannot be sent all at once
sub send($$)
{
	my $self = shift;
	my $data = shift;

	while ( length $data ) {
		# copy, because select overwrites those values
		my $w = $self->[ B_VEC ];

		# wait for write
		select undef, $w, undef, 0;

		# make sure some write bit is set
		next unless vec( $w, $self->[ B_SOCKET ], 1 );

		# actually send the data
		my $sent = $self->SUPER::send( $data );

		# remove from buffer what we sent
		substr $data, 0, $sent, '';
	};
}

# read( SIZE ) -- read SIZE bytes, wait for more data if there
# wasn't enough
sub read($$)
{
	my $self = shift;
	my $size = shift;

	return '' unless $size > 0;

	while ( length $self->[ B_READBUF ] < $size ) {
		my $r = $self->[ B_VEC ];

		# wait for data
		select $r, undef, undef, 0;

		# make sure some read bit is set
		redo unless vec( $r, $self->[ B_SOCKET ], 1 );

		eval {
			my $l = $self->SUPER::recv( $self->[ B_READBUF ],
				$size - length $self->[ B_READBUF ] );
		};
		if ( $@ ) {
			if ( $@ == CURLE_UNSUPPORTED_PROTOCOL ) {
				my $uri = $self->[ B_URI ];
				warn "Connection to $uri closed: $@\n";
				last;
			} elsif ( $@ == CURLE_AGAIN ) {
				warn "nothing to read, this should not happen";
			} else {
				die $@;
			}
		}
	}

	return substr $self->[ B_READBUF ], 0, $size, '';
}

# readline() -- read until $/
sub readline($)
{
	my $self = shift;

	# we allow changing $/, but we don't support $/ = undef.
	local $/;
	$/ = "\n" unless defined $/;

	my $idx;
	until ( ( $idx = index $self->[ B_READBUF ], $/ ) >= 0 ) {
		my $r = $self->[ B_VEC ];

		# wait for data
		select $r, undef, undef, 0;

		# make sure some read bit is set
		next unless vec( $r, $self->[ B_SOCKET ], 1 );

		# read 256 bytes, should be enough in most cases
		eval {
			$self->SUPER::recv( $self->[ B_READBUF ], 256 );
		};
		if ( $@ ) {
			if ( $@ == CURLE_UNSUPPORTED_PROTOCOL ) {
				my $uri = $self->[ B_URI ];
				warn "Connection to $uri closed: $@\n";
				last;
			} elsif ( $@ == CURLE_AGAIN ) {
				warn "nothing to read, this should not happen";
			} else {
				die $@;
			}
		}
	}

	return substr $self->[ B_READBUF ], 0, ($idx + length $/), '';
}

1;

=head2 TEST APPLICATION

Sample application using this module could look like this:

	#!perl
	use strict;
	use warnings;
	use Curl::Transport;
#nopod
=cut
package main;

use strict;
use warnings;
#endnopod

my $host = shift @ARGV || "example.com";

my $t = Curl::Transport->new( "http://$host" );
$t->send( "GET / HTTP/1.0\r\n" );
$t->send( "User-Agent: Curl::Transport test\r\n" );
$t->send( "Accept: */*\r\n" );
$t->send( "Host: $host\r\n" );
$t->send( "Connection: Close\r\n" );
$t->send( "\r\n" );

my $length;
{
	local $/ = "\r\n";
	local $_;
	do {
		$_ = $t->readline();
		$length = 0 | $1 if /Content-Length:\s*(\d+)/;
		chomp;
		print "HEADER: $_\n";
	} while ( length $_ );
}

if ( defined $length ) {
	print "Reading $length bytes of data:\n";
	print $t->read( $length );

	print "\nTrying to read one more byte, should fail:\n";
	print $t->read( 1 );
	print "\n";
} else {
	print "Don't know how much to read\n";
	while ( $_ = $t->readline() ) {
		print;
	}
}

printf "Last error: %s\n", $t->error();
#nopod
# vim: ts=4:sw=4