package Net::Curl::Promiser;

use strict;
use warnings;

our $VERSION = '0.21';

=encoding utf-8

=head1 NAME

Net::Curl::Promiser - Asynchronous L<libcurl|https://curl.haxx.se/libcurl/>, the easy way!

=head1 DESCRIPTION

=begin html

<a href='https://coveralls.io/github/FGasper/p5-Net-Curl-Promiser?branch=master'><img src='https://coveralls.io/repos/github/FGasper/p5-Net-Curl-Promiser/badge.svg?branch=master' alt='Coverage Status' /></a>

=end html

L<Net::Curl::Multi> is powerful but tricky to use: polling, callbacks,
timers, etc. This module does all of that for you and puts a Promise
interface on top of it, so asynchronous I/O becomes almost as simple as
synchronous I/O.

L<Net::Curl::Promiser> itself is a base class; you’ll need to use
a subclass that works with your chosen event interface.

This distribution provides the following usable subclasses:

=over

=item * L<Net::Curl::Promiser::Mojo> (for L<Mojolicious>)

=item * L<Net::Curl::Promiser::AnyEvent> (for L<AnyEvent>)

=item * L<Net::Curl::Promiser::IOAsync> (for L<IO::Async>)

=item * L<Net::Curl::Promiser::Select> (for manually-written
C<select()> loops)

=back

If the event interface you want to use isn’t compatible with one of the
above, you’ll need to create your own L<Net::Curl::Promiser> subclass.
This is undocumented but pretty simple; have a look at the ones above as
well as another based on Linux’s L<epoll(7)> in the distribution’s
F</examples>.

=head1 MEMORY LEAK DETECTION

This module will, by default, C<warn()> if its objects are C<DESTROY()>ed
during Perl’s global destruction phase. To suppress this behavior, set
C<$Net::Curl::Promiser::IGNORE_MEMORY_LEAKS> to a truthy value.

=head1 PROMISE IMPLEMENTATION

This class’s default Promise implementation is L<Promise::ES6>.
You can use a different one by overriding the C<PROMISE_CLASS()> method in
a subclass, as long as the substitute class’s C<new()> method works the
same way as Promise::ES6’s (which itself follows the ECMAScript standard).

(NB: L<Net::Curl::Promiser::Mojo> uses L<Mojo::Promise> instead of
Promise::ES6.)

=head2 B<Experimental> L<Promise::XS> support

Try out experimental Promise::XS support by running with
C<NET_CURL_PROMISER_PROMISE_ENGINE=Promise::XS> in your environment.
This will override C<PROMISE_CLASS()>.

=head1 DESIGN NOTES

Internally each instance of this class uses an instance of
L<Net::Curl::Multi> and an instance of L<Net::Curl::Promiser::Backend>.
(The latter, in turn, is subclassed to provide logic specific to
each event interface.) These are kept separate to avoid circular references.

=cut

#----------------------------------------------------------------------

use parent 'Net::Curl::Promiser::LeakDetector';

# So that Net::Curl::Easy::Code’s overloading gets set up:
use Net::Curl::Easy ();

use Net::Curl::Multi ();

use constant _DEBUG => 0;

use constant _DEFAULT_TIMEOUT => 1000;

our $IGNORE_MEMORY_LEAKS;

#----------------------------------------------------------------------

=head1 GENERAL-USE METHODS

The following are of interest to any code that uses this module:

=head2 I<CLASS>->new(@ARGS)

Instantiates this class, including creation of an underlying
L<Net::Curl::Multi> object.

=cut

sub new {
    my ($class, @args) = @_;

    my %props = (
        callbacks => {},
        to_fail => {},
        ignore_leaks => $IGNORE_MEMORY_LEAKS,
    );

    my $self = bless \%props, $class;

    my $multi = Net::Curl::Multi->new();
    $self->{'multi'} = $multi;

    my $backend = $self->_INIT(\@args);
    $self->{'backend'} = $backend;

    $multi->setopt(
        Net::Curl::Multi::CURLMOPT_SOCKETDATA,
        $backend,
    );

    $multi->setopt(
        Net::Curl::Multi::CURLMOPT_SOCKETFUNCTION,
        \&_socket_fn,
    );

    return $self;
}

#----------------------------------------------------------------------

=head2 promise($EASY) = I<OBJ>->add_handle( $EASY )

A passthrough to the underlying L<Net::Curl::Multi> object’s
method of the same name, but the return is given as a Promise object.

