Skip to content

Commit

Permalink
feat(#28): allow multiple parameters in functions of a @Konverter a…
Browse files Browse the repository at this point in the history
…nnotated interface
  • Loading branch information
mcarleio committed Oct 30, 2023
1 parent dc66e6f commit be7bf9f
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ annotation class Konverter(
val options: Array<Konfig> = []
) {

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Source

/**
* This object can be used to load the generated class of an interface, which is annotated with `@Konverter`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ class CodeBuilder private constructor(
addFunction(
funSpec = funBuilder.apply {
if (Configuration.addGeneratedKonverterAnnotation) {
addAnnotation(
AnnotationSpec.builder(GeneratedKonverter::class)
.addMember("${GeneratedKonverter::priority.name} = %L", priority)
.build()
)
// do not add annotation to functions with multiple parameters
if (this.parameters.size <= 1) {
addAnnotation(
AnnotationSpec.builder(GeneratedKonverter::class)
.addMember("${GeneratedKonverter::priority.name} = %L", priority)
.build()
)
}
}
}.build(),
toType = toType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class CodeGenerator(
targetClassImportName: String?,
source: KSType,
target: KSType,
mappingCodeParentDeclaration: KSDeclaration
mappingCodeParentDeclaration: KSDeclaration,
additionalSourceParameters: List<KSValueParameter>
): CodeBlock {
if (paramName != null) {
val existingTypeConverter = TypeConverterRegistry
Expand All @@ -48,7 +49,7 @@ class CodeGenerator(
}
}

val sourceProperties = PropertyMappingResolver(logger).determinePropertyMappings(paramName, mappings, source)
val sourceProperties = PropertyMappingResolver(logger).determinePropertyMappings(paramName, mappings, source, additionalSourceParameters)

val targetClassDeclaration = target.classDeclaration()!!

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSValueParameter
import io.mcarle.konvert.api.Mapping
import io.mcarle.konvert.converter.api.classDeclaration
import io.mcarle.konvert.converter.api.isNullable

class PropertyMappingResolver(
private val logger: KSPLogger
) {
fun determinePropertyMappings(
mappingParamName: String?,
mappings: List<Mapping>,
type: KSType
type: KSType,
additionalSourceParameters: List<KSValueParameter>
): List<PropertyMappingInfo> {
val classDeclaration = type.classDeclaration()!!
val properties = classDeclaration.getAllProperties().toList()
Expand All @@ -23,11 +24,30 @@ class PropertyMappingResolver(

val propertiesWithoutSource = getPropertyMappingsWithoutSource(mappings, mappingParamName)
val propertiesWithSource = getPropertyMappingsWithSource(mappings, properties, mappingParamName)
val propertiesFromAdditionalParameters = getPropertyMappingsFromAdditionalParameters(additionalSourceParameters)
val propertiesWithoutMappings = getPropertyMappingsWithoutMappings(properties, mappingParamName)

return propertiesWithoutSource + propertiesWithSource + propertiesWithoutMappings
return propertiesWithoutSource + propertiesWithSource + propertiesFromAdditionalParameters + propertiesWithoutMappings
}

private fun getPropertyMappingsFromAdditionalParameters(
properties: List<KSValueParameter>,
) = properties
.map { property ->
val paramName = property.name!!.asString()
PropertyMappingInfo(
mappingParamName = null,
sourceName = null,
targetName = paramName,
constant = paramName,
expression = null,
ignore = false,
enableConverters = emptyList(),
declaration = null,
isBasedOnAnnotation = false
)
}

private fun getPropertyMappingsWithoutMappings(
properties: List<KSPropertyDeclaration>,
mappingParamName: String?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeReference
import com.google.devtools.ksp.symbol.KSValueParameter
import io.mcarle.konvert.api.DEFAULT_KONVERTER_PRIORITY
import io.mcarle.konvert.api.Konfig
import io.mcarle.konvert.api.Konvert
Expand All @@ -15,20 +16,21 @@ import io.mcarle.konvert.api.Priority
import io.mcarle.konvert.converter.api.classDeclaration
import io.mcarle.konvert.processor.from

class KonvertData(
class KonvertData constructor(
val annotationData: AnnotationData,
val isAbstract: Boolean,
val sourceTypeReference: KSTypeReference,
val targetTypeReference: KSTypeReference,
val mapKSFunctionDeclaration: KSFunctionDeclaration,
val additionalParameters: List<KSValueParameter>
) {

val sourceType: KSType = sourceTypeReference.resolve()
val sourceClassDeclaration: KSClassDeclaration = sourceType.classDeclaration()!!
val targetType: KSType = targetTypeReference.resolve()
val targetClassDeclaration: KSClassDeclaration = targetType.classDeclaration()!!
val mapFunctionName: String = mapKSFunctionDeclaration.simpleName.asString()
val paramName: String = mapKSFunctionDeclaration.parameters.first().name!!.asString()
val paramName: String = (mapKSFunctionDeclaration.parameters - additionalParameters).first().name!!.asString()

val priority = annotationData.priority

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeReference
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.ksp.toTypeName
import io.mcarle.konvert.converter.api.config.Configuration
Expand Down Expand Up @@ -47,7 +48,17 @@ object KonverterCodeGenerator {
funBuilder = FunSpec.builder(konvertData.mapFunctionName)
.addModifiers(KModifier.OVERRIDE)
.returns(konvertData.targetTypeReference.toTypeName())
.addParameter(konvertData.paramName, konvertData.sourceTypeReference.toTypeName())
.addParameters(konvertData.mapKSFunctionDeclaration.parameters.map {
val builder = ParameterSpec.builder(
name = it.name!!.asString(),
type = it.type.toTypeName(),
modifiers = emptyArray()
)
if (it.isVararg) {
builder.addModifiers(KModifier.VARARG)
}
builder.build()
})
.addCode(
"return super.${konvertData.mapFunctionName}(${konvertData.paramName})"
),
Expand Down Expand Up @@ -81,7 +92,17 @@ object KonverterCodeGenerator {
funBuilder = FunSpec.builder(konvertData.mapFunctionName)
.addModifiers(KModifier.OVERRIDE)
.returns(konvertData.targetTypeReference.toTypeName())
.addParameter(konvertData.paramName, konvertData.sourceTypeReference.toTypeName())
.addParameters(konvertData.mapKSFunctionDeclaration.parameters.map {
val builder = ParameterSpec.builder(
name = it.name!!.asString(),
type = it.type.toTypeName(),
modifiers = emptyArray()
)
if (it.isVararg) {
builder.addModifiers(KModifier.VARARG)
}
builder.build()
})
.addCode(
mapper.generateCode(
konvertData.annotationData.mappings.asIterable().validated(konvertData.mapKSFunctionDeclaration, logger),
Expand All @@ -90,7 +111,8 @@ object KonverterCodeGenerator {
targetClassImportName,
konvertData.sourceType,
konvertData.targetType,
konvertData.mapKSFunctionDeclaration
konvertData.mapKSFunctionDeclaration,
konvertData.additionalParameters
)
),
priority = konvertData.priority,
Expand Down Expand Up @@ -130,9 +152,11 @@ object KonverterCodeGenerator {
}

fun toFunctionFullyQualifiedNames(data: KonverterData): List<String> {
return data.konvertData.map {
"${data.mapKSClassDeclaration.qualifiedName?.asString()}Impl.${it.mapFunctionName}"
}
return data.konvertData
.filter { it.additionalParameters.isEmpty() } // filter out mappings with more than one parameter
.map {
"${data.mapKSClassDeclaration.qualifiedName?.asString()}Impl.${it.mapFunctionName}"
}
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package io.mcarle.konvert.processor.konvert

import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getClassDeclarationByName
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.isPrivate
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSValueParameter
import com.google.devtools.ksp.symbol.Modifier
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ksp.toTypeName
import io.mcarle.konvert.api.Konvert
Expand Down Expand Up @@ -50,23 +55,26 @@ object KonverterDataCollector {
return@mapNotNull null
}

if (Modifier.INLINE in it.modifiers) {
// ignore inline functions
return@mapNotNull null
}

if (it.extensionReceiver != null) {
// ignore extension functions
return@mapNotNull null
}

val source =
if (it.parameters.size > 1 || it.parameters.isEmpty()) null
else it.parameters.first().type
val target = it.returnType?.let { returnType ->
val sourceValueParameter = determineSourceParam(it, logger)
val source = sourceValueParameter?.type
val target = it.returnType?.let { returnType ->
if (returnType.resolve().declaration == resolver.getClassDeclarationByName<Unit>()) {
null
} else {
returnType
}
}


val annotation = it.annotations.firstOrNull { annotation ->
(annotation.annotationType.toTypeName() as? ClassName)?.canonicalName == Konvert::class.qualifiedName
}?.let { annotation ->
Expand All @@ -85,15 +93,17 @@ object KonverterDataCollector {
isAbstract = true,
sourceTypeReference = source,
targetTypeReference = target,
mapKSFunctionDeclaration = it
mapKSFunctionDeclaration = it,
additionalParameters = determineAdditionalParams(it, sourceValueParameter)
)
} else if (source != null && target != null) {
KonvertData(
annotationData = annotation ?: KonvertData.AnnotationData.default(resolver, it.isAbstract),
isAbstract = it.isAbstract,
sourceTypeReference = source,
targetTypeReference = target,
mapKSFunctionDeclaration = it
mapKSFunctionDeclaration = it,
additionalParameters = determineAdditionalParams(it, sourceValueParameter)
)
} else if (it.isAbstract) {
throw RuntimeException("Method $it is abstract and does not meet criteria for automatic source and target detection")
Expand All @@ -108,4 +118,33 @@ object KonverterDataCollector {
}.toList()
}

@OptIn(KspExperimental::class)
private fun determineSourceParam(function: KSFunctionDeclaration, logger: KSPLogger): KSValueParameter? {
val parameters = function.parameters
return when {
parameters.isEmpty() -> null
parameters.size > 1 -> {
val sourceParameter = parameters.filter { it.isAnnotationPresent(Konverter.Source::class) }
when {
sourceParameter.isEmpty() -> null
sourceParameter.size > 1 -> {
logger.error("Ignored method as multiple parameters were annotated with @Konverter.Source", function)
null
}

else -> sourceParameter.first()
}
}

else -> parameters.first()
}
}

@OptIn(KspExperimental::class)
private fun determineAdditionalParams(function: KSFunctionDeclaration, sourceParam: KSValueParameter?): List<KSValueParameter> {
return function.parameters
.filterNot { it.isAnnotationPresent(Konverter.Source::class) }
.filterNot { it == sourceParam }
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ object KonvertFromCodeGenerator {
data.targetClassDeclaration.simpleName.asString(),
data.sourceClassDeclaration.asStarProjectedType(),
data.targetClassDeclaration.asStarProjectedType(),
data.targetCompanionDeclaration
data.targetCompanionDeclaration,
emptyList()
)
),
priority = data.priority,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ object KonvertToCodeGenerator {
targetClassImportName,
data.sourceClassDeclaration.asStarProjectedType(),
data.targetClassDeclaration.asStarProjectedType(),
data.sourceClassDeclaration
data.sourceClassDeclaration,
emptyList()
)
),
priority = data.priority,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.mcarle.konvert.processor
import com.tschuchort.compiletesting.SourceFile
import io.mcarle.konvert.converter.SameTypeConverter
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

Expand Down Expand Up @@ -185,4 +186,29 @@ interface Mapper: IMapper {
)
}

@Test
fun doNotGenerateLineForKonverterFunctionsWithMultipleParameters() {
val (compilation) = compileWith(
enabledConverters = listOf(SameTypeConverter()),
code = SourceFile.kotlin(
name = "TestCode.kt",
contents =
"""
import io.mcarle.konvert.api.Konverter
class SourceClass(val property: String)
class TargetClass(val property: String, val other: Int)
@Konverter
interface Mapper {
fun toTarget(@Konverter.Source source: SourceClass, other: Int): TargetClass
}
""".trimIndent()
)
)
assertThrows<IllegalArgumentException> {
compilation.generatedSourceFor("io.mcarle.konvert.api.Konvert")
}
}

}
Loading

0 comments on commit be7bf9f

Please sign in to comment.