From 80f9b494619da245a9af4d01081d216e2d08c444 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 29 Jan 2024 13:13:04 -0600 Subject: [PATCH] Add ifPresentOrElse() API to Resolved - Deprecate Resolved#get - Add javadocs - Improve unit test coverage See #380 Add support for AUTO_CONSUME schema type See #380 --- .../springframework/pulsar/core/Resolved.java | 86 ++++++++++- .../core/DefaultTopicResolverTests.java | 6 +- .../pulsar/core/ResolvedTests.java | 135 +++++++++++++++--- 3 files changed, 199 insertions(+), 28 deletions(-) diff --git a/spring-pulsar/src/main/java/org/springframework/pulsar/core/Resolved.java b/spring-pulsar/src/main/java/org/springframework/pulsar/core/Resolved.java index 38d15cb3..134d9288 100644 --- a/spring-pulsar/src/main/java/org/springframework/pulsar/core/Resolved.java +++ b/spring-pulsar/src/main/java/org/springframework/pulsar/core/Resolved.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.lang.Nullable; * * @param the resolved type * @author Christophe Bornet + * @author Chris Bono */ public final class Resolved { @@ -41,28 +42,96 @@ public final class Resolved { this.exception = exception; } + /** + * Factory method to create a {@code Resolved} when resolution succeeds. + * @param value the non-{@code null} resolved value + * @param the type of the value + * @return a {@code Resolved} containing the resolved value + */ public static Resolved of(T value) { return new Resolved<>(value, null); } + /** + * Factory method to create a {@code Resolved} when resolution fails. + * @param reason the non-{@code null} reason the resolution failed + * @param the type of the value + * @return a {@code Resolved} containing an {@link IllegalArgumentException} with the + * reason for the failure + */ public static Resolved failed(String reason) { return new Resolved<>(null, new IllegalArgumentException(reason)); } - public static Resolved failed(RuntimeException e) { - return new Resolved<>(null, e); + /** + * Factory method to create a {@code Resolved} when resolution fails. + * @param reason the non-{@code null} reason the resolution failed + * @param the type of the value + * @return a {@code Resolved} containing the reason for the failure + */ + public static Resolved failed(RuntimeException reason) { + return new Resolved<>(null, reason); } + /** + * Gets the optional resolved value. + * @return an optional with the resolved value or empty if failed to resolve + * @deprecated Use {@link #value()} instead + */ + @Deprecated(since = "1.1.0", forRemoval = true) public Optional get() { + return value(); + } + + /** + * Gets the resolved value. + * @return an optional with the resolved value or empty if failed to resolve + */ + public Optional value() { return Optional.ofNullable(this.value); } + /** + * Gets the exception that may have occurred during resolution. + * @return an optional with the resolution exception or empty if no error occurred + */ + public Optional exception() { + return Optional.ofNullable(this.exception); + } + + /** + * Performs the given action with the resolved value if a value was resolved and no + * exception occurred. + * @param action the action to be performed + */ public void ifResolved(Consumer action) { - if (this.value != null) { + if (this.value != null && this.exception == null) { action.accept(this.value); } } + /** + * Performs the given action with the resolved value if a non-{@code null} value was + * resolved and no exception occurred. Otherwise, if an exception occurred then the + * provided error action is performed with the exception. + * @param action the action to be performed + * @param errorAction the error action to be performed + */ + public void ifResolvedOrElse(Consumer action, Consumer errorAction) { + if (this.value != null && this.exception == null) { + action.accept(this.value); + } + else if (this.exception != null) { + errorAction.accept(this.exception); + } + } + + /** + * Returns the resolved value if a value was resolved and no exception occurred, + * otherwise throws the resolution exception back to the caller. + * @return the resolved value if a value was resolved and no exception occurred + * @throws RuntimeException if an exception occurred during resolution + */ public T orElseThrow() { if (this.value == null && this.exception != null) { throw this.exception; @@ -70,6 +139,15 @@ public final class Resolved { return this.value; } + /** + * Returns the resolved value if a value was resolved and no exception occurred, + * otherwise wraps the resolution exception with the provided error message and throws + * back to the caller. + * @param wrappingErrorMessage additional context to add to the wrapped exception + * @return the resolved value if a value was resolved and no exception occurred + * @throws RuntimeException wrapping the resolution exception if an exception occurred + * during resolution + */ public T orElseThrow(Supplier wrappingErrorMessage) { if (this.value == null && this.exception != null) { throw new RuntimeException(wrappingErrorMessage.get(), this.exception); diff --git a/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java b/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java index b57eea1a..937aa829 100644 --- a/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java +++ b/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java @@ -58,7 +58,7 @@ class DefaultTopicResolverTests { @MethodSource("resolveNoMessageInfoProvider") void resolveNoMessageInfo(String testName, @Nullable String userTopic, @Nullable String defaultTopic, @Nullable String expectedTopic) { - assertThat(resolver.resolveTopic(userTopic, () -> defaultTopic).get().orElse(null)).isEqualTo(expectedTopic); + assertThat(resolver.resolveTopic(userTopic, () -> defaultTopic).value().orElse(null)).isEqualTo(expectedTopic); } static Stream resolveNoMessageInfoProvider() { @@ -76,7 +76,7 @@ class DefaultTopicResolverTests { @MethodSource("resolveByMessageInstanceProvider") void resolveByMessageInstance(String testName, @Nullable String userTopic, T message, @Nullable String defaultTopic, @Nullable String expectedTopic) { - assertThat(resolver.resolveTopic(userTopic, message, () -> defaultTopic).get().orElse(null)) + assertThat(resolver.resolveTopic(userTopic, message, () -> defaultTopic).value().orElse(null)) .isEqualTo(expectedTopic); } @@ -99,7 +99,7 @@ class DefaultTopicResolverTests { @MethodSource("resolveByMessageTypeProvider") void resolveByMessageType(String testName, @Nullable String userTopic, Class messageType, @Nullable String defaultTopic, @Nullable String expectedTopic) { - assertThat(resolver.resolveTopic(userTopic, messageType, () -> defaultTopic).get().orElse(null)) + assertThat(resolver.resolveTopic(userTopic, messageType, () -> defaultTopic).value().orElse(null)) .isEqualTo(expectedTopic); } diff --git a/spring-pulsar/src/test/java/org/springframework/pulsar/core/ResolvedTests.java b/spring-pulsar/src/test/java/org/springframework/pulsar/core/ResolvedTests.java index 2df149b2..1b15534c 100644 --- a/spring-pulsar/src/test/java/org/springframework/pulsar/core/ResolvedTests.java +++ b/spring-pulsar/src/test/java/org/springframework/pulsar/core/ResolvedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatRuntimeException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; /** * Unit tests for {@link Resolved}. @@ -30,33 +37,119 @@ import org.junit.jupiter.api.Test; */ class ResolvedTests { + @SuppressWarnings("removal") @Test - void success() { - assertThat(Resolved.of("good").get()).hasValue("good"); - assertThat(Resolved.of("good").orElseThrow()).isEqualTo("good"); + void deprecatedGetDelegatesToNewValueMethod() { + Resolved resolved = Mockito.spy(Resolved.of("hello")); + resolved.get(); + verify(resolved).value(); } - @Test - void failedWithSimpleReason() { - var resolved = Resolved.failed("oops"); - assertThatIllegalArgumentException().isThrownBy(resolved::orElseThrow).withMessage("oops"); - assertThat(resolved.get()).isEmpty(); + @SuppressWarnings("unchecked") + static Consumer mockValueAction() { + return (Consumer) mock(Consumer.class); } - @Test - void failedWithReason() { - var resolved = Resolved.failed(new IllegalStateException("5150")); - assertThatIllegalStateException().isThrownBy(resolved::orElseThrow).withMessage("5150"); - assertThat(resolved.get()).isEmpty(); + @SuppressWarnings("unchecked") + static Consumer mockErrorAction() { + return (Consumer) mock(Consumer.class); } - @Test - void failedWithAdditionalMessage() { - var resolved = Resolved.failed(new IllegalStateException("5150")); - assertThatRuntimeException().isThrownBy(() -> resolved.orElseThrow(() -> "extra message")) - .withMessage("extra message") - .withCause(new IllegalStateException("5150")); - assertThat(resolved.get()).isEmpty(); + @Nested + class WhenResolutionSucceeds { + + @Test + void valueDoesReturnValue() { + var resolved = Resolved.of("smile"); + assertThat(resolved.value()).hasValue("smile"); + } + + @Test + void orElseThrowDoesReturnValue() { + var resolved = Resolved.of("smile"); + assertThat(resolved.orElseThrow()).isEqualTo("smile"); + } + + @Test + void exceptionDoesReturnEmpty() { + var resolved = Resolved.of("smile"); + assertThat(resolved.exception()).isEmpty(); + } + + @Test + void ifResolvedDoesCallValueAction() { + var resolved = Resolved.of("smile"); + var valueAction = mockValueAction(); + resolved.ifResolved(valueAction); + verify(valueAction).accept("smile"); + } + + @Test + void ifResolvedOrElseDoesCallValueActionAndIgnoresErrorAction() { + var resolved = Resolved.of("smile"); + var valueAction = mockValueAction(); + var errorAction = mockErrorAction(); + resolved.ifResolvedOrElse(valueAction, errorAction); + verify(valueAction).accept("smile"); + verifyNoInteractions(errorAction); + } + + } + + @Nested + class WhenResolutionFails { + + @Test + void valueDoesReturnEmpty() { + var resolved = Resolved.failed("5150"); + assertThat(resolved.value()).isEmpty(); + } + + @Test + void orElseThrowDoesThrowSimpleReason() { + var resolved = Resolved.failed("5150"); + assertThatIllegalArgumentException().isThrownBy(resolved::orElseThrow).withMessage("5150"); + } + + @Test + void orElseThrowDoesThrowReason() { + var resolved = Resolved.failed(new IllegalStateException("5150")); + assertThatIllegalStateException().isThrownBy(resolved::orElseThrow).withMessage("5150"); + } + + @Test + void orElseThrowDoesThrowReasonWithExtraMessage() { + var resolved = Resolved.failed(new IllegalStateException("5150")); + assertThatRuntimeException().isThrownBy(() -> resolved.orElseThrow(() -> "extra message")) + .withMessage("extra message") + .withCause(new IllegalStateException("5150")); + } + + @Test + void exceptionDoesReturnReason() { + var resolved = Resolved.failed("5150"); + assertThat(resolved.exception()).hasValueSatisfying( + (ex) -> assertThat(ex).isInstanceOf(IllegalArgumentException.class).hasMessage("5150")); + } + + @Test + void ifResolvedDoesNotCallValueAction() { + var resolved = Resolved.failed("5150"); + var valueAction = mockValueAction(); + resolved.ifResolved(valueAction); + verifyNoInteractions(valueAction); + } + + @Test + void ifResolvedOrElseDoesIgnoreValueActionAndCallsErrorAction() { + var resolved = Resolved.failed("5150"); + var valueAction = mockValueAction(); + var errorAction = mockErrorAction(); + resolved.ifResolvedOrElse(valueAction, errorAction); + verifyNoInteractions(valueAction); + verify(errorAction).accept(resolved.exception().get()); + } + } }