mcarleio / konvert

This kotlin compiler plugin is using KSP API and generates kotlin code to map one class to another
https://mcarleio.github.io/konvert/
Apache License 2.0
93 stars 8 forks source link

compilation error in micronaut framework if default methods are being used on a mapper interface #91

Open bfg opened 3 months ago

bfg commented 3 months ago

I wanted to use custom mappings and failed to make my custom TypeConverter initialize from within the project, so I've decided to create an interface with default methods to do the custom mappings.

The following code results in a compilation error, if instantToZonedDateTime() is commented out, compilation finishes successfully.

@Konverter(options = [
  Konfig(key = ENFORCE_NOT_NULL, value = "true"),
  Konfig(key = KONVERTER_GENERATE_CLASS, value = "true"),
  Konfig(key = ADD_GENERATED_KONVERTER_ANNOTATION, value = "false"),
])
interface IAPMapper {
  fun instantToZonedDateTime(instant: Instant): ZonedDateTime {
    return instant
        .truncatedTo(ChronoUnit.MILLIS)
        .atZone(ZoneOffset.UTC)
  }
}

I'm not sure whether this is a micronaut or konvert ksp processor issue, full details are here: https://github.com/micronaut-projects/micronaut-core/issues/11058

bfg commented 3 months ago

I've managed to work around this with the following annotation on the entity class:

@KonvertTo(
    IAPDefinitionDto::class,
    mappings = [
      Mapping(target = "createdAt", expression = "it.createdAt!!.truncatedTo(java.time.temporal.ChronoUnit.MILLIS).atZone(java.time.ZoneOffset.UTC)"),
    ],
    options = [
      Konfig(key = ENFORCE_NOT_NULL, value = "true"),
      Konfig(key = ENABLE_CONVERTERS, value = "true")
    ],
    mapFunctionName = "toDto"
)

it works, but I generally don't like this approach on actual classes, I prefer to have entity mapping code in the same place.

mcarleio commented 3 months ago

Interesting finding, never had a problem like this. I did some research about the error (you probably did so, too) and stumbled upon this comment https://github.com/google/ksp/issues/427#issuecomment-906006403. Not sure if this is a konvert issue, a KSP issue, a micronaut issue or a gradle issue. Feels like it could be anything :confused:

Regarding a custom TypeConverter: I know this is not yet documented well, but it should be pretty straightforward:

  1. Create a gradle module using this build.gradle as an exemplary.
  2. Create your custom TypeConverter like this:

    
    @AutoService(TypeConverter::class)
    class InstantToZonedDateTimeConverter : AbstractTypeConverter() {
    
    override val enabledByDefault: Boolean = true
    
    private val sourceType: KSType by lazy {
        resolver.getClassDeclarationByName(Instant::class.qualifiedName!!)!!.asStarProjectedType()
    }
    
    private val targetType: KSType by lazy {
        resolver.getClassDeclarationByName(ZonedDateTime::class.qualifiedName!!)!!.asStarProjectedType()
    }
    
    override fun matches(source: KSType, target: KSType): Boolean {
        return handleNullable(source, target) { sourceNotNullable, targetNotNullable ->
            sourceType.isAssignableFrom(sourceNotNullable) && targetType == targetNotNullable
        }
    }
    
    override fun convert(fieldName: String, source: KSType, target: KSType): CodeBlock {
        val sourceNullable = source.isNullable()
        val nc = if (sourceNullable) "?" else ""
    
        return CodeBlock.of(
            "$fieldName$nc.truncatedTo(%T.MILLIS)$nc.atZone(%T.UTC)" + appendNotNullAssertionOperatorIfNeeded(source, target),
            ChronoUnit::class,
            ZoneOffset::class
        )
    }

}

Important :warning:: If you do not want to use the `@AutoService` KSP stuff, you have to manually create the necessary file. To do so, create a `META-INF/services/io.mcarle.konvert.converter.api.TypeConverter` file in resources and add the FQN to the `InstantToZonedDateTimeConverter` class.

4. In your project where you want to use the `TypeConverter`, add the newly generated module as a KSP dependency `ksp(project(":custom-type-converter"))` (next to the konvert one).

Afterwards you can delete the function `instantToZonedDateTime`.

Tip: If you want to set your konvert configurations globally, you can further amend your `build.gradle` and define them like this (see e.g. https://github.com/mcarleio/konvert/blob/main/example/build.gradle.kts#L52):
```kotlin
ksp {
    arg("konvert.enforce-not-null", "true")
    arg("konvert.konverter.generate-class", "true")
    arg("konvert.add-generated-konverter-annotation", "false")
}
bfg commented 3 months ago

I'll create a reproducer and share it on gh.

Thanks for a great feedback, I was suspecting whether type converter should be in it's own jar, and well, you've confirmed it.