From 7a741aa4d0395b5f6ed2d77c36d793aa75332c21 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 19 Jun 2026 16:16:04 +0200 Subject: [PATCH 1/7] Add Embedded annotation --- .../kotlin/com/ucasoft/komm/annotations/MapEmbedded.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 komm-annotations/src/main/kotlin/com/ucasoft/komm/annotations/MapEmbedded.kt diff --git a/komm-annotations/src/main/kotlin/com/ucasoft/komm/annotations/MapEmbedded.kt b/komm-annotations/src/main/kotlin/com/ucasoft/komm/annotations/MapEmbedded.kt new file mode 100644 index 0000000..0792338 --- /dev/null +++ b/komm-annotations/src/main/kotlin/com/ucasoft/komm/annotations/MapEmbedded.kt @@ -0,0 +1,8 @@ +package com.ucasoft.komm.annotations + +import kotlin.reflect.KClass + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +@Repeatable +annotation class MapEmbedded(val name: String, val `for`: Array> = []) From 23ab624a2b8c15482e36303628b239bfaed5b6da Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 19 Jun 2026 16:16:21 +0200 Subject: [PATCH 2/7] Implement Embedded support --- .../komm/processor/KOMMAnnotationFinder.kt | 8 + .../komm/processor/KOMMPropertyMapper.kt | 167 +++++++-- .../komm/processor/MapEmbeddedTests.kt | 323 ++++++++++++++++++ 3 files changed, 465 insertions(+), 33 deletions(-) create mode 100644 komm-processor/src/test/kotlin/com/ucasoft/komm/processor/MapEmbeddedTests.kt 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 0828ae4..d3443ef 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 @@ -76,6 +76,14 @@ class KOMMAnnotationFinder(private val forClass: KSType) { fun getSuitedNamedAnnotation(member: KSPropertyDeclaration) = filterAnnotationsByClass(forClass.toClassName(), getSuitedNamedAnnotationsForClass(member), member) + fun getSuitedEmbeddedAnnotations(classDeclaration: KSClassDeclaration): List = + classDeclaration.annotations + .filter { it.shortName.asString() == MapEmbedded::class.simpleName } + .associateWith(::associateWithFor) + .filter { it.value.isEmpty() || it.value.contains(forClass.toClassName()) } + .keys + .toList() + private fun getSuitedNamedAnnotationsForClass(member: KSPropertyDeclaration) = member.annotations.filter { it.shortName.asString() in namedAnnotations } .associateWith(::associateWithFor) 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 1998775..8b41cc9 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 @@ -17,12 +17,15 @@ class KOMMPropertyMapper( private val plugins: List ) { - private val annotationFinder = KOMMAnnotationFinder(when (direction) { - KOMMVisitor.Direction.From -> source - KOMMVisitor.Direction.To -> destination - }) + private val annotationFinder = KOMMAnnotationFinder( + when (direction) { + KOMMVisitor.Direction.From -> source + KOMMVisitor.Direction.To -> destination + } + ) private val sourceProperties = getSourceProperties(source) + private val embeddedSourceProperties = getEmbeddedSourceProperties(source, destination) fun map(destination: KSPropertyDeclaration, mapTo: KOMMVisitor.MapTo): String? { val resolver = annotationFinder.findResolver(destination) @@ -31,24 +34,27 @@ class KOMMPropertyMapper( } val sourceName = getMapName(destination) - if (!sourceProperties.containsKey(sourceName)) { - return handleNoSourceProperty(resolver, destination, sourceName, mapTo) - } - val source = sourceProperties[sourceName] + val source = getSourceProperty(sourceName) + ?: return handleNoSourceProperty(resolver, destination, sourceName, mapTo) + val sourceProperty = source.sourceProperty val converter = when (direction) { KOMMVisitor.Direction.From -> annotationFinder.findConverter(destination) - KOMMVisitor.Direction.To -> annotationFinder.findConverter(source as KSPropertyDeclaration) + KOMMVisitor.Direction.To -> (sourceProperty as? KSPropertyDeclaration)?.let { + annotationFinder.findConverter(it) + } } val nullSubstituteResolver = when (direction) { KOMMVisitor.Direction.From -> annotationFinder.findSubstituteResolver(destination) - KOMMVisitor.Direction.To -> annotationFinder.findSubstituteResolver(source as KSPropertyDeclaration) + KOMMVisitor.Direction.To -> (sourceProperty as? KSPropertyDeclaration)?.let { + annotationFinder.findSubstituteResolver(it) + } } return if (converter != null) { - "$destination = $converter(this).convert($sourceName)" + "$destination = $converter(this).convert(${getSourceAccessName(source)})" } else if (nullSubstituteResolver != null) { "$destination = ${ - getSourceWithCast(destination, source, config).trimEnd('!').replace("!!", "?") + getSourceWithCast(destination, source, config, useSafeAccess = true) } ?: ${mapResolver(nullSubstituteResolver, mapTo)}" } else { "$destination = ${getSourceWithCast(destination, source, config)}" @@ -58,6 +64,21 @@ class KOMMPropertyMapper( private fun mapResolver(resolver: String, mapTo: KOMMVisitor.MapTo) = "$resolver(${if (mapTo == KOMMVisitor.MapTo.Constructor) "null" else "it"}).resolve()" + private fun getSourceProperty(sourceName: String): EmbeddedSourceProperty? { + sourceProperties[sourceName]?.let { return EmbeddedSourceProperty(null, it) } + + val embeddedProperties = embeddedSourceProperties[sourceName] ?: return null + if (embeddedProperties.count() > 1) { + throw KOMMException( + "There are more than one embedded property with the same name $sourceName: ${ + embeddedProperties.joinToString { getSourceAccessName(it) } + }." + ) + } + + return embeddedProperties.first() + } + private fun getMapNames(member: KSPropertyDeclaration): List { val mapsFor = annotationFinder.getSuitedNamedAnnotations(member) val result = mutableListOf(member.toString()).apply { @@ -116,26 +137,54 @@ class KOMMPropertyMapper( return result.apply { putAll( sourceClass.getAllFunctions().filter { it.parameters.isEmpty() } - .associateBy { it.toString().substring(3).lowercase() }) + .associateBy { getSourcePropertyName(it) }) } } + private fun getEmbeddedSourceProperties( + source: KSType, + destination: KSType + ): Map> { + val annotationOwner = when (direction) { + KOMMVisitor.Direction.From -> destination.declaration + KOMMVisitor.Direction.To -> source.declaration + } as KSClassDeclaration + + return annotationFinder.getSuitedEmbeddedAnnotations(annotationOwner) + .flatMap { annotation -> + val embeddedName = annotation.arguments + .first { it.name?.asString() == MapEmbedded::name.name } + .value + .toString() + val embeddedSource = sourceProperties[embeddedName] + ?: throw KOMMException("There is no embedded mapping source $embeddedName property from source ${source.declaration.simpleName.asString()}.") + if (embeddedSource !is KSPropertyDeclaration) { + throw KOMMException("Embedded mapping source $embeddedName from source ${source.declaration.simpleName.asString()} is not a property.") + } + val embeddedType = embeddedSource.type.resolve() + if (embeddedType.declaration !is KSClassDeclaration) { + throw KOMMException("Embedded mapping source $embeddedName from source ${source.declaration.simpleName.asString()} is not a class.") + } + + getSourceProperties(embeddedType).map { it.key to EmbeddedSourceProperty(embeddedSource, it.value) } + } + .groupBy({ it.first }, { it.second }) + } + private fun getSourceWithCast( destinationProperty: KSPropertyDeclaration, - sourceProperty: KSDeclaration?, - config: KSAnnotation + source: EmbeddedSourceProperty, + config: KSAnnotation, + useSafeAccess: Boolean = false ): String { - val (propertyName, propertyType) = when (sourceProperty) { - is KSFunctionDeclaration -> sourceProperty.toString().substring(3) - .lowercase() to sourceProperty.returnType!!.resolve() - - is KSPropertyDeclaration -> sourceProperty.toString() to sourceProperty.type.resolve() - else -> throw KOMMException("There is no source property for ${destinationProperty.simpleName}") - } + val propertyName = getSourceAccessName(source, useSafeAccess) + val propertyType = getSourcePropertyType(source.sourceProperty) val destinationType = destinationProperty.type.resolve() + val sourceIsNullable = source.isNullable + val effectiveSourceType = if (sourceIsNullable) propertyType.makeNullable() else propertyType - if (destinationType.isAssignableFrom(propertyType)) { + if (destinationType.isAssignableFrom(effectiveSourceType)) { return propertyName } @@ -143,29 +192,81 @@ class KOMMPropertyMapper( throw KOMMCastException("AutoCast is turned off! You have to use @${MapConvert::class.simpleName} annotation to cast (${destinationProperty.simpleName.asString()}: $destinationType) from ($propertyName: $propertyType).") } - val sourceIsNullable = propertyType.toTypeName().isNullable val destinationIsNullable = destinationType.toTypeName().isNullable - val destinationHasNullSubstitute = destinationProperty.annotations.any { it.shortName.asString() == NullSubstitute::class.simpleName } - val sourceHasNullSubstitute = sourceProperty.annotations.any { it.shortName.asString() == NullSubstitute::class.simpleName } - val destinationIsNullOrNullSubstitute = destinationIsNullable || destinationHasNullSubstitute || sourceHasNullSubstitute + 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) { throw KOMMCastException("Auto Not-Null Assertion is not allowed! You have to use @${NullSubstitute::class.simpleName} annotation for ${destinationProperty.simpleName.asString()} property.") } - val castPlugin = plugins.filter { it.forCast(propertyType, destinationType) } + val castPlugin = plugins.filter { it.forCast(effectiveSourceType, destinationType) } if (castPlugin.count() > 1) { - throw KOMMPluginsException("There are more than one plugin for casting from $propertyType to $destinationType.") + throw KOMMPluginsException("There are more than one plugin for casting from $effectiveSourceType to $destinationType.") } else if (castPlugin.count() == 1) { - return castPlugin.first().cast(sourceProperty, propertyName, propertyType, destinationProperty, destinationType) + return castPlugin.first() + .cast(source.sourceProperty, propertyName, effectiveSourceType, destinationProperty, destinationType) } if (sourceIsNullable && destinationType.isAssignableFrom(propertyType.makeNotNullable())) { - return "$propertyName!!" + return if (useSafeAccess) propertyName else getSourceAccessName(source, assertNotNull = true) + } + + return "${ + if (sourceIsNullable && !useSafeAccess) getSourceAccessName( + source, + assertNotNull = true + ) else propertyName + }${if (sourceIsNullable && useSafeAccess) "?" else ""}.to${destinationProperty.type}()" + } + + private fun getSourceAccessName( + source: EmbeddedSourceProperty, + useSafeAccess: Boolean = false, + assertNotNull: Boolean = false + ): String { + val propertyName = getSourcePropertyName(source.sourceProperty) + val propertyAssertion = if (assertNotNull && isNullable(source.sourceProperty)) "!!" else "" + val embeddedProperty = source.embeddedProperty ?: return "$propertyName$propertyAssertion" + val embeddedName = embeddedProperty.simpleName.asString() + val embeddedAccess = when { + useSafeAccess && isNullable(embeddedProperty) -> "$embeddedName?" + assertNotNull && isNullable(embeddedProperty) -> "$embeddedName!!" + else -> embeddedName + } + + return "$embeddedAccess.$propertyName$propertyAssertion" + } + + private fun getSourcePropertyName(sourceProperty: KSDeclaration) = when (sourceProperty) { + is KSFunctionDeclaration -> sourceProperty.toString().substring(3).lowercase() + else -> sourceProperty.simpleName.asString() + } + + companion object { + internal fun isNullable(sourceProperty: KSDeclaration) = + getSourcePropertyType(sourceProperty).toTypeName().isNullable + + private fun getSourcePropertyType(sourceProperty: KSDeclaration) = when (sourceProperty) { + is KSFunctionDeclaration -> sourceProperty.returnType!!.resolve() + is KSPropertyDeclaration -> sourceProperty.type.resolve() + else -> throw KOMMException("There is no source property for ${sourceProperty.simpleName}") } + } - return "$propertyName${if (sourceIsNullable) "!!" else ""}.to${destinationProperty.type}()" + private data class EmbeddedSourceProperty( + val embeddedProperty: KSPropertyDeclaration?, + val sourceProperty: KSDeclaration + ) { + val isNullable: Boolean + get() = isNullable(sourceProperty) || embeddedProperty?.let(::isNullable) ?: false } -} \ No newline at end of file +} 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 new file mode 100644 index 0000000..97b74c6 --- /dev/null +++ b/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/MapEmbeddedTests.kt @@ -0,0 +1,323 @@ +package com.ucasoft.komm.processor + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.INT +import com.squareup.kotlinpoet.STRING +import com.tschuchort.compiletesting.KotlinCompilation +import com.ucasoft.komm.annotations.KOMMMap +import com.ucasoft.komm.annotations.MapDefault +import com.ucasoft.komm.annotations.MapEmbedded +import com.ucasoft.komm.annotations.NullSubstitute +import com.ucasoft.komm.processor.exceptions.KOMMException +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.reflection.shouldHaveMemberProperty +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import kotlin.test.Test + +internal class MapEmbeddedTests: SatelliteTests() { + + @Test + fun mapEmbeddedConstructorAndMutableProperties() { + val accountSpec = buildFileSpec( + "Account", + mapOf( + "id" to PropertySpecInit(INT), + "name" to PropertySpecInit(STRING) + ) + ) + val accountClassName = accountSpec.typeSpecs.first().name!! + val sourceSpec = buildFileSpec( + "SourceObject", + mapOf("account" to PropertySpecInit(ClassName(packageName, accountClassName))) + ) + val sourceClassName = sourceSpec.typeSpecs.first().name!! + val generated = generate( + accountSpec, + sourceSpec, + buildFileSpec( + "DestinationObject", + mapOf("name" to PropertySpecInit(STRING)), + listOf( + KOMMMap::class to mapOf("from = %L" to listOf("[$sourceClassName::class]")), + MapEmbedded::class to mapOf("name = %S" to listOf("account")) + ), + mapOf("id" to PropertySpecInit(INT, "%L", 0)) + ) + ) + + generated.exitCode.shouldBe(KotlinCompilation.ExitCode.OK) + + val accountClass = generated.classLoader.loadClass("$packageName.$accountClassName") + val sourceClass = generated.classLoader.loadClass("$packageName.$sourceClassName") + val mappingClass = generated.classLoader.loadClass("$packageName.MappingExtensionsKt") + val account = accountClass.constructors.first().newInstance(10, "Main") + val source = sourceClass.constructors.first().newInstance(account) + val destination = mappingClass.declaredMethods.first().invoke(null, source) + + destination.shouldNotBeNull() + destination::class.shouldHaveMemberProperty("name") { + it.getter.call(destination).shouldBe("Main") + } + destination::class.shouldHaveMemberProperty("id") { + it.getter.call(destination).shouldBe(10) + } + } + + @Test + fun directPropertyWinsOverEmbeddedProperty() { + val accountSpec = buildFileSpec("Account", mapOf("name" to PropertySpecInit(STRING))) + val accountClassName = accountSpec.typeSpecs.first().name!! + val sourceSpec = buildFileSpec( + "SourceObject", + mapOf( + "name" to PropertySpecInit(STRING), + "account" to PropertySpecInit(ClassName(packageName, accountClassName)) + ) + ) + val sourceClassName = sourceSpec.typeSpecs.first().name!! + val generated = generate( + accountSpec, + sourceSpec, + buildFileSpec( + "DestinationObject", + mapOf("name" to PropertySpecInit(STRING)), + listOf( + KOMMMap::class to mapOf("from = %L" to listOf("[$sourceClassName::class]")), + MapEmbedded::class to mapOf("name = %S" to listOf("account")) + ) + ) + ) + + generated.exitCode.shouldBe(KotlinCompilation.ExitCode.OK) + + val accountClass = generated.classLoader.loadClass("$packageName.$accountClassName") + val sourceClass = generated.classLoader.loadClass("$packageName.$sourceClassName") + val mappingClass = generated.classLoader.loadClass("$packageName.MappingExtensionsKt") + val account = accountClass.constructors.first().newInstance("Embedded") + val source = sourceClass.constructors.first().newInstance("Direct", account) + val destination = mappingClass.declaredMethods.first().invoke(null, source) + + destination.shouldNotBeNull() + destination::class.shouldHaveMemberProperty("name") { + it.getter.call(destination).shouldBe("Direct") + } + } + + @Test + fun embeddedPropertyAmbiguityFails() { + val accountSpec = buildFileSpec("Account", mapOf("name" to PropertySpecInit(STRING))) + val accountClassName = accountSpec.typeSpecs.first().name!! + val profileSpec = buildFileSpec("Profile", mapOf("name" to PropertySpecInit(STRING))) + val profileClassName = profileSpec.typeSpecs.first().name!! + val sourceSpec = buildFileSpec( + "SourceObject", + mapOf( + "account" to PropertySpecInit(ClassName(packageName, accountClassName)), + "profile" to PropertySpecInit(ClassName(packageName, profileClassName)) + ) + ) + val sourceClassName = sourceSpec.typeSpecs.first().name!! + val generated = generate( + accountSpec, + profileSpec, + sourceSpec, + buildFileSpec( + "DestinationObject", + mapOf("name" to PropertySpecInit(STRING)), + listOf( + KOMMMap::class to mapOf("from = %L" to listOf("[$sourceClassName::class]")), + MapEmbedded::class to mapOf("name = %S" to listOf("account")), + MapEmbedded::class to mapOf("name = %S" to listOf("profile")) + ) + ) + ) + + generated.exitCode.shouldBe(KotlinCompilation.ExitCode.INTERNAL_ERROR) + generated.messages.shouldContain("${KOMMException::class.simpleName}: There are more than one embedded property with the same name name: account.name, profile.name.") + } + + @Test + fun mapEmbeddedBadSourceNameFails() { + val accountSpec = buildFileSpec("Account", mapOf("name" to PropertySpecInit(STRING))) + val accountClassName = accountSpec.typeSpecs.first().name!! + val sourceSpec = buildFileSpec( + "SourceObject", + mapOf("account" to PropertySpecInit(ClassName(packageName, accountClassName))) + ) + val sourceClassName = sourceSpec.typeSpecs.first().name!! + val generated = generate( + accountSpec, + sourceSpec, + buildFileSpec( + "DestinationObject", + mapOf("name" to PropertySpecInit(STRING)), + listOf( + KOMMMap::class to mapOf("from = %L" to listOf("[$sourceClassName::class]")), + MapEmbedded::class to mapOf("name = %S" to listOf("missing")) + ) + ) + ) + + generated.exitCode.shouldBe(KotlinCompilation.ExitCode.INTERNAL_ERROR) + generated.messages.shouldContain("${KOMMException::class.simpleName}: There is no embedded mapping source missing property from source $sourceClassName.") + } + + @Test + fun mapEmbeddedNullableParentWithNullSubstitute() { + val accountSpec = buildFileSpec("Account", mapOf("id" to PropertySpecInit(INT))) + val accountClassName = accountSpec.typeSpecs.first().name!! + val sourceSpec = buildFileSpec( + "SourceObject", + mapOf("account" to PropertySpecInit(ClassName(packageName, accountClassName), isNullable = true)) + ) + val sourceClassName = sourceSpec.typeSpecs.first().name!! + val resolver = buildResolver(ClassName(packageName, "DestinationObject"), INT, "return 25") + val resolverClassName = resolver.typeSpecs.first().name!! + val generated = generate( + accountSpec, + sourceSpec, + resolver, + buildFileSpec( + "DestinationObject", + mapOf( + "id" to PropertySpecInit( + INT, + annotations = listOf( + NullSubstitute::class to mapOf( + "default = %L" to listOf("${MapDefault::class.simpleName}($resolverClassName::class)") + ) + ) + ) + ), + listOf( + KOMMMap::class to mapOf("from = %L" to listOf("[$sourceClassName::class]")), + MapEmbedded::class to mapOf("name = %S" to listOf("account")) + ) + ) + ) + + generated.exitCode.shouldBe(KotlinCompilation.ExitCode.OK) + + val accountClass = generated.classLoader.loadClass("$packageName.$accountClassName") + val sourceClass = generated.classLoader.loadClass("$packageName.$sourceClassName") + val mappingClass = generated.classLoader.loadClass("$packageName.MappingExtensionsKt") + var source = sourceClass.constructors.first().newInstance(null) + var destination = mappingClass.declaredMethods.first().invoke(null, source) + + destination.shouldNotBeNull() + destination::class.shouldHaveMemberProperty("id") { + it.getter.call(destination).shouldBe(25) + } + + val account = accountClass.constructors.first().newInstance(10) + source = sourceClass.constructors.first().newInstance(account) + destination = mappingClass.declaredMethods.first().invoke(null, source) + + destination::class.shouldHaveMemberProperty("id") { + it.getter.call(destination).shouldBe(10) + } + } + + @Test + fun mapEmbeddedUsesForWithMultipleSources() { + val accountSpec = buildFileSpec("Account", mapOf("name" to PropertySpecInit(STRING))) + val accountClassName = accountSpec.typeSpecs.first().name!! + val profileSpec = buildFileSpec("Profile", mapOf("name" to PropertySpecInit(STRING))) + val profileClassName = profileSpec.typeSpecs.first().name!! + val firstSourceSpec = buildFileSpec( + "FirstSourceObject", + mapOf("account" to PropertySpecInit(ClassName(packageName, accountClassName))) + ) + val firstSourceClassName = firstSourceSpec.typeSpecs.first().name!! + val secondSourceSpec = buildFileSpec( + "SecondSourceObject", + mapOf("profile" to PropertySpecInit(ClassName(packageName, profileClassName))) + ) + val secondSourceClassName = secondSourceSpec.typeSpecs.first().name!! + val generated = generate( + accountSpec, + profileSpec, + firstSourceSpec, + secondSourceSpec, + buildFileSpec( + "DestinationObject", + mapOf("name" to PropertySpecInit(STRING)), + listOf( + KOMMMap::class to mapOf("from = %L" to listOf("[$firstSourceClassName::class, $secondSourceClassName::class]")), + MapEmbedded::class to mapOf( + "name = %S" to listOf("account"), + "`for` = %L" to listOf("[$firstSourceClassName::class]") + ), + MapEmbedded::class to mapOf( + "name = %S" to listOf("profile"), + "`for` = %L" to listOf("[$secondSourceClassName::class]") + ) + ) + ) + ) + + generated.exitCode.shouldBe(KotlinCompilation.ExitCode.OK) + + val accountClass = generated.classLoader.loadClass("$packageName.$accountClassName") + val profileClass = generated.classLoader.loadClass("$packageName.$profileClassName") + val firstSourceClass = generated.classLoader.loadClass("$packageName.$firstSourceClassName") + val secondSourceClass = generated.classLoader.loadClass("$packageName.$secondSourceClassName") + val mappingClass = generated.classLoader.loadClass("$packageName.MappingExtensionsKt") + var mappingMethod = mappingClass.declaredMethods.first { it.toString().contains(firstSourceClassName) } + val firstSource = firstSourceClass.constructors.first().newInstance( + accountClass.constructors.first().newInstance("Account") + ) + var destination = mappingMethod.invoke(null, firstSource) + + destination::class.shouldHaveMemberProperty("name") { + it.getter.call(destination).shouldBe("Account") + } + + mappingMethod = mappingClass.declaredMethods.first { it.toString().contains(secondSourceClassName) } + val secondSource = secondSourceClass.constructors.first().newInstance( + profileClass.constructors.first().newInstance("Profile") + ) + destination = mappingMethod.invoke(null, secondSource) + + destination::class.shouldHaveMemberProperty("name") { + it.getter.call(destination).shouldBe("Profile") + } + } + + @Test + fun mapEmbeddedWithMapTo() { + val accountSpec = buildFileSpec("Account", mapOf("name" to PropertySpecInit(STRING))) + val accountClassName = accountSpec.typeSpecs.first().name!! + val destinationSpec = buildFileSpec("DestinationObject", mapOf("name" to PropertySpecInit(STRING))) + val destinationClassName = destinationSpec.typeSpecs.first().name!! + val sourceSpec = buildFileSpec( + "SourceObject", + mapOf("account" to PropertySpecInit(ClassName(packageName, accountClassName))), + listOf( + KOMMMap::class to mapOf("to = %L" to listOf("[$destinationClassName::class]")), + MapEmbedded::class to mapOf("name = %S" to listOf("account")) + ) + ) + val sourceClassName = sourceSpec.typeSpecs.first().name!! + val generated = generate( + accountSpec, + destinationSpec, + sourceSpec + ) + + generated.exitCode.shouldBe(KotlinCompilation.ExitCode.OK) + + val accountClass = generated.classLoader.loadClass("$packageName.$accountClassName") + val sourceClass = generated.classLoader.loadClass("$packageName.$sourceClassName") + val mappingClass = generated.classLoader.loadClass("$packageName.MappingExtensionsKt") + val source = sourceClass.constructors.first().newInstance( + accountClass.constructors.first().newInstance("Account") + ) + val destination = mappingClass.declaredMethods.first().invoke(null, source) + + destination::class.shouldHaveMemberProperty("name") { + it.getter.call(destination).shouldBe("Account") + } + } +} From 2edb0eb0026320794eade02814bbd97ce7fed559 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 19 Jun 2026 16:18:28 +0200 Subject: [PATCH 3/7] Fix sample module name --- {komm-simple => komm-sample}/build.gradle.kts | 0 .../kotlin/com/ucasoft/komm/sample}/ExtendClass.kt | 2 +- .../kotlin/com/ucasoft/komm/sample}/SourceObject.kt | 2 +- .../ucasoft/komm/sample}/other/ThirdDestinationObject.kt | 4 ++-- .../src/jsMain/kotlin/com/ucasoft/komm/sample}/Main.kt | 4 ++-- .../src/jsMain/resources/index.html | 2 +- .../src/jvmMain/kotlin/com/ucasoft/komm/sample}/Extended.kt | 2 +- .../src/jvmMain/kotlin/com/ucasoft/komm/sample}/Main.kt | 6 +++--- .../kotlin/com/ucasoft/komm/sample}/exposed/DtoModel.kt | 4 ++-- .../com/ucasoft/komm/sample}/exposed/database/DbModel.kt | 2 +- .../kotlin/com/ucasoft/komm/sample}/to/SourceWithEnum.kt | 3 +-- settings.gradle.kts | 2 +- 12 files changed, 16 insertions(+), 17 deletions(-) rename {komm-simple => komm-sample}/build.gradle.kts (100%) rename {komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple => komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample}/ExtendClass.kt (98%) rename {komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple => komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample}/SourceObject.kt (87%) rename {komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple => komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample}/other/ThirdDestinationObject.kt (81%) rename {komm-simple/src/jsMain/kotlin/com/ucasoft/komm/simple => komm-sample/src/jsMain/kotlin/com/ucasoft/komm/sample}/Main.kt (79%) rename {komm-simple => komm-sample}/src/jsMain/resources/index.html (82%) rename {komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple => komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample}/Extended.kt (97%) rename {komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple => komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample}/Main.kt (76%) rename {komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple => komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample}/exposed/DtoModel.kt (60%) rename {komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple => komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample}/exposed/database/DbModel.kt (82%) rename {komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple => komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample}/to/SourceWithEnum.kt (93%) diff --git a/komm-simple/build.gradle.kts b/komm-sample/build.gradle.kts similarity index 100% rename from komm-simple/build.gradle.kts rename to komm-sample/build.gradle.kts diff --git a/komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple/ExtendClass.kt b/komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/ExtendClass.kt similarity index 98% rename from komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple/ExtendClass.kt rename to komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/ExtendClass.kt index bd1e6f6..8784ca6 100755 --- a/komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple/ExtendClass.kt +++ b/komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/ExtendClass.kt @@ -1,4 +1,4 @@ -package com.ucasoft.komm.simple +package com.ucasoft.komm.sample import com.ucasoft.komm.abstractions.KOMMConverter import com.ucasoft.komm.abstractions.KOMMResolver diff --git a/komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple/SourceObject.kt b/komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/SourceObject.kt similarity index 87% rename from komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple/SourceObject.kt rename to komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/SourceObject.kt index 82af9fc..b0656b3 100644 --- a/komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple/SourceObject.kt +++ b/komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/SourceObject.kt @@ -1,4 +1,4 @@ -package com.ucasoft.komm.simple +package com.ucasoft.komm.sample class SourceObject { diff --git a/komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple/other/ThirdDestinationObject.kt b/komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/other/ThirdDestinationObject.kt similarity index 81% rename from komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple/other/ThirdDestinationObject.kt rename to komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/other/ThirdDestinationObject.kt index 7539a28..a31fbd3 100755 --- a/komm-simple/src/commonMain/kotlin/com/ucasoft/komm/simple/other/ThirdDestinationObject.kt +++ b/komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/other/ThirdDestinationObject.kt @@ -1,8 +1,8 @@ -package com.ucasoft.komm.simple.other +package com.ucasoft.komm.sample.other import com.ucasoft.komm.annotations.KOMMMap import com.ucasoft.komm.annotations.MapConfiguration -import com.ucasoft.komm.simple.SourceObject +import com.ucasoft.komm.sample.SourceObject @KOMMMap(from = [SourceObject::class], to = [], config = MapConfiguration(allowNotNullAssertion = false, tryAutoCast = true, mapDefaultAsFallback = false, convertFunctionName = "")) data class ThirdDestinationObject( diff --git a/komm-simple/src/jsMain/kotlin/com/ucasoft/komm/simple/Main.kt b/komm-sample/src/jsMain/kotlin/com/ucasoft/komm/sample/Main.kt similarity index 79% rename from komm-simple/src/jsMain/kotlin/com/ucasoft/komm/simple/Main.kt rename to komm-sample/src/jsMain/kotlin/com/ucasoft/komm/sample/Main.kt index 17bbd09..ba31610 100644 --- a/komm-simple/src/jsMain/kotlin/com/ucasoft/komm/simple/Main.kt +++ b/komm-sample/src/jsMain/kotlin/com/ucasoft/komm/sample/Main.kt @@ -1,7 +1,7 @@ -package com.ucasoft.komm.simple +package com.ucasoft.komm.sample import kotlinx.browser.document fun main () { document.body!!.innerText = SourceObject().toDestinationObject().toString() -} \ No newline at end of file +} diff --git a/komm-simple/src/jsMain/resources/index.html b/komm-sample/src/jsMain/resources/index.html similarity index 82% rename from komm-simple/src/jsMain/resources/index.html rename to komm-sample/src/jsMain/resources/index.html index 7e33a69..125399d 100644 --- a/komm-simple/src/jsMain/resources/index.html +++ b/komm-sample/src/jsMain/resources/index.html @@ -6,5 +6,5 @@ KOMM Demo - + \ No newline at end of file diff --git a/komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/Extended.kt b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/Extended.kt similarity index 97% rename from komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/Extended.kt rename to komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/Extended.kt index 32a5330..6943414 100755 --- a/komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/Extended.kt +++ b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/Extended.kt @@ -1,4 +1,4 @@ -package com.ucasoft.komm.simple +package com.ucasoft.komm.sample import com.ucasoft.komm.abstractions.KOMMResolver import com.ucasoft.komm.annotations.* diff --git a/komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/Main.kt b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/Main.kt similarity index 76% rename from komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/Main.kt rename to komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/Main.kt index 9002deb..ee1e962 100644 --- a/komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/Main.kt +++ b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/Main.kt @@ -1,6 +1,6 @@ -package com.ucasoft.komm.simple +package com.ucasoft.komm.sample -import com.ucasoft.komm.simple.other.toThirdDestinationObject +import com.ucasoft.komm.sample.other.toThirdDestinationObject import java.util.* fun main() { @@ -12,4 +12,4 @@ fun main() { val c = Currency.getInstance(Locale.US) println(c.toExCurrency()) -} \ No newline at end of file +} diff --git a/komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/exposed/DtoModel.kt b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/exposed/DtoModel.kt similarity index 60% rename from komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/exposed/DtoModel.kt rename to komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/exposed/DtoModel.kt index af74579..7e7a27d 100644 --- a/komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/exposed/DtoModel.kt +++ b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/exposed/DtoModel.kt @@ -1,7 +1,7 @@ -package com.ucasoft.komm.simple.exposed +package com.ucasoft.komm.sample.exposed import com.ucasoft.komm.annotations.KOMMMap -import com.ucasoft.komm.simple.exposed.database.DbModel +import com.ucasoft.komm.sample.exposed.database.DbModel @KOMMMap(from = [DbModel::class]) data class DtoModel(val id: Int, val user: String, val age: Int) \ No newline at end of file diff --git a/komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/exposed/database/DbModel.kt b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/exposed/database/DbModel.kt similarity index 82% rename from komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/exposed/database/DbModel.kt rename to komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/exposed/database/DbModel.kt index 89cfeb5..0e91587 100644 --- a/komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/exposed/database/DbModel.kt +++ b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/exposed/database/DbModel.kt @@ -1,4 +1,4 @@ -package com.ucasoft.komm.simple.exposed.database +package com.ucasoft.komm.sample.exposed.database import org.jetbrains.exposed.v1.core.Table diff --git a/komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/to/SourceWithEnum.kt b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/to/SourceWithEnum.kt similarity index 93% rename from komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/to/SourceWithEnum.kt rename to komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/to/SourceWithEnum.kt index 4356414..c372411 100644 --- a/komm-simple/src/jvmMain/kotlin/com/ucasoft/komm/simple/to/SourceWithEnum.kt +++ b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/to/SourceWithEnum.kt @@ -1,8 +1,7 @@ -package com.ucasoft.komm.simple.to +package com.ucasoft.komm.sample.to import com.ucasoft.komm.abstractions.KOMMResolver import com.ucasoft.komm.annotations.KOMMMap -import com.ucasoft.komm.annotations.MapConfiguration import com.ucasoft.komm.annotations.MapDefault import com.ucasoft.komm.annotations.NullSubstitute import com.ucasoft.komm.plugins.enum.annotations.KOMMEnum diff --git a/settings.gradle.kts b/settings.gradle.kts index 0b94d29..c0e003b 100755 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,4 +16,4 @@ include("komm-plugins-iterable") include("komm-processor") -include("komm-simple") \ No newline at end of file +include("komm-sample") \ No newline at end of file From d68632a2dbab64f3fbffe24a7d524b0fb2f06e55 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 19 Jun 2026 16:18:45 +0200 Subject: [PATCH 4/7] Add Embedded samples --- .../com/ucasoft/komm/sample/EmbeddedClass.kt | 23 +++++++++++++++++++ .../kotlin/com/ucasoft/komm/sample/Main.kt | 5 +++- .../kotlin/com/ucasoft/komm/sample/Main.kt | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/EmbeddedClass.kt diff --git a/komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/EmbeddedClass.kt b/komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/EmbeddedClass.kt new file mode 100644 index 0000000..1c5ae35 --- /dev/null +++ b/komm-sample/src/commonMain/kotlin/com/ucasoft/komm/sample/EmbeddedClass.kt @@ -0,0 +1,23 @@ +package com.ucasoft.komm.sample + +import com.ucasoft.komm.annotations.KOMMMap +import com.ucasoft.komm.annotations.MapEmbedded +import com.ucasoft.komm.annotations.MapConfiguration + +data class EmbeddedDetails( + val id: Long, + val name: String +) + +data class EmbeddedSourceObject( + val details: EmbeddedDetails, + val description: String +) + +@KOMMMap(from = [EmbeddedSourceObject::class], to = [], config = MapConfiguration(allowNotNullAssertion = false, tryAutoCast = true, mapDefaultAsFallback = false, convertFunctionName = "")) +@MapEmbedded("details") +data class EmbeddedDestinationObject( + val id: Long, + val name: String, + val description: String +) diff --git a/komm-sample/src/jsMain/kotlin/com/ucasoft/komm/sample/Main.kt b/komm-sample/src/jsMain/kotlin/com/ucasoft/komm/sample/Main.kt index ba31610..8699eea 100644 --- a/komm-sample/src/jsMain/kotlin/com/ucasoft/komm/sample/Main.kt +++ b/komm-sample/src/jsMain/kotlin/com/ucasoft/komm/sample/Main.kt @@ -3,5 +3,8 @@ package com.ucasoft.komm.sample import kotlinx.browser.document fun main () { - document.body!!.innerText = SourceObject().toDestinationObject().toString() + document.body!!.innerText = listOf( + SourceObject().toDestinationObject(), + EmbeddedSourceObject(EmbeddedDetails(1L, "Main"), "Embedded sample").toEmbeddedDestinationObject() + ).joinToString(separator = "\n") } diff --git a/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/Main.kt b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/Main.kt index ee1e962..c002a50 100644 --- a/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/Main.kt +++ b/komm-sample/src/jvmMain/kotlin/com/ucasoft/komm/sample/Main.kt @@ -9,6 +9,7 @@ fun main() { println(destination) println(source.toSecondDestinationObject()) println(source.toThirdDestinationObject()) + println(EmbeddedSourceObject(EmbeddedDetails(1L, "Main"), "Embedded sample").toEmbeddedDestinationObject()) val c = Currency.getInstance(Locale.US) println(c.toExCurrency()) From ab16bd4ac889230b3ec7c0aee1ec10ac530b0175 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 19 Jun 2026 16:19:01 +0200 Subject: [PATCH 5/7] Extend README --- README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f60cd1d..72bf297 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ The **Kotlin Object Multiplatform Mapper** provides you a possibility to generat * [Disable AutoCast](#disable-autocast) * [Change Convert Function Name](#change-convert-function-name) * [@MapName](#mapname-annotation) + * [@MapEmbedded](#mapembedded-annotation) * [@MapConverter](#use-converter) * [@MapDefault](#use-resolver) * [@NullSubstitute](#use-nullsubstitute) @@ -264,6 +265,64 @@ fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( } ``` +### @MapEmbedded annotation +Use `@MapEmbedded` when several destination properties should be mapped from the same nested source property. +KOMM checks only the first nested level. Direct source properties have priority over embedded properties. +If two embedded properties can provide the same destination property, generation fails and the mapping should be made explicit. + +#### Classes declaration +```kotlin +data class Account( + val id: Long, + val name: String +) + +data class AccountWithCurrencies( + val account: Account, + val currencies: List +) + +@KOMMMap(from = [AccountWithCurrencies::class]) +@MapEmbedded("account") +data class AccountDto( + val name: String, + val currencies: List +) { + var id: Long = 0L +} +``` +#### Generated extension function +```kotlin +fun AccountWithCurrencies.toAccountDto(): AccountDto = AccountDto( + name = account.name, + currencies = currencies.map { it.toAccountCurrencyDto() } +).also { + it.id = account.id +} +``` +#### Nullable embedded source +```kotlin +data class AccountWithCurrencies( + val account: Account?, + val currencies: List +) + +@KOMMMap(from = [AccountWithCurrencies::class]) +@MapEmbedded("account") +data class AccountDto( + @NullSubstitute(MapDefault(StringResolver::class)) + val name: String, + val currencies: List +) +``` +#### Generated extension function +```kotlin +fun AccountWithCurrencies.toAccountDto(): AccountDto = AccountDto( + name = account?.name ?: StringResolver(null).resolve(), + currencies = currencies.map { it.toAccountCurrencyDto() } +) +``` + ### Use Converter #### Converter declaration ```kotlin @@ -701,4 +760,4 @@ fun SourceObject.toDestinationObject(): toDestinationObject = toDestinationObjec (DestinationObject.DestinationEnum.entries.any { it.name == direction.name }) direction.name else "OTHER") else null) ?: DirectionResolver(null).resolve() ) -``` \ No newline at end of file +``` From f5abedf4ee53c89b42a605108e5b2fb601df1aa0 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 19 Jun 2026 16:21:11 +0200 Subject: [PATCH 6/7] Update version --- README.md | 14 +++++++------- build.gradle.kts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 72bf297..f493ea0 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.50.9" +val kommVersion = "0.60.0" depensencies { implementation("com.ucasoft.komm:komm-annotations:$kommVersion") @@ -97,7 +97,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.50.9" +val kommVersion = "0.60.0" kotlin { jvm { @@ -528,7 +528,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.50.9" +val kommVersion = "0.60.0" depensencies { implementation("com.ucasoft.komm:komm-annotations:$kommVersion") @@ -542,7 +542,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.50.9" +val kommVersion = "0.60.0" //... @@ -602,7 +602,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.50.9" +val kommVersion = "0.60.0" depensencies { implementation("com.ucasoft.komm:komm-annotations:$kommVersion") @@ -646,7 +646,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.50.9" +val kommVersion = "0.60.0" 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.50.9" +val kommVersion = "0.60.0" //... diff --git a/build.gradle.kts b/build.gradle.kts index 7e3128a..8c0f5ae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ tasks.wrapper { allprojects { group = "com.ucasoft.komm" - version = "0.50.9" + version = "0.60.0" repositories { mavenCentral() From 76f17c6bd8d999faee820eb4c4226c54ee6f0f00 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 19 Jun 2026 16:36:42 +0200 Subject: [PATCH 7/7] Reduce Cognitive Complexity --- .../komm/processor/KOMMPropertyMapper.kt | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) 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 8b41cc9..33cca73 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 @@ -207,13 +207,8 @@ class KOMMPropertyMapper( throw KOMMCastException("Auto Not-Null Assertion is not allowed! You have to use @${NullSubstitute::class.simpleName} annotation for ${destinationProperty.simpleName.asString()} property.") } - val castPlugin = plugins.filter { it.forCast(effectiveSourceType, destinationType) } - - if (castPlugin.count() > 1) { - throw KOMMPluginsException("There are more than one plugin for casting from $effectiveSourceType to $destinationType.") - } else if (castPlugin.count() == 1) { - return castPlugin.first() - .cast(source.sourceProperty, propertyName, effectiveSourceType, destinationProperty, destinationType) + getSourceWithPluginCast(source, propertyName, effectiveSourceType, destinationProperty, destinationType)?.let { + return it } if (sourceIsNullable && destinationType.isAssignableFrom(propertyType.makeNotNullable())) { @@ -228,6 +223,22 @@ class KOMMPropertyMapper( }${if (sourceIsNullable && useSafeAccess) "?" else ""}.to${destinationProperty.type}()" } + private fun getSourceWithPluginCast( + source: EmbeddedSourceProperty, + propertyName: String, + sourceType: KSType, + destinationProperty: KSPropertyDeclaration, + destinationType: KSType + ): String? { + val castPlugin = plugins.filter { it.forCast(sourceType, destinationType) } + + return when (castPlugin.count()) { + 0 -> null + 1 -> castPlugin.first().cast(source.sourceProperty, propertyName, sourceType, destinationProperty, destinationType) + else -> throw KOMMPluginsException("There are more than one plugin for casting from $sourceType to $destinationType.") + } + } + private fun getSourceAccessName( source: EmbeddedSourceProperty, useSafeAccess: Boolean = false,