From d6dcd16fec0bcacafaed9751e963c7de6fc18634 Mon Sep 17 00:00:00 2001 From: A Anand Date: Fri, 7 Nov 2025 21:30:53 +0530 Subject: [PATCH 1/3] feature: #98 | Ability to recover from specific exceptions --- .../zeplinko/commons/lang/ext/core/Try.java | 267 +++++++- .../commons/lang/ext/core/TryTest.java | 622 +++++++++++++++++- 2 files changed, 885 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java b/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java index 4786fdc..f3a2b40 100644 --- a/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java +++ b/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java @@ -2,6 +2,7 @@ import jakarta.annotation.Nonnull; +import java.util.Arrays; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Callable; @@ -66,9 +67,8 @@ * instance inherently thread-safe as long as the wrapped value is itself * thread-safe. * - * @author Shivam Nagpal - * * @param the type of the successful value + * @author Shivam Nagpal */ public class Try extends AbstractOutcome { @@ -189,7 +189,7 @@ public Try compose( ) { Objects.requireNonNull(successMapper); Objects.requireNonNull(failureMapper); - return this.isFailure() ? failureMapper.apply(this.getError()) : successMapper.apply(this.getData()); + return isSuccess() ? successMapper.apply(getData()) : failureMapper.apply(getError()); } /** @@ -219,6 +219,130 @@ public Try recover(@Nonnull Function> map return compose(Try::success, mapper); } + /** + * Attempts to recover from a failure if the error matches the specified + * exception type. + *

+ * If this Try is successful, returns it unchanged. If this Try contains an + * error that is NOT an instance of the specified exception type, returns it + * unchanged. If this Try contains an error that IS an instance of the specified + * exception type, applies the failure mapper to attempt recovery. + *

+ * + *

+ * Example usage: + *

+ * + *
+     * {
+     *     @code
+     *     Try<String> result = Try.of(() -> riskyOperation())
+     *             .recover(IOException.class, ex -> Try.success("default value"))
+     *             .recover(TimeoutException.class, ex -> Try.failure(new CustomException(ex)));
+     * }
+     * 
+ * + * @param the type of exception to recover from + * @param exceptionType the class of the exception type to match against + * @param failureMapper function to apply to the exception if it matches the + * specified type; should return a new Try representing the + * recovery attempt + * @return this Try if successful or if the error doesn't match the exception + * type; otherwise the result of applying the failureMapper to the error + * @throws NullPointerException if exceptionType or failureMapper is null + * @see #recover(Function) + * @see #recover(Function, Class[]) + */ + public Try recover( + Class exceptionType, + Function> failureMapper + ) { + Objects.requireNonNull(exceptionType); + Objects.requireNonNull(failureMapper); + if (isSuccess() || !exceptionType.isInstance(getError())) { + return this; + } + return failureMapper.apply(getError()); + } + + /** + * Attempts to recover from a failure if the error matches any of the specified + * exception types. + *

+ * This method provides a convenient way to recover from multiple exception + * types using a single failure mapper. If this Try is successful, it returns + * unchanged. If this Try contains an error that does NOT match any of the + * specified exception types, it returns unchanged. If this Try contains an + * error that matches at least one of the specified exception types, the failure + * mapper is applied to attempt recovery. + *

+ * + *

+ * Null elements in the varargs array are automatically filtered out and ignored + * during exception type matching. + *

+ * + *

+ * Example usage: + *

+ * + *
+     * {
+     *     @code
+     *     // Recover from multiple I/O-related exceptions with a single handler
+     *     Try<String> result = Try.to(() -> readFile())
+     *             .recover(
+     *                     ex -> Try.success("default content"),
+     *                     IOException.class,
+     *                     FileNotFoundException.class,
+     *                     SocketTimeoutException.class
+     *             );
+     *
+     *     // Chain multiple recovery strategies for different exception groups
+     *     Try<Data> processed = Try.to(() -> processData())
+     *             .recover(
+     *                     ex -> Try.success(Data.empty()),
+     *                     IOException.class,
+     *                     TimeoutException.class
+     *             )
+     *             .recover(
+     *                     ex -> Try.failure(new ProcessingException(ex)),
+     *                     ValidationException.class,
+     *                     ParseException.class
+     *             );
+     * }
+     * 
+ * + * @param the type bound for exception types being recovered from + * @param failureMapper function to apply to the exception if it matches any of + * the specified types; should return a new Try + * representing the recovery attempt + * @param exceptionTypes varargs array of exception types to match against; null + * elements are ignored + * @return this Try if successful or if the error doesn't match any exception + * type; otherwise the result of applying the failureMapper to the error + * @throws NullPointerException if failureMapper or exceptionTypes array itself + * is null + * @see #recover(Class, Function) + * @see #recover(Function) + */ + @SafeVarargs + public final Try recover( + Function> failureMapper, + Class... exceptionTypes + ) { + Objects.requireNonNull(failureMapper); + Objects.requireNonNull(exceptionTypes); + if ( + isSuccess() || Arrays.stream(exceptionTypes) + .filter(Objects::nonNull) + .noneMatch(exceptionType -> exceptionType.isInstance(getError())) + ) { + return this; + } + return failureMapper.apply(getError()); + } + /** * Transforms this {@code Try} into another {@code Try} using the provided * transformation function. @@ -281,6 +405,143 @@ public Try onFailure(Consumer errorConsumer) { return onHandle(null, errorConsumer); } + /** + * Executes the provided consumer if this {@code Try} is a failure and the error + * matches the specified exception type. + *

+ * If this Try is successful, nothing happens, and this Try is returned + * unchanged. If this Try contains an error that is NOT an instance of the + * specified exception type, nothing happens, and this Try is returned + * unchanged. If this Try contains an error that IS an instance of the specified + * exception type, the error consumer is executed. + *

+ * + *

+ * This method is useful for handling specific exception types differently, such + * as logging at different levels, recording metrics, or performing + * type-specific cleanup operations. + *

