Skip to content

Instantly share code, notes, and snippets.

@Nagelfar
Created November 14, 2023 07:46
Show Gist options
  • Save Nagelfar/1ee1ea4cd9dafe3f2ed9866fcbcae38f to your computer and use it in GitHub Desktop.
Save Nagelfar/1ee1ea4cd9dafe3f2ed9866fcbcae38f to your computer and use it in GitHub Desktop.
Railway-Oriented-Programming in Java
import java.util.function.Consumer;
import java.util.function.Function;
sealed interface Result<T> {
static <TOK> Result<TOK> ok(TOK value) {
return new Ok<>(value);
}
static <TOK> Result<TOK> err(String error) {
return new Err<>(error);
}
Result<T> flatMap(Function<T, Result<T>> mapper);
<TM> Result<TM> map(Function<T, TM> mapper);
Result<T> tee(Consumer<T> consumer);
<TResult> TResult ifOkOrElse(Function<T, TResult> okConsumer, Function<String, TResult> errorConsumer);
default <TResult> TResult consume(Function<Result<T>, TResult> consumer) {
return consumer.apply(this);
}
record Ok<T>(T value) implements Result<T> {
@Override
public <TM> Result<TM> map(Function<T, TM> mapper) {
return Result.ok(mapper.apply(value));
}
@Override
public Result<T> tee(Consumer<T> consumer) {
consumer.accept(value);
return this;
}
@Override
public <TResult> TResult ifOkOrElse(Function<T, TResult> okConsumer, Function<String, TResult> errorConsumer) {
return okConsumer.apply(value);
}
@Override
public Result<T> flatMap(Function<T, Result<T>> mapper) {
return mapper.apply(value);
}
}
record Err<T>(String error) implements Result<T> {
@Override
public Result<T> flatMap(Function<T, Result<T>> mapper) {
return this;
}
@Override
public <TM> Result<TM> map(Function<T, TM> mapper) {
return Result.err(error);
}
@Override
public Result<T> tee(Consumer<T> consumer) {
return this;
}
@Override
public <TResult> TResult ifOkOrElse(Function<T, TResult> okConsumer, Function<String, TResult> errorConsumer) {
return errorConsumer.apply(error);
}
}
}
// imagine this is io.micronaut.http.HttpResponse
interface HttpResponse<T> {
static <T> HttpResponse<T> ok(T body) {
return new HttpResponse<T>() {
@Override
public int hashCode() {
return ("ok" + body).hashCode();
}
};
}
static <T> HttpResponse<T> badRequest(T body) {
return new HttpResponse<T>() {
@Override
public int hashCode() {
return ("badRequest" + body).hashCode();
}
};
}
}
class Scratch {
public static void main(String[] args) {
var result = executeUsecase_a(new Request(1, "name", ""));
}
public static HttpResponse<?> executeUsecase_a(Request input) {
return Validation.validateRequest(input)
.map(SingleTrack::canonicalizeEmail)
.tee(DeadEnd::updateDB)
.flatMap(DeadEnd::sendEmail)
.ifOkOrElse(HttpResponse::ok, HttpResponse::badRequest);
}
public static HttpResponse<?> executeUsecase_b(Request input) {
return Output.returnMessage(
Validation.validateRequest(input)
.map(SingleTrack::canonicalizeEmail)
.tee(DeadEnd::updateDB)
.flatMap(DeadEnd::sendEmail)
);
}
public static HttpResponse<?> executeUsecase_c(Request input) {
return Validation.validateRequest(input)
.map(SingleTrack::canonicalizeEmail)
.tee(DeadEnd::updateDB)
.flatMap(DeadEnd::sendEmail)
.consume(Output::returnMessage);
}
public static HttpResponse<?> executeUsecase_d(Request input) {
var processedResult =
Validation.validateRequest(input)
.map(SingleTrack::canonicalizeEmail)
.tee(DeadEnd::updateDB)
.flatMap(DeadEnd::sendEmail);
return switch (processedResult) {
case Result.Err err -> HttpResponse.badRequest(err);
case Result.Ok ok -> HttpResponse.ok(ok);
};
}
public static class Validation {
public static Result<Request> validateInput(Request input) {
if (input.name.isEmpty())
return Result.err("Name must not be blank");
else if (input.email.isEmpty())
return Result.err("Email must not be blank");
else
return Result.ok(input);
}
public static Result<Request> nameNotBlank(Request input) {
if (input.name.isEmpty())
return Result.err("Name must not be blank");
else
return Result.ok(input);
}
public static Result<Request> name50(Request input) {
if (input.name.isEmpty())
return Result.err("Name must not be longer than 50 chars");
else
return Result.ok(input);
}
public static Result<Request> emailNotBlank(Request input) {
if (input.email.isEmpty())
return Result.err("Email must not be blank");
else
return Result.ok(input);
}
public static Result<Request> validateRequest(Request input) {
return nameNotBlank(input)
.flatMap(Validation::name50)
.flatMap(Validation::emailNotBlank);
}
}
public static class SingleTrack {
public static Request canonicalizeEmail(Request input) {
return new Request(
input.userId(),
input.name(),
input.email().trim().toLowerCase()
);
}
}
public static class DeadEnd {
public static <T> void updateDB(T result) {
// TODO persist into DB
}
public static Result<Request> sendEmail(Request request) {
try {
// TODO send email
return Result.ok(request);
} catch (Exception e) {
return Result.err(e.toString());
}
}
}
public static class Output {
public static <T> HttpResponse<?> returnMessage(Result<T> result) {
return switch (result) {
case Result.Ok(var body) -> HttpResponse.ok(body);
case Result.Err(var error) -> HttpResponse.badRequest("An error occurred:" + error);
};
}
}
public record Request(
int userId,
String name,
String email) {
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment