From 7696710ad1b7b839e13444f2f3ac857814406c1a Mon Sep 17 00:00:00 2001 From: Vincent Castelluci Date: Sat, 15 May 2021 05:18:49 +0200 Subject: [PATCH] Attempt to implement Try serializer/deserializer --- .../io/vavr/jackson/datatype/VavrModule.java | 4 + .../datatype/deserialize/TryDeserializer.java | 123 ++++++++++++++++++ .../deserialize/VavrDeserializers.java | 4 + .../mixins/StackTraceElementMixin.java | 17 +++ .../datatype/serialize/TrySerializer.java | 82 ++++++++++++ .../datatype/serialize/VavrSerializers.java | 4 + .../io/vavr/jackson/issues/Issue181Test.java | 91 +++++++++++++ 7 files changed, 325 insertions(+) create mode 100644 src/main/java/io/vavr/jackson/datatype/deserialize/TryDeserializer.java create mode 100644 src/main/java/io/vavr/jackson/datatype/mixins/StackTraceElementMixin.java create mode 100644 src/main/java/io/vavr/jackson/datatype/serialize/TrySerializer.java create mode 100644 src/test/java/io/vavr/jackson/issues/Issue181Test.java diff --git a/src/main/java/io/vavr/jackson/datatype/VavrModule.java b/src/main/java/io/vavr/jackson/datatype/VavrModule.java index 921a9d0..8918c02 100644 --- a/src/main/java/io/vavr/jackson/datatype/VavrModule.java +++ b/src/main/java/io/vavr/jackson/datatype/VavrModule.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import io.vavr.jackson.datatype.deserialize.VavrDeserializers; +import io.vavr.jackson.datatype.mixins.StackTraceElementMixin; import io.vavr.jackson.datatype.serialize.VavrSerializers; public class VavrModule extends SimpleModule { @@ -64,8 +65,11 @@ public VavrModule(Settings settings) { @Override public void setupModule(SetupContext context) { super.setupModule(context); + context.addSerializers(new VavrSerializers(settings)); context.addDeserializers(new VavrDeserializers(settings)); context.addTypeModifier(new VavrTypeModifier()); + + context.setMixInAnnotations(StackTraceElement.class, StackTraceElementMixin.class); } } diff --git a/src/main/java/io/vavr/jackson/datatype/deserialize/TryDeserializer.java b/src/main/java/io/vavr/jackson/datatype/deserialize/TryDeserializer.java new file mode 100644 index 0000000..2f0bbb6 --- /dev/null +++ b/src/main/java/io/vavr/jackson/datatype/deserialize/TryDeserializer.java @@ -0,0 +1,123 @@ +/* __ __ __ __ __ ___ + * \ \ / / \ \ / / __/ + * \ \/ / /\ \ \/ / / + * \____/__/ \__\____/__/ + * + * Copyright 2014-2017 Vavr, http://vavr.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.vavr.jackson.datatype.deserialize; + +import static com.fasterxml.jackson.core.JsonToken.END_ARRAY; +import static com.fasterxml.jackson.core.JsonToken.VALUE_NULL; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import io.vavr.control.Try; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +class TryDeserializer extends StdDeserializer> implements ResolvableDeserializer { + + private static final long serialVersionUID = 1L; + + private final JavaType javaType; + private JsonDeserializer stringDeserializer; + private final List> deserializers; + + TryDeserializer(JavaType valueType) { + super(valueType); + this.javaType = valueType; + this.deserializers = new ArrayList<>(2); + } + + JsonDeserializer deserializer(int index) { + return deserializers.get(index); + } + + @Override + public void resolve(DeserializationContext ctxt) throws JsonMappingException { + stringDeserializer = ctxt.findContextualValueDeserializer(ctxt.constructType(String.class), null); + + if (javaType.isCollectionLikeType() || javaType.isReferenceType()) { + deserializers.add(ctxt.findRootValueDeserializer(javaType.getContentType())); + return; + } + + JavaType containedType = javaType.containedTypeOrUnknown(0); + deserializers.add(ctxt.findRootValueDeserializer(containedType)); + deserializers.add(ctxt.findRootValueDeserializer(ctxt.constructType(Throwable.class))); + } + + @Override + public Try deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + boolean success = false; + Object value = null; + int cnt = 0; + + for (JsonToken jsonToken = p.nextToken(); jsonToken != END_ARRAY; jsonToken = p.nextToken()) { + cnt++; + switch (cnt) { + case 1: + String def = (String) stringDeserializer.deserialize(p, ctxt); + if (isSuccess(def)) { + success = true; + } else if (isFailure(def)) { + success = false; + } else { + throw mappingException(ctxt, javaType.getRawClass(), jsonToken); + } + break; + case 2: + JsonDeserializer deserializer = success ? deserializer(0) : deserializer(1); + value = (jsonToken != VALUE_NULL) ? deserializer.deserialize(p, ctxt) : deserializer.getNullValue(ctxt); + break; + } + } + if (cnt != 2) { + throw mappingException(ctxt, javaType.getRawClass(), null); + } + return success ? Try.success(value) : Try.failure((Throwable) value); + } + + private static boolean isSuccess(final String fieldName) { + return "success".equals(fieldName) || "s".equals(fieldName); + } + + private static boolean isFailure(final String fieldName) { + return "failure".equals(fieldName) || "f".equals(fieldName); + } + + static JsonMappingException mappingException(DeserializationContext ctxt, Class targetClass, JsonToken token) { + String tokenDesc = (token == null) ? "" : String.format("%s token", token); + return JsonMappingException.from(ctxt.getParser(), + String.format("Can not deserialize instance of %s out of %s", + _calcName(targetClass), tokenDesc)); + } + + private static String _calcName(Class cls) { + if (cls.isArray()) { + return _calcName(cls.getComponentType())+"[]"; + } + return cls.getName(); + } + +} diff --git a/src/main/java/io/vavr/jackson/datatype/deserialize/VavrDeserializers.java b/src/main/java/io/vavr/jackson/datatype/deserialize/VavrDeserializers.java index 7d7afa1..2572510 100644 --- a/src/main/java/io/vavr/jackson/datatype/deserialize/VavrDeserializers.java +++ b/src/main/java/io/vavr/jackson/datatype/deserialize/VavrDeserializers.java @@ -66,6 +66,7 @@ import io.vavr.collection.Set; import io.vavr.control.Either; import io.vavr.control.Option; +import io.vavr.control.Try; import io.vavr.jackson.datatype.VavrModule; public class VavrDeserializers extends Deserializers.Base { @@ -84,6 +85,9 @@ public JsonDeserializer findBeanDeserializer(JavaType type, if (Either.class.isAssignableFrom(raw)) { return new EitherDeserializer(type); } + if (Try.class.isAssignableFrom(raw)) { + return new TryDeserializer(type); + } if (Tuple0.class.isAssignableFrom(raw)) { return new Tuple0Deserializer(type); diff --git a/src/main/java/io/vavr/jackson/datatype/mixins/StackTraceElementMixin.java b/src/main/java/io/vavr/jackson/datatype/mixins/StackTraceElementMixin.java new file mode 100644 index 0000000..9bccc70 --- /dev/null +++ b/src/main/java/io/vavr/jackson/datatype/mixins/StackTraceElementMixin.java @@ -0,0 +1,17 @@ +package io.vavr.jackson.datatype.mixins; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +public abstract class StackTraceElementMixin { + + @JsonIgnore + private String classLoaderName; + + @JsonIgnore + private String moduleName; + + @JsonIgnore + private String moduleVersion; +} + diff --git a/src/main/java/io/vavr/jackson/datatype/serialize/TrySerializer.java b/src/main/java/io/vavr/jackson/datatype/serialize/TrySerializer.java new file mode 100644 index 0000000..1266183 --- /dev/null +++ b/src/main/java/io/vavr/jackson/datatype/serialize/TrySerializer.java @@ -0,0 +1,82 @@ +/* __ __ __ __ __ ___ + * \ \ / / \ \ / / __/ + * \ \/ / /\ \ \/ / / + * \____/__/ \__\____/__/ + * + * Copyright 2014-2017 Vavr, http://vavr.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.vavr.jackson.datatype.serialize; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import io.vavr.control.Try; +import java.io.IOException; + +class TrySerializer extends StdSerializer> { + + private static final long serialVersionUID = 1L; + + private final JavaType type; + + TrySerializer(JavaType type) { + super(type); + this.type = type; + } + + void write(Object val, JavaType type, JsonGenerator gen, SerializerProvider provider) throws IOException { + if (val != null) { + JsonSerializer ser; + if (type != null && type.hasGenericTypes()) { + JavaType st = provider.constructSpecializedType(type, val.getClass()); + ser = provider.findTypedValueSerializer(st, true, null); + } else { + ser = provider.findTypedValueSerializer(val.getClass(), true, null); + } + ser.serialize(val, gen, provider); + } else { + gen.writeObject(val); + } + } + + @Override + public void serializeWithType(Try value, JsonGenerator gen, SerializerProvider serializers, + TypeSerializer typeSer) throws IOException { + typeSer.writeTypePrefixForScalar(value, gen); + serialize(value, gen, serializers); + typeSer.writeTypeSuffixForScalar(value, gen); + } + + @Override + public void serialize(Try value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartArray(); + if (value.isSuccess()) { + gen.writeString("success"); + write(value.get(), type.containedType(0), gen, provider); + } else { + gen.writeString("failure"); + write(value.getCause(), provider.constructType(Throwable.class), gen, provider); + } + gen.writeEndArray(); + } + + @Override + public boolean isEmpty(SerializerProvider provider, Try value) { + return value.isEmpty(); + } +} diff --git a/src/main/java/io/vavr/jackson/datatype/serialize/VavrSerializers.java b/src/main/java/io/vavr/jackson/datatype/serialize/VavrSerializers.java index 28350f4..40d5b8f 100644 --- a/src/main/java/io/vavr/jackson/datatype/serialize/VavrSerializers.java +++ b/src/main/java/io/vavr/jackson/datatype/serialize/VavrSerializers.java @@ -64,6 +64,7 @@ import io.vavr.collection.Set; import io.vavr.control.Either; import io.vavr.control.Option; +import io.vavr.control.Try; import io.vavr.jackson.datatype.VavrModule; public class VavrSerializers extends Serializers.Base { @@ -82,6 +83,9 @@ public JsonSerializer findSerializer(SerializationConfig config, if (Either.class.isAssignableFrom(raw)) { return new EitherSerializer(type); } + if (Try.class.isAssignableFrom(raw)) { + return new TrySerializer(type); + } if (Tuple0.class.isAssignableFrom(raw)) { return new Tuple0Serializer(type); diff --git a/src/test/java/io/vavr/jackson/issues/Issue181Test.java b/src/test/java/io/vavr/jackson/issues/Issue181Test.java new file mode 100644 index 0000000..1733c94 --- /dev/null +++ b/src/test/java/io/vavr/jackson/issues/Issue181Test.java @@ -0,0 +1,91 @@ +package io.vavr.jackson.issues; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vavr.control.Option; +import io.vavr.control.Try; +import io.vavr.jackson.datatype.VavrModule; +import org.junit.jupiter.api.Test; + +/** + * Can not deserialize explicit Try.Success or Try.Failure + * https://github.com/vavr-io/vavr-jackson/issues/165 + */ +class Issue181Test { + + static class MyTryNormalType { + @JsonProperty("p") + Try p; + + @JsonCreator + MyTryNormalType(@JsonProperty("p") Try p) { + this.p = p; + } + } + + static class MyTryCompositeType { + @JsonProperty("p") + Try> p; + + @JsonCreator + MyTryCompositeType(@JsonProperty("p") Try> p) { + this.p = p; + } + } + + static class MyTrySuccessType { + @JsonProperty("p") + Try.Success p; + + @JsonCreator + MyTrySuccessType(@JsonProperty("p") Try.Success p) { + this.p = p; + } + } + + static class MyTryFailureType { + @JsonProperty("p") + Try.Failure p; + + @JsonCreator + MyTryFailureType(@JsonProperty("p") Try.Failure p) { + this.p = p; + } + } + + @Test + void itShouldSerializeTrys() throws Exception { + ObjectMapper mapper = new ObjectMapper().registerModule(new VavrModule()); + + assertTrue(mapper.canDeserialize(mapper.constructType(MyTryNormalType.class))); + assertTrue(mapper.canDeserialize(mapper.constructType(MyTryCompositeType.class))); + assertTrue(mapper.canDeserialize(mapper.constructType(MyTrySuccessType.class))); + assertTrue(mapper.canDeserialize(mapper.constructType(MyTryFailureType.class))); + + MyTryNormalType normal1 = new MyTryNormalType(Try.of(() -> "vavr")); + MyTryNormalType normal2 = mapper.readValue(mapper.writeValueAsString(normal1), MyTryNormalType.class); + assertEquals(normal1.p, normal2.p); + + MyTryCompositeType composite1 = new MyTryCompositeType(Try.of(() -> Option.some("vavr"))); + MyTryCompositeType composite2 = mapper.readValue(mapper.writeValueAsString(normal1), MyTryCompositeType.class); + assertEquals(composite1.p, composite2.p); + + MyTrySuccessType success1 = new MyTrySuccessType((Try.Success) Try.success("vavr")); + MyTrySuccessType success2 = mapper.readValue(mapper.writeValueAsString(success1), MyTrySuccessType.class); + assertEquals(success1.p, success2.p); + + MyTryFailureType failure1 = new MyTryFailureType((Try.Failure) Try.failure(new Throwable("throwable", new Exception("cause")))); + MyTryFailureType failure2 = mapper.readValue(mapper.writeValueAsString(failure1), MyTryFailureType.class); + assertEquals(failure1.p.isFailure(), failure2.p.isFailure()); + + assertEquals(failure1.p.getCause().getMessage(), failure2.p.getCause().getMessage()); + assertEquals(failure1.p.getCause().getStackTrace().length, failure2.p.getCause().getStackTrace().length); + + assertEquals(failure1.p.getCause().getCause().getMessage(), failure2.p.getCause().getCause().getMessage()); + } + +}