+ * + *

+ * Example usage: + *

+ * + *
+     * {
+     *     @code
+     *     // Log different exception types at different levels
+     *     Try<Data> result = Try.to(() -> fetchData())
+     *             .onFailure(IOException.class, ex -> logger.error("I/O error: {}", ex.getMessage()))
+     *             .onFailure(TimeoutException.class, ex -> logger.warn("Timeout: {}", ex.getMessage()))
+     *             .onFailure(ex -> logger.error("Unexpected error", ex));
+     *
+     *     // Record type-specific metrics
+     *     Try<Response> apiResult = Try.to(() -> callApi())
+     *             .onFailure(TimeoutException.class, ex -> metrics.increment("api.timeout"))
+     *             .onFailure(IOException.class, ex -> metrics.increment("api.network_error"));
+     * }
+     * 
+ * + * @param the type of exception to handle + * @param exceptionType the class of the exception type to match against + * @param errorConsumer consumer to execute if the error matches the specified + * type + * @return this Try instance for method chaining + * @throws NullPointerException if exceptionType or errorConsumer is null + * @see #onFailure(Consumer) + * @see #onFailure(Consumer, Class[]) + * @see #recover(Class, Function) + */ + public Try onFailure( + Class exceptionType, + Consumer errorConsumer + ) { + Objects.requireNonNull(exceptionType); + Objects.requireNonNull(errorConsumer); + if (isFailure() && exceptionType.isInstance(getError())) { + errorConsumer.accept(getError()); + } + return this; + } + + /** + * Executes the provided consumer if this {@code Try} is a failure and the error + * matches any of the specified exception types. + *

+ * This method provides a convenient way to handle multiple exception types + * using a single error consumer. If this Try is successful, nothing happens, + * and this Try is returned unchanged. If this Try contains an error that does + * NOT match any of the specified exception types, nothing happens, and this Try + * is returned unchanged. If this Try contains an error that matches at least + * one of the specified exception types, the error consumer is executed. + *

+ * + *

+ * Null elements in the varargs array are automatically filtered out and ignored + * during exception type matching. + *

+ * + *

+ * Example usage: + *

