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; import nouritsu.types.Store; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import java.util.function.Function; import static nouritsu.Serde.deserialize; import static nouritsu.Serde.serialize; 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(); } private T withRedis(Function f) { try (var redis = pool.getResource()) { return f.apply(redis); } } private Option getItem(String name) { return Option.of(withRedis(redis -> redis.get(ITEM_PREFIX + name))) .map(item -> deserialize(item, ShoppingItem.class)); } public String clearList(String 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 created("Cleared list of user “%s“").formatted(id); } // The .exists() check is necessary because smembers throws an exception instead of returning null or an empty set. private Option> getSet(String key) { return withRedis(redis -> redis.exists(key) ? Option.some(HashSet.ofAll(redis.smembers(key))) : Option.none() ); } public Either> 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 getStore(String name) { return withRedis(redis -> Option.of(redis.get(STORE_PREFIX + name))) .toEither(Resp.BAD_REQUEST.apply("Store %s not found".formatted(name))) .map(s -> deserialize(s, Store.class)); } // Check if an item is known and add it to the list if it is. public Either addToList(String id, String item) { return getItem(item).map(it -> { withRedis(redis -> redis.sadd(USER_PREFIX + id, item)); return created("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(), serialize(item))); return created("Registered new item “%s” under the section “%s”") .formatted(item.name(), item.section().name().toLowerCase()); } private String created(String msg) { return "{\"msg\": \"%s\"}".formatted(msg); } public String addStore(Store store) { // just overwrite it if we already know that store withRedis(redis -> redis.del(STORE_PREFIX + store.name()) // This plus doesn’t actually add anything. // It just allows us to do both steps in one statement in the lambda body. + redis.set(STORE_PREFIX + store.name(), serialize(store)) ); return created("Registered new store “%s” with the sections [%s]").formatted(store.name(), store.sections()); } }