A library idea to make chained Either unwrapping barable in java.
A popular way to make consise and easily testable steps in your business logic is to use Either's. It could look something like this.
Either<ServiceOneError, Integer> findUserId(String cookie) {
if (cookie.startsWith("valid")) {
return Either.right(cookie.length());
} else {
return Either.left(ServiceOneError.UserIdNotFound);
}
}Once we have this collection of nice steps of business logic we want to chain them together. It's important that we can do this in a consise and maintainable way. This code is where we have the overview of our business logic flow.
In java we have a few options how to chain our business logic of Eithers together.
Best way is probably to use nested flatMap but as the number of steps grow this starts looking crazy.
private Either<ServiceOneError, String> execute(String cookie) {
return serviceOne.findUserId(cookie)
.flatMap(id -> serviceOne.findUserName(id));
}In scala there is a nice language feature (for-comprehension) for this perticular problem. As you can se it looks very nice and if you add more steps it would still look nice.
def execute(cookie: String) =
for {
id <- findUserId(cookie)
name <- findUserName(id)
} yield nameIn Rust the Either<E, R> equivalent is Result<T, E> (generics in oposite order). Rust has no for-comprehension but it has the neat macro ?. Notice the suttle question mark in the function call to find_user_id below. The questionmark can be used whenever the enclosing function has the same Err type in the Result return type. You would use it for all steps except the last one.
pub fn execute(cookie: &str) -> Result<String, ServiceOneError> {
let id = find_user_id(cookie)?;
let name = find_user_name(id);
name
}In this library idea we use some Annotation processing and shenanigans to be able to unwrap our Either functions like this.
Annotate class with functions to wrap like this. Like the Rust question mark operator we need to have same error type in the Either.
@Unwrapped(ServiceOneError.class)
public class ServiceOne {
public Either<ServiceOneError, Integer> findUserId(String cookie) {
...
}
public Either<ServiceOneError, String> findUserName(Integer id) {
...
}
}And then you can chain your unwrapped business logic in similar clear way as Rust.
public String apply(String t) {
var userId = findUserId(t);
var userName = findUserName(userId);
return userName;
}<dependency>
<groupId>se.openresult</groupId>
<artifactId>unwrap-either</artifactId>
<version>0.0.12</version>
</dependency>
This error class could be anything from simple String or enum to specific carrying error messages etc.
Example
public enum ServiceOneError {
UserNameNotFound, UserIdNotFound
}@Unwrapped(ServiceOneError.class)
public class ServiceOne {
public Either<ServiceOneError, Integer> findUserId(String cookie) ...
public Either<ServiceOneError, String> findUserName(Integer id) ...
}Extend the generated ServiceOneUnwrappedGen (name of annotated class + "UnwrappedGen") class and implement
the generic apply method.
public class ServiceOneUnwrapped extends ServiceOneUnwrappedGen<String, String> {
public ServiceOneUnwrapped(ServiceOne service) {
super(service);
}
@Override
public String apply(String cookie) {
var userId = findUserId(cookie);
var userName = findUserName(userId);
return userName;
}
} var serviceOneWrapped = new ServiceOneUnwrapped(new ServiceOne());
// Success
assertEquals("17", serviceOneWrapped.execute("valid-cookie-long").getRight().get());
// UserIdNotFound
assertEquals(ServiceOneError.UserIdNotFound, serviceOneWrapped.execute("invalid").getLeft().get());
// UserNameNotFound
assertEquals(ServiceOneError.UserNameNotFound, serviceOneWrapped.execute("valid-short").getLeft().get());- Make unwrapped functions availabe in acutal annotated class
- Guice examples
mvn \
-Pci-cd \
--batch-mode \
-Dgpg.passphrase=<OSSRH_GPG_SECRET_KEY_PASSWORD> \
deploy