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());