Return json for successful API calls
This commit is contained in:
parent
9f8e7dcc1a
commit
e58754b03e
|
@ -36,5 +36,7 @@ dependencies {
|
|||
implementation("org.nanohttpd:nanohttpd:2.3.1")
|
||||
implementation("redis.clients:jedis:3.3.0")
|
||||
implementation("io.vavr:vavr:1.0.0-alpha-3")
|
||||
implementation("io.vavr:vavr-jackson:1.0.0-alpha-3")
|
||||
implementation("com.fasterxml.jackson:jackson-base:2.11.1")
|
||||
testImplementation("junit", "junit", "4.12")
|
||||
}
|
||||
|
|
|
@ -13,6 +13,9 @@ 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
|
||||
|
@ -33,15 +36,14 @@ 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));
|
||||
.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 "Cleared list of user “%s“".formatted(id);
|
||||
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.
|
||||
|
@ -60,41 +62,37 @@ public class Dao {
|
|||
}
|
||||
|
||||
public Either<Resp, Store> getStore(String name) {
|
||||
return withRedis(redis -> Option.of(redis.lrange(STORE_PREFIX + name, 0, 1000L))
|
||||
return withRedis(redis -> Option.of(redis.get(STORE_PREFIX + name)))
|
||||
.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))
|
||||
);
|
||||
.map(s -> deserialize(s, Store.class));
|
||||
}
|
||||
|
||||
// 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 getItem(item).map(it -> {
|
||||
withRedis(redis -> redis.sadd(USER_PREFIX + id, item));
|
||||
return "Added “%s“ to list of user “%s”".formatted(it, id);
|
||||
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(), item.section().name()));
|
||||
return "Registered new item “%s” under the section “%s”"
|
||||
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.
|
||||
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.rpush(
|
||||
STORE_PREFIX + store.name(),
|
||||
store.sections().map(Section::name).toJavaArray(String[]::new)
|
||||
)
|
||||
+ redis.set(STORE_PREFIX + store.name(), serialize(store))
|
||||
);
|
||||
return "Registered new store “%s” with the sections [%s]".formatted(store.name(), store.sections());
|
||||
return created("Registered new store “%s” with the sections [%s]").formatted(store.name(), store.sections());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import io.vavr.collection.HashSet;
|
|||
import io.vavr.collection.Map;
|
||||
import io.vavr.control.Either;
|
||||
import io.vavr.control.Try;
|
||||
import nouritsu.types.ListWithRest;
|
||||
import nouritsu.types.Resp;
|
||||
import nouritsu.types.ShoppingItem;
|
||||
import nouritsu.types.Store;
|
||||
|
@ -35,7 +36,7 @@ public class Nouritsu extends NanoHTTPD {
|
|||
err.printStackTrace();
|
||||
return newFixedLengthResponse(Status.INTERNAL_ERROR, MIME_PLAINTEXT, err.getMessage());
|
||||
},
|
||||
response -> newFixedLengthResponse(response.status(), MIME_PLAINTEXT, response.text())
|
||||
response -> newFixedLengthResponse(response.status(), "application/json", response.text())
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -85,7 +86,7 @@ public class Nouritsu extends NanoHTTPD {
|
|||
private static Resp getList(Map<String, String> params, Dao dao) {
|
||||
return getFromParams(params, "user")
|
||||
.flatMap(dao::getList)
|
||||
.map(shoppingItems -> shoppingItems.mkString(","))
|
||||
.map(Serde::serialize)
|
||||
.fold(Function.identity(), Resp.OK);
|
||||
}
|
||||
|
||||
|
@ -95,6 +96,7 @@ public class Nouritsu extends NanoHTTPD {
|
|||
zip(dao.getList(userId), dao.getStore(storeName))
|
||||
))
|
||||
.map(t -> t.apply(Nouritsu::sortBySection))
|
||||
.map(Serde::serialize)
|
||||
.fold(Function.identity(), Resp.OK);
|
||||
}
|
||||
|
||||
|
@ -106,15 +108,10 @@ public class Nouritsu extends NanoHTTPD {
|
|||
// This kind of breaks the style,
|
||||
// but I can’t come up with a more elegant solution,
|
||||
// so lots of locals it is.
|
||||
private static String sortBySection(HashSet<ShoppingItem> items, Store store) {
|
||||
private static ListWithRest sortBySection(HashSet<ShoppingItem> items, Store store) {
|
||||
final var itemsBySection = items.groupBy(ShoppingItem::section);
|
||||
final var sorted = store.sections().flatMap(s -> itemsBySection.get(s).getOrElse(HashSet.empty()));
|
||||
final var rest = items.removeAll(sorted);
|
||||
final var formattedItems = sorted.map(ShoppingItem::name).mkString(", ");
|
||||
if (rest.isEmpty()) {
|
||||
return formattedItems;
|
||||
}
|
||||
return formattedItems + rest.mkString("\nItems not available or not classified in this store: ", ", ", "");
|
||||
return new ListWithRest(sorted, items.removeAll(sorted));
|
||||
}
|
||||
|
||||
private static Resp addToList(Map<String, String> params, Dao dao) {
|
||||
|
@ -148,11 +145,7 @@ public class Nouritsu extends NanoHTTPD {
|
|||
return zip(getFromParams(params, k1), getFromParams(params, k2));
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
new Nouritsu(14523, new Dao());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
public static void main(String[] args) throws IOException {
|
||||
new Nouritsu(14523, new Dao());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package nouritsu;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.vavr.control.Try;
|
||||
import io.vavr.jackson.datatype.VavrModule;
|
||||
|
||||
/**
|
||||
* *Ser*ialize and *de*serialize. Name stolen from the Rust framework.
|
||||
*/
|
||||
public class Serde {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new VavrModule());
|
||||
|
||||
public static <T> T deserialize(String s, Class<T> clazz) {
|
||||
return Try.of(() -> MAPPER.readValue(s, clazz))
|
||||
// I don’t see a case where this can actually happen
|
||||
.getOrElseThrow((e) -> new IllegalArgumentException("Could not deserialize %s as %s".formatted(s, clazz), e));
|
||||
}
|
||||
|
||||
public static <T> String serialize(T obj) {
|
||||
return Try.of(() -> MAPPER.writeValueAsString(obj))
|
||||
// unreachable, but checked exceptions force me to do this
|
||||
.getOrElseThrow((e) -> new IllegalArgumentException("Could not serialize " + obj.toString(), e));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package nouritsu.types;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.vavr.collection.HashSet;
|
||||
import io.vavr.collection.Seq;
|
||||
|
||||
/**
|
||||
* A sorted shopping list for a store.
|
||||
* Rest contains the items that can’t be purchased at the selected store.
|
||||
*/
|
||||
public record ListWithRest(
|
||||
@JsonProperty("items") Seq<ShoppingItem> items,
|
||||
@JsonProperty("rest") HashSet<ShoppingItem> rest
|
||||
) {}
|
|
@ -1,8 +1,12 @@
|
|||
package nouritsu.types;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.vavr.control.Either;
|
||||
|
||||
public record ShoppingItem(String name, Section section) {
|
||||
public record ShoppingItem(
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("section") Section section
|
||||
) {
|
||||
public static Either<Resp, ShoppingItem> tryParse(String name, String category) {
|
||||
return Section.tryParse(category)
|
||||
.map(c -> new ShoppingItem(name, c))
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package nouritsu.types;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.vavr.collection.List;
|
||||
import io.vavr.collection.Seq;
|
||||
import io.vavr.control.Either;
|
||||
|
@ -7,7 +8,10 @@ import io.vavr.control.Either;
|
|||
/**
|
||||
* A store with an ordered list of sections from entrance to exit.
|
||||
*/
|
||||
public record Store(String name, Seq<Section>sections) {
|
||||
public record Store(
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("sections") 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") + Section.valuesAsString()))
|
||||
|
|
Loading…
Reference in New Issue
Block a user