From be7eb8928e1f0a6a71ac392f344ed95d862bc2c8 Mon Sep 17 00:00:00 2001 From: "adam.mitchell" Date: Thu, 19 Nov 2020 17:20:35 +0000 Subject: [PATCH 1/4] add support and tests for non-string Map keys --- src/main/java/io/vavr/gson/MapConverter.java | 30 +++++++++++++++---- src/main/java/io/vavr/gson/VavrGson.java | 6 ++-- .../java/io/vavr/gson/map/HashMapTest.java | 10 +++++++ .../io/vavr/gson/map/LinkedHashMapTest.java | 11 +++++++ .../java/io/vavr/gson/map/MapLikeTest.java | 16 ++++++++++ src/test/java/io/vavr/gson/map/MapTest.java | 11 +++++++ .../java/io/vavr/gson/map/SortedMapTest.java | 11 +++++++ .../java/io/vavr/gson/map/TreeMapTest.java | 11 +++++++ 8 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/vavr/gson/MapConverter.java b/src/main/java/io/vavr/gson/MapConverter.java index 9724f64..1fe0f85 100644 --- a/src/main/java/io/vavr/gson/MapConverter.java +++ b/src/main/java/io/vavr/gson/MapConverter.java @@ -12,27 +12,45 @@ import io.vavr.collection.Map; import java.lang.reflect.Type; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; class MapConverter> extends JsonObjectConverter { - private final Function>, Map> factory; + private final Function>, Map> factory; - MapConverter(Function>, Map> factory) { + MapConverter(Function>, Map> factory) { this.factory = factory; } @Override @SuppressWarnings("unchecked") T fromJsonObject(JsonObject obj, Type type, Type[] subTypes, JsonDeserializationContext ctx) throws JsonParseException { - Function, Tuple2> mapper; + Function, Tuple2> mapper; if (subTypes.length == 2) { - mapper = e -> Tuple.of(ctx.deserialize(new JsonPrimitive(e.getKey()), subTypes[0]), ctx.deserialize(e.getValue(), subTypes[1])); + mapper = e -> convert(ctx, e, subTypes[0], subTypes[1]); } else { - mapper = e -> Tuple.of(e.getKey(), e.getValue()); + mapper = e -> Tuple.of(ctx.deserialize(new JsonPrimitive(e.getKey()), subTypes[0]), e.getValue().getClass()); } - return (T) factory.apply(obj.entrySet().stream().map(mapper).collect(Collectors.toList())); + final Set> entries = obj.entrySet(); + final Stream> tuple2Stream = entries.stream().map(mapper); + final Iterable> collect = tuple2Stream.collect(Collectors.toList()); + return (T) factory.apply(collect); + } + + private Tuple2 convert(JsonDeserializationContext ctx, java.util.Map.Entry e, Type keyType) { + final JsonPrimitive json = new JsonPrimitive(e.getKey()); + final K deserialize = ctx.deserialize(json, keyType); + return Tuple.of(deserialize, e.getValue()); + } + + private Tuple2 convert(JsonDeserializationContext ctx, java.util.Map.Entry e, Type keyType, Type valueType) { + final JsonPrimitive json = new JsonPrimitive(e.getKey()); + final K deserialize = ctx.deserialize(json, keyType); + final V deserialize1 = ctx.deserialize(e.getValue(), valueType); + return Tuple.of(deserialize, deserialize1); } @Override diff --git a/src/main/java/io/vavr/gson/VavrGson.java b/src/main/java/io/vavr/gson/VavrGson.java index 64398fd..a2eb75d 100644 --- a/src/main/java/io/vavr/gson/VavrGson.java +++ b/src/main/java/io/vavr/gson/VavrGson.java @@ -9,6 +9,7 @@ import java.lang.reflect.Type; import java.util.Arrays; import java.util.Objects; +import java.util.function.Function; import com.google.gson.GsonBuilder; import io.vavr.*; @@ -376,12 +377,13 @@ public static Map multiMapTypeAdapters() { } public static Map mapTypeAdapters() { + final Function>, Map> ofEntries2 = i->TreeMap.ofEntries((a,b)->((Comparable)a).compareTo(b), i); return API.Map() .put(Map.class, new MapConverter<>(HashMap::ofEntries)) - .put(SortedMap.class, new MapConverter<>(TreeMap::ofEntries)) + .put(SortedMap.class, new MapConverter<>(ofEntries2)) .put(HashMap.class, new MapConverter<>(HashMap::ofEntries)) .put(LinkedHashMap.class, new MapConverter<>(LinkedHashMap::ofEntries)) - .put(TreeMap.class, new MapConverter<>(TreeMap::ofEntries)); + .put(TreeMap.class, new MapConverter<>(ofEntries2)); } public static Map collectionTypeAdapters() { diff --git a/src/test/java/io/vavr/gson/map/HashMapTest.java b/src/test/java/io/vavr/gson/map/HashMapTest.java index 6c8cf6d..90cc3df 100644 --- a/src/test/java/io/vavr/gson/map/HashMapTest.java +++ b/src/test/java/io/vavr/gson/map/HashMapTest.java @@ -21,8 +21,18 @@ Type type() { return new TypeToken>(){}.getType(); } + @Override + Type intType() { + return new TypeToken>(){}.getType(); + } + @Override Type typeWithNestedType() { return new TypeToken>>(){}.getType(); } + + @Override + Type typeWithNestedIntType() { + return new TypeToken>>(){}.getType(); + } } diff --git a/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java b/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java index db9d02a..1e784aa 100644 --- a/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java +++ b/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java @@ -1,6 +1,7 @@ package io.vavr.gson.map; import com.google.gson.reflect.TypeToken; +import io.vavr.collection.HashMap; import io.vavr.collection.LinkedHashMap; import java.lang.reflect.Type; @@ -25,4 +26,14 @@ Type type() { Type typeWithNestedType() { return new TypeToken>>(){}.getType(); } + + @Override + Type intType() { + return new TypeToken>(){}.getType(); + } + + @Override + Type typeWithNestedIntType() { + return new TypeToken>>(){}.getType(); + } } diff --git a/src/test/java/io/vavr/gson/map/MapLikeTest.java b/src/test/java/io/vavr/gson/map/MapLikeTest.java index 0aa726c..995c4f0 100644 --- a/src/test/java/io/vavr/gson/map/MapLikeTest.java +++ b/src/test/java/io/vavr/gson/map/MapLikeTest.java @@ -13,7 +13,9 @@ public abstract class MapLikeTest> extends AbstractTest { abstract T of(Object key, Object value); abstract Class clz(); abstract Type type(); + abstract Type intType(); abstract Type typeWithNestedType(); + abstract Type typeWithNestedIntType(); @Test(expected = JsonParseException.class) public void badJson() { @@ -41,6 +43,13 @@ public void deserialize() { assert map.get("1").get() == 2; } + @Test + public void deserializeIntegerKey() { + Map map = gson.fromJson("{\"1\":2}", intType()); + assert clz().isAssignableFrom(map.getClass()); + assert map.get(1).get() == 2; + } + @Test public void deserializeWithCast() { Map map = gson.fromJson("{\"1\":\"2\"}", type()); @@ -54,4 +63,11 @@ public void deserializeNested() { assert clz().isAssignableFrom(map.get("1").get().getClass()); assert map.get("1").get().get("2").get() == 3; } + + @Test + public void deserializeNestedIntegerKey() { + Map> map = gson.fromJson("{\"1\":{\"2\":3}}", typeWithNestedIntType()); + assert clz().isAssignableFrom(map.get("1").get().getClass()); + assert map.get("1").get().get(2).get() == 3; + } } diff --git a/src/test/java/io/vavr/gson/map/MapTest.java b/src/test/java/io/vavr/gson/map/MapTest.java index 5a85958..1fd8f33 100644 --- a/src/test/java/io/vavr/gson/map/MapTest.java +++ b/src/test/java/io/vavr/gson/map/MapTest.java @@ -2,6 +2,7 @@ import com.google.gson.reflect.TypeToken; import io.vavr.collection.HashMap; +import io.vavr.collection.LinkedHashMap; import io.vavr.collection.Map; import java.lang.reflect.Type; @@ -26,4 +27,14 @@ Type type() { Type typeWithNestedType() { return new TypeToken>>(){}.getType(); } + + @Override + Type intType() { + return new TypeToken>(){}.getType(); + } + + @Override + Type typeWithNestedIntType() { + return new TypeToken>>(){}.getType(); + } } diff --git a/src/test/java/io/vavr/gson/map/SortedMapTest.java b/src/test/java/io/vavr/gson/map/SortedMapTest.java index 051db02..df60c39 100644 --- a/src/test/java/io/vavr/gson/map/SortedMapTest.java +++ b/src/test/java/io/vavr/gson/map/SortedMapTest.java @@ -1,6 +1,7 @@ package io.vavr.gson.map; import com.google.gson.reflect.TypeToken; +import io.vavr.collection.LinkedHashMap; import io.vavr.collection.SortedMap; import io.vavr.collection.TreeMap; @@ -27,4 +28,14 @@ Type type() { Type typeWithNestedType() { return new TypeToken>>(){}.getType(); } + + @Override + Type intType() { + return new TypeToken>(){}.getType(); + } + + @Override + Type typeWithNestedIntType() { + return new TypeToken>>(){}.getType(); + } } diff --git a/src/test/java/io/vavr/gson/map/TreeMapTest.java b/src/test/java/io/vavr/gson/map/TreeMapTest.java index 530c997..94ca025 100644 --- a/src/test/java/io/vavr/gson/map/TreeMapTest.java +++ b/src/test/java/io/vavr/gson/map/TreeMapTest.java @@ -1,6 +1,7 @@ package io.vavr.gson.map; import com.google.gson.reflect.TypeToken; +import io.vavr.collection.LinkedHashMap; import io.vavr.collection.TreeMap; import java.lang.reflect.Type; @@ -26,4 +27,14 @@ Type type() { Type typeWithNestedType() { return new TypeToken>>(){}.getType(); } + + @Override + Type intType() { + return new TypeToken>(){}.getType(); + } + + @Override + Type typeWithNestedIntType() { + return new TypeToken>>(){}.getType(); + } } From b932f49eefab5dc1a51fd96d2015d009b7a453d0 Mon Sep 17 00:00:00 2001 From: "adam.mitchell" Date: Thu, 19 Nov 2020 17:34:28 +0000 Subject: [PATCH 2/4] fix type conversion --- src/main/java/io/vavr/gson/MapConverter.java | 26 +++++++++++--------- src/main/java/io/vavr/gson/VavrGson.java | 10 +++++--- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/vavr/gson/MapConverter.java b/src/main/java/io/vavr/gson/MapConverter.java index 1fe0f85..f0fa1c1 100644 --- a/src/main/java/io/vavr/gson/MapConverter.java +++ b/src/main/java/io/vavr/gson/MapConverter.java @@ -6,37 +6,39 @@ */ package io.vavr.gson; -import com.google.gson.*; -import io.vavr.Tuple; -import io.vavr.Tuple2; -import io.vavr.collection.Map; - import java.lang.reflect.Type; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import io.vavr.Tuple; +import io.vavr.Tuple2; +import io.vavr.collection.Map; class MapConverter> extends JsonObjectConverter { - private final Function>, Map> factory; + private final Function>, Map> factory; - MapConverter(Function>, Map> factory) { + MapConverter(Function>, Map> factory) { this.factory = factory; } @Override @SuppressWarnings("unchecked") T fromJsonObject(JsonObject obj, Type type, Type[] subTypes, JsonDeserializationContext ctx) throws JsonParseException { - Function, Tuple2> mapper; + Function, Tuple2> mapper; if (subTypes.length == 2) { mapper = e -> convert(ctx, e, subTypes[0], subTypes[1]); } else { - mapper = e -> Tuple.of(ctx.deserialize(new JsonPrimitive(e.getKey()), subTypes[0]), e.getValue().getClass()); + mapper = e -> Tuple.of((K) e.getKey(), (V) e.getValue()); } final Set> entries = obj.entrySet(); - final Stream> tuple2Stream = entries.stream().map(mapper); - final Iterable> collect = tuple2Stream.collect(Collectors.toList()); + final Iterable> collect = entries.stream().map(mapper).collect(Collectors.toList()); return (T) factory.apply(collect); } diff --git a/src/main/java/io/vavr/gson/VavrGson.java b/src/main/java/io/vavr/gson/VavrGson.java index a2eb75d..1cb419c 100644 --- a/src/main/java/io/vavr/gson/VavrGson.java +++ b/src/main/java/io/vavr/gson/VavrGson.java @@ -377,13 +377,17 @@ public static Map multiMapTypeAdapters() { } public static Map mapTypeAdapters() { - final Function>, Map> ofEntries2 = i->TreeMap.ofEntries((a,b)->((Comparable)a).compareTo(b), i); return API.Map() .put(Map.class, new MapConverter<>(HashMap::ofEntries)) - .put(SortedMap.class, new MapConverter<>(ofEntries2)) + .put(SortedMap.class, new MapConverter<>(VavrGson::getTreeMapMapper)) .put(HashMap.class, new MapConverter<>(HashMap::ofEntries)) .put(LinkedHashMap.class, new MapConverter<>(LinkedHashMap::ofEntries)) - .put(TreeMap.class, new MapConverter<>(ofEntries2)); + .put(TreeMap.class, new MapConverter<>(VavrGson::getTreeMapMapper)); + } + + + private static TreeMap getTreeMapMapper(Iterable> i) { + return TreeMap.ofEntries((a, b) -> ((Comparable) a).compareTo(b), i); } public static Map collectionTypeAdapters() { From 975adeb0ce8c1fb44e5971cbc9a2317270222b67 Mon Sep 17 00:00:00 2001 From: "adam.mitchell" Date: Fri, 20 Nov 2020 14:53:36 +0000 Subject: [PATCH 3/4] update to fully support complex map keys, rename abstract classes to better represent structure as now maps can be array types --- gradle.properties | 2 +- ...Converter.java => ArrayTypeConverter.java} | 2 +- src/main/java/io/vavr/gson/MapConverter.java | 53 ++++++++++++--- ...ctConverter.java => MapTypeConverter.java} | 29 ++++++-- .../java/io/vavr/gson/MultimapConverter.java | 27 +++++--- .../java/io/vavr/gson/OptionConverter.java | 2 +- .../io/vavr/gson/TraversableConverter.java | 2 +- .../java/io/vavr/gson/TupleConverter.java | 2 +- src/test/java/io/vavr/gson/AbstractTest.java | 1 + .../java/io/vavr/gson/map/HashMapTest.java | 39 +++++++++-- .../io/vavr/gson/map/LinkedHashMapTest.java | 6 ++ .../java/io/vavr/gson/map/MapLikeTest.java | 68 ++++++++++++++++++- src/test/java/io/vavr/gson/map/MapTest.java | 24 +++++-- .../java/io/vavr/gson/map/SortedMapTest.java | 24 +++++-- .../java/io/vavr/gson/map/TreeMapTest.java | 24 +++++-- 15 files changed, 249 insertions(+), 56 deletions(-) rename src/main/java/io/vavr/gson/{JsonArrayConverter.java => ArrayTypeConverter.java} (96%) rename src/main/java/io/vavr/gson/{JsonObjectConverter.java => MapTypeConverter.java} (59%) diff --git a/gradle.properties b/gradle.properties index 3568e6e..a5cb525 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ junitVersion=4.12 -vavrVersion=1.0.0-SNAPSHOT +vavrVersion=0.10.3 gsonVersion=2.8.0 diff --git a/src/main/java/io/vavr/gson/JsonArrayConverter.java b/src/main/java/io/vavr/gson/ArrayTypeConverter.java similarity index 96% rename from src/main/java/io/vavr/gson/JsonArrayConverter.java rename to src/main/java/io/vavr/gson/ArrayTypeConverter.java index 7ae3f51..298a540 100644 --- a/src/main/java/io/vavr/gson/JsonArrayConverter.java +++ b/src/main/java/io/vavr/gson/ArrayTypeConverter.java @@ -12,7 +12,7 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -abstract class JsonArrayConverter implements JsonSerializer, JsonDeserializer { +abstract class ArrayTypeConverter implements JsonSerializer, JsonDeserializer { private static final Type[] EMPTY_TYPES = new Type[0]; diff --git a/src/main/java/io/vavr/gson/MapConverter.java b/src/main/java/io/vavr/gson/MapConverter.java index f0fa1c1..9632d17 100644 --- a/src/main/java/io/vavr/gson/MapConverter.java +++ b/src/main/java/io/vavr/gson/MapConverter.java @@ -6,21 +6,26 @@ */ package io.vavr.gson; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; +import com.google.gson.reflect.TypeToken; import io.vavr.Tuple; import io.vavr.Tuple2; import io.vavr.collection.Map; +import io.vavr.collection.Traversable; +import io.vavr.collection.Vector; -class MapConverter> extends JsonObjectConverter { +class MapConverter> extends MapTypeConverter { private final Function>, Map> factory; @@ -42,21 +47,53 @@ T fromJsonObject(JsonObject obj, Type type, Type[] subTypes, JsonDeserialization return (T) factory.apply(collect); } - private Tuple2 convert(JsonDeserializationContext ctx, java.util.Map.Entry e, Type keyType) { - final JsonPrimitive json = new JsonPrimitive(e.getKey()); - final K deserialize = ctx.deserialize(json, keyType); - return Tuple.of(deserialize, e.getValue()); + @Override + @SuppressWarnings("unchecked") + T fromJsonArray(JsonArray arr, Type type, Type[] subTypes, JsonDeserializationContext ctx) throws JsonParseException { + final Awkward tuple2KV = new Awkward(Tuple2.class, subTypes); + final Type traversableType = new TypeToken>() { + }.getType(); + final Awkward traversableTuple2KV = new Awkward(traversableType, tuple2KV); + final TraversableConverter>> traversableConverter = new TraversableConverter<>(Vector::ofAll); + final Traversable> tuple2s = traversableConverter.fromJsonArray(arr, traversableTuple2KV, traversableTuple2KV.getActualTypeArguments(), ctx); + return (T) factory.apply(tuple2s); } private Tuple2 convert(JsonDeserializationContext ctx, java.util.Map.Entry e, Type keyType, Type valueType) { final JsonPrimitive json = new JsonPrimitive(e.getKey()); - final K deserialize = ctx.deserialize(json, keyType); - final V deserialize1 = ctx.deserialize(e.getValue(), valueType); - return Tuple.of(deserialize, deserialize1); + final K key = ctx.deserialize(json, keyType); + final V value = ctx.deserialize(e.getValue(), valueType); + return Tuple.of(key, value); } @Override Map toMap(T src) { return src; } + + private static final class Awkward implements ParameterizedType { + + private final Type rawType; + private final Type[] subTypes; + + private Awkward(Type rawType, Type... subTypes) { + this.rawType = rawType; + this.subTypes = subTypes; + } + + @Override + public Type[] getActualTypeArguments() { + return subTypes; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public Type getOwnerType() { + return null; + } + } } diff --git a/src/main/java/io/vavr/gson/JsonObjectConverter.java b/src/main/java/io/vavr/gson/MapTypeConverter.java similarity index 59% rename from src/main/java/io/vavr/gson/JsonObjectConverter.java rename to src/main/java/io/vavr/gson/MapTypeConverter.java index 20047cb..e506f50 100644 --- a/src/main/java/io/vavr/gson/JsonObjectConverter.java +++ b/src/main/java/io/vavr/gson/MapTypeConverter.java @@ -6,17 +6,27 @@ */ package io.vavr.gson; -import com.google.gson.*; -import io.vavr.collection.Map; - import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -abstract class JsonObjectConverter implements JsonSerializer, JsonDeserializer { +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import io.vavr.collection.Map; + +abstract class MapTypeConverter implements JsonSerializer, JsonDeserializer { private static final Type[] EMPTY_TYPES = new Type[0]; abstract T fromJsonObject(JsonObject arr, Type type, Type[] subTypes, JsonDeserializationContext ctx) throws JsonParseException; + + abstract T fromJsonArray(JsonArray arr, Type type, Type[] subTypes, JsonDeserializationContext ctx) throws JsonParseException; + abstract Map toMap(T src); @Override @@ -29,8 +39,17 @@ public T deserialize(JsonElement json, Type type, JsonDeserializationContext ctx } else { return fromJsonObject(json.getAsJsonObject(), type, EMPTY_TYPES, ctx); } + } else if (json.isJsonArray()) { + // list of tuples + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + Type[] types = parameterizedType.getActualTypeArguments(); + return fromJsonArray(json.getAsJsonArray(), parameterizedType, types, ctx); + } else { + throw new JsonParseException("unsupported non-parameterised types for complex map keys"); + } } else { - throw new JsonParseException("object expected"); + throw new JsonParseException("object or array expected"); } } diff --git a/src/main/java/io/vavr/gson/MultimapConverter.java b/src/main/java/io/vavr/gson/MultimapConverter.java index 36b94f4..8554569 100644 --- a/src/main/java/io/vavr/gson/MultimapConverter.java +++ b/src/main/java/io/vavr/gson/MultimapConverter.java @@ -6,7 +6,18 @@ */ package io.vavr.gson; -import com.google.gson.*; +import java.lang.reflect.Type; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; import io.vavr.Function1; import io.vavr.Tuple; import io.vavr.Tuple2; @@ -15,13 +26,7 @@ import io.vavr.collection.Map; import io.vavr.collection.Multimap; -import java.lang.reflect.Type; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -class MultimapConverter> extends JsonObjectConverter { +class MultimapConverter> extends MapTypeConverter { private final Function>, Multimap> factory; @@ -41,6 +46,12 @@ T fromJsonObject(JsonObject obj, Type type, Type[] subTypes, JsonDeserialization return (T) factory.apply(obj.entrySet().stream().flatMap(mapper).collect(Collectors.toList())); } + @Override + @SuppressWarnings("unchecked") + T fromJsonArray(JsonArray arr, Type type, Type[] subTypes, JsonDeserializationContext ctx) throws JsonParseException { + throw new UnsupportedOperationException("Array types not supported for Multimaps"); + } + @Override @SuppressWarnings("unchecked") Map> toMap(T src) { diff --git a/src/main/java/io/vavr/gson/OptionConverter.java b/src/main/java/io/vavr/gson/OptionConverter.java index cd6d848..7f6dea7 100644 --- a/src/main/java/io/vavr/gson/OptionConverter.java +++ b/src/main/java/io/vavr/gson/OptionConverter.java @@ -15,7 +15,7 @@ import java.lang.reflect.Type; -class OptionConverter extends JsonArrayConverter> { +class OptionConverter extends ArrayTypeConverter> { @Override Option fromJsonArray(JsonArray arr, Type type, Type[] subTypes, JsonDeserializationContext ctx) throws JsonParseException { diff --git a/src/main/java/io/vavr/gson/TraversableConverter.java b/src/main/java/io/vavr/gson/TraversableConverter.java index ed632c0..d0f2552 100644 --- a/src/main/java/io/vavr/gson/TraversableConverter.java +++ b/src/main/java/io/vavr/gson/TraversableConverter.java @@ -15,7 +15,7 @@ import java.lang.reflect.Type; import java.util.function.Function; -class TraversableConverter> extends JsonArrayConverter { +class TraversableConverter> extends ArrayTypeConverter { private final Function, Traversable> factory; diff --git a/src/main/java/io/vavr/gson/TupleConverter.java b/src/main/java/io/vavr/gson/TupleConverter.java index a0c986c..c443d62 100644 --- a/src/main/java/io/vavr/gson/TupleConverter.java +++ b/src/main/java/io/vavr/gson/TupleConverter.java @@ -16,7 +16,7 @@ import java.lang.reflect.Type; -abstract class TupleConverter extends JsonArrayConverter { +abstract class TupleConverter extends ArrayTypeConverter { static class N0 extends TupleConverter { diff --git a/src/test/java/io/vavr/gson/AbstractTest.java b/src/test/java/io/vavr/gson/AbstractTest.java index a69701e..1e3d2a4 100644 --- a/src/test/java/io/vavr/gson/AbstractTest.java +++ b/src/test/java/io/vavr/gson/AbstractTest.java @@ -12,6 +12,7 @@ public class AbstractTest { public static void before() { GsonBuilder builder = new GsonBuilder(); VavrGson.registerAll(builder); + builder = builder.enableComplexMapKeySerialization(); gson = builder.create(); } diff --git a/src/test/java/io/vavr/gson/map/HashMapTest.java b/src/test/java/io/vavr/gson/map/HashMapTest.java index 90cc3df..a8e5de8 100644 --- a/src/test/java/io/vavr/gson/map/HashMapTest.java +++ b/src/test/java/io/vavr/gson/map/HashMapTest.java @@ -1,11 +1,14 @@ package io.vavr.gson.map; +import java.lang.reflect.Type; + import com.google.gson.reflect.TypeToken; import io.vavr.collection.HashMap; +import io.vavr.collection.Map; +import org.junit.Test; -import java.lang.reflect.Type; +public class HashMapTest extends MapLikeTest> { -public class HashMapTest extends MapLikeTest> { @Override HashMap of(Object key, Object value) { return HashMap.of(key, value); @@ -18,21 +21,45 @@ Class clz() { @Override Type type() { - return new TypeToken>(){}.getType(); + return new TypeToken>() { + }.getType(); } @Override Type intType() { - return new TypeToken>(){}.getType(); + return new TypeToken>() { + }.getType(); } @Override Type typeWithNestedType() { - return new TypeToken>>(){}.getType(); + return new TypeToken>>() { + }.getType(); } @Override Type typeWithNestedIntType() { - return new TypeToken>>(){}.getType(); + return new TypeToken>>() { + }.getType(); + } + + @Override + Type getComplexNestedKeyType() { + return new TypeToken>>() { + }.getType(); + } + + @Test + public void deserializeLongKey() { + Map map = gson.fromJson("{\"1\":2}", new TypeToken>(){}.getType()); + assert clz().isAssignableFrom(map.getClass()); + assert map.get(1L).get() == 2; + } + + @Test + public void deserializeDoubleKey() { + Map map = gson.fromJson("{\"1.323\":2}", new TypeToken>(){}.getType()); + assert clz().isAssignableFrom(map.getClass()); + assert map.get(1.323D).get() == 2; } } diff --git a/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java b/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java index 1e784aa..32d7236 100644 --- a/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java +++ b/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java @@ -36,4 +36,10 @@ Type intType() { Type typeWithNestedIntType() { return new TypeToken>>(){}.getType(); } + + @Override + Type getComplexNestedKeyType() { + return new TypeToken>>() { + }.getType(); + } } diff --git a/src/test/java/io/vavr/gson/map/MapLikeTest.java b/src/test/java/io/vavr/gson/map/MapLikeTest.java index 995c4f0..542ad29 100644 --- a/src/test/java/io/vavr/gson/map/MapLikeTest.java +++ b/src/test/java/io/vavr/gson/map/MapLikeTest.java @@ -1,22 +1,31 @@ package io.vavr.gson.map; +import java.lang.reflect.Type; +import java.util.Comparator; +import java.util.Objects; + import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; import io.vavr.collection.Map; import io.vavr.gson.AbstractTest; import org.junit.Test; -import java.lang.reflect.Type; - -public abstract class MapLikeTest> extends AbstractTest { +public abstract class MapLikeTest> extends AbstractTest { abstract T of(Object key, Object value); + abstract Class clz(); + abstract Type type(); + abstract Type intType(); + abstract Type typeWithNestedType(); + abstract Type typeWithNestedIntType(); + abstract Type getComplexNestedKeyType(); + @Test(expected = JsonParseException.class) public void badJson() { gson.fromJson("1", type()); @@ -70,4 +79,57 @@ public void deserializeNestedIntegerKey() { assert clz().isAssignableFrom(map.get("1").get().getClass()); assert map.get("1").get().get(2).get() == 3; } + + @Test + public void deserializeNestedComplexKey() { + final CustomKey innerKey = new CustomKey(3, 4); + final CustomKey outerKey = new CustomKey(1, 2); + Map> map = gson.fromJson("[[{\"a\":1,\"b\":2},[[{\"a\":3,\"b\":4},5]]]]", getComplexNestedKeyType()); + assert clz().isAssignableFrom(map.get(outerKey).get().getClass()); + assert map.get(outerKey).get().get(innerKey).get() == 5; + } + + static final class CustomKey implements Comparable { + + private final int a; + private final int b; + + public CustomKey(int a, int b) { + this.a = a; + this.b = b; + } + + public int getA() { + return a; + } + + public int getB() { + return b; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CustomKey customKey = (CustomKey) o; + return a == customKey.a && + b == customKey.b; + } + + @Override + public int hashCode() { + return Objects.hash(a, b); + } + + @Override + public int compareTo(CustomKey o) { + return Comparator.comparingInt(CustomKey::getA) + .thenComparing(CustomKey::getB) + .compare(this, o); + } + } } diff --git a/src/test/java/io/vavr/gson/map/MapTest.java b/src/test/java/io/vavr/gson/map/MapTest.java index 1fd8f33..9e9b59e 100644 --- a/src/test/java/io/vavr/gson/map/MapTest.java +++ b/src/test/java/io/vavr/gson/map/MapTest.java @@ -1,13 +1,13 @@ package io.vavr.gson.map; +import java.lang.reflect.Type; + import com.google.gson.reflect.TypeToken; import io.vavr.collection.HashMap; -import io.vavr.collection.LinkedHashMap; import io.vavr.collection.Map; -import java.lang.reflect.Type; +public class MapTest extends MapLikeTest> { -public class MapTest extends MapLikeTest> { @Override Map of(Object key, Object value) { return HashMap.of(key, value); @@ -20,21 +20,31 @@ Class clz() { @Override Type type() { - return new TypeToken>(){}.getType(); + return new TypeToken>() { + }.getType(); } @Override Type typeWithNestedType() { - return new TypeToken>>(){}.getType(); + return new TypeToken>>() { + }.getType(); } @Override Type intType() { - return new TypeToken>(){}.getType(); + return new TypeToken>() { + }.getType(); } @Override Type typeWithNestedIntType() { - return new TypeToken>>(){}.getType(); + return new TypeToken>>() { + }.getType(); + } + + @Override + Type getComplexNestedKeyType() { + return new TypeToken>>() { + }.getType(); } } diff --git a/src/test/java/io/vavr/gson/map/SortedMapTest.java b/src/test/java/io/vavr/gson/map/SortedMapTest.java index df60c39..fc80169 100644 --- a/src/test/java/io/vavr/gson/map/SortedMapTest.java +++ b/src/test/java/io/vavr/gson/map/SortedMapTest.java @@ -1,13 +1,13 @@ package io.vavr.gson.map; +import java.lang.reflect.Type; + import com.google.gson.reflect.TypeToken; -import io.vavr.collection.LinkedHashMap; import io.vavr.collection.SortedMap; import io.vavr.collection.TreeMap; -import java.lang.reflect.Type; +public class SortedMapTest extends MapLikeTest> { -public class SortedMapTest extends MapLikeTest> { @Override @SuppressWarnings("unchecked") SortedMap of(Object key, Object value) { @@ -21,21 +21,31 @@ Class clz() { @Override Type type() { - return new TypeToken>(){}.getType(); + return new TypeToken>() { + }.getType(); } @Override Type typeWithNestedType() { - return new TypeToken>>(){}.getType(); + return new TypeToken>>() { + }.getType(); } @Override Type intType() { - return new TypeToken>(){}.getType(); + return new TypeToken>() { + }.getType(); } @Override Type typeWithNestedIntType() { - return new TypeToken>>(){}.getType(); + return new TypeToken>>() { + }.getType(); + } + + @Override + Type getComplexNestedKeyType() { + return new TypeToken>>() { + }.getType(); } } diff --git a/src/test/java/io/vavr/gson/map/TreeMapTest.java b/src/test/java/io/vavr/gson/map/TreeMapTest.java index 94ca025..5664b9c 100644 --- a/src/test/java/io/vavr/gson/map/TreeMapTest.java +++ b/src/test/java/io/vavr/gson/map/TreeMapTest.java @@ -1,12 +1,12 @@ package io.vavr.gson.map; +import java.lang.reflect.Type; + import com.google.gson.reflect.TypeToken; -import io.vavr.collection.LinkedHashMap; import io.vavr.collection.TreeMap; -import java.lang.reflect.Type; +public class TreeMapTest extends MapLikeTest> { -public class TreeMapTest extends MapLikeTest> { @Override @SuppressWarnings("unchecked") TreeMap of(Object key, Object value) { @@ -20,21 +20,31 @@ Class clz() { @Override Type type() { - return new TypeToken>(){}.getType(); + return new TypeToken>() { + }.getType(); } @Override Type typeWithNestedType() { - return new TypeToken>>(){}.getType(); + return new TypeToken>>() { + }.getType(); } @Override Type intType() { - return new TypeToken>(){}.getType(); + return new TypeToken>() { + }.getType(); } @Override Type typeWithNestedIntType() { - return new TypeToken>>(){}.getType(); + return new TypeToken>>() { + }.getType(); + } + + @Override + Type getComplexNestedKeyType() { + return new TypeToken>>() { + }.getType(); } } From 9a8da5b9b08e4e7ccf49320b3575f4762708757e Mon Sep 17 00:00:00 2001 From: "adam.mitchell" Date: Fri, 20 Nov 2020 16:01:33 +0000 Subject: [PATCH 4/4] fully support complex key serialisation also --- .../java/io/vavr/gson/MapTypeConverter.java | 26 ++++++++++++++++--- .../java/io/vavr/gson/map/HashMapTest.java | 5 ++++ .../io/vavr/gson/map/LinkedHashMapTest.java | 24 ++++++++++++----- .../java/io/vavr/gson/map/MapLikeTest.java | 14 ++++++++++ src/test/java/io/vavr/gson/map/MapTest.java | 5 ++++ .../java/io/vavr/gson/map/SortedMapTest.java | 11 ++++++++ .../java/io/vavr/gson/map/TreeMapTest.java | 11 ++++++++ 7 files changed, 85 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/vavr/gson/MapTypeConverter.java b/src/main/java/io/vavr/gson/MapTypeConverter.java index e506f50..0730009 100644 --- a/src/main/java/io/vavr/gson/MapTypeConverter.java +++ b/src/main/java/io/vavr/gson/MapTypeConverter.java @@ -19,6 +19,8 @@ import com.google.gson.JsonSerializer; import io.vavr.collection.Map; +import static jdk.nashorn.internal.runtime.JSType.isPrimitive; + abstract class MapTypeConverter implements JsonSerializer, JsonDeserializer { private static final Type[] EMPTY_TYPES = new Type[0]; @@ -55,9 +57,25 @@ public T deserialize(JsonElement json, Type type, JsonDeserializationContext ctx @Override public JsonElement serialize(T src, Type type, JsonSerializationContext ctx) { - return toMap(src).foldLeft(new JsonObject(), (a, e) -> { - a.add(ctx.serialize(e._1).getAsString(), ctx.serialize(e._2)); - return a; - }); + final Map tuple2s = toMap(src); + if (tuple2s.isEmpty()) { + return new JsonObject(); + } else { + // test the first element + final Object o = tuple2s.head()._1(); + // primitives are fine + if (isPrimitive(o)) { + return tuple2s.foldLeft(new JsonObject(), (a, e) -> { + a.add(ctx.serialize(e._1).getAsString(), ctx.serialize(e._2)); + return a; + }); + } else { + //complex object key serialisation + return tuple2s.foldLeft(new JsonArray(), (a, e) -> { + a.add(ctx.serialize(e)); + return a; + }); + } + } } } diff --git a/src/test/java/io/vavr/gson/map/HashMapTest.java b/src/test/java/io/vavr/gson/map/HashMapTest.java index a8e5de8..da9439f 100644 --- a/src/test/java/io/vavr/gson/map/HashMapTest.java +++ b/src/test/java/io/vavr/gson/map/HashMapTest.java @@ -14,6 +14,11 @@ public class HashMapTest extends MapLikeTest> { return HashMap.of(key, value); } + @Override + Map ofTyped(K key, V value) { + return HashMap.of(key, value); + } + @Override Class clz() { return HashMap.class; diff --git a/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java b/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java index 32d7236..c154170 100644 --- a/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java +++ b/src/test/java/io/vavr/gson/map/LinkedHashMapTest.java @@ -1,17 +1,23 @@ package io.vavr.gson.map; +import java.lang.reflect.Type; + import com.google.gson.reflect.TypeToken; -import io.vavr.collection.HashMap; import io.vavr.collection.LinkedHashMap; +import io.vavr.collection.Map; -import java.lang.reflect.Type; +public class LinkedHashMapTest extends MapLikeTest> { -public class LinkedHashMapTest extends MapLikeTest> { @Override LinkedHashMap of(Object key, Object value) { return LinkedHashMap.of(key, value); } + @Override + Map ofTyped(K key, V value) { + return LinkedHashMap.of(key, value); + } + @Override Class clz() { return LinkedHashMap.class; @@ -19,22 +25,26 @@ Class clz() { @Override Type type() { - return new TypeToken>(){}.getType(); + return new TypeToken>() { + }.getType(); } @Override Type typeWithNestedType() { - return new TypeToken>>(){}.getType(); + return new TypeToken>>() { + }.getType(); } @Override Type intType() { - return new TypeToken>(){}.getType(); + return new TypeToken>() { + }.getType(); } @Override Type typeWithNestedIntType() { - return new TypeToken>>(){}.getType(); + return new TypeToken>>() { + }.getType(); } @Override diff --git a/src/test/java/io/vavr/gson/map/MapLikeTest.java b/src/test/java/io/vavr/gson/map/MapLikeTest.java index 542ad29..9cd0621 100644 --- a/src/test/java/io/vavr/gson/map/MapLikeTest.java +++ b/src/test/java/io/vavr/gson/map/MapLikeTest.java @@ -6,6 +6,7 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; +import io.vavr.collection.HashMap; import io.vavr.collection.Map; import io.vavr.gson.AbstractTest; import org.junit.Test; @@ -14,6 +15,8 @@ public abstract class MapLikeTest> extends AbstractTest { abstract T of(Object key, Object value); + abstract Map ofTyped(K key, V value); + abstract Class clz(); abstract Type type(); @@ -89,6 +92,17 @@ public void deserializeNestedComplexKey() { assert map.get(outerKey).get().get(innerKey).get() == 5; } + @Test + public void serializeNestedComplexKey() { + final CustomKey innerKey = new CustomKey(3, 4); + final Map innerMap = ofTyped(innerKey, 5); + final CustomKey outerKey = new CustomKey(1, 2); + final Map> vavrMap = ofTyped(outerKey, innerMap); + Map> map = gson.fromJson(gson.toJson(vavrMap), getComplexNestedKeyType()); + assert clz().isAssignableFrom(map.get(outerKey).get().getClass()); + assert map.get(outerKey).get().get(innerKey).get() == 5; + } + static final class CustomKey implements Comparable { private final int a; diff --git a/src/test/java/io/vavr/gson/map/MapTest.java b/src/test/java/io/vavr/gson/map/MapTest.java index 9e9b59e..c9e2411 100644 --- a/src/test/java/io/vavr/gson/map/MapTest.java +++ b/src/test/java/io/vavr/gson/map/MapTest.java @@ -13,6 +13,11 @@ public class MapTest extends MapLikeTest> { return HashMap.of(key, value); } + @Override + Map ofTyped(K key, V value) { + return HashMap.of(key, value); + } + @Override Class clz() { return Map.class; diff --git a/src/test/java/io/vavr/gson/map/SortedMapTest.java b/src/test/java/io/vavr/gson/map/SortedMapTest.java index fc80169..454c8e9 100644 --- a/src/test/java/io/vavr/gson/map/SortedMapTest.java +++ b/src/test/java/io/vavr/gson/map/SortedMapTest.java @@ -3,6 +3,7 @@ import java.lang.reflect.Type; import com.google.gson.reflect.TypeToken; +import io.vavr.collection.Map; import io.vavr.collection.SortedMap; import io.vavr.collection.TreeMap; @@ -14,6 +15,16 @@ public class SortedMapTest extends MapLikeTest> { return TreeMap.of((Comparable) key, value); } + @Override + @SuppressWarnings("unchecked") + Map ofTyped(K key, V value) { + if (key instanceof Comparable) { + final Comparable typedKey = (Comparable) key; + final Map of = TreeMap.of(typedKey, value); + return (Map) of; + } else throw new IllegalArgumentException("Must be Comparable"); + } + @Override Class clz() { return SortedMap.class; diff --git a/src/test/java/io/vavr/gson/map/TreeMapTest.java b/src/test/java/io/vavr/gson/map/TreeMapTest.java index 5664b9c..c5ae612 100644 --- a/src/test/java/io/vavr/gson/map/TreeMapTest.java +++ b/src/test/java/io/vavr/gson/map/TreeMapTest.java @@ -3,6 +3,7 @@ import java.lang.reflect.Type; import com.google.gson.reflect.TypeToken; +import io.vavr.collection.Map; import io.vavr.collection.TreeMap; public class TreeMapTest extends MapLikeTest> { @@ -13,6 +14,16 @@ public class TreeMapTest extends MapLikeTest> { return TreeMap.of((Comparable) key, value); } + @Override + @SuppressWarnings("unchecked") + Map ofTyped(K key, V value) { + if (key instanceof Comparable) { + final Comparable typedKey = (Comparable) key; + final Map of = TreeMap.of(typedKey, value); + return (Map) of; + } else throw new IllegalArgumentException("Must be Comparable"); + } + @Override Class clz() { return TreeMap.class;