finish mvp
This commit is contained in:
parent
43f192178e
commit
ff16085c59
@ -1,7 +1,9 @@
|
|||||||
package nouritsu;
|
package nouritsu;
|
||||||
|
|
||||||
import io.vavr.collection.HashSet;
|
import io.vavr.collection.HashSet;
|
||||||
|
import io.vavr.collection.List;
|
||||||
import io.vavr.control.Either;
|
import io.vavr.control.Either;
|
||||||
|
import io.vavr.control.Option;
|
||||||
import nouritsu.types.Resp;
|
import nouritsu.types.Resp;
|
||||||
import nouritsu.types.Section;
|
import nouritsu.types.Section;
|
||||||
import nouritsu.types.ShoppingItem;
|
import nouritsu.types.ShoppingItem;
|
||||||
@ -13,6 +15,11 @@ import java.util.function.Function;
|
|||||||
|
|
||||||
public class Dao {
|
public class Dao {
|
||||||
private final JedisPool pool;
|
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() {
|
Dao() {
|
||||||
pool = new JedisPool();
|
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) {
|
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.
|
// Just always return okay, even if we didn’t have data to begin with.
|
||||||
// Idempotency yay \o/
|
// Idempotency yay \o/
|
||||||
return "Cleared list of user “%s“".formatted(id);
|
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 ->
|
return withRedis(redis ->
|
||||||
redis.exists(id)
|
redis.exists(key)
|
||||||
? Either.right(HashSet.ofAll(redis.smembers(id)).map(s -> new ShoppingItem(s, Section.CANS)))
|
? Option.some(HashSet.ofAll(redis.smembers(key)))
|
||||||
: Either.left(Resp.NOT_FOUND.apply("No list found for user “%s”".formatted(id)))
|
: 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) {
|
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) {
|
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”"
|
return "Registered new item “%s” under the section “%s”"
|
||||||
.formatted(item.name(), item.section().name().toLowerCase());
|
.formatted(item.name(), item.section().name().toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Either<Resp, String> addStore(Store store) {
|
public String addStore(Store store) {
|
||||||
return Either.right("Registered new store “%s” with the sections [%s]".formatted(store.name(), store.sections()));
|
// 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.Tuple;
|
||||||
import io.vavr.Tuple2;
|
import io.vavr.Tuple2;
|
||||||
import io.vavr.collection.HashMap;
|
import io.vavr.collection.HashMap;
|
||||||
|
import io.vavr.collection.HashSet;
|
||||||
import io.vavr.collection.Map;
|
import io.vavr.collection.Map;
|
||||||
import io.vavr.control.Either;
|
import io.vavr.control.Either;
|
||||||
import io.vavr.control.Try;
|
import io.vavr.control.Try;
|
||||||
@ -30,7 +31,10 @@ public class Nouritsu extends NanoHTTPD {
|
|||||||
return Try.of(() -> processRequest(session, dao))
|
return Try.of(() -> processRequest(session, dao))
|
||||||
.fold(
|
.fold(
|
||||||
// This won’t be exposed to the outside world, so returning the error message is fine
|
// 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())
|
response -> newFixedLengthResponse(response.status(), MIME_PLAINTEXT, response.text())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -38,9 +42,10 @@ public class Nouritsu extends NanoHTTPD {
|
|||||||
private static final String HELP = """
|
private static final String HELP = """
|
||||||
Valid endpoints are (not case sensitive):
|
Valid endpoints are (not case sensitive):
|
||||||
/addStore/?name=storeName§ions=first,second,third
|
/addStore/?name=storeName§ions=first,second,third
|
||||||
/addItem/?name=itemName&category=itemCategory
|
/addItem/?name=itemName§ion=itemSection
|
||||||
/addToList/?user=userId&item=itemName
|
/addToList/?user=userId&item=itemName
|
||||||
/getList/?user=userId&store=optionalStoreName
|
/getList/?user=userId
|
||||||
|
/getListForStore/?user=userId&store=StoreName
|
||||||
/clearList/?user=userId
|
/clearList/?user=userId
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@ -52,6 +57,7 @@ public class Nouritsu extends NanoHTTPD {
|
|||||||
case "additem" -> Nouritsu::addItem;
|
case "additem" -> Nouritsu::addItem;
|
||||||
case "addtolist" -> Nouritsu::addToList;
|
case "addtolist" -> Nouritsu::addToList;
|
||||||
case "getlist" -> Nouritsu::getList;
|
case "getlist" -> Nouritsu::getList;
|
||||||
|
case "getlistforstore" -> Nouritsu::getListOrdered;
|
||||||
case "clearlist" -> Nouritsu::clearList;
|
case "clearlist" -> Nouritsu::clearList;
|
||||||
default -> (_1, _2) -> Resp.NOT_FOUND.apply(HELP);
|
default -> (_1, _2) -> Resp.NOT_FOUND.apply(HELP);
|
||||||
};
|
};
|
||||||
@ -83,6 +89,31 @@ public class Nouritsu extends NanoHTTPD {
|
|||||||
.fold(Function.identity(), Resp.OK);
|
.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) {
|
private static Resp addToList(Map<String, String> params, Dao dao) {
|
||||||
return getAsTuple(params, "user", "item")
|
return getAsTuple(params, "user", "item")
|
||||||
// This is where Kotlin-like destructuring would be really nice to have:
|
// 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) {
|
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))
|
.flatMap(t -> t.apply(ShoppingItem::tryParse))
|
||||||
.map(dao::addItem)
|
.map(dao::addItem)
|
||||||
.fold(Function.identity(), Resp.CREATED);
|
.fold(Function.identity(), Resp.CREATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Resp addStore(Map<String, String> params, Dao dao) {
|
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(t -> t.apply(Store::tryParse))
|
||||||
.flatMap(dao::addStore)
|
.map(dao::addStore)
|
||||||
.fold(Function.identity(), Resp.CREATED);
|
.fold(Function.identity(), Resp.CREATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ public enum Section {
|
|||||||
RICE,
|
RICE,
|
||||||
MEAT,
|
MEAT,
|
||||||
FISH,
|
FISH,
|
||||||
|
FROZEN,
|
||||||
DAIRY,
|
DAIRY,
|
||||||
HYGIENE,
|
HYGIENE,
|
||||||
BEVERAGES;
|
BEVERAGES;
|
||||||
|
Loading…
Reference in New Issue
Block a user