diff --git a/README.md b/README.md index 1923d77..675d363 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.70.6" +val kommVersion = "0.70.8" depensencies { implementation("com.ucasoft.komm:komm-annotations:$kommVersion") @@ -100,7 +100,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.70.6" +val kommVersion = "0.70.8" kotlin { jvm { @@ -646,7 +646,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.70.6" +val kommVersion = "0.70.8" depensencies { implementation("com.ucasoft.komm:komm-annotations:$kommVersion") @@ -660,7 +660,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.70.6" +val kommVersion = "0.70.8" //... @@ -720,7 +720,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.70.6" +val kommVersion = "0.70.8" depensencies { implementation("com.ucasoft.komm:komm-annotations:$kommVersion") @@ -764,7 +764,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.70.6" +val kommVersion = "0.70.8" depensencies { implementation("com.ucasoft.komm:komm-annotations:$kommVersion") @@ -778,7 +778,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.70.6" +val kommVersion = "0.70.8" //... diff --git a/build.gradle.kts b/build.gradle.kts index 2baf052..9a17fed 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ tasks.wrapper { allprojects { group = "com.ucasoft.komm" - version = "0.70.6" + version = "0.70.8" repositories { mavenCentral() diff --git a/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/KOMMAnnotationFinder.kt b/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/KOMMAnnotationFinder.kt index a7199b9..cd33d45 100644 --- a/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/KOMMAnnotationFinder.kt +++ b/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/KOMMAnnotationFinder.kt @@ -99,7 +99,10 @@ class KOMMAnnotationFinder(private val forClass: KSType) { } fun getSuitedNamedAnnotations(member: KSPropertyDeclaration) = - getSuitedNamedAnnotationsForClass(member).keys.toList() + getSuitedNamedAnnotationsForClass(member) + .filter { it.value.isEmpty() || it.value.contains(forClass.toClassName()) } + .keys + .toList() fun getSuitedNamedAnnotation(member: KSPropertyDeclaration) = filterAnnotationsByClass(forClass.toClassName(), getSuitedNamedAnnotationsForClass(member), member) diff --git a/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/KOMMPropertyMapper.kt b/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/KOMMPropertyMapper.kt index 7a2faa3..d5ff153 100644 --- a/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/KOMMPropertyMapper.kt +++ b/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/KOMMPropertyMapper.kt @@ -239,29 +239,28 @@ class KOMMPropertyMapper( val propertyType = getSourcePropertyType(source.sourceProperty) val destinationType = destinationProperty.type.resolve() + val destinationIsNullable = destinationType.toTypeName().isNullable val sourceIsNullable = source.isNullable val effectiveSourceType = if (sourceIsNullable) propertyType.makeNullable() else propertyType - if (destinationType.isAssignableFrom(effectiveSourceType)) { - return propertyName + getAssignableSource( + source, + propertyName, + destinationType, + destinationIsNullable, + sourceIsNullable, + effectiveSourceType, + useSafeAccess + )?.let { + return it } if (!config.getConfigValue(MapConfiguration::tryAutoCast.name)) { throw KOMMCastException("AutoCast is turned off! You have to use @${MapConvert::class.simpleName} annotation to cast (${destinationProperty.simpleName.asString()}: $destinationType) from ($propertyName: $propertyType).") } - val destinationIsNullable = destinationType.toTypeName().isNullable - val destinationHasNullSubstitute = - destinationProperty.annotations.any { it.shortName.asString() == NullSubstitute::class.simpleName } - val sourceHasNullSubstitute = (source.sourceProperty as? KSPropertyDeclaration) - ?.annotations - ?.any { it.shortName.asString() == NullSubstitute::class.simpleName } - ?: false - val destinationIsNullOrNullSubstitute = - destinationIsNullable || destinationHasNullSubstitute || sourceHasNullSubstitute val allowNotNullAssertion = config.getConfigValue(MapConfiguration::allowNotNullAssertion.name) - - if (sourceIsNullable && !destinationIsNullOrNullSubstitute && !allowNotNullAssertion) { + if (!canMapNullableSource(source, destinationProperty, destinationIsNullable, allowNotNullAssertion)) { throw KOMMCastException("Auto Not-Null Assertion is not allowed! You have to use @${NullSubstitute::class.simpleName} annotation for ${destinationProperty.simpleName.asString()} property.") } @@ -290,6 +289,45 @@ class KOMMPropertyMapper( return "$receiverPrefix$functionName()" } + private fun getAssignableSource( + source: EmbeddedSourceProperty, + propertyName: String, + destinationType: KSType, + destinationIsNullable: Boolean, + sourceIsNullable: Boolean, + effectiveSourceType: KSType, + useSafeAccess: Boolean + ): String? { + if (!destinationType.isAssignableFrom(effectiveSourceType)) { + return null + } + + return if (sourceIsNullable && destinationIsNullable) { + getSourceAccessName(source, useSafeAccess = true) + } else { + propertyName + } + } + + private fun canMapNullableSource( + source: EmbeddedSourceProperty, + destinationProperty: KSPropertyDeclaration, + destinationIsNullable: Boolean, + allowNotNullAssertion: Boolean + ): Boolean { + if (!source.isNullable || destinationIsNullable || allowNotNullAssertion) { + return true + } + + return destinationProperty.hasNullSubstitute || source.sourceProperty.hasNullSubstitute + } + + private val KSDeclaration.hasNullSubstitute + get() = (this as? KSPropertyDeclaration) + ?.annotations + ?.any { it.shortName.asString() == NullSubstitute::class.simpleName } + ?: false + private fun getSourceWithPluginCast( source: EmbeddedSourceProperty, propertyName: String, diff --git a/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/MapEmbeddedTests.kt b/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/MapEmbeddedTests.kt index 97b74c6..1f37254 100644 --- a/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/MapEmbeddedTests.kt +++ b/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/MapEmbeddedTests.kt @@ -2,11 +2,16 @@ package com.ucasoft.komm.processor import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.INT +import com.squareup.kotlinpoet.LONG import com.squareup.kotlinpoet.STRING +import com.squareup.kotlinpoet.asTypeName +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.tschuchort.compiletesting.KotlinCompilation import com.ucasoft.komm.annotations.KOMMMap +import com.ucasoft.komm.annotations.MapConvert import com.ucasoft.komm.annotations.MapDefault import com.ucasoft.komm.annotations.MapEmbedded +import com.ucasoft.komm.annotations.MapName import com.ucasoft.komm.annotations.NullSubstitute import com.ucasoft.komm.processor.exceptions.KOMMException import io.kotest.matchers.nulls.shouldNotBeNull @@ -320,4 +325,98 @@ internal class MapEmbeddedTests: SatelliteTests() { it.getter.call(destination).shouldBe("Account") } } + + @Test + fun mapEmbeddedWithMapToAndNestedMapName() { + val bankSpec = buildFileSpec( + "Bank", + mapOf("name" to PropertySpecInit(STRING)), + properties = mapOf( + "id" to PropertySpecInit( + LONG, + "%LL", + 0, + annotations = listOf( + MapName::class to mapOf( + "name = %S" to listOf("bankId"), + "`for` = %L" to listOf("[DestinationObject::class]") + ) + ) + ) + ) + ) + val bankClassName = bankSpec.typeSpecs.first().name!! + val sourceObjectClassName = ClassName(packageName, "SourceObject") + val otherSourceSpec = buildFileSpec("OtherSourceObject", mapOf("bankId" to PropertySpecInit(LONG))) + val otherSourceClassName = ClassName(packageName, otherSourceSpec.typeSpecs.first().name!!) + val converterSpec = buildConverter( + otherSourceClassName, + LONG, + sourceObjectClassName, + ClassName(packageName, bankClassName), + "return Bank(\"Resolved\")" + ) + val converterClassName = converterSpec.typeSpecs.first().name!! + val destinationSpec = buildFileSpec("DestinationObject", mapOf("bankId" to PropertySpecInit(LONG, isNullable = true))) + val destinationClassName = destinationSpec.typeSpecs.first().name!! + val sourceSpec = buildFileSpec( + sourceObjectClassName.simpleName, + mapOf( + "bank" to PropertySpecInit( + ClassName(packageName, bankClassName), + isNullable = true, + parametrizedAnnotations = listOf( + MapConvert::class.asTypeName() + .parameterizedBy( + otherSourceClassName, + sourceObjectClassName, + ClassName(packageName, converterClassName) + ) to mapOf( + "converter = %L" to listOf("$converterClassName::class"), + "name = %S" to listOf("bankId") + ) + ) + ) + ), + listOf( + KOMMMap::class to mapOf("to = %L" to listOf("[$destinationClassName::class]")), + MapEmbedded::class to mapOf( + "name = %S" to listOf("bank"), + "`for` = %L" to listOf("[$destinationClassName::class]") + ) + ) + ) + val sourceClassName = sourceSpec.typeSpecs.first().name!! + val generated = generate( + bankSpec, + otherSourceSpec, + converterSpec, + destinationSpec, + sourceSpec + ) + + generated.exitCode.shouldBe(KotlinCompilation.ExitCode.OK) + + val bankClass = generated.classLoader.loadClass("$packageName.$bankClassName") + val sourceClass = generated.classLoader.loadClass("$packageName.$sourceClassName") + val mappingClass = generated.classLoader.loadClass("$packageName.MappingExtensionsKt") + val bank = bankClass.constructors.first().newInstance("Primary") + bankClass.getDeclaredField("id").apply { + isAccessible = true + set(bank, 42L) + } + val source = sourceClass.constructors.first().newInstance(bank) + val destination = mappingClass.declaredMethods.first().invoke(null, source) + + destination::class.shouldHaveMemberProperty("bankId") { + it.getter.call(destination).shouldBe(42L) + } + + val sourceWithoutBank = sourceClass.constructors.first().newInstance(null) + val destinationWithoutBank = mappingClass.declaredMethods.first().invoke(null, sourceWithoutBank) + + destinationWithoutBank::class.shouldHaveMemberProperty("bankId") { + it.getter.call(destinationWithoutBank).shouldBe(null) + } + } }