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
94 changes: 87 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The **Kotlin Object Multiplatform Mapper** provides you a possibility to generat
* [@MapName](#mapname-annotation)
* [@MapEmbedded](#mapembedded-annotation)
* [@MapConverter](#use-converter)
* [Context](#use-context)
* [@MapDefault](#use-resolver)
* [@NullSubstitute](#use-nullsubstitute)
* [Allow Not-Null Assertion](#mapping-configuration-1)
Expand Down Expand Up @@ -86,7 +87,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.61.6"
val kommVersion = "0.70.6"

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

val kommVersion = "0.61.6"
val kommVersion = "0.70.6"

kotlin {
jvm {
Expand Down Expand Up @@ -397,6 +398,85 @@ fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject(
it.otherCost = CostConverter(this).convert(cost)
}
```
`@MapConvert` can also use a context-aware converter when the mapping has `KOMMMap.context`.

```kotlin
data class AccountMapContext(
val banks: Map<Long, Bank>
)

class BankConverter(
source: FullAccount,
context: AccountMapContext
) : KOMMContextConverter<FullAccount, Long?, AccountMapContext, Account, Bank?>(source, context) {

override fun convert(sourceMember: Long?): Bank? =
sourceMember?.let(context.banks::get)
}
```
#### Classes declaration
```kotlin
@KOMMMap(from = [FullAccount::class], context = AccountMapContext::class)
data class Account(
//...
@MapConvert<FullAccount, Account, BankConverter>(BankConverter::class, "bankId")
val bank: Bank?
)
```
#### Generated extension function
```kotlin
fun FullAccount.toAccount(kommContext: AccountMapContext): Account = Account(
//...
bank = BankConverter(this, kommContext).convert(bankId)
)
```

### Use Context
Use mapping context when destination members depend on data that is not part of the source object, such as lookup tables produced by other flows.

#### Context declaration
```kotlin
data class TransactionMapContext(
val accounts: Map<Long, Account>,
val accountCurrencies: Map<Long, AccountCurrency>,
val categories: Map<Long, Category>
)
```
#### Context resolver declaration
```kotlin
class FallbackAccountResolver(
destination: Transaction?,
context: TransactionMapContext
) : KOMMContextResolver<TransactionMapContext, Transaction, Account?>(destination, context) {

override fun resolve(): Account? {
return context.accounts.values.firstOrNull()
}
}
```
#### Classes declaration
```kotlin
@KOMMMap(from = [DbTransaction::class], context = TransactionMapContext::class)
data class Transaction(
//...
@MapDefault<FallbackAccountResolver>(FallbackAccountResolver::class)
val expenseAccount: Account?
)
```
#### Generated extension function
```kotlin
fun DbTransaction.toTransaction(kommContext: TransactionMapContext): Transaction = Transaction(
//...
expenseAccount = FallbackAccountResolver(null, kommContext).resolve()
)
```
The context is a snapshot. Combine reactive inputs before mapping, then build a fresh context whenever any dependency emits.
```kotlin
combine(transactions, accountCurrencies, categories, accounts) { items, currencies, cats, accs ->
val context = TransactionMapContext(accs, currencies, cats)
items.map { it.toTransaction(context) }
}
```
### Use Resolver
#### Resolver declaration
```kotlin
Expand Down Expand Up @@ -566,7 +646,7 @@ plugins {
id("com.google.devtools.ksp") version "2.3.9"
}

val kommVersion = "0.61.6"
val kommVersion = "0.70.6"

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

val kommVersion = "0.61.6"
val kommVersion = "0.70.6"

//...

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

val kommVersion = "0.61.6"
val kommVersion = "0.70.6"

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

val kommVersion = "0.61.6"
val kommVersion = "0.70.6"

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

val kommVersion = "0.61.6"
val kommVersion = "0.70.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.61.6"
version = "0.70.6"

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

abstract class KOMMContextConverter<S, SM, C, D, DM>(
source: S,
protected val context: C
) : KOMMConverter<S, SM, D, DM>(source)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ucasoft.komm.abstractions

abstract class KOMMContextResolver<C, D, DM>(
destination: D?,
protected val context: C
): KOMMResolver<D, DM>(destination)
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ import kotlin.reflect.KClass
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
@Repeatable
annotation class KOMMMap(val from: Array<KClass<*>> = [], val to: Array<KClass<*>> = [], val config: MapConfiguration = MapConfiguration())
annotation class KOMMMap(
val from: Array<KClass<*>> = [],
val to: Array<KClass<*>> = [],
val context: KClass<*> = Unit::class,
val config: MapConfiguration = MapConfiguration()
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeArgument
import com.google.devtools.ksp.symbol.KSTypeParameter
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
import com.ucasoft.komm.processor.extensions.resolveTypeArgument

class KOMMAnnotationFinder(private val forClass: KSType) {

Expand Down Expand Up @@ -55,7 +56,7 @@ class KOMMAnnotationFinder(private val forClass: KSType) {
return packageName to name
}

fun findSubstituteResolver(member: KSPropertyDeclaration): String? {
fun findSubstituteResolver(member: KSPropertyDeclaration): KSType? {
val annotations = member.annotations.filter { it.shortName.asString() == NullSubstitute::class.simpleName }
.associateWith(::associateWithFor)

Expand All @@ -64,7 +65,7 @@ class KOMMAnnotationFinder(private val forClass: KSType) {
if (annotation != null) {
val resolverArgument =
annotation.arguments.first { it.name?.asString() == NullSubstitute::default.name }.value as KSAnnotation
return resolverArgument.arguments.first { it.name?.asString() == MapDefault<*>::resolver.name }.value.toString()
return resolverArgument.arguments.first { it.name?.asString() == MapDefault<*>::resolver.name }.value as? KSType
}

return null
Expand All @@ -75,20 +76,28 @@ class KOMMAnnotationFinder(private val forClass: KSType) {
member: KSPropertyDeclaration,
annotationName: String?,
argumentName: String
): String? {
val annotations =
member.annotations.filter { it.shortName.asString() == annotationName }.associateWith(::associateWithFor)

val annotation = filterAnnotationsByClass(forClass, annotations, member)
): KSType? {
val annotation = findAnnotation(forClass, member, annotationName)

if (annotation != null) {
val resolverArgument = annotation.arguments.first { it.name?.asString() == argumentName }
return resolverArgument.value.toString()
return resolverArgument.value as? KSType
}

return null
}

private fun findAnnotation(
forClass: ClassName,
member: KSPropertyDeclaration,
annotationName: String?
): KSAnnotation? {
val annotations =
member.annotations.filter { it.shortName.asString() == annotationName }.associateWith(::associateWithFor)

return filterAnnotationsByClass(forClass, annotations, member)
}

fun getSuitedNamedAnnotations(member: KSPropertyDeclaration) =
getSuitedNamedAnnotationsForClass(member).keys.toList()

Expand Down Expand Up @@ -128,31 +137,42 @@ class KOMMAnnotationFinder(private val forClass: KSType) {
?: return emptyList()

val converterDeclaration = converterArgument.declaration as? KSClassDeclaration ?: return emptyList()
return getKOMMConverterAssociations(converterDeclaration).map { it.toClassName() }
return getKOMMAssociations(converterDeclaration, KOMMConverter::class.qualifiedName!!, 0, 2)
.ifEmpty { getKOMMAssociations(converterDeclaration, KOMMContextConverter::class.qualifiedName!!, 0, 3) }
.map { it.toClassName() }
}

private fun getKOMMConverterAssociations(
private fun getKOMMAssociations(
declaration: KSClassDeclaration,
superClassName: String,
sourceIndex: Int,
destinationIndex: Int,
typeSubstitutions: Map<String, KSType> = emptyMap()
): List<KSType> {
for (superTypeReference in declaration.superTypes) {
val superType = superTypeReference.resolve()
val superDeclaration = superType.declaration as? KSClassDeclaration ?: continue

if (superDeclaration.qualifiedName?.asString() == KOMMConverter::class.qualifiedName) {
if (superDeclaration.qualifiedName?.asString() == superClassName) {
return listOfNotNull(
resolveTypeArgument(superType.arguments.getOrNull(0), typeSubstitutions),
resolveTypeArgument(superType.arguments.getOrNull(2), typeSubstitutions)
superType.arguments.getOrNull(sourceIndex).resolveTypeArgument(typeSubstitutions),
superType.arguments.getOrNull(destinationIndex).resolveTypeArgument(typeSubstitutions)
)
}

val superSubstitutions = superDeclaration.typeParameters
.zip(superType.arguments)
.mapNotNull { (parameter, argument) ->
resolveTypeArgument(argument, typeSubstitutions)?.let { parameter.name.asString() to it }
argument.resolveTypeArgument(typeSubstitutions)?.let { parameter.name.asString() to it }
}
.toMap()
val associations = getKOMMConverterAssociations(superDeclaration, superSubstitutions)
val associations = getKOMMAssociations(
superDeclaration,
superClassName,
sourceIndex,
destinationIndex,
superSubstitutions
)
if (associations.isNotEmpty()) {
return associations
}
Expand All @@ -161,12 +181,6 @@ class KOMMAnnotationFinder(private val forClass: KSType) {
return emptyList()
}

private fun resolveTypeArgument(argument: KSTypeArgument?, typeSubstitutions: Map<String, KSType>): KSType? {
val type = argument?.type?.resolve() ?: return null
val typeParameter = type.declaration as? KSTypeParameter ?: return type
return typeSubstitutions[typeParameter.name.asString()]
}

private fun filterAnnotationsByClass(
forClass: ClassName,
annotationMap: Map<KSAnnotation, List<ClassName>>,
Expand Down
Loading