From 6d4447fb144c70402566d33b3cafa2507e5286bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Carl=C3=A9?= Date: Sun, 25 Feb 2024 22:01:53 +0100 Subject: [PATCH] fix(#49): mapping for nested classes --- .../processor/codegen/MappingCodeGenerator.kt | 38 ++++-- .../konvert/KonverterCodeGenerator.kt | 7 +- .../konvertfrom/KonvertFromCodeGenerator.kt | 2 +- .../konvertto/KonvertToCodeGenerator.kt | 9 +- .../konvert/processor/konvert/KonvertITest.kt | 123 +++++++++++++++++- .../processor/konvertfrom/KonvertFromITest.kt | 77 +++++++++++ .../processor/konvertto/KonvertToITest.kt | 71 ++++++++++ 7 files changed, 304 insertions(+), 23 deletions(-) diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/MappingCodeGenerator.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/MappingCodeGenerator.kt index eb999cc..61ebe9d 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/MappingCodeGenerator.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/MappingCodeGenerator.kt @@ -1,5 +1,6 @@ package io.mcarle.konvert.processor.codegen +import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.google.devtools.ksp.symbol.KSPropertyDeclaration import com.google.devtools.ksp.symbol.KSType @@ -8,6 +9,7 @@ import com.google.devtools.ksp.symbol.KSValueParameter import com.google.devtools.ksp.symbol.Origin import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.joinToCode +import com.squareup.kotlinpoet.ksp.toClassName import io.mcarle.konvert.converter.api.TypeConverterRegistry import io.mcarle.konvert.converter.api.config.Configuration import io.mcarle.konvert.converter.api.config.enableConverters @@ -29,14 +31,18 @@ class MappingCodeGenerator { targetClassImportName: String?, targetProperties: List ): CodeBlock { - val typeName = targetClassImportName ?: constructor.parentDeclaration?.qualifiedName!!.asString() val className = constructor.parentDeclaration!!.simpleName.asString() - val constructorCode = constructorCode(typeName, constructor, sourceProperties) + val constructorCode = constructorCode( + className = targetClassImportName, + classDeclaration = constructor.parentDeclaration as? KSClassDeclaration, + constructor = constructor, + sourceProperties = sourceProperties + ) val propertyCode = propertyCode( - className, - functionParamName, - sourceProperties, - targetProperties + className = className, + functionParamName = functionParamName, + sourceProperties = sourceProperties, + targetProperties = targetProperties ) return if (source.isNullable()) { // source can only be nullable in case of @Konverter/@Konvert which require a functionParamName @@ -56,10 +62,25 @@ class MappingCodeGenerator { } private fun constructorCode( - className: String, + className: String?, + classDeclaration: KSClassDeclaration?, constructor: KSFunctionDeclaration, sourceProperties: List ): CodeBlock { + if (className == null) { + return if (constructor.parameters.isEmpty()) { + CodeBlock.of("%T()", classDeclaration?.toClassName()) + } else { + CodeBlock.of( + """ +%T(${"⇥\n%L"} +⇤) + """.trimIndent(), + classDeclaration?.toClassName(), + constructorParamsCode(constructor = constructor, sourceProperties = sourceProperties) + ) + } + } return if (constructor.parameters.isEmpty()) { CodeBlock.of("$className()") } else { @@ -67,7 +88,8 @@ class MappingCodeGenerator { """ $className(${"⇥\n%L"} ⇤) - """.trimIndent(), constructorParamsCode(constructor = constructor, sourceProperties = sourceProperties) + """.trimIndent(), + constructorParamsCode(constructor = constructor, sourceProperties = sourceProperties) ) } } diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterCodeGenerator.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterCodeGenerator.kt index 191cccb..3706285 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterCodeGenerator.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterCodeGenerator.kt @@ -39,7 +39,8 @@ object KonverterCodeGenerator { withIsolatedConfiguration(konvertData.annotationData.options) { if (isAlias(konvertData.sourceTypeReference, konvertData.sourceType)) { // @Konverter annotated interface used alias for source, so the implementation should also use the same alias - codeBuilder.addImport(konvertData.sourceType, konvertData.sourceTypeReference.toString())} + codeBuilder.addImport(konvertData.sourceType, konvertData.sourceTypeReference.toString()) + } val targetClassImportName = if (isAlias(konvertData.targetTypeReference, konvertData.targetType)) { @@ -47,10 +48,8 @@ object KonverterCodeGenerator { val alias = konvertData.targetTypeReference.toString() codeBuilder.addImport(konvertData.targetType, alias) alias - } else if (konvertData.sourceTypeReference.toString() == konvertData.targetTypeReference.toString()) { - null } else { - konvertData.targetClassDeclaration.simpleName.asString() + null } codeBuilder.addFunction( diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromCodeGenerator.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromCodeGenerator.kt index 3301043..ac2531a 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromCodeGenerator.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromCodeGenerator.kt @@ -32,7 +32,7 @@ object KonvertFromCodeGenerator { data.annotationData.mappings.validated(data.sourceClassDeclaration, logger), data.annotationData.constructor, data.paramName, - data.targetClassDeclaration.simpleName.asString(), + null, data.sourceClassDeclaration.asStarProjectedType(), data.targetClassDeclaration.asStarProjectedType(), data.targetCompanionDeclaration, diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToCodeGenerator.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToCodeGenerator.kt index e12bba0..d698bad 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToCodeGenerator.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToCodeGenerator.kt @@ -22,13 +22,6 @@ object KonvertToCodeGenerator { data.sourceClassDeclaration.simpleName.asString(), ) - val targetClassImportName = - if (data.sourceClassDeclaration.simpleName.asString() != data.targetClassDeclaration.simpleName.asString()) { - data.targetClassDeclaration.simpleName.asString() - } else { - null - } - fileSpecBuilder.addFunction( funBuilder = FunSpec.builder(data.mapFunctionName) .returns(data.targetClassDeclaration.asStarProjectedType().toTypeName()) @@ -38,7 +31,7 @@ object KonvertToCodeGenerator { data.annotationData.mappings.validated(data.sourceClassDeclaration, logger), data.annotationData.constructor, null, - targetClassImportName, + null, data.sourceClassDeclaration.asStarProjectedType(), data.targetClassDeclaration.asStarProjectedType(), data.sourceClassDeclaration, diff --git a/processor/src/test/kotlin/io/mcarle/konvert/processor/konvert/KonvertITest.kt b/processor/src/test/kotlin/io/mcarle/konvert/processor/konvert/KonvertITest.kt index fcaf360..fbb2bef 100644 --- a/processor/src/test/kotlin/io/mcarle/konvert/processor/konvert/KonvertITest.kt +++ b/processor/src/test/kotlin/io/mcarle/konvert/processor/konvert/KonvertITest.kt @@ -651,7 +651,7 @@ interface SomeConverter { import b.SomeClass as BSomeClass public object SomeConverterImpl : SomeConverter { - override fun toSomeClass(source: ASomeClass): BSomeClass = b.SomeClass().also { someClass -> + override fun toSomeClass(source: ASomeClass): BSomeClass = BSomeClass().also { someClass -> someClass.property = source.property } } @@ -711,7 +711,7 @@ interface SomeConverter { import b.SomeClass as BSomeClass public object SomeConverterImpl : SomeConverter { - override fun toSomeClass(source: ASomeClass): BSomeClass? = b.SomeClass().also { someClass -> + override fun toSomeClass(source: ASomeClass): BSomeClass? = BSomeClass().also { someClass -> someClass.property = source.property } } @@ -1520,6 +1520,125 @@ interface OtherMapper { assertContains(mapperCode, "Konverter.get().toTarget(") } + + @Test + fun nestedClass() { + val (compilation) = compileWith( + enabledConverters = listOf(SameTypeConverter()), + expectResultCode = KotlinCompilation.ExitCode.OK, + code = arrayOf( + SourceFile.kotlin( + name = "a/Person.kt", + contents = + """ +package a + +data class Person(val firstName: String, val lastName: String, val age: Int, val address: Address) { + data class Address(val address1: String, val address2: String) +} + """.trimIndent() + ), + SourceFile.kotlin( + name = "b/PersonDto.kt", + contents = + """ +package b + +data class PersonDto(val firstName: String, val lastName: String, val age: Int, val address: AddressDto) { + data class AddressDto(val address1: String, val address2: String) +} + """.trimIndent() + ), + SourceFile.kotlin( + name = "c/DomainMapper.kt", + contents = + """ +package c + +import io.mcarle.konvert.api.Konverter +import a.Person +import b.PersonDto + +@Konverter +interface DomainMapper { + fun toAddressDto(address: Person.Address): PersonDto.AddressDto +} + """.trimIndent() + ), + SourceFile.kotlin( + name = "d/DtoMapper.kt", + contents = + """ +package d + +import io.mcarle.konvert.api.Konverter +import a.Person.Address as AddressDomain +import b.PersonDto + +@Konverter +interface DtoMapper { + fun toAddress(address: PersonDto.AddressDto): AddressDomain +} + """.trimIndent() + ), + ) + ) + val domainMapperCode = compilation.generatedSourceFor("DomainMapperKonverter.kt") + println(domainMapperCode) + val dtoMapperCode = compilation.generatedSourceFor("DtoMapperKonverter.kt") + println(dtoMapperCode) + + assertSourceEquals( + """ + package c + + import a.Person + import b.PersonDto + + public object DomainMapperImpl : DomainMapper { + override fun toAddressDto(address: Person.Address): PersonDto.AddressDto = PersonDto.AddressDto( + address1 = address.address1, + address2 = address.address2 + ) + } + """.trimIndent(), + domainMapperCode + ) + assertSourceEquals( + """ + package d + + import a.Person + import b.PersonDto + import a.Person.Address as AddressDomain + + public object DtoMapperImpl : DtoMapper { + override fun toAddress(address: PersonDto.AddressDto): a.Person.Address = AddressDomain( + address1 = address.address1, + address2 = address.address2 + ) + } + """.trimIndent(), + dtoMapperCode + ) + // TODO: After https://github.com/square/kotlinpoet/issues/1838 is fixed, the following assertion should succeed +// assertSourceEquals( +// """ +// package d +// +// import b.PersonDto +// import a.Person.Address as AddressDomain +// +// public object DtoMapperImpl : DtoMapper { +// override fun toAddress(address: PersonDto.AddressDto): AddressDomain = AddressDomain( +// address1 = address.address1, +// address2 = address.address2 +// ) +// } +// """.trimIndent(), +// dtoMapperCode +// ) + } } private fun Konverter.Companion.getWithClassLoader(classFQN: String, classLoader: ClassLoader): Any { diff --git a/processor/src/test/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromITest.kt b/processor/src/test/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromITest.kt index 3d497b1..b048bc3 100644 --- a/processor/src/test/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromITest.kt +++ b/processor/src/test/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromITest.kt @@ -498,4 +498,81 @@ class TargetClass(val children: List) { ) } + @Test + fun nestedClass() { + val (compilation) = compileWith( + enabledConverters = listOf(SameTypeConverter()), + expectResultCode = KotlinCompilation.ExitCode.OK, + code = arrayOf( + SourceFile.kotlin( + name = "a/Person.kt", + contents = + """ +package a + +import io.mcarle.konvert.api.KonvertFrom +import b.PersonDto + +data class Person(val firstName: String, val lastName: String, val age: Int, val address: Address) { + data class Address(val address1: String, val address2: String) { + @KonvertFrom(PersonDto.AddressDto::class) + companion object + } +} + """.trimIndent() + ), + SourceFile.kotlin( + name = "b/PersonDto.kt", + contents = + """ +package b + +import io.mcarle.konvert.api.KonvertFrom +import a.Person.Address as AddressDomain + +data class PersonDto(val firstName: String, val lastName: String, val age: Int, val address: AddressDto) { + data class AddressDto(val address1: String, val address2: String) { + @KonvertFrom(AddressDomain::class) + companion object + } +} + """.trimIndent() + ) + ) + ) + val addressExtensionFunctionCode = compilation.generatedSourceFor("AddressKonverter.kt") + println(addressExtensionFunctionCode) + val addressDtoExtensionFunctionCode = compilation.generatedSourceFor("AddressDtoKonverter.kt") + println(addressDtoExtensionFunctionCode) + + assertSourceEquals( + """ + package a + + import b.PersonDto + + public fun Person.Address.Companion.fromAddressDto(addressDto: PersonDto.AddressDto): Person.Address + = Person.Address( + address1 = addressDto.address1, + address2 = addressDto.address2 + ) + """.trimIndent(), + addressExtensionFunctionCode + ) + assertSourceEquals( + """ + package b + + import a.Person + + public fun PersonDto.AddressDto.Companion.fromAddress(address: Person.Address): PersonDto.AddressDto + = PersonDto.AddressDto( + address1 = address.address1, + address2 = address.address2 + ) + """.trimIndent(), + addressDtoExtensionFunctionCode + ) + } + } diff --git a/processor/src/test/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToITest.kt b/processor/src/test/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToITest.kt index f19c484..1be4723 100644 --- a/processor/src/test/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToITest.kt +++ b/processor/src/test/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToITest.kt @@ -426,4 +426,75 @@ class TargetClass(val children: List) ) } + @Test + fun nestedClass() { + val (compilation) = compileWith( + enabledConverters = listOf(SameTypeConverter()), + expectResultCode = KotlinCompilation.ExitCode.OK, + code = arrayOf( + SourceFile.kotlin( + name = "a/Person.kt", + contents = + """ +package a + +import io.mcarle.konvert.api.KonvertTo +import b.PersonDto + +data class Person(val firstName: String, val lastName: String, val age: Int, val address: Address) { + @KonvertTo(PersonDto.AddressDto::class) + data class Address(val address1: String, val address2: String) +} + """.trimIndent() + ), + SourceFile.kotlin( + name = "b/PersonDto.kt", + contents = + """ +package b + +import io.mcarle.konvert.api.KonvertTo +import a.Person.Address as AddressDomain + +data class PersonDto(val firstName: String, val lastName: String, val age: Int, val address: AddressDto) { + @KonvertTo(AddressDomain::class) + data class AddressDto(val address1: String, val address2: String) +} + """.trimIndent() + ) + ) + ) + val addressExtensionFunctionCode = compilation.generatedSourceFor("AddressKonverter.kt") + println(addressExtensionFunctionCode) + val addressDtoExtensionFunctionCode = compilation.generatedSourceFor("AddressDtoKonverter.kt") + println(addressDtoExtensionFunctionCode) + + assertSourceEquals( + """ + package a + + import b.PersonDto + + public fun Person.Address.toAddressDto(): PersonDto.AddressDto = PersonDto.AddressDto( + address1 = address1, + address2 = address2 + ) + """.trimIndent(), + addressExtensionFunctionCode + ) + assertSourceEquals( + """ + package b + + import a.Person + + public fun PersonDto.AddressDto.toAddress(): Person.Address = Person.Address( + address1 = address1, + address2 = address2 + ) + """.trimIndent(), + addressDtoExtensionFunctionCode + ) + } + }