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
38 changes: 31 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.8"
val kommVersion = "0.71.1"

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.8"
val kommVersion = "0.71.1"

kotlin {
jvm {
Expand Down Expand Up @@ -224,6 +224,30 @@ fun SourceObject.convertToDestination(): DestinationObject = DestinationObject(
it.intToString = intToString.toString()
}
```
#### Nullable Context
Set `nullableContext = true` when a mapping context should be optional at the mapping function boundary.
KOMM generates a nullable context parameter with a default `null` value.
If a context-aware converter or resolver is used, the generated mapper checks that the context was provided before calling it.

###### Classes declaration
```kotlin
@KOMMMap(
from = [SourceObject::class],
context = SourceMapContext::class,
config = MapConfiguration(
nullableContext = true
)
)
data class DestinationObject(
val id: Int
)
```
###### Generated extension function
```kotlin
fun SourceObject.toDestinationObject(kommContext: SourceMapContext? = null): DestinationObject = DestinationObject(
id = id
)
```

### @MapFunction annotation
Use `@MapFunction` when the automatic `toType()` cast should call a top-level extension function from another package.
Expand Down Expand Up @@ -646,7 +670,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.70.8"
val kommVersion = "0.71.1"

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

val kommVersion = "0.70.8"
val kommVersion = "0.71.1"

//...

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

val kommVersion = "0.70.8"
val kommVersion = "0.71.1"

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

val kommVersion = "0.70.8"
val kommVersion = "0.71.1"

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

val kommVersion = "0.70.8"
val kommVersion = "0.71.1"

//...

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.8"
version = "0.71.1"

repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ annotation class MapConfiguration(
val tryAutoCast: Boolean = true,
val allowNotNullAssertion: Boolean = false,
val mapDefaultAsFallback: Boolean = false,
val nullableContext: Boolean = false,
val convertFunctionName: String = ""
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import com.google.devtools.ksp.symbol.KSType
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ksp.toClassName
import com.ucasoft.komm.abstractions.KOMMContextConverter
import com.ucasoft.komm.abstractions.KOMMContextResolver
import com.ucasoft.komm.abstractions.KOMMConverter
import com.ucasoft.komm.annotations.*
import com.ucasoft.komm.processor.exceptions.KOMMException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import com.ucasoft.komm.plugins.exceptions.KOMMPluginsException
import com.ucasoft.komm.processor.exceptions.KOMMCastException
import com.ucasoft.komm.processor.exceptions.KOMMException
import com.ucasoft.komm.processor.extensions.getConfigValue
import com.ucasoft.komm.processor.extensions.resolveTypeArgument

class KOMMPropertyMapper(
source: KSType,
Expand Down Expand Up @@ -69,10 +68,10 @@ class KOMMPropertyMapper(
"$destination = $converter(this).convert(${getSourceAccessName(source)})"
nullSubstituteResolver != null ->
"$destination = ${
getSourceWithCast(destination, source, config, function, useSafeAccess = true)
getSourceWithCast(destination, source, function, useSafeAccess = true)
} ?: ${mapResolver(nullSubstituteResolver, mapTo)}"
else ->
"$destination = ${getSourceWithCast(destination, source, config, function)}"
"$destination = ${getSourceWithCast(destination, source, function)}"
}
}

Expand All @@ -93,8 +92,7 @@ class KOMMPropertyMapper(

private fun isContext(
declaration: KSClassDeclaration,
superClassName: String,
typeSubstitutions: Map<String, KSType> = emptyMap()
superClassName: String
): Boolean {
for (superTypeReference in declaration.superTypes) {
val superType = superTypeReference.resolve()
Expand All @@ -104,22 +102,24 @@ class KOMMPropertyMapper(
return true
}

val superSubstitutions = superDeclaration.typeParameters
.zip(superType.arguments)
.mapNotNull { (parameter, argument) ->
argument.resolveTypeArgument(typeSubstitutions)?.let { parameter.name.asString() to it }
}
.toMap()
if (isContext(superDeclaration, superClassName, superSubstitutions)) {
if (isContext(superDeclaration, superClassName)) {
return true
}
}

return false
}

private fun getRequiredContextParameterName() = contextParameterName
?: throw KOMMException("${KOMMMap::class.simpleName}.context is required when using context-aware @${MapConvert::class.simpleName} or @${MapDefault::class.simpleName}.")
private fun getRequiredContextParameterName(): String {
val parameterName = contextParameterName
?: throw KOMMException("${KOMMMap::class.simpleName}.context is required when using context-aware @${MapConvert::class.simpleName} or @${MapDefault::class.simpleName}.")

return if (config.getConfigValue<Boolean>(MapConfiguration::nullableContext.name)) {
"$parameterName ?: throw IllegalArgumentException(\"KOMM context is required for context-aware mapping.\")"
} else {
parameterName
}
}

private fun getSourceProperty(sourceName: String): EmbeddedSourceProperty? {
sourceProperties[sourceName]?.let { return EmbeddedSourceProperty(null, it) }
Expand Down Expand Up @@ -231,7 +231,6 @@ class KOMMPropertyMapper(
private fun getSourceWithCast(
destinationProperty: KSPropertyDeclaration,
source: EmbeddedSourceProperty,
config: KSAnnotation,
function: Pair<String, String>?,
useSafeAccess: Boolean = false
): String {
Expand All @@ -243,16 +242,12 @@ class KOMMPropertyMapper(
val sourceIsNullable = source.isNullable
val effectiveSourceType = if (sourceIsNullable) propertyType.makeNullable() else propertyType

getAssignableSource(
source,
propertyName,
destinationType,
destinationIsNullable,
sourceIsNullable,
effectiveSourceType,
useSafeAccess
)?.let {
return it
if (destinationType.isAssignableFrom(effectiveSourceType)) {
return if (sourceIsNullable && destinationIsNullable) {
getSourceAccessName(source, useSafeAccess = true)
} else {
propertyName
}
}

if (!config.getConfigValue<Boolean>(MapConfiguration::tryAutoCast.name)) {
Expand All @@ -272,41 +267,37 @@ class KOMMPropertyMapper(
return if (useSafeAccess) propertyName else getSourceAccessName(source, assertNotNull = true)
}

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 "."}"
val receiverPrefix = getCastReceiverPrefix(
source,
propertyName,
sourceIsNullable,
destinationIsNullable,
useSafeAccess
)

return "$receiverPrefix$functionName()"
}

private fun getAssignableSource(
private fun getCastReceiverPrefix(
source: EmbeddedSourceProperty,
propertyName: String,
destinationType: KSType,
destinationIsNullable: Boolean,
sourceIsNullable: Boolean,
effectiveSourceType: KSType,
destinationIsNullable: Boolean,
useSafeAccess: Boolean
): String? {
if (!destinationType.isAssignableFrom(effectiveSourceType)) {
return null
): String {
val shouldUseSafeCall = sourceIsNullable && (useSafeAccess || destinationIsNullable)
val sourceAccessName = when {
shouldUseSafeCall -> getSourceAccessName(source, useSafeAccess = true)
sourceIsNullable -> getSourceAccessName(source, assertNotNull = true)
else -> propertyName
}

return if (sourceIsNullable && destinationIsNullable) {
getSourceAccessName(source, useSafeAccess = true)
} else {
propertyName
}
return "$sourceAccessName${if (shouldUseSafeCall) "?." else "."}"
}

private fun canMapNullableSource(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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.ParameterSpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
Expand Down Expand Up @@ -89,11 +90,17 @@ class KOMMVisitor(
) : FunSpec {
val convertFunctionName = config.getConfigValue<String>(MapConfiguration::convertFunctionName.name)
val fromSourceFunctionName = convertFunctionName.ifEmpty { "to${destination.toClassName().simpleName}" }
val nullableContext = config.getConfigValue<Boolean>(MapConfiguration::nullableContext.name)
return FunSpec.builder(fromSourceFunctionName)
.receiver(getSourceName(source))
.apply {
if (context != null) {
addParameter("kommContext", context.toTypeName())
val contextParameterBuilder = ParameterSpec
.builder("kommContext", context.toTypeName().copy(nullable = nullableContext))
if (nullableContext) {
contextParameterBuilder.defaultValue("null")
}
addParameter(contextParameterBuilder.build())
}
}
.returns(destination.toClassName())
Expand Down
Loading