+ * + *
+     * {
+     *     @code
+     *     // Handle multiple I/O-related exceptions with a single handler
+     *     Try<String> result = Try.to(() -> readFile())
+     *             .onFailure(
+     *                     ex -> logger.error("I/O operation failed: {}", ex.getMessage()),
+     *                     IOException.class,
+     *                     FileNotFoundException.class,
+     *                     SocketTimeoutException.class
+     *             );
+     *
+     *     // Combine different error handling strategies
+     *     Try<Data> processed = Try.to(() -> processData())
+     *             .onFailure(
+     *                     ex -> metrics.increment("errors.io"),
+     *                     IOException.class,
+     *                     TimeoutException.class
+     *             )
+     *             .onFailure(
+     *                     ex -> metrics.increment("errors.validation"),
+     *                     ValidationException.class,
+     *                     ParseException.class
+     *             )
+     *             .onFailure(ex -> metrics.increment("errors.other"));
+     * }
+     * 
+ * + * @param the type bound for exception types being handled + * @param errorConsumer consumer to execute if the error matches any of the + * specified types + * @param exceptionTypes varargs array of exception types to match against; null + * elements are ignored + * @return this Try instance for method chaining + * @throws NullPointerException if errorConsumer or exceptionTypes array itself + * is null + * @see #onFailure(Consumer) + * @see #onFailure(Class, Consumer) + * @see #recover(Function, Class[]) + */ + @SafeVarargs + public final Try onFailure( + Consumer errorConsumer, + Class... exceptionTypes + ) { + Objects.requireNonNull(errorConsumer); + Objects.requireNonNull(exceptionTypes); + if ( + isFailure() && Arrays.stream(exceptionTypes) + .filter(Objects::nonNull) + .anyMatch(exceptionType -> exceptionType.isInstance(getError())) + ) { + errorConsumer.accept(getError()); + } + return this; + } + /** * Converts this {@code Try} into a {@code Result} object. * diff --git a/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java b/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java index e10d61b..24ad65c 100644 --- a/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java +++ b/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java @@ -320,7 +320,6 @@ void test_givenFailure_whenFlatMapIsCalled_thenSameFailureIsReturned() { @Test void test_givenNullMapper_whenRecoverIsCalled_thenExceptionIsThrown() { Try failure = Try.failure(new RuntimeException("error")); - @SuppressWarnings("DataFlowIssue") NullPointerException nullPointerException = assertThrows( NullPointerException.class, @@ -348,6 +347,67 @@ void test_givenSuccess_whenRecoverIsCalled_thenSameSuccessIsReturned() { assertEquals("data", result.getData()); } + @Test + void test_givenNullExceptionType_whenRecoverWithExceptionTypeIsCalled_thenExceptionIsThrown() { + Try failure = Try.failure(new RuntimeException("error")); + @SuppressWarnings("DataFlowIssue") + NullPointerException nullPointerException = assertThrows( + NullPointerException.class, + () -> failure.recover(null, e -> Try.success(0)) + ); + assertNotNull(nullPointerException); + } + + @Test + void test_givenSuccess_whenRecoverWithExceptionTypeIsCalled_thenSameSuccessIsReturned() { + Try success = Try.success("data"); + Try result = success.recover(RuntimeException.class, e -> Try.success("recovered")); + + assertTrue(result.isSuccess()); + assertEquals("data", result.getData()); + } + + @Test + void test_givenFailureWithNonMatchingException_whenRecoverWithExceptionTypeIsCalled_thenSameFailureIsReturned() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + Try result = failure.recover(IllegalStateException.class, e -> Try.success("recovered")); + + assertTrue(result.isFailure()); + assertSame(error, result.getError()); + } + + @Test + void test_givenFailureWithMatchingException_whenRecoverWithExceptionTypeIsCalled_thenRecoveredTryIsReturned() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + Try result = failure.recover(IllegalArgumentException.class, e -> Try.success("recovered")); + + assertTrue(result.isSuccess()); + assertEquals("recovered", result.getData()); + } + + @Test + void test_givenFailureWithMatchingExceptionSubclass_whenRecoverWithExceptionTypeIsCalled_thenRecoveredTryIsReturned() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + Try result = failure.recover(RuntimeException.class, e -> Try.success("recovered")); + + assertTrue(result.isSuccess()); + assertEquals("recovered", result.getData()); + } + + @Test + void test_givenFailureWithMatchingException_whenRecoverWithExceptionTypeReturnsFailure_thenNewFailureIsReturned() { + Exception originalError = new IllegalArgumentException("original"); + Exception newError = new IllegalStateException("new error"); + Try failure = Try.failure(originalError); + Try result = failure.recover(IllegalArgumentException.class, e -> Try.failure(newError)); + + assertTrue(result.isFailure()); + assertSame(newError, result.getError()); + } + @Test void test_givenNullMapper_whenTransformIsCalled_thenExceptionIsThrown() { Try success = Try.success(10); @@ -449,6 +509,344 @@ void test_givenSuccess_whenOnFailureIsCalled_thenConsumerIsExecuted() { assertEquals(0, consumed.length()); } + // ========== Tests for single exception type onFailure method ========== + + @Test + void test_givenNullExceptionType_whenOnFailureWithExceptionTypeIsCalled_thenExceptionIsThrown() { + Try failure = Try.failure(new RuntimeException("error")); + @SuppressWarnings("DataFlowIssue") + NullPointerException nullPointerException = assertThrows( + NullPointerException.class, + () -> failure.onFailure(null, e -> { + }) + ); + assertNotNull(nullPointerException); + } + + @Test + void test_givenNullConsumer_whenOnFailureWithExceptionTypeIsCalled_thenExceptionIsThrown() { + Try failure = Try.failure(new RuntimeException("error")); + @SuppressWarnings("DataFlowIssue") + NullPointerException nullPointerException = assertThrows( + NullPointerException.class, + () -> failure.onFailure(RuntimeException.class, null) + ); + assertNotNull(nullPointerException); + } + + @Test + void test_givenSuccess_whenOnFailureWithExceptionTypeIsCalled_thenConsumerIsNotExecuted() { + Try success = Try.success("data"); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = success.onFailure(RuntimeException.class, e -> consumed.append(e.getMessage())); + + assertSame(success, returnedTry); + assertEquals(0, consumed.length()); + } + + @Test + void test_givenFailureWithNonMatchingException_whenOnFailureWithExceptionTypeIsCalled_thenConsumerIsNotExecuted() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure.onFailure(IllegalStateException.class, e -> consumed.append(e.getMessage())); + + assertSame(failure, returnedTry); + assertEquals(0, consumed.length()); + } + + @Test + void test_givenFailureWithMatchingException_whenOnFailureWithExceptionTypeIsCalled_thenConsumerIsExecuted() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure + .onFailure(IllegalArgumentException.class, e -> consumed.append(e.getMessage())); + + assertSame(failure, returnedTry); + assertEquals("error", consumed.toString()); + } + + @Test + void test_givenFailureWithMatchingExceptionSubclass_whenOnFailureWithExceptionTypeIsCalled_thenConsumerIsExecuted() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure.onFailure(RuntimeException.class, e -> consumed.append(e.getMessage())); + + assertSame(failure, returnedTry); + assertEquals("error", consumed.toString()); + } + + @Test + void test_givenChainedOnFailureCallsWithDifferentTypes_whenFailureMatches_thenOnlyMatchingConsumerIsExecuted() { + Exception error = new IllegalArgumentException("validation error"); + Try failure = Try.failure(error); + List log = new ArrayList<>(); + + Try result = failure + .onFailure(IllegalStateException.class, e -> log.add("state error")) + .onFailure(IllegalArgumentException.class, e -> log.add("argument error")) + .onFailure(NullPointerException.class, e -> log.add("null error")); + + assertSame(failure, result); + assertEquals(1, log.size()); + assertEquals("argument error", log.get(0)); + } + + @Test + void test_givenChainedOnFailureWithCatchAll_whenFailureOccurs_thenAllMatchingConsumersAreExecuted() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + List log = new ArrayList<>(); + + Try result = failure + .onFailure(IllegalArgumentException.class, e -> log.add("specific")) + .onFailure(RuntimeException.class, e -> log.add("general")) + .onFailure(e -> log.add("catch-all")); + + assertSame(failure, result); + assertEquals(3, log.size()); + assertEquals("specific", log.get(0)); + assertEquals("general", log.get(1)); + assertEquals("catch-all", log.get(2)); + } + + // ========== Tests for varargs exception types onFailure method ========== + + @Test + void test_givenNullConsumer_whenOnFailureWithVarargsIsCalled_thenExceptionIsThrown() { + Try failure = Try.failure(new RuntimeException("error")); + @SuppressWarnings("DataFlowIssue") + NullPointerException nullPointerException = assertThrows( + NullPointerException.class, + () -> failure.onFailure(null, RuntimeException.class, IllegalArgumentException.class) + ); + assertNotNull(nullPointerException); + } + + @Test + void test_givenNullExceptionTypesArray_whenOnFailureWithVarargsIsCalled_thenExceptionIsThrown() { + Try failure = Try.failure(new RuntimeException("error")); + @SuppressWarnings({ "DataFlowIssue", "unchecked" }) + NullPointerException nullPointerException = assertThrows( + NullPointerException.class, + () -> failure.onFailure(e -> { + }, (Class[]) null) + ); + assertNotNull(nullPointerException); + } + + @Test + void test_givenSuccess_whenOnFailureWithVarargsIsCalled_thenConsumerIsNotExecuted() { + Try success = Try.success("data"); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = success.onFailure( + e -> consumed.append(e.getMessage()), + RuntimeException.class, + IllegalArgumentException.class + ); + + assertSame(success, returnedTry); + assertEquals(0, consumed.length()); + } + + @Test + void test_givenFailureWithNoMatchingException_whenOnFailureWithVarargsIsCalled_thenConsumerIsNotExecuted() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure.onFailure( + e -> consumed.append(e.getMessage()), + IllegalStateException.class, + NullPointerException.class + ); + + assertSame(failure, returnedTry); + assertEquals(0, consumed.length()); + } + + @Test + void test_givenFailureWithEmptyVarargsArray_whenOnFailureWithVarargsIsCalled_thenConsumerIsNotExecuted() { + Exception error = new RuntimeException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure.onFailure( + e -> consumed.append(e.getMessage()), + new Class[0] + ); + + assertSame(failure, returnedTry); + assertEquals(0, consumed.length()); + } + + @Test + void test_givenFailureWithSingleMatchingException_whenOnFailureWithVarargsIsCalled_thenConsumerIsExecuted() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure.onFailure( + e -> consumed.append(e.getMessage()), + IllegalArgumentException.class + ); + + assertSame(failure, returnedTry); + assertEquals("error", consumed.toString()); + } + + @Test + void test_givenFailureWithFirstMatchingException_whenOnFailureWithVarargsIsCalled_thenConsumerIsExecuted() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure.onFailure( + e -> consumed.append(e.getMessage()), + IllegalArgumentException.class, + IllegalStateException.class, + NullPointerException.class + ); + + assertSame(failure, returnedTry); + assertEquals("error", consumed.toString()); + } + + @Test + void test_givenFailureWithSecondMatchingException_whenOnFailureWithVarargsIsCalled_thenConsumerIsExecuted() { + Exception error = new IllegalStateException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure.onFailure( + e -> consumed.append(e.getMessage()), + IllegalArgumentException.class, + IllegalStateException.class, + NullPointerException.class + ); + + assertSame(failure, returnedTry); + assertEquals("error", consumed.toString()); + } + + @Test + void test_givenFailureWithLastMatchingException_whenOnFailureWithVarargsIsCalled_thenConsumerIsExecuted() { + Exception error = new NullPointerException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure.onFailure( + e -> consumed.append(e.getMessage()), + IllegalArgumentException.class, + IllegalStateException.class, + NullPointerException.class + ); + + assertSame(failure, returnedTry); + assertEquals("error", consumed.toString()); + } + + @Test + void test_givenFailureWithMatchingExceptionSubclass_whenOnFailureWithVarargsIsCalled_thenConsumerIsExecuted() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure.onFailure( + e -> consumed.append(e.getMessage()), + RuntimeException.class, + Exception.class + ); + + assertSame(failure, returnedTry); + assertEquals("error", consumed.toString()); + } + + @Test + void test_givenFailureAndNullExceptionTypesInVarargs_whenOnFailureWithVarargsIsCalled_thenNullsAreIgnored() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + @SuppressWarnings("unchecked") + Try returnedTry = failure.onFailure( + e -> consumed.append(e.getMessage()), + null, + IllegalArgumentException.class, + null + ); + + assertSame(failure, returnedTry); + assertEquals("error", consumed.toString()); + } + + @Test + void test_givenFailureAndAllNullExceptionTypesInVarargs_whenOnFailureWithVarargsIsCalled_thenConsumerIsNotExecuted() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + @SuppressWarnings("unchecked") + Try returnedTry = failure.onFailure( + e -> consumed.append(e.getMessage()), + null, + null + ); + + assertSame(failure, returnedTry); + assertEquals(0, consumed.length()); + } + + @Test + void test_givenMultipleIOExceptions_whenOnFailureWithVarargsIsCalled_thenSingleHandlerIsExecuted() { + Exception error = new java.io.FileNotFoundException("file not found"); + Try failure = Try.failure(error); + List log = new ArrayList<>(); + Try returnedTry = failure.onFailure( + e -> log.add("I/O error: " + e.getMessage()), + java.io.IOException.class, + java.io.FileNotFoundException.class, + java.net.SocketTimeoutException.class + ); + + assertSame(failure, returnedTry); + assertEquals(1, log.size()); + assertEquals("I/O error: file not found", log.get(0)); + } + + @Test + void test_givenChainedOnFailureCallsWithVarargs_whenDifferentExceptionGroups_thenCorrectHandlerIsExecuted() { + Exception error = new IllegalArgumentException("validation error"); + Try failure = Try.failure(error); + List log = new ArrayList<>(); + + Try result = failure + .onFailure( + e -> log.add("io"), + java.io.IOException.class, + java.util.concurrent.TimeoutException.class + ) + .onFailure( + e -> log.add("validation"), + IllegalArgumentException.class, + IllegalStateException.class + ); + + assertSame(failure, result); + assertEquals(1, log.size()); + assertEquals("validation", log.get(0)); + } + + @Test + void test_givenRealWorldLoggingScenario_whenOnFailureWithMultipleTypes_thenCorrectLoggingOccurs() { + Exception error = new java.io.IOException("network error"); + Try failure = Try.failure(error); + List logMessages = new ArrayList<>(); + + Try result = failure + .onFailure(java.io.IOException.class, e -> logMessages.add("ERROR: " + e.getMessage())) + .onFailure(java.util.concurrent.TimeoutException.class, e -> logMessages.add("WARN: " + e.getMessage())) + .onFailure(e -> logMessages.add("UNKNOWN: " + e.getMessage())); + + assertSame(failure, result); + assertEquals(2, logMessages.size()); + assertEquals("ERROR: network error", logMessages.get(0)); + assertEquals("UNKNOWN: network error", logMessages.get(1)); + } + @Test void test_givenSuccess_whenToResultIsCalled_thenResultIsReturned() { Try success = Try.success("data"); @@ -468,6 +866,228 @@ void test_givenFailure_whenToResultIsCalled_thenResultIsReturned() { assertSame(error, result.getError()); } + // ========== Tests for varargs recover method ========== + + @Test + void test_givenNullFailureMapper_whenRecoverWithVarargsIsCalled_thenExceptionIsThrown() { + Try failure = Try.failure(new RuntimeException("error")); + @SuppressWarnings("DataFlowIssue") + NullPointerException nullPointerException = assertThrows( + NullPointerException.class, + () -> failure.recover(null, RuntimeException.class, IllegalArgumentException.class) + ); + assertNotNull(nullPointerException); + } + + @Test + void test_givenNullExceptionTypesArray_whenRecoverWithVarargsIsCalled_thenExceptionIsThrown() { + Try failure = Try.failure(new RuntimeException("error")); + @SuppressWarnings({ "DataFlowIssue", "unchecked" }) + NullPointerException nullPointerException = assertThrows( + NullPointerException.class, + () -> failure.recover(e -> Try.success(0), (Class[]) null) + ); + assertNotNull(nullPointerException); + } + + @Test + void test_givenSuccess_whenRecoverWithVarargsIsCalled_thenSameSuccessIsReturned() { + Try success = Try.success("data"); + Try result = success.recover( + e -> Try.success("recovered"), + RuntimeException.class, + IllegalArgumentException.class + ); + + assertTrue(result.isSuccess()); + assertSame(success, result); + assertEquals("data", result.getData()); + } + + @Test + void test_givenFailureWithNoMatchingException_whenRecoverWithVarargsIsCalled_thenSameFailureIsReturned() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + Try result = failure.recover( + e -> Try.success("recovered"), + IllegalStateException.class, + NullPointerException.class + ); + + assertTrue(result.isFailure()); + assertSame(failure, result); + assertSame(error, result.getError()); + } + + @Test + void test_givenFailureWithEmptyVarargsArray_whenRecoverWithVarargsIsCalled_thenSameFailureIsReturned() { + Exception error = new RuntimeException("error"); + Try failure = Try.failure(error); + @SuppressWarnings("unchecked") + Try result = failure.recover( + e -> Try.success("recovered"), + new Class[0] // Explicitly pass empty array to call varargs version + ); + + assertTrue(result.isFailure()); + assertSame(failure, result); + assertSame(error, result.getError()); + } + + @Test + void test_givenFailureWithSingleMatchingException_whenRecoverWithVarargsIsCalled_thenRecoveredTryIsReturned() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + Try result = failure.recover( + e -> Try.success("recovered"), + IllegalArgumentException.class + ); + + assertTrue(result.isSuccess()); + assertEquals("recovered", result.getData()); + } + + @Test + void test_givenFailureWithFirstMatchingException_whenRecoverWithVarargsIsCalled_thenRecoveredTryIsReturned() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + Try result = failure.recover( + e -> Try.success("recovered"), + IllegalArgumentException.class, + IllegalStateException.class, + NullPointerException.class + ); + + assertTrue(result.isSuccess()); + assertEquals("recovered", result.getData()); + } + + @Test + void test_givenFailureWithSecondMatchingException_whenRecoverWithVarargsIsCalled_thenRecoveredTryIsReturned() { + Exception error = new IllegalStateException("error"); + Try failure = Try.failure(error); + Try result = failure.recover( + e -> Try.success("recovered"), + IllegalArgumentException.class, + IllegalStateException.class, + NullPointerException.class + ); + + assertTrue(result.isSuccess()); + assertEquals("recovered", result.getData()); + } + + @Test + void test_givenFailureWithLastMatchingException_whenRecoverWithVarargsIsCalled_thenRecoveredTryIsReturned() { + Exception error = new NullPointerException("error"); + Try failure = Try.failure(error); + Try result = failure.recover( + e -> Try.success("recovered"), + IllegalArgumentException.class, + IllegalStateException.class, + NullPointerException.class + ); + + assertTrue(result.isSuccess()); + assertEquals("recovered", result.getData()); + } + + @Test + void test_givenFailureWithMatchingExceptionSubclass_whenRecoverWithVarargsIsCalled_thenRecoveredTryIsReturned() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + Try result = failure.recover( + e -> Try.success("recovered"), + RuntimeException.class, + Exception.class + ); + + assertTrue(result.isSuccess()); + assertEquals("recovered", result.getData()); + } + + @Test + void test_givenFailureWithMatchingException_whenRecoverWithVarargsReturnsFailure_thenNewFailureIsReturned() { + Exception originalError = new IllegalArgumentException("original"); + Exception newError = new IllegalStateException("new error"); + Try failure = Try.failure(originalError); + Try result = failure.recover( + e -> Try.failure(newError), + IllegalArgumentException.class, + NullPointerException.class + ); + + assertTrue(result.isFailure()); + assertSame(newError, result.getError()); + } + + @Test + void test_givenFailureAndNullExceptionTypesInVarargs_whenRecoverWithVarargsIsCalled_thenNullsAreIgnored() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + @SuppressWarnings("unchecked") + Try result = failure.recover( + e -> Try.success("recovered"), + null, + IllegalArgumentException.class, + null + ); + + assertTrue(result.isSuccess()); + assertEquals("recovered", result.getData()); + } + + @Test + void test_givenFailureAndAllNullExceptionTypesInVarargs_whenRecoverWithVarargsIsCalled_thenSameFailureIsReturned() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + @SuppressWarnings("unchecked") + Try result = failure.recover( + e -> Try.success("recovered"), + null, + null + ); + + assertTrue(result.isFailure()); + assertSame(failure, result); + assertSame(error, result.getError()); + } + + @Test + void test_givenMultipleIOExceptions_whenRecoverWithVarargsIsCalled_thenSingleHandlerIsApplied() { + Exception error = new java.io.FileNotFoundException("file not found"); + Try failure = Try.failure(error); + Try result = failure.recover( + e -> Try.success("default content"), + java.io.IOException.class, + java.io.FileNotFoundException.class, + java.net.SocketTimeoutException.class + ); + + assertTrue(result.isSuccess()); + assertEquals("default content", result.getData()); + } + + @Test + void test_givenChainedRecoverCallsWithVarargs_whenDifferentExceptionGroups_thenCorrectHandlerIsApplied() { + Exception error = new IllegalArgumentException("validation error"); + Try failure = Try.failure(error); + Try result = failure + .recover( + e -> Try.success("io recovered"), + java.io.IOException.class, + java.util.concurrent.TimeoutException.class + ) + .recover( + e -> Try.success("validation recovered"), + IllegalArgumentException.class, + IllegalStateException.class + ); + + assertTrue(result.isSuccess()); + assertEquals("validation recovered", result.getData()); + } + @MethodSource("provideHashcodeCoverageCases") @ParameterizedTest void test_givenTry_whenHashcodeIsInvoked_thenRespectiveOutcomeIsReturned(Try tryObject, int expectedHashcode) { From d586ca4229d17eb09dfe5e1af086882fe8e2c9be Mon Sep 17 00:00:00 2001 From: Shivam Nagpal Date: Thu, 13 Nov 2025 14:24:17 +0530 Subject: [PATCH 2/3] fix: #98 | Enhance the Try conditional recover and onFailure methods to use the predicate based methods --- .../zeplinko/commons/lang/ext/core/Try.java | 149 ++++++++++++------ .../commons/lang/ext/core/TryTest.java | 128 +++++++++++---- 2 files changed, 201 insertions(+), 76 deletions(-) diff --git a/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java b/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java index f3a2b40..a1cd506 100644 --- a/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java +++ b/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java @@ -1,6 +1,7 @@ package org.zeplinko.commons.lang.ext.core; import jakarta.annotation.Nonnull; +import org.zeplinko.commons.lang.ext.annotations.Preview; import java.util.Arrays; import java.util.Objects; @@ -8,6 +9,7 @@ import java.util.concurrent.Callable; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; /** * An immutable container that captures the successful result of a @@ -189,7 +191,7 @@ public Try compose( ) { Objects.requireNonNull(successMapper); Objects.requireNonNull(failureMapper); - return isSuccess() ? successMapper.apply(getData()) : failureMapper.apply(getError()); + return this.isFailure() ? failureMapper.apply(this.getError()) : successMapper.apply(this.getData()); } /** @@ -219,6 +221,35 @@ public Try recover(@Nonnull Function> map return compose(Try::success, mapper); } + /** + * Attempts to recover from a failure if the error satisfies the predicated + *

