Search code examples
javakotlinmicronautmicronaut-data

Is there a way to make a generic AttributeConverter in Micronaut Data?


I have a lot of value objects that basically represent UUIDs as the primary identifier of the entity / aggregate:

@Singleton
class FooIdConverter : AttributeConverter<FooId, UUID> {
    override fun convertToPersistedValue(
        @Nullable entityValue: FooId?, @NonNull context: ConversionContext
    ): UUID? {
        return entityValue?.id
    }

    override fun convertToEntityValue(
        persistedValue: UUID?, context: ConversionContext
    ): FooId? {
        return if (persistedValue == null) null else FooId(persistedValue)
    }
}

@TypeDef(type = DataType.UUID, converter = FooIdConverter::class)
data class FooId(val id: UUID)

Of course, I could copy the converter for every other ID value object (BarIdConverter, FooBarIdConverter, ...) but this does not seem to be really DRY.

Is there a better way to do this in Kotlin?

I tried something like this but it does not work:

abstract class EntityId(open val id: UUID)

fun <Id : EntityId> createConverterFor(clazz: KClass<Id>): KClass<out AttributeConverter<Id, UUID>> {
    return object : AttributeConverter<Id, UUID> {
        override fun convertToPersistedValue(entityValue: Id?, context: ConversionContext): UUID? {
            return entityValue?.id
        }

        override fun convertToEntityValue(persistedValue: UUID?, context: ConversionContext): Id? {
            return if (persistedValue == null) null else clazz.primaryConstructor!!.call(
                persistedValue
            )
        }
    }::class
}

val ProjectIdConverter = createConverterFor(FooId::class)

@TypeDef(type = DataType.UUID, converter = ProjectIdConverter)
data class FooId(override val id: UUID) : EntityId(id)

// EDIT (Solution):

Thanks to @Denis, I have now two solutions. The first and most straight forward:

@Singleton
class EntityIdConverter() : AttributeConverter<EntityId, UUID> {
    override fun convertToPersistedValue(
        @Nullable entityValue: EntityId?, @NonNull context: ConversionContext
    ): UUID? {
        return entityValue?.id
    }

    override fun convertToEntityValue(
        persistedValue: UUID?, context: ConversionContext
    ): EntityId? {
        val ctx = context as ArgumentConversionContext<*>
        return if (persistedValue == null) {
            null
        } else {
            ctx.argument.type.getDeclaredConstructor(UUID::class.java)
                .newInstance(persistedValue) as EntityId
        }
    }
}
@TypeDef(type = DataType.UUID, converter = EntityIdConverter::class)
abstract class EntityId(open val id: UUID)
data class ProjectId(override val id: UUID) : EntityId(id)

The second option is to use KSP and generate the AttributeConverter. This option is described in an answer by myself.


Solution

  • You can have a shared converted and recognize the required type using the context which should be of type ArgumentConversionContext and getArgument will return the type.