=== modified file 'lib/Future.pm'
--- lib/Future.pm 2015-07-28 15:28:42 +0000
+++ lib/Future.pm 2015-07-28 18:38:51 +0000
@@ -76,6 +76,31 @@
See also L<Future::Utils> which contains useful loop-constructing functions,
to run a future-returning function repeatedly in a loop.
+=head2 FAILURE CATEGORIES
+
+While not directly required by C<Future> or its related modules, a growing
+convention of C<Future>-using code is to encode extra semantics in the
+arguments given to the C<fail> method, to represent different kinds of
+failure.
+
+The convention is that after the initial message string as the first required
+argument (intended for display to humans), the second argument is a short
+lowercase string that relates in some way to the kind of failure that
+occurred. Following this is a list of details about that kind of failure,
+whose exact arrangement or structure are determined by the failure category.
+For example, L<IO::Async> and L<Net::Async::HTTP> use this convention to
+indicate at what stage a given HTTP request has failed:
+
+ ->fail( $message, http => ... ) # an HTTP-level error during protocol
+ ->fail( $message, connect => ... ) # a TCP-level failure to connect a
+ # socket
+ ->fail( $message, resolve => ... ) # a resolver (likely DNS) failure
+ # to resolve a hostname
+
+By following this convention, a module remains consistent with other
+C<Future>-based modules, and makes it easy for program logic to gracefully
+handle and manage failures by use of the C<catch> method.
+
=head2 SUBCLASSING
This class easily supports being subclassed to provide extra behavior, such as
@@ -1079,9 +1104,11 @@
sub then
{
my $self = shift;
- my ( $done_code, $fail_code ) = @_;
+ my $done_code = shift;
+ my $fail_code = ( @_ % 2 ) ? pop : undef;
+ my @catch_list = @_;
- if( $done_code and !$fail_code ) {
+ if( $done_code and !@catch_list and !$fail_code ) {
return $self->_sequence( $done_code, CB_SEQ_ONDONE|CB_RESULT );
}
@@ -1091,6 +1118,7 @@
Carp::croak "Expected \$fail_code to be callable in ->then";
# Complex
+ my %catch_handlers = @catch_list;
return $self->_sequence( sub {
my $self = shift;
if( !$self->{failure} ) {
@@ -1098,6 +1126,10 @@
return $done_code->( $self->get );
}
else {
+ my $name = $self->{failure}[1];
+ if( defined $name and $catch_handlers{$name} ) {
+ return $catch_handlers{$name}->( $self->failure );
+ }
return $self unless $fail_code;
return $fail_code->( $self->failure );
}
@@ -1112,6 +1144,59 @@
return $self->_sequence( $fail_code, CB_SEQ_ONFAIL|CB_RESULT );
}
+=head2 catch
+
+ $future = $f1->catch(
+ name => \&code,
+ name => \&code, ...
+ )
+
+Returns a new sequencing C<Future> that behaves like an C<else> call which
+dispatches to a choice of several alternative handling functions depending on
+the kind of failure that occurred. If C<$f1> fails with a category name (i.e.
+the second argument to the C<fail> call) which exactly matches one of the
+string names given, then the corresponding code is invoked, being passed the
+same arguments as a plain C<else> call would take, and is expected to return a
+C<Future> in the same way.
+
+ $f2 = $code->( $exception, $name, @other_details )
+
+If C<$f1> does not fail, fails without a category name at all, or fails with a
+category name that does not match any given to the C<catch> method, then the
+returned sequence future immediately completes with the same result, and no
+block of code is invoked.
+
+If passed an odd-sized list, the final argument gives a function to invoke on
+failure if no other handler matches.
+
+ $future = $f1->catch(
+ name => \&code, ...
+ \&fail_code,
+ )
+
+This feature is currently still a work-in-progress. It currently can only cope
+with category names that are literal strings, which are all distinct. A later
+version may define other kinds of match (e.g. regexp), may specify some sort
+of ordering on the arguments, or any of several other semantic extensions. For
+more detail on the ongoing design, see
+L<
https://rt.cpan.org/Ticket/Display.html?id=103545>.
+
+=head2 then I<(multiple arguments)>
+
+ $future = $f1->then( \&done_code, @catch_list, \&fail_code )
+
+The C<then> method can be passed an even-sized list inbetween the
+C<$done_code> and the C<$fail_code>, with the same meaning as the C<catch>
+method.
+
+=cut
+
+sub catch
+{
+ my $self = shift;
+ return $self->then( undef, @_ );
+}
+
=head2 transform
$future = $f1->transform( %args )
=== added file 't/07catch.t'
--- t/07catch.t 1970-01-01 00:00:00 +0000
+++ t/07catch.t 2015-07-28 18:40:43 +0000
@@ -0,0 +1,94 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Refcount;
+
+use Future;
+
+# catch success
+{
+ my $f1 = Future->new;
+
+ my $fseq = $f1->catch(
+ test => sub { die "catch of successful Future should not be invoked" },
+ );
+
+ ok( defined $fseq, '$fseq defined' );
+ isa_ok( $fseq, "Future", '$fseq' );
+
+ is_oneref( $fseq, '$fseq has refcount 1 initially' );
+
+ $f1->done( results => "here" );
+
+ is_deeply( [ $fseq->get ], [ results => "here" ], '$fseq succeeds when $f1 succeeds' );
+
+ undef $f1;
+ is_oneref( $fseq, '$fseq has refcount 1 before EOF' );
+}
+
+# catch matching failure
+{
+ my $f1 = Future->new;
+
+ my $f2;
+ my $fseq = $f1->catch(
+ test => sub {
+ is( $_[0], "f1 failure\n", 'catch block passed result of $f1' );
+ return $f2 = Future->done;
+ },
+ );
+
+ ok( defined $fseq, '$fseq defined' );
+ isa_ok( $fseq, "Future", '$fseq' );
+
+ is_oneref( $fseq, '$fseq has refcount 1 initially' );
+
+ $f1->fail( "f1 failure\n", test => );
+
+ undef $f1;
+ is_oneref( $fseq, '$fseq has refcount 1 after $f1 fail and dropped' );
+
+ ok( defined $f2, '$f2 now defined after $f1 fails' );
+
+ ok( $fseq->is_ready, '$fseq is done after $f2 done' );
+}
+
+# catch non-matching failure
+{
+ my $f1 = Future->new;
+
+ my $fseq = $f1->catch(
+ test => sub { die "catch of non-matching Failure should not be invoked" },
+ );
+
+ $f1->fail( "f1 failure\n", different => );
+
+ ok( $fseq->is_ready, '$fseq is done after $f1 fail' );
+ is( scalar $fseq->failure, "f1 failure\n", '$fseq failure' );
+}
+
+# catch default handler
+{
+ my $fseq = Future->fail( "failure", other => )
+ ->catch(
+ test => sub { die "'test' catch should not match" },
+ sub { Future->done( default => "handler" ) },
+ );
+
+ is_deeply( [ $fseq->get ], [ default => "handler" ],
+ '->catch accepts a default handler' );
+}
+
+# catch via 'then'
+{
+ is( scalar ( Future->fail( "message", test => )
+ ->then( sub { die "then &done should not be invoked" },
+ test => sub { Future->done( 1234 ) },
+ sub { die "then &fail should not be invoked" } )->get ),
+ 1234, 'catch semantics via ->then' );
+}
+
+done_testing;