+ * If this Try is successful, returns it unchanged. If this Try contains an + * error that is doesn't satisfy the predicate, returns it unchanged. If this + * Try contains an error that satisfies the predicate, applies the failure + * mapper to attempt recovery. + *

+ * + * @param predicate the predicate to test the error + * @param mapper function to apply to the exception if it matches the + * specified type; should return a new Try representing the + * recovery attempt + * @return this Try if successful or if the error doesn't match the exception + * type; otherwise the result of applying the failureMapper to the error + * @throws NullPointerException if predicate or failureMapper is null + */ + public Try recover( + Predicate predicate, + Function> mapper + ) { + Objects.requireNonNull(predicate); + Objects.requireNonNull(mapper); + if (this.isFailure() && predicate.test(this.getError())) { + return mapper.apply(this.getError()); + } + return compose(Try::success, Try::failure); + } + /** * Attempts to recover from a failure if the error matches the specified * exception type. @@ -242,9 +273,8 @@ public Try recover(@Nonnull Function> map * } * * - * @param the type of exception to recover from * @param exceptionType the class of the exception type to match against - * @param failureMapper function to apply to the exception if it matches the + * @param mapper function to apply to the exception if it matches the * specified type; should return a new Try representing the * recovery attempt * @return this Try if successful or if the error doesn't match the exception @@ -253,16 +283,13 @@ public Try recover(@Nonnull Function> map * @see #recover(Function) * @see #recover(Function, Class[]) */ - public Try recover( - Class exceptionType, - Function> failureMapper + public Try recover( + Class exceptionType, + Function> mapper ) { Objects.requireNonNull(exceptionType); - Objects.requireNonNull(failureMapper); - if (isSuccess() || !exceptionType.isInstance(getError())) { - return this; - } - return failureMapper.apply(getError()); + Objects.requireNonNull(mapper); + return recover(exceptionType::isInstance, mapper); } /** @@ -313,8 +340,7 @@ public Try recover( * } * * - * @param the type bound for exception types being recovered from - * @param failureMapper function to apply to the exception if it matches any of + * @param mapper function to apply to the exception if it matches any of * the specified types; should return a new Try * representing the recovery attempt * @param exceptionTypes varargs array of exception types to match against; null @@ -327,20 +353,19 @@ public Try recover( * @see #recover(Function) */ @SafeVarargs - public final Try recover( - Function> failureMapper, - Class... exceptionTypes + @Preview + public final Try recover( + Function> mapper, + Class... exceptionTypes ) { - Objects.requireNonNull(failureMapper); + Objects.requireNonNull(mapper); Objects.requireNonNull(exceptionTypes); - if ( - isSuccess() || Arrays.stream(exceptionTypes) - .filter(Objects::nonNull) - .noneMatch(exceptionType -> exceptionType.isInstance(getError())) - ) { - return this; - } - return failureMapper.apply(getError()); + return recover( + e -> Arrays.stream(exceptionTypes) + .filter(Objects::nonNull) + .anyMatch(exceptionType -> exceptionType.isInstance(e)), + mapper + ); } /** @@ -405,6 +430,43 @@ public Try onFailure(Consumer errorConsumer) { return onHandle(null, errorConsumer); } + /** + * Executes the provided consumer if this {@code Try} is a failure and the error + * satisfies the predicate. + *

+ * If this Try is successful, nothing happens, and this Try is returned + * unchanged. If this Try contains an error that is doesn't satisfy the + * predicate, nothing happens, and this Try is returned unchanged. If this Try + * contains an error that satisfies the predicate, the error consumer is + * executed. + *

+ * + *

+ * This method is useful for handling specific exception types differently, such + * as logging at different levels, recording metrics, or performing + * type-specific cleanup operations. + *

+ * + * @param predicate the class of the exception type to match against + * @param errorConsumer consumer to execute if the error matches the specified + * type + * @return this Try instance for method chaining + * @throws NullPointerException if predicate is null + * @see #onFailure(Consumer) + * @see #onFailure(Consumer, Class[]) + * @see #recover(Class, Function) + */ + public Try onFailure( + Predicate predicate, + Consumer errorConsumer + ) { + Objects.requireNonNull(predicate); + if (isFailure() && predicate.test(getError())) { + return onHandle(null, errorConsumer); + } + return this; + } + /** * Executes the provided consumer if this {@code Try} is a failure and the error * matches the specified exception type. @@ -442,26 +504,21 @@ public Try onFailure(Consumer errorConsumer) { * } * * - * @param the type of exception to handle * @param exceptionType the class of the exception type to match against * @param errorConsumer consumer to execute if the error matches the specified * type * @return this Try instance for method chaining - * @throws NullPointerException if exceptionType or errorConsumer is null + * @throws NullPointerException if exceptionType is null * @see #onFailure(Consumer) * @see #onFailure(Consumer, Class[]) * @see #recover(Class, Function) */ - public Try onFailure( - Class exceptionType, + public Try onFailure( + Class exceptionType, Consumer errorConsumer ) { Objects.requireNonNull(exceptionType); - Objects.requireNonNull(errorConsumer); - if (isFailure() && exceptionType.isInstance(getError())) { - errorConsumer.accept(getError()); - } - return this; + return onFailure(exceptionType::isInstance, errorConsumer); } /** @@ -513,33 +570,29 @@ public Try onFailure( * } * * - * @param the type bound for exception types being handled * @param errorConsumer consumer to execute if the error matches any of the * specified types * @param exceptionTypes varargs array of exception types to match against; null * elements are ignored * @return this Try instance for method chaining - * @throws NullPointerException if errorConsumer or exceptionTypes array itself - * is null + * @throws NullPointerException if exceptionTypes array itself is null * @see #onFailure(Consumer) * @see #onFailure(Class, Consumer) * @see #recover(Function, Class[]) */ @SafeVarargs - public final Try onFailure( + @Preview + public final Try onFailure( Consumer errorConsumer, - Class... exceptionTypes + Class... exceptionTypes ) { - Objects.requireNonNull(errorConsumer); Objects.requireNonNull(exceptionTypes); - if ( - isFailure() && Arrays.stream(exceptionTypes) - .filter(Objects::nonNull) - .anyMatch(exceptionType -> exceptionType.isInstance(getError())) - ) { - errorConsumer.accept(getError()); - } - return this; + return onFailure( + e -> Arrays.stream(exceptionTypes) + .filter(Objects::nonNull) + .anyMatch(exceptionType -> exceptionType.isInstance(e)), + errorConsumer + ); } /** diff --git a/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java b/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java index 24ad65c..89321ef 100644 --- a/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java +++ b/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java @@ -11,6 +11,7 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.Callable; +import java.util.function.Predicate; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; @@ -347,13 +348,64 @@ void test_givenSuccess_whenRecoverIsCalled_thenSameSuccessIsReturned() { assertEquals("data", result.getData()); } + @Test + void test_givenNullMapper_whenPredicateRecoverIsCalled_thenExceptionIsThrown() { + Try failure = Try.failure(new RuntimeException("error")); + @SuppressWarnings("DataFlowIssue") + NullPointerException nullPointerException = assertThrows( + NullPointerException.class, + () -> failure.recover((Predicate) IllegalArgumentException.class::isInstance, null) + ); + assertNotNull(nullPointerException); + } + + @Test + void test_givenNullPredicate_whenPredicateRecoverIsCalled_thenExceptionIsThrown() { + Try failure = Try.failure(new RuntimeException("error")); + @SuppressWarnings("DataFlowIssue") + NullPointerException nullPointerException = assertThrows( + NullPointerException.class, + () -> failure.recover((Predicate) null, e -> Try.success("ignored")) + ); + assertNotNull(nullPointerException); + } + + @Test + void test_givenSuccess_whenPredicateRecoverIsCalled_thenSameSuccessIsReturned() { + Try success = Try.success("data"); + Try result = success.recover(IllegalArgumentException.class::isInstance, e -> Try.success("ignored")); + + assertTrue(result.isSuccess()); + assertEquals("data", result.getData()); + } + + @Test + void test_givenFailureWhichDoesNotSatisfyThePredicate_whenPredicateRecoverIsCalled_thenSameTryIsReturned() { + Exception error = new RuntimeException("error"); + Try failure = Try.failure(error); + Try result = failure.recover(IllegalArgumentException.class::isInstance, e -> Try.success("recovered")); + + assertTrue(result.isFailure()); + assertSame(error, result.getError()); + } + + @Test + void test_givenFailureWhichSatisfiesThePredicate_whenPredicateRecoverIsCalled_thenSameTryIsReturned() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + Try result = failure.recover(IllegalArgumentException.class::isInstance, e -> Try.success("recovered")); + + assertTrue(result.isSuccess()); + assertEquals("recovered", result.getData()); + } + @Test void test_givenNullExceptionType_whenRecoverWithExceptionTypeIsCalled_thenExceptionIsThrown() { Try failure = Try.failure(new RuntimeException("error")); @SuppressWarnings("DataFlowIssue") NullPointerException nullPointerException = assertThrows( NullPointerException.class, - () -> failure.recover(null, e -> Try.success(0)) + () -> failure.recover((Class) null, e -> Try.success(0)) ); assertNotNull(nullPointerException); } @@ -500,7 +552,7 @@ void test_givenFailure_whenOnFailureIsCalled_thenConsumerIsExecuted() { } @Test - void test_givenSuccess_whenOnFailureIsCalled_thenConsumerIsExecuted() { + void test_givenSuccess_whenOnFailureIsCalled_thenConsumerIsNotExecuted() { Try success = Try.success("data"); StringBuilder consumed = new StringBuilder(); Try returnedTry = success.onFailure(e -> consumed.append(e.getMessage())); @@ -509,27 +561,62 @@ void test_givenSuccess_whenOnFailureIsCalled_thenConsumerIsExecuted() { assertEquals(0, consumed.length()); } - // ========== Tests for single exception type onFailure method ========== - @Test - void test_givenNullExceptionType_whenOnFailureWithExceptionTypeIsCalled_thenExceptionIsThrown() { - Try failure = Try.failure(new RuntimeException("error")); - @SuppressWarnings("DataFlowIssue") - NullPointerException nullPointerException = assertThrows( + void test_givenNullPredicate_whenPredicateOnFailureIsCalled_thenThrowsException() { + Try failure = Try.failure(new RuntimeException("error")); + StringBuilder consumed = new StringBuilder(); + Assertions.assertThrows( NullPointerException.class, - () -> failure.onFailure(null, e -> { - }) + () -> failure.onFailure((Predicate) null, e -> consumed.append(e.getMessage())) ); - assertNotNull(nullPointerException); + assertEquals(0, consumed.length()); + } + + @Test + void test_givenSuccess_whenPredicateOnFailureIsCalled_thenConsumerIsNotExecuted() { + Try success = Try.success("data"); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = success + .onFailure(IllegalArgumentException.class::isInstance, e -> consumed.append(e.getMessage())); + + assertSame(success, returnedTry); + assertEquals(0, consumed.length()); + } + + @Test + void test_givenFailureWhichDoesNotSatisfyPredicate_whenPredicateOnFailureIsCalled_thenConsumerIsNotExecuted() { + Exception error = new RuntimeException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure + .onFailure(IllegalArgumentException.class::isInstance, e -> consumed.append(e.getMessage())); + + assertSame(failure, returnedTry); + assertEquals(0, consumed.length()); } @Test - void test_givenNullConsumer_whenOnFailureWithExceptionTypeIsCalled_thenExceptionIsThrown() { + void test_givenFailureWhichSatisfiesPredicate_whenPredicateOnFailureIsCalled_thenConsumerIsExecuted() { + Exception error = new IllegalArgumentException("error"); + Try failure = Try.failure(error); + StringBuilder consumed = new StringBuilder(); + Try returnedTry = failure + .onFailure(IllegalArgumentException.class::isInstance, e -> consumed.append(e.getMessage())); + + assertSame(failure, returnedTry); + assertEquals("error", consumed.toString()); + } + + // ========== Tests for single exception type onFailure method ========== + + @Test + void test_givenNullExceptionType_whenOnFailureWithExceptionTypeIsCalled_thenExceptionIsThrown() { Try failure = Try.failure(new RuntimeException("error")); @SuppressWarnings("DataFlowIssue") NullPointerException nullPointerException = assertThrows( NullPointerException.class, - () -> failure.onFailure(RuntimeException.class, null) + () -> failure.onFailure((Class) null, e -> { + }) ); assertNotNull(nullPointerException); } @@ -614,17 +701,6 @@ void test_givenChainedOnFailureWithCatchAll_whenFailureOccurs_thenAllMatchingCon // ========== Tests for varargs exception types onFailure method ========== - @Test - void test_givenNullConsumer_whenOnFailureWithVarargsIsCalled_thenExceptionIsThrown() { - Try failure = Try.failure(new RuntimeException("error")); - @SuppressWarnings("DataFlowIssue") - NullPointerException nullPointerException = assertThrows( - NullPointerException.class, - () -> failure.onFailure(null, RuntimeException.class, IllegalArgumentException.class) - ); - assertNotNull(nullPointerException); - } - @Test void test_givenNullExceptionTypesArray_whenOnFailureWithVarargsIsCalled_thenExceptionIsThrown() { Try failure = Try.failure(new RuntimeException("error")); @@ -900,7 +976,6 @@ void test_givenSuccess_whenRecoverWithVarargsIsCalled_thenSameSuccessIsReturned( ); assertTrue(result.isSuccess()); - assertSame(success, result); assertEquals("data", result.getData()); } @@ -915,7 +990,6 @@ void test_givenFailureWithNoMatchingException_whenRecoverWithVarargsIsCalled_the ); assertTrue(result.isFailure()); - assertSame(failure, result); assertSame(error, result.getError()); } @@ -930,7 +1004,6 @@ void test_givenFailureWithEmptyVarargsArray_whenRecoverWithVarargsIsCalled_thenS ); assertTrue(result.isFailure()); - assertSame(failure, result); assertSame(error, result.getError()); } @@ -1049,7 +1122,6 @@ void test_givenFailureAndAllNullExceptionTypesInVarargs_whenRecoverWithVarargsIs ); assertTrue(result.isFailure()); - assertSame(failure, result); assertSame(error, result.getError()); } From 56453a188863ce6fb35e9e6c072e854f6b5b8de5 Mon Sep 17 00:00:00 2001 From: Shivam Nagpal Date: Fri, 14 Nov 2025 00:01:43 +0530 Subject: [PATCH 3/3] chore: #1 | Promote to snapshot version --- Makefile | 7 +++++++ pom.xml | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..71e6418 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: build test + +build: + @./mvnw clean spotless:apply package + +test: + @./mvnw test diff --git a/pom.xml b/pom.xml index b0dad8b..1d94d0c 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.zeplinko commons-lang-ext - 1.3.0 + 1.3.1-SNAPSHOT 8 @@ -42,7 +42,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.4.0 + 0.9.0 true false