io.camunda.tasklist.util.Either Maven / Gradle / Ivy
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
* one or more contributor license agreements. See the NOTICE file distributed
* with this work for additional information regarding copyright ownership.
* Licensed under the Camunda License 1.0. You may not use this file
* except in compliance with the Camunda License 1.0.
*/
package io.camunda.tasklist.util;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collector;
public interface Either {
/**
* Returns a {@link Right} describing the given value.
*
* @param right the value to describe
* @param the type of the left value
* @param the type of the right value
* @return a {@link Right} of the value
*/
static Either right(final R right) {
return new Right<>(right);
}
/**
* Returns a {@link Left} describing the given value.
*
* @param left the value to describe
* @param the type of the left value
* @param the type of the right value
* @return a {@link Left} of the value
*/
static Either left(final L left) {
return new Left<>(left);
}
/**
* Convenience method to convert an {@link Optional} of R to an {@code Either, R>}, using an
* intermediary representation of the Optional in the form of {@link EitherOptional}.
*
* Example:
*
*
{@code
* Either.ofOptional(Optional.of(1))
* .orElse("left value")
* .ifRightOrLeft(
* right -> System.out.println("If Optional is present, right is the contained value"),
* left -> System.out.println("If Optional is empty, left is the value provided by orElse")
* );
* }
*
* @param right The optional that may contain the right value
* @param the type of the right value
* @return An intermediary representation {@link EitherOptional}
*/
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
static EitherOptional ofOptional(final Optional right) {
return new EitherOptional<>(right);
}
/**
* Returns a collector for {@code Either} that collects them into {@code
* Either,List} and favors {@link Left} over {@link Right}.
*
* This is commonly used to collect a stream of either objects where a right is considered a
* success and a left is considered an error. If any error has occurred, we're often only
* interested in the errors and not in the successes. Otherwise, we'd like to collect all success
* values.
*
*
This collector groups all the lefts into one {@link List} and all the rights into another.
* When all elements of the stream have been collected and any lefts were encountered, it outputs
* a left of the list of encountered left values. Otherwise, it outputs a right of all right
* values it encountered.
*
*
Examples:
*
*
{@code
* Stream.of(Either.right(1), Either.right(2), Either.right(3))
* .collect(Either.collector()) // => a Right
* .get(); // => List.of(1,2,3)
*
* Stream.of(Either.right(1), Either.left("oops"), Either.right(3))
* .collect(Either.collector()) // => a Left
* .getLeft(); // => List.of("oops")
* }
*
* @param the type of the left values
* @param the type of the right values
* @return a collector that favors left over right
*/
static
Collector, Tuple, List>, Either, List>> collector() {
return Collector.of(
() -> new Tuple<>(new ArrayList<>(), new ArrayList<>()),
(acc, next) ->
next.ifRightOrLeft(right -> acc.getRight().add(right), left -> acc.getLeft().add(left)),
(a, b) -> {
a.getLeft().addAll(b.getLeft());
a.getRight().addAll(b.getRight());
return a;
},
acc -> {
if (!acc.getLeft().isEmpty()) {
return left(acc.getLeft());
} else {
return right(acc.getRight());
}
});
}
/**
* Returns a collector for {@code Either} that collects them into {@code Either}
* and favors {@link Left} over {@link Right}. While collecting the rights, it folds the left into
* the first encountered.
*
* This is commonly used to collect a stream of either objects where a right is considered a
* success and a left is considered an error. If any error has occurred, we're often only
* interested in the first error and not in the successes. Otherwise, we'd like to collect all
* success values.
*
*
This collector looks for a left while it groups all the rights into one {@link List}. When
* all elements of the stream have been collected and a left was encountered, it outputs the
* encountered left. Otherwise, it outputs a right of all right values it encountered.
*
*
Examples:
*
*
{@code
* * Stream.of(Either.right(1), Either.right(2), Either.right(3))
* * .collect(Either.collector()) // => a Right
* * .get(); // => List.of(1,2,3)
* *
* * Stream.of(Either.right(1), Either.left("oops"), Either.left("another oops"))
* * .collect(Either.collector()) // => a Left
* * .getLeft(); // => "oops"
* *
* }
*
* @param the type of the left values
* @param the type of the right values
* @return a collector that favors left over right
*/
static
Collector, Tuple, List>, Either>>
collectorFoldingLeft() {
return Collector.of(
() -> new Tuple<>(Optional.empty(), new ArrayList<>()),
(acc, next) ->
next.ifRightOrLeft(
right -> acc.getRight().add(right),
left -> acc.setLeft(acc.getLeft().or(() -> Optional.of(left)))),
(a, b) -> {
if (a.getLeft().isEmpty() && b.getLeft().isPresent()) {
a.setLeft(b.getLeft());
}
a.getRight().addAll(b.getRight());
return a;
},
acc -> acc.getLeft().map(Either::>left).orElse(Either.right(acc.getRight())));
}
/**
* Returns true if this Either is a {@link Right}.
*
* @return true if right, false if left
*/
boolean isRight();
/**
* Returns true if this Either is a {@link Left}.
*
* @return true if left, false if right
*/
boolean isLeft();
/**
* Returns the right value, if this is a {@link Right}.
*
* @return the right value
* @throws NoSuchElementException if this is a {@link Left}
*/
R get();
/**
* Returns the right value, or a default value if this is a {@link Left}.
*
* @param defaultValue the default value
* @return the right value, or the default value if this is a {@link Left}
*/
R getOrElse(R defaultValue);
/**
* Returns the left value, if this is a {@link Left}.
*
* @return the left value
* @throws NoSuchElementException if this is a {@link Right}
*/
L getLeft();
/**
* Maps the right value, if this is a {@link Right}.
*
* @param right the mapping function for the right value
* @param the type of the resulting right value
* @return a mapped {@link Right} or the same {@link Left}
*/
Either map(Function super R, ? extends T> right);
/**
* Maps the left value, if this is a {@link Left}.
*
* @param left the mapping function for the left value
* @param the type of the resulting left value
* @return a mapped {@link Left} or the same {@link Right}
*/
Either mapLeft(Function super L, ? extends T> left);
/**
* Flatmaps the right value into a new Either, if this is a {@link Right}.
*
* A common use case is to map a right value to a new right, unless some error occurs in which
* case the value can be mapped to a new left. Note that this flatMap does not allow to alter the
* type of the left side. Example:
*
*
{@code
* Either.right(0) // => Right(0)
* .flatMap(x -> Either.right(x + 1)) // => Right(1)
* .flatMap(x -> Either.left("an error occurred")) // => Left("an error occurred")
* .getLeft(); // => "an error occurred"
* }
*
* @param right the flatmapping function for the right value
* @param the type of the right side of the resulting either
* @return either a mapped {@link Right} or a new {@link Left} if this is a right; otherwise the
* same left, but cast to consider the new type of the right.
*/
Either flatMap(Function super R, ? extends Either> right);
/**
* Performs the given action with the value if this is a {@link Right}, otherwise does nothing.
*
* @param action the consuming function for the right value
*/
void ifRight(Consumer action);
/**
* Performs the given action with the value if this is a {@link Left}, otherwise does nothing.
*
* @param action the consuming function for the left value
*/
void ifLeft(Consumer action);
/**
* Performs the given right action with the value if this is a {@link Right}, otherwise performs
* the given left action with the value.
*
* @param rightAction the consuming function for the right value
* @param leftAction the consuming function for the left value
*/
void ifRightOrLeft(Consumer rightAction, Consumer leftAction);
/**
* A right for either a left or right. By convention, right is used for success and left for
* error.
*
* @param The left type
* @param The right type
*/
@SuppressWarnings("java:S2972")
final class Right implements Either {
private final R value;
private Right(final R value) {
this.value = value;
}
@Override
public boolean isRight() {
return true;
}
@Override
public boolean isLeft() {
return false;
}
@Override
public R get() {
return value;
}
@Override
public R getOrElse(final R defaultValue) {
return value;
}
@Override
public L getLeft() {
throw new NoSuchElementException("Expected a left, but this is right");
}
@Override
public Either map(final Function super R, ? extends T> right) {
return Either.right(right.apply(value));
}
@Override
@SuppressWarnings("unchecked")
public Either mapLeft(final Function super L, ? extends T> left) {
return (Either) this;
}
@Override
public Either flatMap(final Function super R, ? extends Either> right) {
return right.apply(value);
}
@Override
public void ifRight(final Consumer right) {
right.accept(value);
}
@Override
public void ifLeft(final Consumer action) {
// do nothing
}
@Override
public void ifRightOrLeft(final Consumer rightAction, final Consumer leftAction) {
rightAction.accept(value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final Right, ?> right = (Right, ?>) o;
return Objects.equals(value, right.value);
}
@Override
public String toString() {
return "Right[" + value + ']';
}
}
/**
* A left for either a left or right. By convention, right is used for success and left for error.
*
* @param The left type
* @param The right type
*/
@SuppressWarnings("java:S2972")
final class Left implements Either {
private final L value;
private Left(final L value) {
this.value = value;
}
@Override
public boolean isRight() {
return false;
}
@Override
public boolean isLeft() {
return true;
}
@Override
public R get() {
throw new NoSuchElementException("Expected a right, but this is left");
}
@Override
public R getOrElse(final R defaultValue) {
return defaultValue;
}
@Override
public L getLeft() {
return value;
}
@Override
@SuppressWarnings("unchecked")
public Either map(final Function super R, ? extends T> right) {
return (Either) this;
}
@Override
public Either mapLeft(final Function super L, ? extends T> left) {
return Either.left(left.apply(value));
}
@Override
@SuppressWarnings("unchecked")
public Either flatMap(final Function super R, ? extends Either> right) {
return (Either) this;
}
@Override
public void ifRight(final Consumer right) {
// do nothing
}
@Override
public void ifLeft(final Consumer action) {
action.accept(value);
}
@Override
public void ifRightOrLeft(final Consumer rightAction, final Consumer leftAction) {
leftAction.accept(value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final Left, ?> left = (Left, ?>) o;
return Objects.equals(value, left.value);
}
@Override
public String toString() {
return "Left[" + value + ']';
}
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
final class EitherOptional {
private final Optional right;
private EitherOptional(final Optional right) {
this.right = right;
}
public Either orElse(final L left) {
return right.>map(Either::right).orElse(Either.left(left));
}
}
}