nouritsu/src/main/java/nouritsu/Nouritsu.java

152 lines
5.4 KiB
Java

package nouritsu;
import fi.iki.elonen.NanoHTTPD;
import fi.iki.elonen.NanoHTTPD.Response.Status;
import io.vavr.Tuple;
import io.vavr.Tuple2;
import io.vavr.collection.HashMap;
import io.vavr.collection.HashSet;
import io.vavr.collection.Map;
import io.vavr.control.Either;
import io.vavr.control.Try;
import nouritsu.types.ListWithRest;
import nouritsu.types.Resp;
import nouritsu.types.ShoppingItem;
import nouritsu.types.Store;
import java.io.IOException;
import java.util.function.BiFunction;
import java.util.function.Function;
public class Nouritsu extends NanoHTTPD {
private final Dao dao;
public Nouritsu(int port, Dao dao) throws IOException {
super(port);
start(10_000, false);
this.dao = dao;
}
@Override
public Response serve(IHTTPSession session) {
return Try.of(() -> processRequest(session, dao))
.fold(
// This won’t be exposed to the outside world, so returning the error message is fine
err -> {
err.printStackTrace();
return newFixedLengthResponse(Status.INTERNAL_ERROR, MIME_PLAINTEXT, err.getMessage());
},
response -> newFixedLengthResponse(response.status(), "application/json", response.text())
);
}
private static final String HELP = """
Valid endpoints are (not case sensitive):
/addStore/?name=storeName&sections=first,second,third
/addItem/?name=itemName&section=itemSection
/addToList/?user=userId&item=itemName
/getList/?user=userId
/getListForStore/?user=userId&store=StoreName
/clearList/?user=userId
""";
private static Resp processRequest(IHTTPSession session, Dao dao) {
// Type inference can’t handle this :feelsBadMan:
final BiFunction<Map<String, String>, Dao, Resp> handler =
switch (session.getUri().replaceAll("/", "").toLowerCase()) {
case "addstore" -> Nouritsu::addStore;
case "additem" -> Nouritsu::addItem;
case "addtolist" -> Nouritsu::addToList;
case "getlist" -> Nouritsu::getList;
case "getlistforstore" -> Nouritsu::getListOrdered;
case "clearlist" -> Nouritsu::clearList;
default -> (_1, _2) -> Resp.NOT_FOUND.apply(HELP);
};
return handler.apply(extractParams(session), dao);
}
// Repack the parameters into a vavr Map<Key, Head(Value)>.
// Each parameter should only occur once per request.
private static Map<String, String> extractParams(IHTTPSession session) {
return session.getParameters()
.entrySet()
.stream()
.map(Tuple::fromEntry)
.collect(HashMap.collector())
// guaranteed to be non-empty
.mapValues(list -> list.get(0));
}
private static Resp clearList(Map<String, String> params, Dao dao) {
return getFromParams(params, "user")
.map(dao::clearList)
.fold(Function.identity(), Resp.CREATED);
}
private static Resp getList(Map<String, String> params, Dao dao) {
return getFromParams(params, "user")
.flatMap(dao::getList)
.map(Serde::serialize)
.fold(Function.identity(), Resp.OK);
}
private static Resp getListOrdered(Map<String, String> params, Dao dao) {
return getAsTuple(params, "user", "store")
.flatMap(t -> t.apply((userId, storeName) ->
zip(dao.getList(userId), dao.getStore(storeName))
))
.map(t -> t.apply(Nouritsu::sortBySection))
.map(Serde::serialize)
.fold(Function.identity(), Resp.OK);
}
// Return a Tuple2 containing the values of two eithers if both were Either.Right
private static <L, R1, R2> Either<L, Tuple2<R1, R2>> zip(Either<L, R1> first, Either<L, R2> second) {
return first.flatMap(f -> second.map(s -> Tuple.of(f, s)));
}
// This kind of breaks the style,
// but I can’t come up with a more elegant solution,
// so lots of locals it is.
private static ListWithRest sortBySection(HashSet<ShoppingItem> items, Store store) {
final var itemsBySection = items.groupBy(ShoppingItem::section);
final var sorted = store.sections().flatMap(s -> itemsBySection.get(s).getOrElse(HashSet.empty()));
return new ListWithRest(sorted, items.removeAll(sorted));
}
private static Resp addToList(Map<String, String> params, Dao dao) {
return getAsTuple(params, "user", "item")
// This is where Kotlin-like destructuring would be really nice to have:
// flatMap(id, item -> ...)
.flatMap(t -> t.apply(dao::addToList))
.fold(Function.identity(), Resp.CREATED);
}
private static Resp addItem(Map<String, String> params, Dao dao) {
return getAsTuple(params, "name", "section")
.flatMap(t -> t.apply(ShoppingItem::tryParse))
.map(dao::addItem)
.fold(Function.identity(), Resp.CREATED);
}
private static Resp addStore(Map<String, String> params, Dao dao) {
return getAsTuple(params, "name", "sections")
.flatMap(t -> t.apply(Store::tryParse))
.map(dao::addStore)
.fold(Function.identity(), Resp.CREATED);
}
private static Either<Resp, String> getFromParams(Map<String, String> params, String key) {
return params.get(key).toEither(Resp.NOT_FOUND.apply("Parameter “%s” not found".formatted(key)));
}
// Get k1 and k2 as a tuple in Either.Right or Either.Left if k1 or k2 is/are not in the map.
private static Either<Resp, Tuple2<String, String>> getAsTuple(Map<String, String> params, String k1, String k2) {
return zip(getFromParams(params, k1), getFromParams(params, k2));
}
public static void main(String[] args) throws IOException {
new Nouritsu(14523, new Dao());
}
}