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.Map; import io.vavr.control.Either; import io.vavr.control.Try; 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 -> newFixedLengthResponse(Status.INTERNAL_ERROR, MIME_PLAINTEXT, err.getMessage()), response -> newFixedLengthResponse(response.status(), MIME_PLAINTEXT, response.text()) ); } private static final String HELP = """ Valid endpoints are (not case sensitive): /addStore/?name=storeName§ions=first,second,third /addItem/?name=itemName&category=itemCategory /addToList/?user=userId&item=itemName /getList/?user=userId&store=optionalStoreName /clearList/?user=userId """; private static Resp processRequest(IHTTPSession session, Dao dao) { // Type inference can’t handle this :feelsBadMan: 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 "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(shoppingItems -> shoppingItems.mkString(",")) .fold(Function.identity(), Resp.OK); } 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", "category") .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", "categories") .flatMap(t -> t.apply(Store::tryParse)) .flatMap(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 getFromParams(params, k1) .map(Tuple::of) .flatMap(name -> getFromParams(params, k2).map(name::append)); } public static void main(String[] args) { try { new Nouritsu(14523, new Dao()); } catch (IOException e) { e.printStackTrace(); } } }