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§ions=first,second,third /addItem/?name=itemName§ion=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, 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. // Each parameter should only occur once per request. private static Map 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 params, Dao dao) { return getFromParams(params, "user") .map(dao::clearList) .fold(Function.identity(), Resp.CREATED); } private static Resp getList(Map params, Dao dao) { return getFromParams(params, "user") .flatMap(dao::getList) .map(Serde::serialize) .fold(Function.identity(), Resp.OK); } private static Resp getListOrdered(Map 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 Either> zip(Either first, Either 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 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 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 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 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 getFromParams(Map 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> getAsTuple(Map 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()); } }