package Async::Methods;
our $VERSION = '0.000004'; # v0.0.4
$VERSION = eval $VERSION;
use strict;
use warnings;
use Carp ();
use Hash::Util qw(fieldhash);
fieldhash my %start;
fieldhash my %then;
fieldhash my %else;
package start;
sub start::_ {
my ($self, $method, @args) = @_;
my $f = $self->$method(@args);
$start{$f} = $self;
return $f;
}
sub AUTOLOAD {
my ($self, @args) = @_;
my ($method) = our $AUTOLOAD =~ /^start::(.+)$/;
$self->start::_($method => @args);
}
package then;
sub then::_ {
my ($self, $method, @args) = @_;
my $f_type = ref($self);
my $f; $f = $self->then(
sub { my $obj = shift; $obj->$method(@args, @_) },
sub {
if (my $else = $else{$f}) {
$else->(@_)
} else {
$f_type->AWAIT_NEW_FAIL(@_)
}
},
);
if (my $start_obj = $start{$self}) {
$then{$f} = $start{$f} = $start_obj;
}
return $f;
}
sub AUTOLOAD {
my ($self, @args) = @_;
my ($method) = our $AUTOLOAD =~ /^then::(.+)$/;
$self->then::_($method => @args);
}
package else;
sub else::_ {
my ($self, $method, @args) = @_;
Carp::croak "Can only call else on result of start:: -> then::"
unless my $start_obj = $then{$self};
$else{$self} = sub { $start_obj->$method(@args, @_) };
return $self;
}
sub AUTOLOAD {
my ($self, @args) = @_;
my ($method) = our $AUTOLOAD =~ /^else::(.+)$/;
$self->else::_($method => @args);
}
package catch;
sub catch::_ {
my ($self, $method, @args) = @_;
Carp::croak "Can only call catch on start:: or start:: -> then:: object"
unless my $start_obj = $start{$self};
$self->catch(sub { $start_obj->$method(@args, @_) });
}
sub AUTOLOAD {
my ($self, @args) = @_;
my ($method) = our $AUTOLOAD =~ /^catch::(.+)$/;
$self->catch::_($method => @args);
}
package await;
sub this {
my ($self) = @_;
return $self->get if $self->can('get');
if ($self->isa('Mojo::Promise')) {
# This logic stolen from Mojo::Promis::Role::Get v0.1.2
Carp::croak "'get' cannot be called when the event loop is running"
if $self->ioloop->is_running;
my (@result, $rejected);
$self->then(sub { @result = @_ }, sub { $rejected = 1; @result = @_ })
->wait;
if ($rejected) {
my $reason = $result[0] // 'Promise was rejected';
die $reason if ref $reason or $reason =~ m/\n\z/;
Carp::croak $reason;
}
return wantarray ? @result : $result[0];
}
Carp::croak "Don't know how to await::this for $self";
}
sub await::_ {
my ($self, $method, @args) = @_;
if ($self eq 'await') {
Carp::croak "Call of '${method} await' should be '${method} +await'";
}
my $f = ($self->can('then')
? $self->then::_($method, @args)
: $self->$method(@args)
);
$f->await::this;
}
sub AUTOLOAD {
my ($self, @args) = @_;
my ($method) = our $AUTOLOAD =~ /^await::(.+)$/;
$self->await::_($method => @args);
}
1;
=head1 NAME
Async::Methods - Namespaced sugar methods for async/await and future/promise based code
=head1 SYNOPSIS
use Mojo::UserAgent;
my $ua = Mojo::UserAgent->new;
# Normal synchronous code
print $ua->get('http://trout.me.uk/')->result->body;
# Equivalent code running synchronously atop promises
print $ua->get_p('http://trout.me.uk')->then::result->await::body;
# Equivalent code within an async subroutine
use Mojo::Base -async_await, -signatures;
async sub fetch ($url) {
await $ua->get_p($url)->then::result->then::body;
}
print fetch($url)->await::this;
=head1 DESCRIPTION
L<Async::Methods> provides a set of helper methods operating via namespace
that make chaining together asynchronous methods easier. This is not at all
meant to be a replacement for the C<async> and C<await> keywords available
via L<Future::AsyncAwait> or the C<-async_await> flag to L<Mojo::Base> and
in fact is largely meant to be used I<with> such facilities.
Note that in the following code I use C<$p> for example variables but they
can be L<Future> or L<Mojo::Promise> objects or (hopefully) objects of any
other class that provides a similar interface.
Note that methods of each type provided can be called three ways:
$obj->the_type::some_method(@args);
will call C<some_method> on a relevant object, and is effectively simply
sugar for the second type,
$obj->the_type::_(some_method => @args);
which calls the method name given in its first argument (yes, this means that
you can't use the first syntax to call a method called C<_> but the author of
this module strongly suspects that won't be an inconvience in most cases).
Thirdly, to match perl's capacity to allow <$obj->$cb(@args)> as a syntax, you
can also call:
$obj->the_type::_(sub { ... } => @args);
$obj->the_type::_($cb => @args);
to call that code reference as a method.
=head1 METHODS
=head2 start::
my $p = $obj->start::some_method(@args);
my $p = $obj->start::_(some_method => @args);
my $p = $obj->start::_(sub { ... } => @args);
L</start::> methods don't do anything special in and of themselves but
register the C<$obj> with L<Async::Methods> to allow L</catch::> and
L</else::> to work correctly (see their documentation below for why you
might find that useful). Other than the registration part, this is
entirely equivalent to
my $p = $obj->some_method(@args);
=head2 then::
my $then_p = $p->then::some_method(@args);
my $then_p = $p->then::_(some_method => @args);
my $then_p = $p->then::_(sub { ... } => @args);
L</then::> allows for chaining an additional method call from the return
value of the previous promise (assuming it's successful). As such, on its own
this is equivalent to
my $then_p = $p->then(
sub ($obj, @rest) { $obj->some_method(@args, @rest)) }
);
Note that L</then::> does not require anything special of the promise upon
which it's called to provide the base functionality, but I<does> need to be
called on the result of something rooted in L</start::> if you want to be
able to chain L</else::> or L</catch::> from the return value.
=head2 else::
my $else_p = $p->else::some_method(@args);
my $else_p = $p->else::_(some_method => @args);
my $else_p = $p->else::_(sub { ... } => @args);
L</else::> must be called on the result of a L</start::> chained to a
L</then::>, and provides a callback if the L<start::>ed method fails,
invoked on the I<original> invocant. This makes it the "other half" of
L<Async::Methods>' support for two-arg C<<->then>>, so:
my $else_p = $obj->start::one(@args1)
->then::two(@args2)
->else::three(@args3);
is functionally equivalent to:
my $else_p = $obj->one(@args1)
->then(
sub ($then_obj, @then_rest) {
$then_obj->two(@args2, @then_rest)
},
sub (@error) {
$obj->three(@args3, @error)
},
);
which the author hopes explains why you might, on the whole, not really
mind being forced to type L<start::>.
Note that because L</else::> always resolves to the second argument to a
two-arg C<then> call, it can't be used in isolation. Fortunately, we already
provide L</catch::> for that, which is documented next.
=head2 catch::
my $catch_p = $p->catch::some_method(@args);
my $catch_p = $p->catch::_(some_method => @args);
my $catch_p = $p->catch::_(sub { ... } => @args);
L</catch::> can be called on the result of either a L</start::> call or
a L</start::> -> L</then::> chain, and will catch any/all errors produced
up to this point, as opposed to L</else::> which catches errors I<before>
the preceding L</then::> call.
As such, morally equivalent to:
my $catch_p = $obj->start::whatever(...)
->catch(sub ($obj, @error) {
$obj->some_method(@args, @error)
});
=head2 await::
my $ret = $p->await::this;
C<await::this> is simple generic sugar for (at top level of your code outside
of an already-running event loop) spinning the event loop until the promise
completes and then either returning the result on success or C<die()>ing with
the error on failure. For a future, it's equivalent to
my $ret = $f->get;
but if called on a L<Mojo::Promise> loads L<Mojo::Promise::Role::Get> and uses
that to complete the operation, so C<await::this> can be called on either and
still provides a uniform interface. Assuming you install
L<Mojo::Promise::Role::Get> if you need it of course - otherwise you'll get
an exception from the relevant C<require> call.
my $ret = $p->await::some_method(@args);
my $ret = $p->await::_(some_method => @args);
my $ret = $p->await::_(sub { ... } => @args);
L</await::> requires absolutely nothing of the promise upon which it's called,
and other than the special case of C<this> is equivalent to
my $ret = $p->then::some_method(@args)->await::this;
Hopefully obvious caveat: If you want to await a method called C<this> you'll
need to call one of
my $ret = $p->then::this(@args)->await::this;
my $ret = $p->await::_(this => @args);
but C<this> did not strike the author as a sufficiently common method name
to be a deal-breaker in practice.
=head1 AUTHOR
mst - Matt S. Trout (cpan:MSTROUT) <mst@shadowcat.co.uk>
=head1 CONTRIBUTORS
Grinnz - Dan Book (cpan:DBOOK) <dbook@cpan.org>
=head1 COPYRIGHT
Copyright (c) 2020 the Async::Methods L</AUTHOR> and L</CONTRIBUTORS>
as listed above.
=head1 LICENSE
This library is free software and may be distributed under the same terms
as perl itself.