Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down Expand Up @@ -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")
Expand All @@ -660,7 +660,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.70.6"
val kommVersion = "0.70.8"

//...

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -778,7 +778,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.70.6"
val kommVersion = "0.70.8"

//...

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ tasks.wrapper {
allprojects {
group = "com.ucasoft.komm"

version = "0.70.6"
version = "0.70.8"

repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean>(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<Boolean>(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.")
}

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}