finish mvp

This commit is contained in:
kageru 2020-07-05 00:20:47 +02:00
parent 43f192178e
commit ff16085c59
Signed by: kageru
GPG Key ID: 8282A2BEA4ADA3D2
3 changed files with 94 additions and 14 deletions

@ -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 didnt have data to begin with. // Just always return okay, even if we didnt 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 doesnt 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 wont be exposed to the outside world, so returning the error message is fine // This wont 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&sections=first,second,third /addStore/?name=storeName&sections=first,second,third
/addItem/?name=itemName&category=itemCategory /addItem/?name=itemName&section=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;