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
This commit is contained in:
Chris Bono
2024-01-29 13:13:04 -06:00
parent 0d59e2bd80
commit 80f9b49461
3 changed files with 199 additions and 28 deletions

View File

@@ -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 <T> the resolved type
* @author Christophe Bornet
* @author Chris Bono
*/
public final class Resolved<T> {
@@ -41,28 +42,96 @@ public final class Resolved<T> {
this.exception = exception;
}
/**
* Factory method to create a {@code Resolved} when resolution succeeds.
* @param value the non-{@code null} resolved value
* @param <T> the type of the value
* @return a {@code Resolved} containing the resolved value
*/
public static <T> Resolved<T> 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 <T> the type of the value
* @return a {@code Resolved} containing an {@link IllegalArgumentException} with the
* reason for the failure
*/
public static <T> Resolved<T> failed(String reason) {
return new Resolved<>(null, new IllegalArgumentException(reason));
}
public static <T> Resolved<T> 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 <T> the type of the value
* @return a {@code Resolved} containing the reason for the failure
*/
public static <T> Resolved<T> 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<T> get() {
return value();
}
/**
* Gets the resolved value.
* @return an optional with the resolved value or empty if failed to resolve
*/
public Optional<T> 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<RuntimeException> 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<? super T> 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<? super T> action, Consumer<RuntimeException> 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<T> {
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<String> wrappingErrorMessage) {
if (this.value == null && this.exception != null) {
throw new RuntimeException(wrappingErrorMessage.get(), this.exception);

View File

@@ -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<Arguments> resolveNoMessageInfoProvider() {
@@ -76,7 +76,7 @@ class DefaultTopicResolverTests {
@MethodSource("resolveByMessageInstanceProvider")
<T> 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);
}

View File

@@ -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<String> 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<String> mockValueAction() {
return (Consumer<String>) 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<RuntimeException> mockErrorAction() {
return (Consumer<RuntimeException>) 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.<String>failed("5150");
var valueAction = mockValueAction();
resolved.ifResolved(valueAction);
verifyNoInteractions(valueAction);
}
@Test
void ifResolvedOrElseDoesIgnoreValueActionAndCallsErrorAction() {
var resolved = Resolved.<String>failed("5150");
var valueAction = mockValueAction();
var errorAction = mockErrorAction();
resolved.ifResolvedOrElse(valueAction, errorAction);
verifyNoInteractions(valueAction);
verify(errorAction).accept(resolved.exception().get());
}
}
}