finish mvp
This commit is contained in:
parent
43f192178e
commit
ff16085c59
@ -1,7 +1,9 @@
|
||||
package nouritsu;
|
||||
|
||||
import io.vavr.collection.HashSet;
|
||||
import io.vavr.collection.List;
|
||||
import io.vavr.control.Either;
|
||||
import io.vavr.control.Option;
|
||||
import nouritsu.types.Resp;
|
||||
import nouritsu.types.Section;
|
||||
import nouritsu.types.ShoppingItem;
|
||||
@ -13,6 +15,11 @@ import java.util.function.Function;
|
||||
|
||||
public class Dao {
|
||||
private final JedisPool pool;
|
||||
// Prefixes for the redis keys because other applications might also be using the redis
|
||||
private static final String PREFIX = "nouritsu_";
|
||||
private static final String USER_PREFIX = PREFIX + "user_";
|
||||
private static final String STORE_PREFIX = PREFIX + "store_";
|
||||
private static final String ITEM_PREFIX = PREFIX + "item_";
|
||||
|
||||
Dao() {
|
||||
pool = new JedisPool();
|
||||
@ -24,31 +31,72 @@ public class Dao {
|
||||
}
|
||||
}
|
||||
|
||||
private Option<ShoppingItem> getItem(String name) {
|
||||
return Option.of(withRedis(redis -> redis.get(ITEM_PREFIX + name)))
|
||||
.flatMap(section -> Section.tryParse(section).toOption())
|
||||
.map(section -> new ShoppingItem(name, section));
|
||||
}
|
||||
|
||||
public String clearList(String id) {
|
||||
withRedis(redis -> redis.del(id));
|
||||
withRedis(redis -> redis.del(USER_PREFIX + 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) {
|
||||
private Option<HashSet<String>> getSet(String key) {
|
||||
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)))
|
||||
redis.exists(key)
|
||||
? Option.some(HashSet.ofAll(redis.smembers(key)))
|
||||
: Option.none()
|
||||
);
|
||||
}
|
||||
|
||||
public Either<Resp, HashSet<ShoppingItem>> getList(String id) {
|
||||
return getSet(USER_PREFIX + id)
|
||||
.map(items -> items.map(this::getItem).map(Option::get))
|
||||
.toEither(Resp.NOT_FOUND.apply("No list found for user “%s”".formatted(id)));
|
||||
}
|
||||
|
||||
public Either<Resp, Store> getStore(String name) {
|
||||
return withRedis(redis -> Option.of(redis.lrange(STORE_PREFIX + name, 0, 1000L))
|
||||
.toEither(Resp.BAD_REQUEST.apply("Store %s not found".formatted(name)))
|
||||
.map(sections -> sections.stream()
|
||||
.map(Section::tryParse)
|
||||
// Safe; elements in persistence must have a valid section
|
||||
.map(Either::get)
|
||||
.collect(List.collector()))
|
||||
.map(sections -> new Store(name, sections))
|
||||
);
|
||||
}
|
||||
|
||||
// Check if an item is known and add it to the list if it is.
|
||||
public Either<Resp, String> addToList(String id, String item) {
|
||||
return Either.right("Added “%s“ to list of user “%s”".formatted(item, id));
|
||||
return getItem(item).map(it -> {
|
||||
withRedis(redis -> redis.sadd(USER_PREFIX + id, item));
|
||||
return "Added “%s“ to list of user “%s”".formatted(it, id);
|
||||
}).toEither(Resp.BAD_REQUEST.apply("Item %s not found".formatted(item)));
|
||||
}
|
||||
|
||||
public String addItem(ShoppingItem item) {
|
||||
withRedis(redis -> redis.set(ITEM_PREFIX + item.name(), item.section().name()));
|
||||
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()));
|
||||
public String addStore(Store store) {
|
||||
// just overwrite it if we already know that store
|
||||
withRedis(redis -> {
|
||||
redis.del(STORE_PREFIX + store.name());
|
||||
redis.rpush(
|
||||
STORE_PREFIX + store.name(),
|
||||
store.sections().map(Section::name).toJavaArray(String[]::new)
|
||||
);
|
||||
// There needs to be some kind of return type unless I make a second util function with Consumer<Jedis>.
|
||||
// Why doesn’t Java just have a `Unit` type?
|
||||
return true;
|
||||
}
|
||||
);
|
||||
return "Registered new store “%s” with the sections [%s]".formatted(store.name(), store.sections());
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ 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;
|
||||
@ -30,7 +31,10 @@ public class Nouritsu extends NanoHTTPD {
|
||||
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()),
|
||||
err -> {
|
||||
err.printStackTrace();
|
||||
return newFixedLengthResponse(Status.INTERNAL_ERROR, MIME_PLAINTEXT, err.getMessage());
|
||||
},
|
||||
response -> newFixedLengthResponse(response.status(), MIME_PLAINTEXT, response.text())
|
||||
);
|
||||
}
|
||||
@ -38,9 +42,10 @@ public class Nouritsu extends NanoHTTPD {
|
||||
private static final String HELP = """
|
||||
Valid endpoints are (not case sensitive):
|
||||
/addStore/?name=storeName§ions=first,second,third
|
||||
/addItem/?name=itemName&category=itemCategory
|
||||
/addItem/?name=itemName§ion=itemSection
|
||||
/addToList/?user=userId&item=itemName
|
||||
/getList/?user=userId&store=optionalStoreName
|
||||
/getList/?user=userId
|
||||
/getListForStore/?user=userId&store=StoreName
|
||||
/clearList/?user=userId
|
||||
""";
|
||||
|
||||
@ -52,6 +57,7 @@ public class Nouritsu extends NanoHTTPD {
|
||||
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);
|
||||
};
|
||||
@ -83,6 +89,31 @@ public class Nouritsu extends NanoHTTPD {
|
||||
.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))
|
||||
.fold(Function.identity(), Resp.OK);
|
||||
}
|
||||
|
||||
private static String sortBySection(HashSet<ShoppingItem> items, Store store) {
|
||||
var itemsBySection = items.groupBy(ShoppingItem::section);
|
||||
var sorted = store.sections().flatMap(s -> itemsBySection.get(s).getOrElse(HashSet.empty()));
|
||||
var rest = items.removeAll(sorted);
|
||||
var sortedString = sorted.map(ShoppingItem::name).mkString(", ");
|
||||
if (rest.isEmpty()) {
|
||||
return sortedString;
|
||||
}
|
||||
return sortedString + rest.mkString("\nItems not available or not classified in this store: ", ", ", "");
|
||||
}
|
||||
|
||||
// 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)));
|
||||
}
|
||||
|
||||
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:
|
||||
@ -92,16 +123,16 @@ public class Nouritsu extends NanoHTTPD {
|
||||
}
|
||||
|
||||
private static Resp addItem(Map<String, String> params, Dao dao) {
|
||||
return getAsTuple(params, "name", "category")
|
||||
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", "categories")
|
||||
return getAsTuple(params, "name", "sections")
|
||||
.flatMap(t -> t.apply(Store::tryParse))
|
||||
.flatMap(dao::addStore)
|
||||
.map(dao::addStore)
|
||||
.fold(Function.identity(), Resp.CREATED);
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ public enum Section {
|
||||
RICE,
|
||||
MEAT,
|
||||
FISH,
|
||||
FROZEN,
|
||||
DAIRY,
|
||||
HYGIENE,
|
||||
BEVERAGES;
|
||||
|
Loading…
Reference in New Issue
Block a user