Repository: skiptests/astraea
Pull request #606 (2022-08-21 17:27:16)

Refactor PostRequest
1. 重新排序各個方法
2. 新增`requests`讓使用者可以解析陣列成各個不同的PostRequest


Patch:
diff --git a/app/src/main/java/org/astraea/app/web/PostRequest.java b/app/src/main/java/org/astraea/app/web/PostRequest.java
index 6dd9ca56..2c093cd3 100644
--- a/app/src/main/java/org/astraea/app/web/PostRequest.java
+++ b/app/src/main/java/org/astraea/app/web/PostRequest.java
@@ -38,14 +38,10 @@ public interface PostRequest {
 
   @SuppressWarnings("unchecked")
   static PostRequest of(String json) {
-    return of(
-        ((Map<String, Object>) new Gson().fromJson(json, Map.class))
-            .entrySet().stream()
-                .filter(e -> e.getValue() != null)
-                .collect(Collectors.toMap(Map.Entry::getKey, e -> handleDouble(e.getValue()))));
+    return of((Map<String, Object>) new Gson().fromJson(json, Map.class));
   }
 
-  static String handleDouble(Object obj) {
+  static String handle(Object obj) {
     // the number in GSON is always DOUBLE
     if (obj instanceof Double) {
       var value = (double) obj;
@@ -60,7 +56,11 @@ public interface PostRequest {
     return obj.toString();
   }
 
-  static PostRequest of(Map<String, String> raw) {
+  static PostRequest of(Map<String, Object> objs) {
+    var raw =
+        objs.entrySet().stream()
+            .filter(e -> e.getValue() != null)
+            .collect(Collectors.toMap(Map.Entry::getKey, e -> handle(e.getValue())));
     return () -> raw;
   }
 
@@ -75,72 +75,101 @@ public interface PostRequest {
     return Arrays.stream(keys).allMatch(raw()::containsKey);
   }
 
-  default Optional<String> get(String key) {
-    return Optional.ofNullable(raw().get(key));
+  default <T> Optional<T> get(String key, Class<T> clz) {
+    return Optional.ofNullable(raw().get(key)).map(v -> new Gson().fromJson(v, clz));
   }
 
-  default <T> List<T> values(String key, Class<T> clz) {
-    return new Gson()
-        .fromJson(value(key), TypeToken.getParameterized(ArrayList.class, clz).getType());
+  default <T> T value(String key, Class<T> clz) {
+    return get(key, clz)
+        .orElseThrow(() -> new NoSuchElementException("the value for " + key + " is nonexistent"));
   }
 
-  default <T> T value(String key, Class<T> clz) {
-    return new Gson().fromJson(value(key), TypeToken.get(clz).getType());
+  // -----------------------------[string]-----------------------------//
+
+  default Optional<String> get(String key) {
+    return Optional.ofNullable(raw().get(key));
   }
 
   default String value(String key) {
-    var value = raw().get(key);
-    if (value == null) throw new NoSuchElementException("the value for " + key + " is nonexistent");
-    return value;
+    return get(key)
+        .orElseThrow(() -> new NoSuchElementException("the value for " + key + " is nonexistent"));
   }
 
-  /**
-   * parse the value as a string array
-   *
-   * @param key to search value
-   * @return string array
-   */
   default List<String> values(String key) {
-    return new Gson().fromJson(value(key), new TypeToken<ArrayList<String>>() {}.getType());
+    return values(key, String.class);
   }
 
-  default double doubleValue(String key, double defaultValue) {
-    return get(key).map(Double::parseDouble).orElse(defaultValue);
+  // -----------------------------[boolean]-----------------------------//
+  default Optional<Boolean> getBoolean(String key) {
+    return get(key).map(Boolean::parseBoolean);
   }
 
-  default double doubleValue(String key) {
-    return Double.parseDouble(value(key));
+  // -----------------------------[numbers]-----------------------------//
+
+  default short shortValue(String key) {
+    return Short.parseShort(value(key));
+  }
+
+  default List<Short> shortValues(String key) {
+    return doubleValues(key).stream()
+        .map(Double::shortValue)
+        .collect(Collectors.toUnmodifiableList());
   }
 
-  default int intValue(String key, int defaultValue) {
-    return get(key).map(Integer::parseInt).orElse(defaultValue);
+  default Optional<Short> getShort(String key) {
+    return get(key).map(Short::parseShort);
   }
 
   default int intValue(String key) {
     return Integer.parseInt(value(key));
   }
 
-  default List<Integer> ints(String key) {
-    return values(key).stream()
-        // the number in GSON is always DOUBLE
-        .map(Double::valueOf)
+  default List<Integer> intValues(String key) {
+    return doubleValues(key).stream()
         .map(Double::intValue)
         .collect(Collectors.toUnmodifiableList());
   }
 
-  default short shortValue(String key, short defaultValue) {
-    return get(key).map(Short::parseShort).orElse(defaultValue);
+  default Optional<Integer> getInt(String key) {
+    return get(key).map(Integer::parseInt);
   }
 
-  default short shortValue(String key) {
-    return Short.parseShort(value(key));
+  default long longValue(String key) {
+    return Long.parseLong(value(key));
+  }
+
+  default List<Long> longValues(String key) {
+    return doubleValues(key).stream()
+        .map(Double::longValue)
+        .collect(Collectors.toUnmodifiableList());
+  }
+
+  default Optional<Long> getLong(String key) {
+    return get(key).map(Long::parseLong);
+  }
+
+  default double doubleValue(String key) {
+    return Double.parseDouble(value(key));
+  }
+
+  default List<Double> doubleValues(String key) {
+    return values(key, Double.class);
+  }
+
+  default Optional<Double> getDouble(String key) {
+    return get(key).map(Double::parseDouble);
   }
 
-  default boolean booleanValue(String key, boolean defaultValue) {
-    return get(key).map(Boolean::parseBoolean).orElse(defaultValue);
+  // -----------------------------[others]-----------------------------//
+
+  default <T> List<T> values(String key, Class<T> clz) {
+    return new Gson()
+        .fromJson(value(key), TypeToken.getParameterized(ArrayList.class, clz).getType());
   }
 
-  default boolean booleanValue(String key) {
-    return Boolean.parseBoolean(value(key));
+  default List<PostRequest> requests(String key) {
+    return values(key, Map.class).stream()
+        .map(PostRequest::of)
+        .collect(Collectors.toUnmodifiableList());
   }
 }
diff --git a/app/src/main/java/org/astraea/app/web/QuotaHandler.java b/app/src/main/java/org/astraea/app/web/QuotaHandler.java
index d212e56e..30c0ea40 100644
--- a/app/src/main/java/org/astraea/app/web/QuotaHandler.java
+++ b/app/src/main/java/org/astraea/app/web/QuotaHandler.java
@@ -57,7 +57,7 @@ public class QuotaHandler implements Handler {
       admin
           .quotaCreator()
           .ip(request.value(IP_KEY))
-          .connectionRate(request.intValue(CONNECTION_RATE_KEY, Integer.MAX_VALUE))
+          .connectionRate(request.getInt(CONNECTION_RATE_KEY).orElse(Integer.MAX_VALUE))
           .create();
       return new Quotas(admin.quotas(org.astraea.app.admin.Quota.Target.IP, request.value(IP_KEY)));
     }
diff --git a/app/src/main/java/org/astraea/app/web/ReassignmentHandler.java b/app/src/main/java/org/astraea/app/web/ReassignmentHandler.java
index 862517c1..380340b6 100644
--- a/app/src/main/java/org/astraea/app/web/ReassignmentHandler.java
+++ b/app/src/main/java/org/astraea/app/web/ReassignmentHandler.java
@@ -50,7 +50,7 @@ public class ReassignmentHandler implements Handler {
       admin
           .migrator()
           .partition(request.value(TOPIC_KEY), request.intValue(PARTITION_KEY))
-          .moveTo(request.ints(TO_KEY));
+          .moveTo(request.intValues(TO_KEY));
       return Response.ACCEPT;
     }
     return Response.BAD_REQUEST;
diff --git a/app/src/main/java/org/astraea/app/web/RecordHandler.java b/app/src/main/java/org/astraea/app/web/RecordHandler.java
index d43ee1a5..3ecfab41 100644
--- a/app/src/main/java/org/astraea/app/web/RecordHandler.java
+++ b/app/src/main/java/org/astraea/app/web/RecordHandler.java
@@ -18,7 +18,6 @@ package org.astraea.app.web;
 
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
-import static org.astraea.app.web.PostRequest.handleDouble;
 
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonDeserializationContext;
@@ -158,7 +157,7 @@ public class RecordHandler implements Handler {
 
   @Override
   public Response post(PostRequest request) {
-    var async = request.booleanValue(ASYNC, false);
+    var async = request.getBoolean(ASYNC).orElse(false);
     var timeout = request.get(TIMEOUT).map(DurationField::toDuration).orElse(Duration.ofSeconds(5));
     var records = request.values(RECORDS, PostRecord.class);
     if (records.isEmpty()) {
@@ -300,9 +299,9 @@ public class RecordHandler implements Handler {
             .orElse(SerDe.STRING.serializer);
 
     Optional.ofNullable(postRecord.key)
-        .ifPresent(key -> sender.key(keySerializer.apply(topic, handleDouble(key))));
+        .ifPresent(key -> sender.key(keySerializer.apply(topic, PostRequest.handle(key))));
     Optional.ofNullable(postRecord.value)
-        .ifPresent(value -> sender.value(valueSerializer.apply(topic, handleDouble(value))));
+        .ifPresent(value -> sender.value(valueSerializer.apply(topic, PostRequest.handle(value))));
     Optional.ofNullable(postRecord.timestamp).ifPresent(sender::timestamp);
     Optional.ofNullable(postRecord.partition).ifPresent(sender::partition);
     return sender;
diff --git a/app/src/main/java/org/astraea/app/web/TopicHandler.java b/app/src/main/java/org/astraea/app/web/TopicHandler.java
index e83dd89b..74786dcc 100644
--- a/app/src/main/java/org/astraea/app/web/TopicHandler.java
+++ b/app/src/main/java/org/astraea/app/web/TopicHandler.java
@@ -86,7 +86,10 @@ class TopicHandler implements Handler {
   }
 
   static Map<String, String> remainingConfigs(PostRequest request) {
-    var configs = new HashMap<>(request.raw());
+    var configs =
+        new HashMap<>(
+            request.raw().entrySet().stream()
+                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())));
     configs.remove(TOPIC_NAME_KEY);
     configs.remove(NUMBER_OF_PARTITIONS_KEY);
     configs.remove(NUMBER_OF_REPLICAS_KEY);
@@ -98,8 +101,8 @@ class TopicHandler implements Handler {
     admin
         .creator()
         .topic(request.value(TOPIC_NAME_KEY))
-        .numberOfPartitions(request.intValue(NUMBER_OF_PARTITIONS_KEY, 1))
-        .numberOfReplicas(request.shortValue(NUMBER_OF_REPLICAS_KEY, (short) 1))
+        .numberOfPartitions(request.getInt(NUMBER_OF_PARTITIONS_KEY).orElse(1))
+        .numberOfReplicas(request.getShort(NUMBER_OF_REPLICAS_KEY).orElse((short) 1))
         .configs(remainingConfigs(request))
         .create();
     if (admin.topicNames().contains(request.value(TOPIC_NAME_KEY))) {


Test patch:
diff --git a/app/src/test/java/org/astraea/app/web/PostRequestTest.java b/app/src/test/java/org/astraea/app/web/PostRequestTest.java
index 811854bb..c759d607 100644
--- a/app/src/test/java/org/astraea/app/web/PostRequestTest.java
+++ b/app/src/test/java/org/astraea/app/web/PostRequestTest.java
@@ -22,7 +22,9 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.Optional;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
@@ -43,18 +45,17 @@ public class PostRequestTest {
   }
 
   @Test
-  void testHandleDouble() {
-    Assertions.assertEquals("10", PostRequest.handleDouble(10.00));
-    Assertions.assertEquals("10.01", PostRequest.handleDouble(10.01));
-    Assertions.assertEquals("xxxx", PostRequest.handleDouble("xxxx"));
-    Assertions.assertEquals("{\"foo\":1}", PostRequest.handleDouble(Map.of("foo", 1)));
-    Assertions.assertEquals("[\"a\",1]", PostRequest.handleDouble(List.of("a", 1)));
+  void testHandle() {
+    Assertions.assertEquals("10", PostRequest.handle(10.00));
+    Assertions.assertEquals("10.01", PostRequest.handle(10.01));
+    Assertions.assertEquals("xxxx", PostRequest.handle("xxxx"));
+    Assertions.assertEquals("{\"foo\":1}", PostRequest.handle(Map.of("foo", 1)));
+    Assertions.assertEquals("[\"a\",1]", PostRequest.handle(List.of("a", 1)));
     Assertions.assertEquals(
-        "[{\"foo\":{\"bar\":10}}]",
-        PostRequest.handleDouble(List.of(Map.of("foo", Map.of("bar", 10)))));
+        "[{\"foo\":{\"bar\":10}}]", PostRequest.handle(List.of(Map.of("foo", Map.of("bar", 10)))));
     Assertions.assertEquals(
         "[{\"foo\":{\"bar\":1.1}}]",
-        PostRequest.handleDouble(List.of(Map.of("foo", Map.of("bar", 1.1d)))));
+        PostRequest.handle(List.of(Map.of("foo", Map.of("bar", 1.1d)))));
   }
 
   @Test
@@ -88,9 +89,9 @@ public class PostRequestTest {
     Mockito.when(exchange.getRequestBody()).thenReturn(input);
 
     var request = PostRequest.of(exchange);
-    Assertions.assertEquals(2, request.ints("c").size());
-    Assertions.assertEquals(1, request.ints("c").get(0));
-    Assertions.assertEquals(2, request.ints("c").get(1));
+    Assertions.assertEquals(2, request.intValues("c").size());
+    Assertions.assertEquals(1, request.intValues("c").get(0));
+    Assertions.assertEquals(2, request.intValues("c").get(1));
   }
 
   @Test
@@ -120,7 +121,78 @@ public class PostRequestTest {
     Assertions.assertEquals(new ForTestValue("r1", 1), request.value("a", ForTestValue.class));
   }
 
-  class ForTestValue {
+  @Test
+  void testRequests() {
+    var data = "{\"key\":[{\"a\":\"aa\",\"b\":2}, {\"c\":[\"a\", \"bbb\"]}]}";
+    var elements = PostRequest.of(data).requests("key");
+    Assertions.assertEquals(2, elements.size());
+    Assertions.assertEquals("aa", elements.get(0).value("a"));
+    Assertions.assertEquals(2, elements.get(0).intValue("b"));
+    Assertions.assertEquals(List.of("a", "bbb"), elements.get(1).values("c"));
+  }
+
+  @Test
+  void testStrings() {
+    var data = "{\"key\": \"v\",\"key2\": [\"v2\", \"v3\"]}";
+    Assertions.assertEquals("v", PostRequest.of(data).value("key"));
+    Assertions.assertEquals(List.of("v2", "v3"), PostRequest.of(data).values("key2"));
+    Assertions.assertEquals(Optional.empty(), PostRequest.of(data).get("nonexistent"));
+    Assertions.assertThrows(
+        NoSuchElementException.class, () -> PostRequest.of(data).value("nonexistent"));
+  }
+
+  @Test
+  void testShort() {
+    var data = "{\"key\":1, \"key2\": [1, " + Short.MAX_VALUE + "]}";
+    Assertions.assertEquals(1, PostRequest.of(data).shortValue("key"));
+    Assertions.assertEquals(
+        List.of((short) 1, Short.MAX_VALUE), PostRequest.of(data).shortValues("key2"));
+    Assertions.assertEquals(Optional.empty(), PostRequest.of(data).getShort("nonexistent"));
+    Assertions.assertThrows(
+        NoSuchElementException.class, () -> PostRequest.of(data).shortValue("nonexistent"));
+  }
+
+  @Test
+  void testInt() {
+    var data = "{\"key\":1, \"key2\": [1, " + Integer.MAX_VALUE + "]}";
+    Assertions.assertEquals(1, PostRequest.of(data).intValue("key"));
+    Assertions.assertEquals(List.of(1, Integer.MAX_VALUE), PostRequest.of(data).intValues("key2"));
+    Assertions.assertEquals(Optional.empty(), PostRequest.of(data).getInt("nonexistent"));
+    Assertions.assertThrows(
+        NoSuchElementException.class, () -> PostRequest.of(data).intValue("nonexistent"));
+  }
+
+  @Test
+  void testLong() {
+    var data = "{\"key\":1, \"key2\": [1, " + Long.MAX_VALUE + "]}";
+    Assertions.assertEquals(1, PostRequest.of(data).longValue("key"));
+    Assertions.assertEquals(List.of(1L, Long.MAX_VALUE), PostRequest.of(data).longValues("key2"));
+    Assertions.assertEquals(Optional.empty(), PostRequest.of(data).getLong("nonexistent"));
+    Assertions.assertThrows(
+        NoSuchElementException.class, () -> PostRequest.of(data).longValue("nonexistent"));
+  }
+
+  @Test
+  void testDouble() {
+    var data = "{\"key\":1, \"key2\": [1, " + Double.MAX_VALUE + "]}";
+    Assertions.assertEquals(1, PostRequest.of(data).doubleValue("key"));
+    Assertions.assertEquals(
+        List.of(1D, Double.MAX_VALUE), PostRequest.of(data).doubleValues("key2"));
+    Assertions.assertEquals(Optional.empty(), PostRequest.of(data).getDouble("nonexistent"));
+    Assertions.assertThrows(
+        NoSuchElementException.class, () -> PostRequest.of(data).doubleValue("nonexistent"));
+  }
+
+  @Test
+  void testHas() {
+    var data = "{\"key\":1, \"key2\": \"a\"}";
+    Assertions.assertTrue(PostRequest.of(data).has("key"));
+    Assertions.assertTrue(PostRequest.of(data).has("key", "key2"));
+    Assertions.assertFalse(PostRequest.of(data).has("key", "key2", "key3"));
+    Assertions.assertFalse(PostRequest.of(data).has("xxx"));
+  }
+
+  static class ForTestValue {
     final String foo;
     final Integer bar;