Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

generify tristate module as reusable module #108

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,43 @@ public final class arrow/integrations/jackson/module/EitherSerializerResolver :
public fun findSerializer (Lcom/fasterxml/jackson/databind/SerializationConfig;Lcom/fasterxml/jackson/databind/JavaType;Lcom/fasterxml/jackson/databind/BeanDescription;)Lcom/fasterxml/jackson/databind/JsonSerializer;
}

public final class arrow/integrations/jackson/module/GenericTriStateDeserializationConfig {
public fun <init> (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V
public final fun component1 ()Lkotlin/jvm/functions/Function0;
public final fun component2 ()Lkotlin/jvm/functions/Function0;
public final fun component3 ()Lkotlin/jvm/functions/Function1;
public final fun copy (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Larrow/integrations/jackson/module/GenericTriStateDeserializationConfig;
public static synthetic fun copy$default (Larrow/integrations/jackson/module/GenericTriStateDeserializationConfig;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Larrow/integrations/jackson/module/GenericTriStateDeserializationConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getIfAbsent ()Lkotlin/jvm/functions/Function0;
public final fun getIfDefined ()Lkotlin/jvm/functions/Function1;
public final fun getIfNull ()Lkotlin/jvm/functions/Function0;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class arrow/integrations/jackson/module/GenericTriStateModule : com/fasterxml/jackson/databind/module/SimpleModule {
public static final field Companion Larrow/integrations/jackson/module/GenericTriStateModule$Companion;
public fun <init> (Ljava/lang/Class;Larrow/integrations/jackson/module/GenericTriStateSerializationConfig;Larrow/integrations/jackson/module/GenericTriStateDeserializationConfig;)V
public fun setupModule (Lcom/fasterxml/jackson/databind/Module$SetupContext;)V
}

public final class arrow/integrations/jackson/module/GenericTriStateModule$Companion {
}

public final class arrow/integrations/jackson/module/GenericTriStateSerializationConfig {
public fun <init> (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public final fun component1 ()Lkotlin/jvm/functions/Function1;
public final fun component2 ()Lkotlin/jvm/functions/Function1;
public final fun copy (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Larrow/integrations/jackson/module/GenericTriStateSerializationConfig;
public static synthetic fun copy$default (Larrow/integrations/jackson/module/GenericTriStateSerializationConfig;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Larrow/integrations/jackson/module/GenericTriStateSerializationConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getSerializeValue ()Lkotlin/jvm/functions/Function1;
public fun hashCode ()I
public final fun isPresent ()Lkotlin/jvm/functions/Function1;
public fun toString ()Ljava/lang/String;
}

public final class arrow/integrations/jackson/module/IorDeserializerResolver : com/fasterxml/jackson/databind/deser/Deserializers$Base {
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public fun findBeanDeserializer (Lcom/fasterxml/jackson/databind/JavaType;Lcom/fasterxml/jackson/databind/DeserializationConfig;Lcom/fasterxml/jackson/databind/BeanDescription;)Lcom/fasterxml/jackson/databind/JsonDeserializer;
Expand Down Expand Up @@ -80,6 +117,8 @@ public final class arrow/integrations/jackson/module/OptionDeserializer : com/fa
public fun createContextual (Lcom/fasterxml/jackson/databind/DeserializationContext;Lcom/fasterxml/jackson/databind/BeanProperty;)Lcom/fasterxml/jackson/databind/JsonDeserializer;
public fun deserialize (Lcom/fasterxml/jackson/core/JsonParser;Lcom/fasterxml/jackson/databind/DeserializationContext;)Larrow/core/Option;
public synthetic fun deserialize (Lcom/fasterxml/jackson/core/JsonParser;Lcom/fasterxml/jackson/databind/DeserializationContext;)Ljava/lang/Object;
public fun getAbsentValue (Lcom/fasterxml/jackson/databind/DeserializationContext;)Larrow/core/Option;
public synthetic fun getAbsentValue (Lcom/fasterxml/jackson/databind/DeserializationContext;)Ljava/lang/Object;
public fun getEmptyAccessPattern ()Lcom/fasterxml/jackson/databind/util/AccessPattern;
public fun getEmptyValue (Lcom/fasterxml/jackson/databind/DeserializationContext;)Larrow/core/Option;
public synthetic fun getEmptyValue (Lcom/fasterxml/jackson/databind/DeserializationContext;)Ljava/lang/Object;
Expand Down Expand Up @@ -112,6 +151,28 @@ public final class arrow/integrations/jackson/module/OptionTypeModifier : com/fa
public fun modifyType (Lcom/fasterxml/jackson/databind/JavaType;Ljava/lang/reflect/Type;Lcom/fasterxml/jackson/databind/type/TypeBindings;Lcom/fasterxml/jackson/databind/type/TypeFactory;)Lcom/fasterxml/jackson/databind/JavaType;
}

public abstract class arrow/integrations/jackson/module/SerializedValue {
}

public final class arrow/integrations/jackson/module/SerializedValue$AbsentOrNull : arrow/integrations/jackson/module/SerializedValue {
public static final field INSTANCE Larrow/integrations/jackson/module/SerializedValue$AbsentOrNull;
}

public final class arrow/integrations/jackson/module/SerializedValue$ExplicitNull : arrow/integrations/jackson/module/SerializedValue {
public static final field INSTANCE Larrow/integrations/jackson/module/SerializedValue$ExplicitNull;
}

public final class arrow/integrations/jackson/module/SerializedValue$Value : arrow/integrations/jackson/module/SerializedValue {
public fun <init> (Ljava/lang/Object;)V
public final fun component1 ()Ljava/lang/Object;
public final fun copy (Ljava/lang/Object;)Larrow/integrations/jackson/module/SerializedValue$Value;
public static synthetic fun copy$default (Larrow/integrations/jackson/module/SerializedValue$Value;Ljava/lang/Object;ILjava/lang/Object;)Larrow/integrations/jackson/module/SerializedValue$Value;
public fun equals (Ljava/lang/Object;)Z
public final fun getValue ()Ljava/lang/Object;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class arrow/integrations/jackson/module/ValidatedDeserializerResolver : com/fasterxml/jackson/databind/deser/Deserializers$Base {
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public fun findBeanDeserializer (Lcom/fasterxml/jackson/databind/JavaType;Lcom/fasterxml/jackson/databind/DeserializationConfig;Lcom/fasterxml/jackson/databind/BeanDescription;)Lcom/fasterxml/jackson/databind/JsonDeserializer;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
package arrow.integrations.jackson.module

import arrow.core.Option
import arrow.core.getOrElse
import arrow.core.orElse
import arrow.core.toOption
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.json.PackageVersion
import com.fasterxml.jackson.databind.BeanDescription
import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import com.fasterxml.jackson.databind.jsontype.TypeSerializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.NullNode
import com.fasterxml.jackson.databind.ser.Serializers
import com.fasterxml.jackson.databind.ser.std.ReferenceTypeSerializer
import com.fasterxml.jackson.databind.type.ReferenceType
import com.fasterxml.jackson.databind.type.TypeBindings
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.databind.type.TypeModifier
import com.fasterxml.jackson.databind.util.AccessPattern
import com.fasterxml.jackson.databind.util.NameTransformer
import java.lang.reflect.Type

/**
* The [GenericTriStateModule] is a special module that can be used to handle generic types with
* optionality. This module can be used to create serialization/deserialization module for bespoke
* generic containers with special handling for absence, null, and defined.
*
* An example for creating a jackson module for `TriState<A>` can be seen as follows.
*
* ```kotlin
* private sealed class TriState<out A> {
* companion object {
* fun <T> T.defined(): TriState<T> = Defined(this)
* fun <T> absent(): TriState<T> = Absent
* fun <T> nul(): TriState<T> = Null
* }
*
* object Absent : TriState<Nothing>()
* object Null : TriState<Nothing>()
* data class Defined<T>(val value: T) : TriState<T>()
* }
*
* // defining jackson module for TriState<T>
* private val tristateModule: GenericTriStateModule<TriState<*>> =
* GenericTriStateModule(
* serializationConfig =
* GenericTriStateSerializationConfig(
* isPresent = {
* when (it) {
* TriState.Absent -> false
* is TriState.Defined -> true
* TriState.Null -> true
* }
* },
* serializeValue = {
* when (it) {
* TriState.Absent -> SerializedValue.AbsentOrNull
* is TriState.Defined -> SerializedValue.Value(it.value)
* TriState.Null -> SerializedValue.ExplicitNull
* }
* }
* ),
* deserializationConfig =
* GenericTriStateDeserializationConfig(
* ifAbsent = { TriState.Absent },
* ifNull = { TriState.Null },
* ifDefined = { TriState.Defined(it) }
* )
* )
*
* // consuming TriState<T>
* private data class Nested(val value: String)
*
* @JsonInclude(JsonInclude.Include.NON_ABSENT)
* private data class Example(val nested: TriState<Nested>)
* ```
*/
public class GenericTriStateModule<T>(
clazz: Class<T>,
serializationConfig: GenericTriStateSerializationConfig<T>,
deserializationConfig: GenericTriStateDeserializationConfig<T>,
) :
SimpleModule(
"${GenericTriStateModule::class.java.canonicalName}-${clazz.canonicalName}",
PackageVersion.VERSION
) {

public companion object {
public inline operator fun <reified T> invoke(
serializationConfig: GenericTriStateSerializationConfig<T>,
deserializationConfig: GenericTriStateDeserializationConfig<T>
): GenericTriStateModule<T> =
GenericTriStateModule(T::class.java, serializationConfig, deserializationConfig)
}

init {
addDeserializer(
clazz,
GenericTriStateDeserializer(
clazz,
deserializationConfig.ifAbsent,
deserializationConfig.ifNull,
deserializationConfig.ifDefined
)
)
}

override fun setupModule(context: SetupContext) {
super.setupModule(context)
context.addSerializers(serializerResolver)
context.addTypeModifier(typeModifier)
}

private val serializerResolver: GenericTriStateSerializerResolver<T> =
GenericTriStateSerializerResolver(
clazz,
serializationConfig.isPresent,
serializationConfig.serializeValue
)

private val typeModifier: GenericTriStateTypeModifier<T> = GenericTriStateTypeModifier(clazz)
}

public data class GenericTriStateDeserializationConfig<T>(
val ifAbsent: () -> T,
val ifNull: () -> T,
val ifDefined: (Any) -> T
)

public data class GenericTriStateSerializationConfig<T>(
val isPresent: (T) -> Boolean,
val serializeValue: (T) -> SerializedValue
)

public sealed class SerializedValue {
public object ExplicitNull : SerializedValue()
public object AbsentOrNull : SerializedValue()
public data class Value(@get:JsonValue val value: Any?) : SerializedValue()
}

private class GenericTriStateSerializerResolver<T>(
private val clazz: Class<T>,
private val isPresent: (T) -> Boolean,
private val serializeValue: (T) -> SerializedValue
) : Serializers.Base() {
override fun findReferenceSerializer(
config: SerializationConfig,
type: ReferenceType,
beanDesc: BeanDescription?,
contentTypeSerializer: TypeSerializer?,
contentValueSerializer: JsonSerializer<Any>?
): JsonSerializer<*>? =
if (clazz.isAssignableFrom(type.rawClass)) {
val staticTyping =
(contentTypeSerializer == null && config.isEnabled(MapperFeature.USE_STATIC_TYPING))
GenericTriStateSerializer(isPresent, serializeValue)
.createSerializer(type, staticTyping, contentTypeSerializer, contentValueSerializer)
} else {
null
}
}

private class GenericTriStateTypeModifier<T>(private val clazz: Class<T>) : TypeModifier() {
override fun modifyType(
type: JavaType,
jdkType: Type,
context: TypeBindings?,
typeFactory: TypeFactory?
): JavaType =
when {
type.isReferenceType || type.isContainerType -> type
type.rawClass == clazz -> ReferenceType.upgradeFrom(type, type.containedTypeOrUnknown(0))
else -> type
}
}

private class GenericTriStateSerializer<T>(
private val isPresent: (T) -> Boolean,
private val serializeValue: (T) -> SerializedValue
) {
inner class GenericSerializer : ReferenceTypeSerializer<T> {
constructor(
fullType: ReferenceType,
staticTyping: Boolean,
typeSerializer: TypeSerializer?,
jsonSerializer: JsonSerializer<Any>?
) : super(fullType, staticTyping, typeSerializer, jsonSerializer)

constructor(
base: GenericSerializer,
property: BeanProperty?,
typeSerializer: TypeSerializer?,
valueSer: JsonSerializer<*>?,
unwrapper: NameTransformer?,
suppressableValue: Any?,
suppressNulls: Boolean
) : super(base, property, typeSerializer, valueSer, unwrapper, suppressableValue, suppressNulls)

override fun withContentInclusion(
suppressableValue: Any?,
suppressNulls: Boolean
): ReferenceTypeSerializer<T> =
GenericSerializer(
this,
_property,
_valueTypeSerializer,
_valueSerializer,
_unwrapper,
suppressableValue,
suppressNulls
)

override fun _isValuePresent(value: T): Boolean = isPresent(value)

override fun _getReferenced(value: T): Any? =
when (val serialized = serializeValue(value)) {
SerializedValue.AbsentOrNull -> null
SerializedValue.ExplicitNull -> NullNode.getInstance()
is SerializedValue.Value -> serialized.value
}

override fun _getReferencedIfPresent(value: T): Any? = _getReferenced(value)

override fun withResolved(
prop: BeanProperty?,
vts: TypeSerializer?,
valueSer: JsonSerializer<*>?,
unwrapper: NameTransformer?
): ReferenceTypeSerializer<T> =
GenericSerializer(this, prop, vts, valueSer, unwrapper, _suppressableValue, _suppressNulls)
}

fun createSerializer(
fullType: ReferenceType,
staticTyping: Boolean,
typeSerializer: TypeSerializer?,
jsonSerializer: JsonSerializer<Any>?
): GenericSerializer = GenericSerializer(fullType, staticTyping, typeSerializer, jsonSerializer)
}

private class GenericTriStateDeserializer<T>(
private val clazz: Class<T>,
private val ifAbsent: () -> T,
private val ifNull: () -> T,
private val ifDefined: (Any) -> T
) : JsonDeserializer<T>(), ContextualDeserializer {
private lateinit var valueType: JavaType
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext): T =
p.toOption()
.fold(
{ ifNull() },
{
val value = ctxt.readValue<Any>(it, valueType)
ifDefined(value)
}
)

override fun createContextual(
ctxt: DeserializationContext,
property: BeanProperty?
): JsonDeserializer<*> {
val valueType =
Option.fromNullable(property)
.map { it.type.containedTypeOrUnknown(0) }
.orElse { Option.fromNullable(ctxt.contextualType?.containedTypeOrUnknown(0)) }
.getOrElse { ctxt.constructType(Any::class.java) }

val deserializer = GenericTriStateDeserializer(clazz, ifAbsent, ifNull, ifDefined)
deserializer.valueType = valueType
return deserializer
}

override fun getAbsentValue(ctxt: DeserializationContext?): T = ifAbsent()
override fun getNullValue(ctxt: DeserializationContext): T = ifNull()
override fun getEmptyValue(ctxt: DeserializationContext?): T = ifAbsent()
override fun getNullAccessPattern(): AccessPattern = AccessPattern.CONSTANT
override fun getEmptyAccessPattern(): AccessPattern = AccessPattern.CONSTANT
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ public class OptionDeserializer : JsonDeserializer<Option<*>>(), ContextualDeser

override fun getNullValue(ctxt: DeserializationContext): Option<*> = None
override fun getEmptyValue(ctxt: DeserializationContext?): Option<*> = None
override fun getAbsentValue(ctxt: DeserializationContext?): Option<*> = None

override fun getNullAccessPattern(): AccessPattern = AccessPattern.CONSTANT
override fun getEmptyAccessPattern(): AccessPattern = AccessPattern.CONSTANT
}
Loading