diff --git a/src/main/java/org/kiwiproject/jaxrs/KiwiResponses.java b/src/main/java/org/kiwiproject/jaxrs/KiwiResponses.java
index f9596163..edb15f27 100644
--- a/src/main/java/org/kiwiproject/jaxrs/KiwiResponses.java
+++ b/src/main/java/org/kiwiproject/jaxrs/KiwiResponses.java
@@ -2,16 +2,20 @@
import static java.util.Objects.nonNull;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull;
+import static org.kiwiproject.base.KiwiPreconditions.checkOnlyOneArgumentIsNull;
+import com.google.common.annotations.VisibleForTesting;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status.Family;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.function.Supplier;
/**
* Static utilities related to evaluating and acting upon Jakarta REST responses. For example, this class contains
@@ -304,34 +308,90 @@ public static boolean hasFamily(Response response, Family family) {
return response.getStatusInfo().getFamily() == family;
}
+ /**
+ * Given a {@link Response} Supplier, perform an action depending on whether it was
+ * successful ({@code successConsumer}), failed ({@code failureConsumer}), or if the
+ * Supplier threw an exception ({@code exceptionConsumer}).
+ *
+ * Ensures the response is closed after performing the action.
+ *
+ * @param responseSupplier a Supplier that provides the response
+ * @param successConsumer the action to run if the response is successful
+ * @param failureConsumer the action to run if the response is not successful
+ * @param exceptionConsumer the action to run if the Supplier throws an exception
+ */
+ public static void onSuccessOrFailure(Supplier responseSupplier,
+ Consumer successConsumer,
+ Consumer failureConsumer,
+ Consumer exceptionConsumer) {
+
+ checkArgumentNotNull(responseSupplier);
+ checkArgumentNotNull(exceptionConsumer);
+
+ var result = getResponse(responseSupplier);
+
+ if (result.hasResponse()) {
+ onSuccessOrFailure(result.response(), successConsumer, failureConsumer);
+ } else {
+ exceptionConsumer.accept(result.error());
+ }
+ }
+
/**
* Given a {@link Response}, perform an action depending on whether it was successful ({@code successConsumer})
- * or failed ({@code failedConsumer}).
+ * or failed ({@code failureConsumer}).
*
* Ensures the response is closed after performing the action.
*
* @param response the response object
* @param successConsumer the action to run if the response is successful
- * @param failedConsumer the action to run if the response is not successful
+ * @param failureConsumer the action to run if the response is not successful
*/
public static void onSuccessOrFailure(Response response,
Consumer successConsumer,
- Consumer failedConsumer) {
+ Consumer failureConsumer) {
checkArgumentNotNull(response);
checkArgumentNotNull(successConsumer);
- checkArgumentNotNull(failedConsumer);
+ checkArgumentNotNull(failureConsumer);
try {
if (successful(response)) {
successConsumer.accept(response);
} else {
- failedConsumer.accept(response);
+ failureConsumer.accept(response);
}
} finally {
closeQuietly(response);
}
}
+ /**
+ * Given a {@link Response} Supplier, perform an action if it was successful ({@code successConsumer}.
+ * If the response was unsuccessful, throw the exception supplied by {@code throwingFun}.
+ * If the Supplier throws an exception, then that exception is rethrown.
+ *
+ * Ensures the response is closed after performing the action.
+ *
+ * @param responseSupplier a Supplier that provides the response
+ * @param successConsumer the action to run if the response is successful
+ * @param throwingFun a function that creates an appropriate (subclass of) RuntimeException
+ * @throws RuntimeException the result of {@code throwingFun}, or the exception thrown by the Supplier
+ */
+ public static void onSuccessOrFailureThrow(Supplier responseSupplier,
+ Consumer successConsumer,
+ Function throwingFun) {
+
+ checkArgumentNotNull(responseSupplier);
+
+ var result = getResponse(responseSupplier);
+
+ if (result.hasResponse()) {
+ onSuccessOrFailureThrow(responseSupplier.get(), successConsumer, throwingFun);
+ } else {
+ throw result.error();
+ }
+ }
+
/**
* Given a {@link Response}, perform an action if it was successful ({@code successConsumer} or throw an
* exception supplied by {@code throwingFun}.
@@ -362,8 +422,31 @@ public static void onSuccessOrFailureThrow(Response response,
}
/**
- * Given a {@link Response}, perform an action only if it was successful ({@code successConsumer}. No action
- * is performed for an unsuccessful response.
+ * Given a {@link Response} Supplier, perform an action only if it was successful ({@code successConsumer}.
+ *
+ * No action is performed for an unsuccessful response, and exceptions thrown by the Supplier are ignored.
+ *
+ * Ensures the response is closed after performing the action.
+ *
+ * @param responseSupplier a Supplier that provides the response
+ * @param successConsumer the action to run if the response is successful
+ */
+ public static void onSuccess(Supplier responseSupplier,
+ Consumer successConsumer) {
+
+ checkArgumentNotNull(responseSupplier);
+
+ var result = getResponse(responseSupplier);
+
+ if (result.hasResponse()) {
+ onSuccess(result.response(), successConsumer);
+ }
+ }
+
+ /**
+ * Given a {@link Response}, perform an action only if it was successful ({@code successConsumer}.
+ *
+ * No action is performed for an unsuccessful response.
*
* Ensures the response is closed after performing the action.
*
@@ -374,9 +457,38 @@ public static void onSuccess(Response response, Consumer successConsum
onSuccessOrFailure(response, successConsumer, NO_OP_RESPONSE_CONSUMER);
}
+ /**
+ * Given a {@link Response} Supplier, perform an action that returns a result only if it was
+ * successful ({@code successFun}).
+ *
+ * No action is performed for an unsuccessful response, and exceptions
+ * thrown by the Supplier are ignored.
+ *
+ * Ensures the response is closed after performing the action.
+ *
+ * @param responseSupplier a Supplier that provides the response
+ * @param successFun the function to apply if the response is successful
+ * @param the result type
+ * @return an Optional containing a result for successful responses, or an empty Optional
+ */
+ public static Optional onSuccessWithResult(Supplier responseSupplier,
+ Function successFun) {
+
+ checkArgumentNotNull(responseSupplier);
+
+ var result = getResponse(responseSupplier);
+
+ if (result.hasResponse()) {
+ return onSuccessWithResult(result.response(), successFun);
+ }
+
+ return Optional.empty();
+ }
+
/**
* Given a {@link Response}, perform an action that returns a result only if it was successful ({@code successFun}).
- * No action is performer for an unsuccessful response.
+ *
+ * No action is performed for an unsuccessful response.
*
* Ensures the response is closed after performing the action.
*
@@ -390,21 +502,80 @@ public static Optional onSuccessWithResult(Response response, Functionnot successful ({@code failedConsumer}).
+ * Given a {@link Response} Supplier, perform an action only if it was
+ * not successful ({@code failureConsumer}), or if the Supplier
+ * threw an exception ({@code exceptionConsumer}).
+ *
+ * No action is performed for a successful response.
+ *
+ * Ensures the response is closed after performing the action.
+ *
+ * @param responseSupplier a Supplier that provides the response
+ * @param failureConsumer the action to run if the response is not successful
+ * @param exceptionConsumer the action to run if the Supplier throws an exception
+ */
+ public static void onFailure(Supplier responseSupplier,
+ Consumer failureConsumer,
+ Consumer exceptionConsumer) {
+
+ checkArgumentNotNull(responseSupplier);
+ checkArgumentNotNull(exceptionConsumer);
+
+ var result = getResponse(responseSupplier);
+
+ if (result.hasResponse()) {
+ onFailure(result.response(), failureConsumer);
+ } else {
+ exceptionConsumer.accept(result.error());
+ }
+ }
+
+ /**
+ * Given a {@link Response}, perform an action only if it was not successful ({@code failureConsumer}).
+ *
* No action is performed for a successful response.
*
* Ensures the response is closed after performing the action.
*
* @param response the response object
- * @param failedConsumer the action to run if the response is not successful
+ * @param failureConsumer the action to run if the response is not successful
*/
- public static void onFailure(Response response, Consumer failedConsumer) {
- onSuccessOrFailure(response, NO_OP_RESPONSE_CONSUMER, failedConsumer);
+ public static void onFailure(Response response, Consumer failureConsumer) {
+ onSuccessOrFailure(response, NO_OP_RESPONSE_CONSUMER, failureConsumer);
+ }
+
+ /**
+ * Given a {@link Response} Supplier, throw a (subclass of) {@link RuntimeException} for failed
+ * responses using {@code throwingFun}.
+ * If the Supplier throws an exception, that exception is rethrown.
+ *
+ * No action is performed for a successful response.
+ *
+ * Ensures the response is closed after performing the action.
+ *
+ * @param responseSupplier a Supplier that provides the response
+ * @param throwingFun function that creates an appropriate (subclass of) RuntimeException
+ * @throws RuntimeException the result of {@code throwingFun}, or the exception thrown by the Supplier
+ */
+ public static void onFailureThrow(Supplier responseSupplier,
+ Function throwingFun) {
+
+ checkArgumentNotNull(responseSupplier);
+
+ var result = getResponse(responseSupplier);
+
+ if (result.hasResponse()) {
+ onFailureThrow(result.response(), throwingFun);
+ } else {
+ throw result.error();
+ }
}
/**
* Given a {@link Response}, throw a (subclass of) {@link RuntimeException} for failed responses using
- * {@code throwingFun}. No action is performed for a successful response.
+ * {@code throwingFun}.
+ *
+ * No action is performed for a successful response.
*
* Ensures the response is closed after performing the action.
*
@@ -426,31 +597,65 @@ public static void onFailureThrow(Response response,
}
}
+ /**
+ * Given a {@link Response} Supplier, perform an action that returns a result if the response was
+ * successful ({@code successFun}). Perform an action if the response was unsuccessful ({@code failureConsumer},
+ * or if the Supplier threw an exception ({@code exceptionConsumer}).
+ *
+ * Ensures the response is closed after performing the action.
+ *
+ * @param responseSupplier a Supplier that provides the response
+ * @param successFun the function to apply if the response is successful
+ * @param failureConsumer the action to run if the response is not successful
+ * @param exceptionConsumer the action to run if the Supplier throws an exception
+ * @param the result type
+ * @return the result from {@code successFun} for successful responses, or an empty Optional
+ * for unsuccessful responses or if the Supplier throws an exception
+ */
+ public static Optional onSuccessWithResultOrFailure(Supplier responseSupplier,
+ Function successFun,
+ Consumer failureConsumer,
+ Consumer exceptionConsumer) {
+
+ checkArgumentNotNull(responseSupplier);
+ checkArgumentNotNull(exceptionConsumer);
+
+ var result = getResponse(responseSupplier);
+
+ if (result.hasResponse()) {
+ return onSuccessWithResultOrFailure(result.response(), successFun, failureConsumer);
+ } else {
+ exceptionConsumer.accept(result.error());
+ }
+
+ return Optional.empty();
+ }
+
/**
* Given a {@link Response}, perform an action that returns a result if the response was
- * successful ({@code successFun}) or perform an action if the response was unsuccessful ({@code failedConsumer}.
+ * successful ({@code successFun}) or perform an action if the response was unsuccessful ({@code failureConsumer}.
*
* Ensures the response is closed after performing the action.
*
* @param response the response object
* @param successFun the function to apply if the response is successful
- * @param failedConsumer the action to run if the response is not successful
+ * @param failureConsumer the action to run if the response is not successful
* @param the result type
* @return the result from {@code successFun} for successful responses, or an empty Optional for unsuccessful ones
*/
public static Optional onSuccessWithResultOrFailure(Response response,
Function successFun,
- Consumer failedConsumer) {
+ Consumer failureConsumer) {
checkArgumentNotNull(response);
checkArgumentNotNull(successFun);
- checkArgumentNotNull(failedConsumer);
+ checkArgumentNotNull(failureConsumer);
T result = null;
try {
if (successful(response)) {
result = successFun.apply(response);
} else {
- failedConsumer.accept(response);
+ failureConsumer.accept(response);
}
} finally {
closeQuietly(response);
@@ -459,6 +664,38 @@ public static Optional onSuccessWithResultOrFailure(Response response,
return Optional.ofNullable(result);
}
+ /**
+ * Given a {@link Response} Supplier, perform an action that returns a result if the response was
+ * successful ({@code successFun}. If the response was not successful return the result
+ * of a function ({@code failedFun}). If the Supplier threw an exception, return the result
+ * of a different function ({@code exceptionFun}).
+ *
+ * Ensures the response is closed after performing the action.
+ *
+ * @param responseSupplier a Supplier that provides the response
+ * @param successFun the function to apply if the response is successful
+ * @param failedFun the function to apply if the response is not successful
+ * @param exceptionFun the function to apply if the Supplier throws an exception
+ * @param the result type
+ * @return the result from applying {@code successFun}, {@code failedFun}, or {@code exceptionFun}
+ */
+ public static T onSuccessOrFailureWithResult(Supplier responseSupplier,
+ Function successFun,
+ Function failedFun,
+ Function exceptionFun) {
+
+ checkArgumentNotNull(responseSupplier);
+ checkArgumentNotNull(exceptionFun);
+
+ var result = getResponse(responseSupplier);
+
+ if (result.hasResponse()) {
+ return onSuccessOrFailureWithResult(result.response(), successFun, failedFun);
+ }
+
+ return exceptionFun.apply(result.error());
+ }
+
/**
* Given a {@link Response}, perform an action that returns a result if the response was
* successful ({@code successFun}) or if not successful ({@code failedFun}).
@@ -485,9 +722,39 @@ public static T onSuccessOrFailureWithResult(Response response,
}
}
+ /**
+ * Given a {@link Response} Supplier, perform an action that returns a result if it was
+ * successful ({@code successFun} or throw a (subclass of) {@link RuntimeException} if it
+ * failed ({@code throwingFun}).
+ * If the Supplier threw an exception, then that exception is rethrown.
+ *
+ * Ensures the response is closed after performing the action.
+ *
+ * @param responseSupplier a Supplier that provides the response
+ * @param successFun the function to apply if the response is successful
+ * @param throwingFun a function that creates an appropriate (subclass of) RuntimeException
+ * @param the result type
+ * @return the result from applying {@code successFun}
+ * @throws RuntimeException the result of {@code throwingFun} or the exception thrown by the Supplier
+ */
+ public static T onSuccessWithResultOrFailureThrow(Supplier responseSupplier,
+ Function successFun,
+ Function throwingFun) {
+
+ checkArgumentNotNull(responseSupplier);
+
+ var result = getResponse(responseSupplier);
+
+ if (result.hasResponse()) {
+ return onSuccessWithResultOrFailureThrow(result.response(), successFun, throwingFun);
+ }
+
+ throw result.error();
+ }
+
/**
* Given a {@link Response}, perform an action that returns a result if it was successful ({@code successFun}
- * or throw a (subclass of ) {@link RuntimeException} if it failed ({@code throwingFun}).
+ * or throw a (subclass of) {@link RuntimeException} if it failed ({@code throwingFun}).
*
* Ensures the response is closed after performing the action.
*
@@ -516,6 +783,31 @@ public static T onSuccessWithResultOrFailureThrow(Response response,
}
}
+ /**
+ * Given a {@link Response} Supplier, perform some action using one of the supplied consumers.
+ *
+ * Ensures the response is closed after performing the action.
+ *
+ * @param responseSupplier a Supplier that provides the response
+ * @param responseConsumer the action to run on any response
+ * @param exceptionConsumer the action to run if the Supplier throws an exception
+ */
+ public static void accept(Supplier responseSupplier,
+ Consumer responseConsumer,
+ Consumer exceptionConsumer) {
+
+ checkArgumentNotNull(responseSupplier);
+ checkArgumentNotNull(exceptionConsumer);
+
+ var result = getResponse(responseSupplier);
+
+ if (result.hasResponse()) {
+ accept(result.response(), responseConsumer);
+ } else {
+ exceptionConsumer.accept(result.error());
+ }
+ }
+
/**
* Given a {@link Response}, perform some action using the supplied consumer.
*
@@ -535,6 +827,33 @@ public static void accept(Response response, Consumer responseConsumer
}
}
+ /**
+ * Given a {@link Response} Supplier, perform an action tha returns a result using one of the given functions.
+ *
+ * Ensures the response is closed after performing the action.
+ *
+ * @param responseSupplier a Supplier that provides the response
+ * @param fun the function to apply to the response
+ * @param exceptionFun the function to apply if the Supplier throws an exception
+ * @param the result type
+ * @return the result of applying the given function
+ */
+ public static T apply(Supplier responseSupplier,
+ Function fun,
+ Function exceptionFun) {
+
+ checkArgumentNotNull(responseSupplier);
+ checkArgumentNotNull(exceptionFun);
+
+ var result = getResponse(responseSupplier);
+
+ if (result.hasResponse()) {
+ return apply(result.response(), fun);
+ }
+
+ return exceptionFun.apply(result.error());
+ }
+
/**
* Given a {@link Response}, perform an action tha returns a result using the given function.
*
@@ -556,6 +875,62 @@ public static T apply(Response response, Function fun) {
}
}
+ @VisibleForTesting
+ record WebCallResult(RuntimeException error, Response response) {
+
+ // This is really an "either" type...which sadly, Java does not have.
+
+ WebCallResult {
+ checkOnlyOneArgumentIsNull(error, response,
+ "Either the Response or the RuntimeException can be null, but not both");
+ }
+
+ static WebCallResult ofResponse(Response response) {
+ return new WebCallResult(null, response);
+ }
+
+ static WebCallResult ofError(RuntimeException error) {
+ return new WebCallResult(error, null);
+ }
+
+ boolean hasResponse() {
+ return nonNull(response);
+ }
+ }
+
+ @VisibleForTesting
+ static WebCallResult getResponse(Supplier responseSupplier) {
+ Response response = null;
+ RuntimeException error = null;
+ try {
+ response = responseSupplier.get();
+ } catch (RuntimeException e) {
+ error = e;
+ }
+
+ if (nonNull(response)) {
+ return WebCallResult.ofResponse(response);
+ }
+
+ if (nonNull(error)) {
+ logResponseSupplierException(LOG, error);
+ return WebCallResult.ofError(error);
+ }
+
+ LOG.warn("Response Supplier returned a null Response, which is not permitted");
+ throw new IllegalStateException("Response returned by Supplier must not be null");
+ }
+
+ @VisibleForTesting
+ static void logResponseSupplierException(Logger logger, RuntimeException error) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Response Supplier threw an exception", error);
+ } else {
+ logger.warn("Response Supplier unexpectedly threw: {}: {} (enable TRACE level to see stack trace)",
+ error.getClass().getName(), error.getMessage());
+ }
+ }
+
/**
* Closes the given {@link Response}, which can be {@code null}, swallowing any exceptions and logging them
* at INFO level.
diff --git a/src/test/java/org/kiwiproject/jaxrs/KiwiResponsesTest.java b/src/test/java/org/kiwiproject/jaxrs/KiwiResponsesTest.java
index bb5ecf91..e650c6a5 100644
--- a/src/test/java/org/kiwiproject/jaxrs/KiwiResponsesTest.java
+++ b/src/test/java/org/kiwiproject/jaxrs/KiwiResponsesTest.java
@@ -10,11 +10,15 @@
import static jakarta.ws.rs.core.MediaType.WILDCARD_TYPE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import jakarta.ws.rs.ProcessingException;
@@ -27,11 +31,14 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.kiwiproject.junit.jupiter.ClearBoxTest;
+import org.slf4j.Logger;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
+import java.util.function.Supplier;
import java.util.stream.IntStream;
@ExtendWith(SoftAssertionsExtension.class)
@@ -39,11 +46,13 @@ class KiwiResponsesTest {
private AtomicInteger successCount;
private AtomicInteger failureCount;
+ private AtomicInteger exceptionCount;
@BeforeEach
void setUp() {
successCount = new AtomicInteger();
failureCount = new AtomicInteger();
+ exceptionCount = new AtomicInteger();
}
@Nested
@@ -413,6 +422,43 @@ void shouldCheckOther() {
}
}
+ @Nested
+ class OnSuccessOrFailure_UsingResponseSupplier {
+
+ @Test
+ void shouldUseResponseFromSupplier() {
+ var response = newMockResponseWithStatus(Response.Status.CREATED);
+ Supplier supplier = () -> response;
+
+ KiwiResponses.onSuccessOrFailure(supplier,
+ successResponse -> successCount.incrementAndGet(),
+ failResponse -> failureCount.incrementAndGet(),
+ supplierException -> exceptionCount.incrementAndGet());
+
+ assertThat(successCount).hasValue(1);
+ assertThat(failureCount).hasValue(0);
+ assertThat(exceptionCount).hasValue(0);
+
+ verify(response).close();
+ }
+
+ @Test
+ void shouldCallExceptionConsumer_WhenResponseSupplierThrowsException() {
+ Supplier supplier = () -> {
+ throw new ProcessingException("request processing failed");
+ };
+
+ KiwiResponses.onSuccessOrFailure(supplier,
+ successResponse -> successCount.incrementAndGet(),
+ failResponse -> failureCount.incrementAndGet(),
+ supplierException -> exceptionCount.incrementAndGet());
+
+ assertThat(successCount).hasValue(0);
+ assertThat(failureCount).hasValue(0);
+ assertThat(exceptionCount).hasValue(1);
+ }
+ }
+
@Nested
class OnSuccessOrFailure {
@@ -461,6 +507,49 @@ void shouldCallFailedConsumer_ForUnsuccessfulResponse() {
}
}
+ @Nested
+ class OnSuccessOrFailureThrow_UsingResponseSupplier {
+
+ @Test
+ void shouldUseResponseFromSupplier() {
+ var response = newMockResponseWithStatus(Response.Status.OK);
+ Supplier supplier = () -> response;
+
+ KiwiResponses.onSuccessOrFailureThrow(supplier,
+ successResponse -> successCount.incrementAndGet(),
+ failResponse -> {
+ failureCount.incrementAndGet();
+ return new CustomKiwiResponsesRuntimeException(failResponse);
+ });
+
+ assertThat(successCount).hasValue(1);
+ assertThat(failureCount).hasValue(0);
+
+ verify(response).close();
+ }
+
+ @Test
+ void shouldRethrowSupplierException_WhenResponseSupplierThrowsException() {
+ Supplier supplier = () -> {
+ throw new ProcessingException("request processing failed");
+ };
+
+ var thrown = catchThrowable(() ->
+ KiwiResponses.onSuccessOrFailureThrow(supplier,
+ successResponse -> successCount.incrementAndGet(),
+ failResponse -> {
+ failureCount.incrementAndGet();
+ return new CustomKiwiResponsesRuntimeException(failResponse);
+ }));
+
+ assertThat(thrown).isExactlyInstanceOf(ProcessingException.class)
+ .hasMessage("request processing failed");
+
+ assertThat(successCount).hasValue(0);
+ assertThat(failureCount).hasValue(0);
+ }
+ }
+
@Nested
class OnSuccessOrFailureThrow {
@@ -502,6 +591,33 @@ void shouldThrow_ForUnsuccessfulResponse() {
}
}
+ @Nested
+ class OnSuccess_UsingResponseSupplier {
+
+ @Test
+ void shouldUseResponseFromSupplier() {
+ var response = newMockResponseWithStatus(Response.Status.CREATED);
+ Supplier supplier = () -> response;
+
+ KiwiResponses.onSuccess(supplier, successResponse -> successCount.incrementAndGet());
+
+ assertThat(successCount).hasValue(1);
+
+ verify(response).close();
+ }
+
+ @Test
+ void shouldIgnoreExceptionsThrownBySupplier() {
+ Supplier supplier = () -> {
+ throw new ProcessingException("request processing failed");
+ };
+
+ KiwiResponses.onSuccess(supplier, successResponse -> successCount.incrementAndGet());
+
+ assertThat(successCount).hasValue(0);
+ }
+ }
+
@Nested
class OnSuccess {
@@ -528,6 +644,37 @@ void shouldNotCallSuccessConsumer_ForUnsuccessfulResponse() {
}
}
+ @Nested
+ class OnSuccessWithResult_UsingResponseSupplier {
+
+ @Test
+ void shouldUseResponseFromSupplier() {
+ var response = newMockResponseWithStatus(Response.Status.ACCEPTED);
+ Supplier supplier = () -> response;
+
+ Optional count = KiwiResponses.onSuccessWithResult(supplier,
+ successResponse -> successCount.incrementAndGet());
+
+ assertThat(count).hasValue(1);
+ assertThat(successCount).hasValue(1);
+
+ verify(response).close();
+ }
+
+ @Test
+ void shouldIgnoreExceptionsThrownBySupplier() {
+ Supplier supplier = () -> {
+ throw new ProcessingException("request processing failed");
+ };
+
+ Optional count = KiwiResponses.onSuccessWithResult(supplier,
+ successResponse -> successCount.incrementAndGet());
+
+ assertThat(count).isEmpty();
+ assertThat(successCount).hasValue(0);
+ }
+ }
+
@Nested
class OnSuccessWithResult {
@@ -558,6 +705,39 @@ void shouldReturnEmptyOptional_ForUnsuccessfulResponse() {
}
}
+ @Nested
+ class OnFailure_UsingResponseSupplier {
+
+ @Test
+ void shouldUseResponseFromSupplier() {
+ var response = newMockResponseWithStatus(Response.Status.BAD_REQUEST);
+ Supplier supplier = () -> response;
+
+ KiwiResponses.onFailure(supplier,
+ failResponse -> failureCount.incrementAndGet(),
+ exceptionConsumer -> exceptionCount.incrementAndGet());
+
+ assertThat(failureCount).hasValue(1);
+ assertThat(exceptionCount).hasValue(0);
+
+ verify(response).close();
+ }
+
+ @Test
+ void shouldCallExceptionConsumer_WhenResponseSupplierThrowsException() {
+ Supplier supplier = () -> {
+ throw new ProcessingException("request processing failed");
+ };
+
+ KiwiResponses.onFailure(supplier,
+ failResponse -> failureCount.incrementAndGet(),
+ exceptionConsumer -> exceptionCount.incrementAndGet());
+
+ assertThat(failureCount).hasValue(0);
+ assertThat(exceptionCount).hasValue(1);
+ }
+ }
+
@Nested
class OnFailure {
@@ -584,6 +764,43 @@ void shouldCallFailConsumer_ForUnsuccessfulResponse() {
}
}
+ @Nested
+ class OnFailureThrow_UsingResponseSupplier {
+
+ @Test
+ void shouldUseResponseFromSupplier() {
+ var response = newMockResponseWithStatus(Response.Status.CREATED);
+ Supplier supplier = () -> response;
+
+ KiwiResponses.onFailureThrow(supplier, failResponse -> {
+ failureCount.incrementAndGet();
+ return new CustomKiwiResponsesRuntimeException(response);
+ });
+
+ assertThat(failureCount).hasValue(0);
+
+ verify(response).close();
+ }
+
+ @Test
+ void shouldRethrowSupplierException_WhenResponseSupplierThrowsException() {
+ Supplier supplier = () -> {
+ throw new ProcessingException("request processing failed");
+ };
+
+ var thrown = catchThrowable(() ->
+ KiwiResponses.onFailureThrow(supplier, failResponse -> {
+ failureCount.incrementAndGet();
+ return new RuntimeException("should not be called");
+ }));
+
+ assertThat(thrown).isExactlyInstanceOf(ProcessingException.class)
+ .hasMessage("request processing failed");
+
+ assertThat(failureCount).hasValue(0);
+ }
+ }
+
@Nested
class OnFailureThrow {
@@ -602,7 +819,7 @@ void shouldNotThrow_ForSuccessfulResponse() {
}
@Test
- void shouldThrow_ForSuccessfulResponse() {
+ void shouldThrow_ForUnsuccessfulResponse() {
var response = newMockResponseWithStatus(Response.Status.EXPECTATION_FAILED);
var thrown = catchThrowable(() ->
@@ -619,6 +836,47 @@ void shouldThrow_ForSuccessfulResponse() {
}
}
+ @Nested
+ class OnSuccessWithResultOrFailure_UsingResponseSupplier {
+
+ @Test
+ void shouldUseResponseFromSupplier() {
+ var response = newMockResponseWithStatus(Response.Status.NO_CONTENT);
+ Supplier supplier = () -> response;
+
+ Optional count = KiwiResponses.onSuccessWithResultOrFailure(supplier,
+ successResponse -> successCount.incrementAndGet(),
+ failResponse -> failureCount.incrementAndGet(),
+ exceptionConsumer -> exceptionCount.incrementAndGet());
+
+ assertThat(count).hasValue(1);
+
+ assertThat(successCount).hasValue(1);
+ assertThat(failureCount).hasValue(0);
+ assertThat(exceptionCount).hasValue(0);
+
+ verify(response).close();
+ }
+
+ @Test
+ void shouldCallExceptionConsumer_WhenResponseSupplierThrowsException() {
+ Supplier supplier = () -> {
+ throw new ProcessingException("request processing failed");
+ };
+
+ Optional count = KiwiResponses.onSuccessWithResultOrFailure(supplier,
+ successResponse -> successCount.incrementAndGet(),
+ failResponse -> failureCount.incrementAndGet(),
+ exceptionConsumer -> exceptionCount.incrementAndGet());
+
+ assertThat(count).isEmpty();
+
+ assertThat(successCount).hasValue(0);
+ assertThat(failureCount).hasValue(0);
+ assertThat(exceptionCount).hasValue(1);
+ }
+ }
+
@Nested
class OnSuccessWithResultOrFailure {
@@ -655,6 +913,39 @@ void shouldReturnEmptyOptional_ForUnsuccessfulResult() {
}
}
+ @Nested
+ class OnSuccessOrFailureWithResult_UsingResponseSupplier {
+
+ @Test
+ void shouldUseResponseFromSupplier() {
+ var response = newMockResponseWithStatus(Response.Status.OK);
+ Supplier supplier = () -> response;
+
+ var result = KiwiResponses.onSuccessOrFailureWithResult(supplier,
+ successResponse -> 42,
+ failResponse -> -1,
+ supplierException -> 84);
+
+ assertThat(result).isEqualTo(42);
+
+ verify(response).close();
+ }
+
+ @Test
+ void shouldCallExceptionFunction_WhenResponseSupplierThrowsException() {
+ Supplier supplier = () -> {
+ throw new ProcessingException("request processing failed");
+ };
+
+ var result = KiwiResponses.onSuccessOrFailureWithResult(supplier,
+ successResponse -> 42,
+ failResponse -> -1,
+ supplierException -> 84);
+
+ assertThat(result).isEqualTo(84);
+ }
+ }
+
@Nested
class OnSuccessOrFailureWithResult {
@@ -685,6 +976,39 @@ void shouldCallFailFunction_ForUnsuccessfulResponse() {
}
}
+ @Nested
+ class OnSuccessWithResultOrFailureThrow_UsingResponseSupplier {
+
+ @Test
+ void shouldUseResponseFromSupplier() {
+ var response = newMockResponseWithStatus(Response.Status.PARTIAL_CONTENT);
+ Supplier supplier = () -> response;
+
+ var result = KiwiResponses.onSuccessWithResultOrFailureThrow(supplier,
+ successResponse -> 42,
+ CustomKiwiResponsesRuntimeException::new);
+
+ assertThat(result).isEqualTo(42);
+
+ verify(response).close();
+ }
+
+ @Test
+ void shouldRethrowSupplierException_WhenResponseSupplierThrowsException() {
+ Supplier supplier = () -> {
+ throw new ProcessingException("request processing failed");
+ };
+
+ var thrown = catchThrowable(() ->
+ KiwiResponses.onSuccessWithResultOrFailureThrow(supplier,
+ successResponse -> 42,
+ CustomKiwiResponsesRuntimeException::new));
+
+ assertThat(thrown).isExactlyInstanceOf(ProcessingException.class)
+ .hasMessage("request processing failed");
+ }
+ }
+
@Nested
class OnSuccessWithResultOrFailureThrow {
@@ -717,6 +1041,39 @@ void shouldThrow_ForUnsuccessfulResponse() {
}
}
+ @Nested
+ class Accept_UsingResponseSupplier {
+
+ @Test
+ void shouldUseResponseFromSupplier() {
+ var response = newMockResponseWithStatus(Response.Status.UNSUPPORTED_MEDIA_TYPE);
+ Supplier supplier = () -> response;
+
+ KiwiResponses.accept(supplier,
+ anyResponse -> successCount.incrementAndGet(),
+ supplierException -> exceptionCount.incrementAndGet());
+
+ assertThat(successCount).hasValue(1);
+ assertThat(exceptionCount).hasValue(0);
+
+ verify(response).close();
+ }
+
+ @Test
+ void shouldCallExceptionConsumer_WhenResponseSupplierThrowsException() {
+ Supplier supplier = () -> {
+ throw new ProcessingException("request processing failed");
+ };
+
+ KiwiResponses.accept(supplier,
+ anyResponse -> successCount.incrementAndGet(),
+ supplierException -> exceptionCount.incrementAndGet());
+
+ assertThat(successCount).hasValue(0);
+ assertThat(exceptionCount).hasValue(1);
+ }
+ }
+
@Nested
class Accept {
@@ -743,6 +1100,37 @@ private void assertAccepts(Response response) {
}
}
+ @Nested
+ class Apply_UsingResponseSupplier {
+
+ @Test
+ void shouldUseResponseFromSupplier() {
+ var response = newMockResponseWithStatus(Response.Status.CREATED);
+ Supplier supplier = () -> response;
+
+ var result = KiwiResponses.apply(supplier,
+ anyResponse -> 42,
+ supplierException -> 24);
+
+ assertThat(result).isEqualTo(42);
+
+ verify(response).close();
+ }
+
+ @Test
+ void shouldCallExceptionFunction_WhenResponseSupplierThrowsException() {
+ Supplier supplier = () -> {
+ throw new ProcessingException("request processing failed");
+ };
+
+ var result = KiwiResponses.apply(supplier,
+ anyResponse -> 42,
+ supplierException -> 24);
+
+ assertThat(result).isEqualTo(24);
+ }
+ }
+
@Nested
class Apply {
@@ -769,6 +1157,129 @@ private void assertApplies(Response response) {
}
}
+ @Nested
+ class WebCallResultRecord {
+
+ @Test
+ void shouldConstructWithResponse() {
+ var response = Response.accepted().build();
+
+ var result = KiwiResponses.WebCallResult.ofResponse(response);
+
+ assertThat(result.hasResponse()).isTrue();
+ assertThat(result.response()).isSameAs(response);
+ assertThat(result.error()).isNull();
+ }
+
+ @Test
+ void shouldConstructWithError() {
+ var error = new ProcessingException("something failed");
+
+ var result = KiwiResponses.WebCallResult.ofError(error);
+
+ assertThat(result.hasResponse()).isFalse();
+ assertThat(result.response()).isNull();
+ assertThat(result.error()).isSameAs(error);
+ }
+
+ @Test
+ void shouldThrowIllegalArgument_WhenResponseAndError_AreBothNull() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> new KiwiResponses.WebCallResult(null, null));
+ }
+
+ @Test
+ void shouldThrowIllegalArgument_WhenResponseAndError_AreBothNonNull() {
+ var error = new ProcessingException("something failed");
+ var response = Response.serverError().build();
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> new KiwiResponses.WebCallResult(error, response));
+ }
+ }
+
+ @Nested
+ class GetResponseFromSupplier {
+
+ @ClearBoxTest
+ void shouldRequireNonNullResponse() {
+ Supplier responseSupplier = () -> null;
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> KiwiResponses.getResponse(responseSupplier))
+ .withMessage("Response returned by Supplier must not be null");
+ }
+
+ @ClearBoxTest
+ void shouldReturnResponseFromSupplier() {
+ var response = Response.accepted().build();
+ Supplier responseSupplier = () -> response;
+
+ var webCallResult = KiwiResponses.getResponse(responseSupplier);
+
+ assertAll(
+ () -> assertThat(webCallResult.hasResponse()).isTrue(),
+ () -> assertThat(webCallResult.response()).isSameAs(response),
+ () -> assertThat(webCallResult.error()).isNull()
+ );
+ }
+
+ @ClearBoxTest
+ void shouldReturnRuntimeExceptionFromSupplier() {
+ var processingException = new ProcessingException("request processing failed");
+ Supplier responseSupplier = () -> {
+ throw processingException;
+ };
+
+ var webCallResult = KiwiResponses.getResponse(responseSupplier);
+
+ assertAll(
+ () -> assertThat(webCallResult.hasResponse()).isFalse(),
+ () -> assertThat(webCallResult.response()).isNull(),
+ () -> assertThat(webCallResult.error()).isSameAs(processingException)
+ );
+ }
+
+ @Nested
+ class LogResponseSupplierException {
+
+ private Logger logger;
+ private RuntimeException error;
+
+ @BeforeEach
+ void setUp() {
+ logger = mock(Logger.class);
+ error = new ProcessingException("error processing");
+ }
+
+ @ClearBoxTest
+ void shouldLogAtTraceWhenTraceLevelEnabled() {
+ when(logger.isTraceEnabled()).thenReturn(true);
+
+ KiwiResponses.logResponseSupplierException(logger, error);
+
+ verify(logger).isTraceEnabled();
+ verify(logger).trace("Response Supplier threw an exception", error);
+ verifyNoMoreInteractions(logger);
+ }
+
+ @ClearBoxTest
+ void shouldLogAtWarnWhenTraceLevelNotEnabled() {
+ when(logger.isTraceEnabled()).thenReturn(false);
+
+ KiwiResponses.logResponseSupplierException(logger, error);
+
+ verify(logger).isTraceEnabled();
+ verify(logger).warn(
+ "Response Supplier unexpectedly threw: {}: {} (enable TRACE level to see stack trace)",
+ ProcessingException.class.getName(),
+ "error processing"
+ );
+ verifyNoMoreInteractions(logger);
+ }
+ }
+ }
+
private static class CustomKiwiResponsesRuntimeException extends RuntimeException {
CustomKiwiResponsesRuntimeException(Response response) {
super("Kiwi received failed response with status: " + response.getStatus());