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
52 changes: 45 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The **Kotlin Object Multiplatform Mapper** provides you a possibility to generat
* [Configuration](#mapping-configuration)
* [Disable AutoCast](#disable-autocast)
* [Change Convert Function Name](#change-convert-function-name)
* [@MapFunction](#mapfunction-annotation)
* [@MapName](#mapname-annotation)
* [@MapEmbedded](#mapembedded-annotation)
* [@MapConverter](#use-converter)
Expand Down Expand Up @@ -55,6 +56,7 @@ The **Kotlin Object Multiplatform Mapper** provides you a possibility to generat
* Has next properties annotations:
* Specify mapping from property with different name
* Specify a converter to map data from source unusual way
* Specify a top-level extension function for property casting
* Specify a resolver to map default values into properties
* Specify null substitute to map nullable properties into not-nullable
* Support extension via plugins
Expand Down Expand Up @@ -84,7 +86,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.60.0"
val kommVersion = "0.61.6"

depensencies {
implementation("com.ucasoft.komm:komm-annotations:$kommVersion")
Expand All @@ -97,7 +99,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.60.0"
val kommVersion = "0.61.6"

kotlin {
jvm {
Expand Down Expand Up @@ -222,6 +224,42 @@ fun SourceObject.convertToDestination(): DestinationObject = DestinationObject(
}
```

### @MapFunction annotation
Use `@MapFunction` when the automatic `toType()` cast should call a top-level extension function from another package.
KOMM imports the function and keeps extension-call syntax in generated code.

#### Function declaration
```kotlin
fun ByteArray.toImageBitmap(): ImageBitmap = //...
```
#### Classes declaration
```kotlin
data class SourceObject(
val logo: ByteArray?
)

@KOMMMap(from = [SourceObject::class])
data class DestinationObject(
@MapFunction(packageName = "com.test.converters")
val logo: ImageBitmap?
)
```
or specify the function name explicitly:
```kotlin
@MapFunction(
packageName = "com.test.converters",
name = "toImageBitmap"
)
```
#### Generated extension function
```kotlin
import com.test.converters.toImageBitmap

fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject(
logo = logo?.toImageBitmap()
)
```

### @MapName annotation
#### Classes declaration
```kotlin
Expand Down Expand Up @@ -528,7 +566,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.60.0"
val kommVersion = "0.61.6"

depensencies {
implementation("com.ucasoft.komm:komm-annotations:$kommVersion")
Expand All @@ -542,7 +580,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.60.0"
val kommVersion = "0.61.6"

//...

Expand Down Expand Up @@ -602,7 +640,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.60.0"
val kommVersion = "0.61.6"

depensencies {
implementation("com.ucasoft.komm:komm-annotations:$kommVersion")
Expand Down Expand Up @@ -646,7 +684,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.60.0"
val kommVersion = "0.61.6"

depensencies {
implementation("com.ucasoft.komm:komm-annotations:$kommVersion")
Expand All @@ -660,7 +698,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.60.0"
val kommVersion = "0.61.6"

//...

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.60.0"
version = "0.61.6"

repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ucasoft.komm.annotations

import kotlin.reflect.KClass

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.SOURCE)
@Repeatable
annotation class MapFunction(
val packageName: String,
val name: String = "",
val `for`: Array<KClass<*>> = []
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ class KOMMAnnotationFinder(private val forClass: KSType) {
MapConvert<*, *, *>::converter.name
)

fun findFunction(member: KSPropertyDeclaration): Pair<String, String>? {
val annotations = member.annotations.filter { it.shortName.asString() == MapFunction::class.simpleName }
.associateWith(::associateWithFor)

val annotation = filterAnnotationsByClass(forClass.toClassName(), annotations, member)
?: return null

val packageName = annotation.arguments
.first { it.name?.asString() == MapFunction::packageName.name }
.value
.toString()
val name = annotation.arguments
.first { it.name?.asString() == MapFunction::name.name }
.value
.toString()

return packageName to name
}

fun findSubstituteResolver(member: KSPropertyDeclaration): String? {
val annotations = member.annotations.filter { it.shortName.asString() == NullSubstitute::class.simpleName }
.associateWith(::associateWithFor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ class KOMMPropertyMapper(
destination: KSType,
private val direction: KOMMVisitor.Direction,
private val config: KSAnnotation,
private val plugins: List<KOMMCastPlugin>
private val plugins: List<KOMMCastPlugin>,
private val imports: MutableMap<String, List<String>>
) {

private val annotationFinder = KOMMAnnotationFinder(
Expand Down Expand Up @@ -44,6 +45,12 @@ class KOMMPropertyMapper(
annotationFinder.findConverter(it)
}
}
val function = when (direction) {
KOMMVisitor.Direction.From -> annotationFinder.findFunction(destination)
KOMMVisitor.Direction.To -> (sourceProperty as? KSPropertyDeclaration)?.let {
annotationFinder.findFunction(it)
}
}
val nullSubstituteResolver = when (direction) {
KOMMVisitor.Direction.From -> annotationFinder.findSubstituteResolver(destination)
KOMMVisitor.Direction.To -> (sourceProperty as? KSPropertyDeclaration)?.let {
Expand All @@ -54,10 +61,10 @@ class KOMMPropertyMapper(
"$destination = $converter(this).convert(${getSourceAccessName(source)})"
} else if (nullSubstituteResolver != null) {
"$destination = ${
getSourceWithCast(destination, source, config, useSafeAccess = true)
getSourceWithCast(destination, source, config, function, useSafeAccess = true)
} ?: ${mapResolver(nullSubstituteResolver, mapTo)}"
} else {
"$destination = ${getSourceWithCast(destination, source, config)}"
"$destination = ${getSourceWithCast(destination, source, config, function)}"
}
}

Expand Down Expand Up @@ -175,6 +182,7 @@ class KOMMPropertyMapper(
destinationProperty: KSPropertyDeclaration,
source: EmbeddedSourceProperty,
config: KSAnnotation,
function: Pair<String, String>?,
useSafeAccess: Boolean = false
): String {
val propertyName = getSourceAccessName(source, useSafeAccess)
Expand Down Expand Up @@ -215,12 +223,21 @@ class KOMMPropertyMapper(
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}()"
val shouldUseSafeCall = sourceIsNullable && (useSafeAccess || destinationIsNullable)
val sourceAccessName = when {
shouldUseSafeCall -> getSourceAccessName(source, useSafeAccess = true)
sourceIsNullable -> getSourceAccessName(source, assertNotNull = true)
else -> propertyName
}

val conversionFunctionName = "to${destinationType.declaration.simpleName.asString()}"
val functionName = function?.second?.ifEmpty { conversionFunctionName } ?: conversionFunctionName
if (function != null) {
imports[function.first] = imports[function.first].orEmpty() + functionName
}
val receiverPrefix = "$sourceAccessName${if (shouldUseSafeCall) "?." else "."}"

return "$receiverPrefix$functionName()"
}

private fun getSourceWithPluginCast(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ class KOMMSymbolProcessor(

val symbols =
resolver.getSymbolsWithAnnotation(KOMMMap::class.qualifiedName!!).filterIsInstance<KSClassDeclaration>()
.toList()

if (!symbols.iterator().hasNext()) {
if (symbols.isEmpty()) {
return emptyList()
}

Expand All @@ -41,15 +42,15 @@ class KOMMSymbolProcessor(
val file = FileSpec
.builder(packageName.asString(), "MappingExtensions")
.apply {
imports.forEach { this.addImport(it.key, it.value) }
imports.forEach { addImport(it.key, it.value) }
functions.forEach { this.addFunction(it) }
}
.build()

file.writeTo(codeGenerator, false)
file.writeTo(codeGenerator, true, classDeclarations.mapNotNull { it.containingFile })
}

return symbols.filterNot { it.validate() }.toList()
return symbols.filterNot { it.validate() }
}

private fun loadPlugins(): Map<KClass<out KOMMPlugin>, List<Class<*>>> {
Expand All @@ -66,4 +67,4 @@ class KOMMSymbolProcessor(
}.mapValues { it.value.map { it.second } }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.ucasoft.komm.processor

import com.google.devtools.ksp.isPrivate
import com.google.devtools.ksp.symbol.*
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.asTypeName
Expand Down Expand Up @@ -64,10 +65,13 @@ class KOMMVisitor(
}

private fun syncImports(destination: KSType, source: KSType, imports: MutableMap<String, List<String>>) {
val destinationPackageName = destination.toClassName().packageName
val sourcePackageName = source.toClassName().packageName
if (sourcePackageName != destinationPackageName) {
imports[sourcePackageName] = imports[sourcePackageName].orEmpty() + source.toClassName().simpleName
val destinationClassName = destination.toClassName()
val sourceClassName = source.toClassName()
if (
sourceClassName.packageName != destinationClassName.packageName &&
sourceClassName.simpleName != destinationClassName.simpleName
) {
imports[sourceClassName.packageName] = imports[sourceClassName.packageName].orEmpty() + sourceClassName.simpleName
}
}

Expand All @@ -77,7 +81,7 @@ class KOMMVisitor(
return FunSpec.builder(fromSourceFunctionName)
.receiver(getSourceName(source))
.returns(destination.toClassName())
.addStatement(buildStatement(source, destination.declaration as KSClassDeclaration, direction, config))
.addCode(buildStatement(source, destination.declaration as KSClassDeclaration, direction, config))
.build()
}

Expand All @@ -95,13 +99,13 @@ class KOMMVisitor(
return source.toTypeName()
}

private fun buildStatement(source: KSType, destination: KSClassDeclaration, direction: Direction, config: KSAnnotation): String {
private fun buildStatement(source: KSType, destination: KSClassDeclaration, direction: Direction, config: KSAnnotation): CodeBlock {
val castPlugins = typePlugins.toMutableList().apply {
addAll(plugins[KOMMCastPlugin::class]
?.map { it.getDeclaredConstructor().newInstance() } ?: emptyList())
}.filterIsInstance<KOMMCastPlugin>()

val propertyMapper = KOMMPropertyMapper(source, destination.asStarProjectedType(), direction, config, castPlugins)
val propertyMapper = KOMMPropertyMapper(source, destination.asStarProjectedType(), direction, config, castPlugins, imports)
val properties = destination.getAllProperties().groupBy { p ->
destination.primaryConstructor?.parameters?.any { it.name == p.simpleName }
}
Expand All @@ -116,8 +120,8 @@ class KOMMVisitor(
propertyMapper.map(it, MapTo.Also)
}

return buildString {
appendLine("return $destination(")
val statement = buildString {
appendLine("return %T(")
constructorProperties?.forEach { appendLine("\t$it,") }
deleteLast(2)
if (noConstructorProperties.isNullOrEmpty()) {
Expand All @@ -128,9 +132,11 @@ class KOMMVisitor(
append("}")
}
}

return CodeBlock.of(statement, destination.toClassName())
}

private fun StringBuilder.deleteLast(length: Int) = this.delete(this.length - length, this.length - 1)!!

private fun KSType.isKotlinClass() = declaration.origin == Origin.KOTLIN
}
}
Loading