diff --git a/README.md b/README.md index 8e28133..d6a9045 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,23 @@ List> input = asList( CompletableFuture> joined = CompletableFutures.successfulAsList(input, t -> "default"); ``` +There are also variants that accept a list of arguments and a function to turn them into CompletableFutures: +```java + List input = asList(1, 2, 3, 4); + Function> futureMapper = a -> completedFuture("" + a); + CompletableFuture> joined = CompletableFutures.successfulAsList(input, futureMapper, (s, t) -> "default"); +``` + +and a map to allow passing other information about each future into the default object: +```java +Map> input = asMap( + asList(1, 2), + asList( + completedFuture("a"), + exceptionallyCompletedFuture(new RuntimeException("boom")))); +CompletableFuture> joined = CompletableFutures.successfulAsList(input, (num, t) -> num + " default"); +``` + #### joinList `joinList` is a stream collector that combines multiple futures into a list. This is handy if you diff --git a/src/main/java/com/spotify/futures/CompletableFutures.java b/src/main/java/com/spotify/futures/CompletableFutures.java index 4d06c7d..b6766bb 100644 --- a/src/main/java/com/spotify/futures/CompletableFutures.java +++ b/src/main/java/com/spotify/futures/CompletableFutures.java @@ -41,6 +41,8 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * A collection of static utility methods that extend the @@ -139,6 +141,34 @@ public static CompletableFuture> allAsMap( }); } + /** + * Returns a new {@link CompletableFuture} which completes to a list of values of those input + * stages that succeeded. The list of results is in the same order as the input stages. For failed + * stages, the defaultValueMapper will be called, and the value returned from that function will + * be put in the resulting list. + * + *

If no stages are provided in the map, returns a future holding an empty list. + * + * @param stagesMap the stages to combine. + * @param defaultValueMapper a function that will be called when a future completes exceptionally + * to provide a default value to place in the resulting list + * @param the key or reference to each completion stage, used to map on exceptions + * @param the common type of all the input stages, that determines the type of the output + * future + * @return a future that completes to a list of the results of the supplied stages + * @throws NullPointerException if the stages map or any of its values are {@code null} + */ + public static CompletableFuture> successfulAsList( + Map> stagesMap, + BiFunction defaultValueMapper) { + return stagesMap.entrySet().stream() + .map( + f -> + f.getValue() + .exceptionally(throwable -> defaultValueMapper.apply(f.getKey(), throwable))) + .collect(joinList()); + } + /** * Returns a new {@link CompletableFuture} which completes to a list of values of those input * stages that succeeded. The list of results is in the same order as the input stages. For failed @@ -163,6 +193,36 @@ public static CompletableFuture> successfulAsList( .collect(joinList()); } + /** + * Returns a new {@link CompletableFuture} which completes to a list of values of those input + * stages that succeeded. The list of results is in the same order as the input stages. For failed + * stages, the defaultValueMapper will be called, and the value returned from that function will + * be put in the resulting list. + * + *

If no stages are provided, returns a future holding an empty list. + * + * @param arguments the arguments from which to create a completable future + * @param the common type of argument + * @param mapToCompletableFuture a function which transforms S into a future of type T + * @param defaultValueMapper a function that will be called when a future completes exceptionally + * to provide a default value to place in the resulting list + * @param the common type of all of the input stages, that determines the type of the + * output future + * @return a future that completes to a list of the results of the supplied stages + * @throws NullPointerException if the stages list or any of its elements are {@code null} + */ + public static CompletableFuture> successfulAsList( + List arguments, + Function> mapToCompletableFuture, + BiFunction defaultValueMapper) { + final Map> futureMap = arguments.stream() + .collect(Collectors.toMap( + a -> a, + mapToCompletableFuture + )); + return successfulAsList(futureMap, defaultValueMapper); + } + /** * Returns a new {@code CompletableFuture} that is already exceptionally completed with * the given exception. diff --git a/src/test/java/com/spotify/futures/CompletableFuturesTest.java b/src/test/java/com/spotify/futures/CompletableFuturesTest.java index 23bf97f..d2dcb52 100644 --- a/src/test/java/com/spotify/futures/CompletableFuturesTest.java +++ b/src/test/java/com/spotify/futures/CompletableFuturesTest.java @@ -77,6 +77,8 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeoutException; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; import org.hamcrest.CustomTypeSafeMatcher; @@ -223,6 +225,68 @@ public void successfulAsList_exceptionalAndNull() { assertThat(successfulAsList(input, t -> "default"), completesTo(expected)); } + @Test + public void successfulAsListFromMap_whenMixOfFailureAndSuccess() { + Map> map = new HashMap<>(); + map.put("hello2", completedFuture(true)); + map.put("hello", exceptionallyCompletedFuture(new RuntimeException("I failed"))); + BiFunction resultsMapper = (s, t) -> s.length() < 3; + + final List expected = asList(true, false); + final CompletableFuture> actual = successfulAsList(map, resultsMapper); + assertThat(actual, completesTo(expected)); + } + + @Test + public void successfulAsListFromMap_throwWhenMapIsNull() { + Map> map = null; + BiFunction resultsMapper = (s, t) -> true; + + assertThrows(NullPointerException.class, () -> successfulAsList(map, resultsMapper)); + } + + @Test + public void successfulAsListFromMap_throwWhenOneValueIsNull() { + Map> map = + asMap(asList("hello", "hello"), asList(completedFuture(true), null)); + BiFunction resultsMapper = (s, t) -> s.length() < 3; + + assertThrows(NullPointerException.class, () -> successfulAsList(map, resultsMapper)); + } + + @Test + public void successfulAsListFromMap_exceptionalAndNull() { + Map> map = + asMap( + asList("1", "2", "3", "4"), + asList( + completedFuture("a"), + exceptionallyCompletedFuture(new RuntimeException("boom")), + completedFuture(null), + completedFuture("d"))); + final List expected = asList("a", "2 default", null, "d"); + assertThat(successfulAsList(map, (s, t) -> s + " default"), completesTo(expected)); + } + + @Test + public void successfulAsListFromArguments_exceptionalAndNull() { + List arguments = asList(1, 2, 3, 4); + Function> futureMapper = a -> { + if (a == 2) { + return exceptionallyCompletedFuture(new RuntimeException("OH NO")); + } else if (a == 3) { + return completedFuture(null); + } + return completedFuture("" + a); + }; + BiFunction resultsMapper = (s, t) -> s + " default"; + final List expected = asList("1", "2 default", null, "4"); + + assertThat(successfulAsList( + arguments, futureMapper, resultsMapper + ), completesTo(expected)); + } + @Test public void getCompleted_done() { final CompletionStage future = completedFuture("hello");