finish http handling, start persistence work
This commit is contained in:
parent
c2b3bc4515
commit
50f1ca6757
@ -34,6 +34,7 @@ application {
|
||||
|
||||
dependencies {
|
||||
implementation("org.nanohttpd:nanohttpd:2.3.1")
|
||||
implementation("redis.clients:jedis:3.3.0")
|
||||
implementation("io.vavr:vavr:1.0.0-alpha-3")
|
||||
testImplementation("junit", "junit", "4.12")
|
||||
}
|
||||
|
@ -1,18 +1,54 @@
|
||||
package nouritsu;
|
||||
|
||||
import io.vavr.collection.List;
|
||||
import io.vavr.collection.HashSet;
|
||||
import io.vavr.control.Either;
|
||||
import nouritsu.types.Message;
|
||||
import nouritsu.types.Resp;
|
||||
import nouritsu.types.Section;
|
||||
import nouritsu.types.ShoppingItem;
|
||||
import nouritsu.types.Store;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class Dao {
|
||||
public Either<Message, String> clearList(String id) {
|
||||
return Either.right("Cleared list of user “%s“".formatted(id));
|
||||
private final JedisPool pool;
|
||||
|
||||
Dao() {
|
||||
pool = new JedisPool();
|
||||
}
|
||||
|
||||
public Either<Message, String> getList(String id) {
|
||||
return Either.right(List.of("first", "second", "third")
|
||||
.collect(Collectors.joining(",")));
|
||||
private <T> T withRedis(Function<Jedis, T> f) {
|
||||
try (var redis = pool.getResource()) {
|
||||
return f.apply(redis);
|
||||
}
|
||||
}
|
||||
|
||||
public String clearList(String id) {
|
||||
withRedis(redis -> redis.del(id));
|
||||
// Just always return okay, even if we didn’t have data to begin with.
|
||||
// Idempotency yay \o/
|
||||
return "Cleared list of user “%s“".formatted(id);
|
||||
}
|
||||
|
||||
public Either<Resp, HashSet<ShoppingItem>> getList(String id) {
|
||||
return withRedis(redis ->
|
||||
redis.exists(id)
|
||||
? Either.right(HashSet.ofAll(redis.smembers(id)).map(s -> new ShoppingItem(s, Section.CANS)))
|
||||
: Either.left(Resp.NOT_FOUND.apply("No list found for user “%s”".formatted(id)))
|
||||
);
|
||||
}
|
||||
|
||||
public Either<Resp, String> addToList(String id, String item) {
|
||||
return Either.right("Added “%s“ to list of user “%s”".formatted(item, id));
|
||||
}
|
||||
|
||||
public String addItem(ShoppingItem item) {
|
||||
return "Registered new item “%s” under the section “%s”"
|
||||
.formatted(item.name(), item.section().name().toLowerCase());
|
||||
}
|
||||
|
||||
public Either<Resp, String> addStore(Store store) {
|
||||
return Either.right("Registered new store “%s” with the sections [%s]".formatted(store.name(), store.sections()));
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,18 @@ 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.List;
|
||||
import io.vavr.collection.Map;
|
||||
import io.vavr.control.Either;
|
||||
import io.vavr.control.Try;
|
||||
import nouritsu.types.Message;
|
||||
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 {
|
||||
@ -26,6 +29,7 @@ public class Nouritsu extends NanoHTTPD {
|
||||
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())
|
||||
);
|
||||
@ -35,61 +39,81 @@ public class Nouritsu extends NanoHTTPD {
|
||||
Valid endpoints are (not case sensitive):
|
||||
/addStore/?name=storeName§ions=first,second,third
|
||||
/addItem/?name=itemName&category=itemCategory
|
||||
/addToList/?user=userId&itemName
|
||||
/addToList/?user=userId&item=itemName
|
||||
/getList/?user=userId&store=optionalStoreName
|
||||
/clearList/?user=userId
|
||||
""";
|
||||
|
||||
private static Message processRequest(IHTTPSession session, Dao dao) {
|
||||
var params = extractParams(session);
|
||||
return switch (session.getUri().replaceAll("/", "").toLowerCase()) {
|
||||
case "", "index.html" -> new Message(HELP, Status.OK);
|
||||
case "addstore" -> addStore(params, dao);
|
||||
case "additem" -> addItem(params, dao);
|
||||
case "addtolist" -> addToList(params, dao);
|
||||
case "getlist" -> getList(params, dao);
|
||||
case "clearlist" -> clearList(params, dao);
|
||||
default -> new Message(HELP, Status.NOT_FOUND);
|
||||
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 vavr collections.
|
||||
private static Map<String, List<String>> extractParams(IHTTPSession session) {
|
||||
// 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(e -> new Tuple2<>(e.getKey(), List.ofAll(e.getValue())))
|
||||
.collect(HashMap.collector());
|
||||
.map(Tuple::fromEntry)
|
||||
.collect(HashMap.collector())
|
||||
// guaranteed to be non-empty
|
||||
.mapValues(list -> list.get(0));
|
||||
}
|
||||
|
||||
private static Message clearList(Map<String, List<String>> params, Dao dao) {
|
||||
private static Resp clearList(Map<String, String> params, Dao dao) {
|
||||
return getFromParams(params, "user")
|
||||
.map(List::head) // save, the list can’t be empty
|
||||
.flatMap(dao::clearList)
|
||||
.fold(Function.identity(), Message.OK);
|
||||
.map(dao::clearList)
|
||||
.fold(Function.identity(), Resp.CREATED);
|
||||
}
|
||||
|
||||
private static Message getList(Map<String, List<String>> params, Dao dao) {
|
||||
private static Resp getList(Map<String, String> params, Dao dao) {
|
||||
return getFromParams(params, "user")
|
||||
.map(List::head) // save, the list can’t be empty
|
||||
.flatMap(dao::getList)
|
||||
.fold(Function.identity(), Message.OK);
|
||||
.map(shoppingItems -> shoppingItems.mkString(","))
|
||||
.fold(Function.identity(), Resp.OK);
|
||||
}
|
||||
|
||||
private static Message addToList(Map<String, List<String>> params, Dao dao) {
|
||||
return new Message("", Status.NOT_IMPLEMENTED);
|
||||
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 Message addItem(Map<String, List<String>> params, Dao dao) {
|
||||
return new Message("", Status.NOT_IMPLEMENTED);
|
||||
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 Message addStore(Map<String, List<String>> params, Dao dao) {
|
||||
return new Message("", Status.NOT_IMPLEMENTED);
|
||||
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<Message, List<String>> getFromParams(Map<String, List<String>> params, String key) {
|
||||
return params.get(key).toEither(new Message("Parameter “%s” not found".formatted(key), Status.NOT_FOUND));
|
||||
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) {
|
||||
|
@ -1,12 +0,0 @@
|
||||
package nouritsu.types;
|
||||
|
||||
public enum Category {
|
||||
CANS,
|
||||
FRUITS,
|
||||
VEGETABLES,
|
||||
PASTA,
|
||||
RICE,
|
||||
MEAT,
|
||||
DAIRY,
|
||||
HYGIENE,
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package nouritsu.types;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD.Response.Status;
|
||||
import io.vavr.Function1;
|
||||
|
||||
public record Message(String text, Status status) {
|
||||
public static Function1<String, Message> OK = Function1.of(s -> new Message(s, Status.OK));
|
||||
public static Function1<String, Message> BAD_REQUEST = Function1.of(s -> new Message(s, Status.BAD_REQUEST));
|
||||
public static Function1<String, Message> CREATED = Function1.of(s -> new Message(s, Status.CREATED));
|
||||
public static Function1<String, Message> NOT_FOUND = Function1.of(s -> new Message(s, Status.NOT_FOUND));
|
||||
}
|
11
src/main/java/nouritsu/types/Resp.java
Normal file
11
src/main/java/nouritsu/types/Resp.java
Normal file
@ -0,0 +1,11 @@
|
||||
package nouritsu.types;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD.Response.Status;
|
||||
import io.vavr.Function1;
|
||||
|
||||
public record Resp(String text, Status status) {
|
||||
public static Function1<String, Resp> OK = Function1.of(s -> new Resp(s, Status.OK));
|
||||
public static Function1<String, Resp> BAD_REQUEST = Function1.of(s -> new Resp(s, Status.BAD_REQUEST));
|
||||
public static Function1<String, Resp> CREATED = Function1.of(s -> new Resp(s, Status.CREATED));
|
||||
public static Function1<String, Resp> NOT_FOUND = Function1.of(s -> new Resp(s, Status.NOT_FOUND));
|
||||
}
|
21
src/main/java/nouritsu/types/Section.java
Normal file
21
src/main/java/nouritsu/types/Section.java
Normal file
@ -0,0 +1,21 @@
|
||||
package nouritsu.types;
|
||||
|
||||
import io.vavr.collection.Stream;
|
||||
import io.vavr.control.Either;
|
||||
|
||||
public enum Section {
|
||||
CANS,
|
||||
FRUITS,
|
||||
VEGETABLES,
|
||||
PASTA,
|
||||
RICE,
|
||||
MEAT,
|
||||
DAIRY,
|
||||
HYGIENE;
|
||||
|
||||
public static Either<Resp, Section> tryParse(String catName) {
|
||||
return Stream.of(Section.values())
|
||||
.find(c -> c.name().equalsIgnoreCase(catName))
|
||||
.toEither(Resp.BAD_REQUEST.apply("Category “%s” not found".formatted(catName)));
|
||||
}
|
||||
}
|
@ -1,4 +1,9 @@
|
||||
package nouritsu.types;
|
||||
|
||||
public record ShoppingItem(String name, Category category) {
|
||||
import io.vavr.control.Either;
|
||||
|
||||
public record ShoppingItem(String name, Section section) {
|
||||
public static Either<Resp, ShoppingItem> tryParse(String name, String category) {
|
||||
return Section.tryParse(category).map(c -> new ShoppingItem(name, c));
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,16 @@
|
||||
package nouritsu.types;
|
||||
|
||||
import java.util.List;
|
||||
import io.vavr.collection.List;
|
||||
import io.vavr.collection.Seq;
|
||||
import io.vavr.control.Either;
|
||||
|
||||
/**
|
||||
* A store with an ordered list of sections from entrance to exit.
|
||||
*/
|
||||
public record Store(String name, List<Category> sections) {
|
||||
public record Store(String name, Seq<Section>sections) {
|
||||
public static Either<Resp, Store> tryParse(String name, String sections) {
|
||||
return Either.traverse(List.of(sections.split(",")), Section::tryParse)
|
||||
.mapLeft(errors -> Resp.BAD_REQUEST.apply(errors.mkString("\n")))
|
||||
.map(secs -> new Store(name, secs));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user