Skip to content

Commit

Permalink
feat(errors): Add .mapError(..) operations (#235)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoseLion committed Feb 12, 2024
1 parent c460d9e commit 14f057d
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 7 deletions.
44 changes: 44 additions & 0 deletions src/main/java/io/github/joselion/maybe/EffectHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,50 @@ public EffectHandler<E> catchError(final Consumer<Throwable> handler) {
.orElse(this);
}

/**
* If an error is present and matches the specified {@code ofType} class, map
* it to another throwable using the {@code mapper} function which receives
* the previous error in its argument. If the error is not present or it does
* not match the specified type, the {@code mapper} is never applied and the
* next handler will be the same as it was.
*
* @param <C> the type of error to match
* @param <X> the type of the mapped error
* @param ofType a class instance of the error type to match
* @param mapper a function which takes the error as argument and returns
* another error
* @return a handler with either the mapped error or empty
*/
public <C extends Throwable, X extends Throwable> EffectHandler<X> mapError(
final Class<C> ofType,
final Function<? super C, ? extends X> mapper
) {
return this.error
.filter(ofType::isInstance)
.map(Commons::<C>cast)
.map(mapper)
.map(EffectHandler::<X>failure)
.orElseGet(() -> Commons.cast(this));
}

/**
* If an error is present, map it to another throwable using the {@code mapper}
* function which receives the previous error in its argument. If the error
* is not present, the {@code mapper} is never applied and the next handler
* will be empty.
*
* @param <X> the type of the mapped error
* @param mapper a function which takes the error as argument and returns
* another error
* @return a handler with either the mapped error or empty
*/
public <X extends Throwable> EffectHandler<X> mapError(final Function<Throwable, ? extends X> mapper) {
return this.error
.map(mapper)
.map(EffectHandler::<X>failure)
.orElseGet(EffectHandler::empty);
}

/**
* Chain another effect covering both cases of success or error of the
* previous effect in two different callbacks.
Expand Down
54 changes: 52 additions & 2 deletions src/main/java/io/github/joselion/maybe/SolveHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,9 @@ public <X extends Throwable> EffectHandler<X> effect(final ThrowingConsumer<? su
/**
* If the value is present, map it to another value using the {@code mapper}
* function. If an error is present, the {@code mapper} function is never
* applied and the next handler will still contain the error.
* applied and the next handler will just contain the error.
*
* @param <U> the type the value is mapped to
* @param <U> the type the mapped value
* @param mapper a function which takes the value as argument and returns
* another value
* @return a new handler with either the mapped value or an error
Expand All @@ -328,6 +328,56 @@ public <U> SolveHandler<U, E> map(final Function<? super T, ? extends U> mapper)
);
}

/**
* If an error is present and matches the specified {@code ofType} class, map
* it to another throwable using the {@code mapper} function which receives
* the mtching error in its argument. If the error is not present or it does
* not match the specified type, the {@code mapper} is never applied and the
* next handler will just contain the solved value.
*
* @param <C> the type of error to match
* @param <X> the type of the mapped error
* @param ofType a class instance of the error type to match
* @param mapper a function which takes the error as argument and returns
* another error
* @return a new handler with either the mapped error or the value
*/
public <C extends Throwable, X extends Throwable> SolveHandler<T, X> mapError(
final Class<C> ofType,
final Function<? super C, ? extends X> mapper
) {
return this.value.unwrap(
e -> {
final var nextError = ofType.isInstance(e)
? mapper.apply(Commons.cast(e))
: Commons.<X>cast(e);

return SolveHandler.failure(nextError);
},
SolveHandler::from
);
}

/**
* If an error is present, map it to another throwable using the {@code mapper}
* function which receives the previous error in its argument. If the error
* is not present, the {@code mapper} is never applied and the next handler
* will just contain the solved value.
*
* @param <X> the type of the mapped error
* @param mapper a function which takes the error as argument and returns
* another error
* @return a new handler with either the mapped error or the value
*/
public <X extends Throwable> SolveHandler<T, X> mapError(final Function<Throwable, ? extends X> mapper) {
return this.value
.mapLeft(mapper)
.unwrap(
SolveHandler::failure,
SolveHandler::from
);
}

/**
* If the value is present, map it to another value using the {@code mapper}
* function. If an error is present, the {@code mapper} function is never
Expand Down
75 changes: 75 additions & 0 deletions src/test/java/io/github/joselion/maybe/EffectHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,81 @@
}
}

@Nested class mapError {
@Nested class when_the_error_is_present {
@Nested class and_the_error_type_is_provided {
@Nested class and_the_error_is_an_instance_of_the_provided_type {
@Test void returns_a_handler_with_the_mapped_error() {
final var nextError = new RuntimeException("OTHER");
final var mapperSpy = Spy.function((FileSystemException e) -> nextError);
final var handler = EffectHandler.failure(FAILURE).mapError(FileSystemException.class, mapperSpy);

assertThat(handler.error()).contains(nextError);

verify(mapperSpy).apply(FAILURE);
}
}

@Nested class and_the_error_is_not_an_instance_of_the_provided_type {
@Test void returns_an_empty_handler_and_never_calls_the_mapper() {
final var nextError = new RuntimeException("OTHER");
final var mapperSpy = Spy.function((FileSystemException e) -> nextError);
final var handler = EffectHandler.failure(FAILURE).mapError(AccessDeniedException.class, mapperSpy);

assertThat(handler.error()).get().isEqualTo(FAILURE);

verify(mapperSpy, never()).apply(FAILURE);
}
}
}

@Nested class and_the_error_type_is_not_provided {
@Nested class and_the_error_matches_the_type_of_the_arg {
@Test void returns_a_handler_with_the_mapped_error() {
final var nextError = new RuntimeException("OTHER");
final var mapperSpy = Spy.function((Throwable e) -> nextError);
final var handler = EffectHandler.failure(FAILURE).mapError(mapperSpy);

assertThat(handler.error()).contains(nextError);

verify(mapperSpy).apply(FAILURE);
}
}

@Nested class and_the_error_does_not_match_the_type_of_the_arg {
@Test void returns_a_handler_with_the_mapped_error() {
final var nextError = new RuntimeException("OTHER");
final var mapperSpy = Spy.function((Throwable e) -> nextError);
final var handler = EffectHandler.failure(FAILURE).effect(noop).mapError(mapperSpy);

assertThat(handler.error()).contains(nextError);

verify(mapperSpy).apply(FAILURE);
}
}
}
}

@Nested class when_the_error_is_not_present {
@Test void returns_an_empty_handler_and_never_calls_the_mapper() {
final var runtimeSpy = Spy.function((RuntimeException e) -> FAILURE);
final var throwableSpy = Spy.function((Throwable e) -> FAILURE);
final var handler = EffectHandler.empty();
final var overloads = List.of(
handler.mapError(RuntimeException.class, runtimeSpy),
handler.mapError(throwableSpy)
);

assertThat(overloads).isNotEmpty().allSatisfy(overload -> {
assertThat(overload.error()).isEmpty();
});

verify(runtimeSpy, never()).apply(any());
verify(throwableSpy, never()).apply(any());
}
}
}

@Nested class effect {
@Nested class when_the_error_is_not_present {
@Test void calls_the_effect_callback_and_returns_a_new_handler() throws FileSystemException {
Expand Down
88 changes: 83 additions & 5 deletions src/test/java/io/github/joselion/maybe/SolveHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

@Nested class from {
@Nested class when_the_value_is_not_null {
@Test void returns_a_handler_withThe_value() {
@Test void returns_a_handler_with_the_value() {
final var handler = SolveHandler.from(OK);

assertThat(handler.success()).containsSame(OK);
Expand Down Expand Up @@ -480,8 +480,7 @@
@Nested class map {
@Nested class when_the_value_is_present {
@Test void returns_a_handler_applying_the_mapper_function() {
final var handler = SolveHandler.from("Hello world!")
.map(String::length);
final var handler = SolveHandler.from("Hello world!").map(String::length);

assertThat(handler.success()).contains(12);

Expand All @@ -491,15 +490,94 @@

@Nested class when_the_error_is_present {
@Test void returns_a_handler_with_the_previous_error() {
final var handler = SolveHandler.failure(FAILURE)
.map(Object::toString);
final var handler = SolveHandler.failure(FAILURE).map(Object::toString);

assertThat(handler.success()).isEmpty();
assertThat(handler.error()).contains(FAILURE);
}
}
}

@Nested class mapError {
@Nested class when_the_error_is_present {
@Nested class and_the_error_type_is_provided {
@Nested class and_the_error_is_an_instance_of_the_provided_type {
@Test void returns_a_handler_with_the_mapped_error() {
final var nextError = new RuntimeException("OTHER");
final var mapperSpy = Spy.function((FileSystemException e) -> nextError);
final var handler = SolveHandler.failure(FAILURE).mapError(FileSystemException.class, mapperSpy);

assertThat(handler.success()).isEmpty();
assertThat(handler.error()).contains(nextError);

verify(mapperSpy).apply(FAILURE);
}
}

@Nested class and_the_error_is_not_an_instance_of_the_provided_type {
@Test void returns_a_handler_with_the_value_and_the_mapper_is_never_called() {
final var nextError = new RuntimeException("OTHER");
final var mapperSpy = Spy.function((FileSystemException e) -> nextError);
final var handler = SolveHandler.failure(FAILURE).mapError(AccessDeniedException.class, mapperSpy);

assertThat(handler.success()).isEmpty();
assertThat(handler.error()).get().isEqualTo(FAILURE);

verify(mapperSpy, never()).apply(any());
}
}
}

@Nested class and_the_error_type_is_not_provided {
@Nested class and_the_error_matches_the_type_of_the_arg {
@Test void returns_a_handler_with_the_mapped_error() {
final var nextError = new RuntimeException("OTHER");
final var mapperSpy = Spy.function((Throwable e) -> nextError);
final var handler = SolveHandler.failure(FAILURE).mapError(mapperSpy);

assertThat(handler.success()).isEmpty();
assertThat(handler.error()).contains(nextError);

verify(mapperSpy).apply(FAILURE);
}
}

@Nested class and_the_error_does_not_match_the_type_of_the_arg {
@Test void returns_a_handler_with_the_mapped_error() {
final var nextError = new RuntimeException("OTHER");
final var mapperSpy = Spy.function((Throwable e) -> nextError);
final var handler = SolveHandler.failure(FAILURE).cast(String.class).mapError(mapperSpy);

assertThat(handler.success()).isEmpty();
assertThat(handler.error()).contains(nextError);

verify(mapperSpy).apply(FAILURE);
}
}
}
}

@Nested class when_the_value_is_present {
@Test void returns_a_handler_with_the_value_and_the_mapper_is_never_called() {
final var runtimeSpy = Spy.function((RuntimeException e) -> FAILURE);
final var throwableSpy = Spy.function((Throwable e) -> FAILURE);
final var handler = SolveHandler.from(OK);
final var overloads = List.of(
handler.mapError(RuntimeException.class, runtimeSpy),
handler.mapError(throwableSpy)
);

assertThat(overloads).isNotEmpty().allSatisfy(overload -> {
assertThat(overload.success()).contains(OK);
assertThat(overload.error()).isEmpty();
});

verify(runtimeSpy, never()).apply(any());
verify(throwableSpy, never()).apply(any());
}
}
}

@Nested class flatMap {
@Nested class when_the_value_is_present {
@Test void returns_a_handler_applying_the_mapper_function() {
Expand Down

0 comments on commit 14f057d

Please sign in to comment.