Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fluent use of Result #1837

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import com.fasterxml.jackson.annotation.JsonIgnore;

import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
* Base result type used by services to indicate success or failure.
Expand Down Expand Up @@ -68,4 +70,46 @@ public boolean failed() {
public String getFailureDetail() {
return failure == null ? null : String.join(", ", getFailureMessages());
}

/**
* Executes a {@link Consumer} if this {@link Result} is successful
*/
public AbstractResult<T, F> onSuccess(Consumer<T> successAction) {
if (succeeded()) {
successAction.accept(getContent());
}
return this;
}

/**
* Executes a {@link Consumer} if this {@link Result} failed. Passes the {@link Failure} to the consumer
*/
public AbstractResult<T, F> onFailure(Consumer<F> failureAction) {
if (failed()) {
failureAction.accept(getFailure());
}
return this;
}

/**
* Alias for {@link AbstractResult#onFailure(Consumer)} to make code a bit more easily readable.
*/
public AbstractResult<T, F> orElse(Consumer<F> failureAction) {
return onFailure(failureAction);
}

/**
* Throws an exception supplied by the {@link Supplier} if this {@link Result} is not successful.
*
* @param exceptionSupplier provides an instance of the exception to throw
*/
public <X extends Throwable> AbstractResult<T, F> orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (failed()) {
throw exceptionSupplier.get();
} else {
return this;
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import java.util.Optional;
import java.util.function.Function;

import static java.util.Optional.empty;
import static java.util.Optional.of;

/**
* A generic result type.
*/
Expand All @@ -47,6 +50,17 @@ public static <T> Result<T> failure(List<String> failures) {
return new Result<>(null, new Failure(failures));
}

/**
* Converts a {@link Optional} into a result, interpreting the Optional's value as content.
*
* @return {@link Result#failure(String)} if the Optional is empty, {@link Result#success(Object)} using the
* Optional's value otherwise.
*/
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public static <T> Result<T> from(Optional<T> opt) {
return opt.map(Result::success).orElse(Result.failure("Empty optional"));
}

/**
* Merges this result with another one. If both Results are successful, a new one is created with no content. If
* either this result or {@code other} is a failure, the merged result will be a {@code failure()} and will contain
Expand All @@ -72,5 +86,63 @@ public <R> Result<R> map(Function<T, R> mapFunction) {
}
}

}
/**
* Maps this {@link Result} into another, maintaining the basic semantics (failed vs success). If this
* {@link Result} is successful, the content is discarded. If this {@link Result} failed, the failures are carried
* over. This method is intended for use when the return type is implicit, for example:
* <pre>
* public Result&lt;Void&gt; someMethod() {
* Result&lt;String&gt; result = getStringResult();
* return result.mapTo();
* }
* </pre>
*
* @see org.eclipse.dataspaceconnector.spi.result.Result#map(Function)
* @see org.eclipse.dataspaceconnector.spi.result.Result#mapTo(Class)
*/
public <R> Result<R> mapTo() {
if (succeeded()) {
return new Result<>(null, null);
} else {
return Result.failure(getFailureMessages());
}
}


/**
* Maps this {@link Result} into another, maintaining the basic semantics (failed vs success). If this
* {@link Result} is successful, the content is discarded. If this {@link Result} failed, the failures are carried
* over. This method is intended for use when an explicit return type is needed, for example when using var:
* <pre>
* Result&lt;String&gt; result = getStringResult();
* var voidResult = result.mapTo(Void.class);
* </pre>
*
* @param clazz type of the result, with which the resulting {@link Result} should be parameterized
* @see org.eclipse.dataspaceconnector.spi.result.Result#map(Function)
* @see org.eclipse.dataspaceconnector.spi.result.Result#mapTo()
*/
public <R> Result<R> mapTo(Class<R> clazz) {

Check notice

Code scanning / CodeQL

Useless parameter

The parameter clazz is unused.
return mapTo();
}

/**
* Maps one result into another, applying the mapping function.
*
* @param mappingFunction a function converting this result into another
* @return the result of the mapping function
*/
public <U> Result<U> flatMap(Function<Result<T>, Result<U>> mappingFunction) {
return mappingFunction.apply(this);
}

/**
* Converts this result into an {@link Optional}. When this result is failed, or there is no content,
* {@link Optional#isEmpty()} is returned, otherwise the content is the {@link Optional}'s value
*
* @return {@link Optional#empty()} if failed, or no content, {@link Optional#of(Object)} otherwise.
*/
public Optional<T> asOptional() {
return succeeded() && getContent() != null ? of(getContent()) : empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,18 @@

import org.junit.jupiter.api.Test;

import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;

class ResultTest {

Expand Down Expand Up @@ -57,8 +68,7 @@ void merge_twoFailures() {

var result = r1.merge(r2);
assertThat(result.failed()).isTrue();
assertThat(result.getFailureMessages()).hasSize(2)
.containsExactly("reason 1", "reason 2");
assertThat(result.getFailureMessages()).hasSize(2).containsExactly("reason 1", "reason 2");
}

@Test
Expand All @@ -69,9 +79,7 @@ void merge_failureAndSuccess() {

var result = r1.merge(r2);
assertThat(result.failed()).isTrue();
assertThat(result.getFailureMessages())
.hasSize(1)
.containsExactly("reason 1");
assertThat(result.getFailureMessages()).hasSize(1).containsExactly("reason 1");

}

Expand All @@ -83,4 +91,141 @@ void merge_twoSuccesses() {
var result = r1.merge(r2);
assertThat(result.succeeded()).isTrue();
}

@Test
void onSuccess_whenSucceeded() {
var result = Result.success("foo");
Consumer<String> consumer = mock(Consumer.class);

assertThat(result.onSuccess(consumer)).isEqualTo(result);
verify(consumer).accept(eq("foo"));
}

@Test
void onSuccess_whenFailed() {
Consumer<String> consumer = mock(Consumer.class);

Result<String> result = Result.failure("bar");
assertThat(result.onSuccess(consumer)).isEqualTo(result);
verifyNoInteractions(consumer);
}

@Test
void onFailure_whenSucceeded() {
var result = Result.success("foo");
Consumer<Failure> consumer = mock(Consumer.class);

assertThat(result.onFailure(consumer)).isEqualTo(result);
verifyNoInteractions(consumer);
}

@Test
void onFailure_whenFailed() {
Consumer<Failure> consumer = mock(Consumer.class);

Result<String> result = Result.failure("bar");
assertThat(result.onFailure(consumer)).isEqualTo(result);
verify(consumer).accept(argThat(f -> f.getMessages().contains("bar")));
}

@Test
void mapTo_succeeded() {
var res = Result.success("foobar");
Result<Void> mapped = res.mapTo();
assertThat(mapped.succeeded()).isTrue();
assertThat(mapped.getContent()).isNull();
}

@Test
void mapTo_failed() {
var res = Result.failure("foobar");
Result<String> mapped = res.mapTo();
assertThat(mapped.failed()).isTrue();
assertThat(mapped.getFailureDetail()).isEqualTo("foobar");
}

@Test
void mapTo_explicitType_succeeded() {
var res = Result.success("foobar");
var mapped = res.mapTo(Object.class);
assertThat(mapped.succeeded()).isTrue();
assertThat(mapped.getContent()).isNull();
}

@Test
void mapTo_explicitType_failed() {
var res = Result.failure("foobar");
var mapped = res.mapTo(String.class);
assertThat(mapped.failed()).isTrue();
assertThat(mapped.getFailureDetail()).isEqualTo("foobar");
}


@Test
void whenSuccess_chainsSuccesses() {
var result1 = Result.success("res1");
var finalResult = result1.flatMap(r -> Result.success("res2")).flatMap(r -> Result.success("res3"));

assertThat(finalResult.succeeded()).isTrue();
assertThat(finalResult.getContent()).isEqualTo("res3");
}

@Test
void whenSuccess_middleOneFails() {
var result1 = Result.success("res1");
var finalResult = result1
.flatMap(r -> Result.success("res2"))
.flatMap(r -> Result.failure("some failure"))
.flatMap(Result::mapTo);

assertThat(finalResult.failed()).isTrue();
assertThat(finalResult.getFailureDetail()).isEqualTo("some failure");
}

@Test
void whenSuccess_firstOneFails() {
Result<String> result1 = Result.failure("fail1");
var finalResult = result1
.flatMap(r -> Result.failure("fail2"))
.flatMap(r -> Result.failure("fail3"));

assertThat(finalResult.failed()).isTrue();
assertThat(finalResult.getFailureDetail()).isEqualTo("fail3");
}

@Test
void asOptional() {
assertThat(Result.success("some value").asOptional()).hasValue("some value");
assertThat(Result.success().asOptional()).isEmpty();
assertThat(Result.failure("foobar").asOptional()).isEmpty();
}

@Test
void from() {
var res = Result.from(Optional.of("some val"));
assertThat(res.succeeded()).isTrue();
assertThat(res.getContent()).isEqualTo("some val");

var failedRes = Result.from(Optional.empty());
assertThat(failedRes.failed()).isTrue();
assertThat(failedRes.getFailureDetail()).isNotNull();
}

@Test
void orElseThrow() {
assertThat(Result.success("foobar").orElseThrow(RuntimeException::new))
.extracting(AbstractResult::getContent)
.isEqualTo("foobar");

assertThatThrownBy(() -> Result.failure("barbaz").orElseThrow(RuntimeException::new))
.isInstanceOf(RuntimeException.class);
}

private <U> Function<Result<U>, Result<String>> failWhenCalled() {
return r -> {
fail("should not be called!");
return Result.success("next result");
};
}

}
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
/*
* Copyright (c) 2022 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.dataspaceconnector.spi.transformer;

import org.jetbrains.annotations.Nullable;
Expand Down