127 lines
4.3 KiB
Java
127 lines
4.3 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.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<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 "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(shoppingItems -> shoppingItems.mkString(","))
|
|
.fold(Function.identity(), Resp.OK);
|
|
}
|
|
|
|
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 -> dao.addToList(t._1, t._2))
|
|
.fold(Function.identity(), Resp.CREATED);
|
|
}
|
|
|
|
private static Resp addItem(Map<String, String> params, Dao dao) {
|
|
return getAsTuple(params, "name", "category")
|
|
.flatMap(t -> ShoppingItem.tryParse(t._1, t._2))
|
|
.map(dao::addItem)
|
|
.fold(Function.identity(), Resp.CREATED);
|
|
}
|
|
|
|
private static Resp addStore(Map<String, String> params, Dao dao) {
|
|
return getAsTuple(params, "name", "categories")
|
|
.flatMap(t -> Store.tryParse(t._1, t._2))
|
|
.flatMap(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 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();
|
|
}
|
|
}
|
|
}
|