That promise resolves with the passed-in $EASY object.
It rejects with either the error given to C<fail_handle()> or the
error that L<Net::Curl::Multi> object’s C<info_read()> returns.

B<IMPORTANT:> As with libcurl itself, HTTP-level failures
(e.g., 4xx and 5xx responses) are B<NOT> considered failures at this level.

=cut

sub add_handle {
    my ($self, $easy) = @_;

    return $self->{'backend'}->add_handle($easy, $self->{'multi'});
}

=head2 $obj = I<OBJ>->cancel_handle( $EASY )

Prematurely cancels $EASY. The associated promise will be abandoned
in pending state, never to resolve nor reject.

Returns I<OBJ>.

=cut

sub cancel_handle {
    my ($self, $easy) = @_;

    $self->{'backend'}->cancel_handle($easy, $self->{'multi'});

    return $self;
}

=head2 $obj = I<OBJ>->fail_handle( $EASY, $REASON )

Like C<cancel_handle()> but rejects $EASY’s associated promise
with the given $REASON.

Returns I<OBJ>.

=cut

sub fail_handle {
    my ($self, $easy, $reason) = @_;

    if (!defined $reason || !length $reason) {
        require Carp;
        Carp::carp("fail_handle(): no reason given");
    }

    $self->{'backend'}->fail_handle($easy, $reason, $self->{'multi'});

    return $self;
}

#----------------------------------------------------------------------

=head2 $obj = I<OBJ>->setopt( … )

A passthrough to the underlying L<Net::Curl::Multi> object’s
method of the same name. Returns I<OBJ> to facilitate chaining.

This class requires control of certain L<Net::Curl::Multi> options;
if you attempt to set one of these here you’ll get an exception.

=cut

sub _SETOPT_FORBIDDEN { qw( SOCKETFUNCTION  SOCKETDATA ) };

sub setopt {
    my $self = shift;

    for my $opt ( $self->_SETOPT_FORBIDDEN() ) {
        my $fullopt = "CURLMOPT_$opt";

        if ($_[0] == Net::Curl::Multi->can($fullopt)->()) {
            my $ref = ref $self;
            die "Don’t set $fullopt via $ref!";
        }
    }

    $self->{'multi'}->setopt(@_);
    return $self;
}

=head2 $obj = I<OBJ>->handles( … )

A passthrough to the underlying L<Net::Curl::Multi> object’s
method of the same name.

=cut

sub handles {
   return shift()->{'multi'}->handles();
}

#----------------------------------------------------------------------

sub _socket_fn {
    my ( $multi, $fd, $action, $backend ) = @_[0, 2, 3, 5];

    # IMPORTANT: Removing handles within this function is likely to
    # corrupt libcurl.

    if ($action == Net::Curl::Multi::CURL_POLL_IN) {
        print STDERR "FD $fd, IN\n" if _DEBUG;

        $backend->SET_POLL_IN($fd, $multi);
    }
    elsif ($action == Net::Curl::Multi::CURL_POLL_OUT) {
        print STDERR "FD $fd, OUT\n" if _DEBUG;

        $backend->SET_POLL_OUT($fd, $multi);
    }
    elsif ($action == Net::Curl::Multi::CURL_POLL_INOUT) {
        print STDERR "FD $fd, INOUT\n" if _DEBUG;

        $backend->SET_POLL_INOUT($fd, $multi);
    }
    elsif ($action == Net::Curl::Multi::CURL_POLL_REMOVE) {
        print STDERR "FD $fd, STOP\n" if _DEBUG;

        $backend->STOP_POLL($fd, $multi);
    }
    else {
        warn( __PACKAGE__ . ": Unrecognized action $action on FD $fd\n" );
    }

    return 0;
}

#----------------------------------------------------------------------

=head1 EXAMPLES

See the distribution’s F</examples> directory.

=head1 SEE ALSO

Try L<Net::Curl::Easier> for a more polished variant of Net::Curl::Easy.

L<Net::Curl::Simple> implements a similar idea to this module but
doesn’t return promises. It has a more extensive interface that provides
a more “perlish” experience than L<Net::Curl::Easy>.

If you use L<AnyEvent>, then L<AnyEvent::XSPromises> with
L<AnyEvent::YACurl> may be a nicer fit for you.

=head1 REPOSITORY

L<https://github.com/FGasper/p5-Net-Curl-Promiser>

=head1 LICENSE & COPYRIGHT

Copyright 2019-2020 Gasper Software Consulting.

This library is licensed under the same terms as Perl itself.

=cut

1;
