diff --git a/README.md b/README.md index a4e505d..e3960c6 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The **Kotlin Object Multiplatform Mapper** provides you a possibility to generat * [@MapConverter](#use-converter) * [Context](#use-context) * [@MapDefault](#use-resolver) + * [@MapTargetDefault](#class-level-target-default) * [@NullSubstitute](#use-nullsubstitute) * [Allow Not-Null Assertion](#mapping-configuration-1) * [Multi Sources](#multi-sources-support) @@ -59,6 +60,7 @@ The **Kotlin Object Multiplatform Mapper** provides you a possibility to generat * 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 target property default values from class-level annotations * Specify null substitute to map nullable properties into not-nullable * Support extension via plugins @@ -87,7 +89,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.71.1" +val kommVersion = "0.80.3" depensencies { implementation("com.ucasoft.komm:komm-annotations:$kommVersion") @@ -100,7 +102,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.71.1" +val kommVersion = "0.80.3" kotlin { jvm { @@ -532,6 +534,43 @@ fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( it.otherDate = DateResolver(it).resolve() } ``` +### Class-level target default +Use `@MapTargetDefault` when a target property needs `@MapDefault`, but the target class cannot be annotated. +This is useful for `to` mappings into external models. + +#### Classes declaration +```kotlin +data class AccountCardMapContext(val accountId: Long) + +class AccountIdResolver( + destination: DbAccountCard?, + context: AccountCardMapContext +) : KOMMContextResolver(destination, context) { + + override fun resolve(): Long = context.accountId +} + +@KOMMMap(to = [DbAccountCard::class], context = AccountCardMapContext::class) +@MapTargetDefault( + name = "accountId", + default = MapDefault(AccountIdResolver::class), + `for` = [DbAccountCard::class] +) +data class AccountCard( + val id: Long, + val type: String, + val number: String +) +``` +#### Generated extension function +```kotlin +fun AccountCard.toDbAccountCard(kommContext: AccountCardMapContext): DbAccountCard = DbAccountCard( + id = id, + accountId = AccountIdResolver(null, kommContext).resolve(), + type = type, + number = number +) +``` ### Use NullSubstitute #### Mapping configuration ###### Classes declaration @@ -670,7 +709,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.71.1" +val kommVersion = "0.80.3" depensencies { implementation("com.ucasoft.komm:komm-annotations:$kommVersion") @@ -684,7 +723,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.71.1" +val kommVersion = "0.80.3" //... @@ -744,7 +783,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.71.1" +val kommVersion = "0.80.3" depensencies { implementation("com.ucasoft.komm:komm-annotations:$kommVersion") @@ -788,7 +827,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.71.1" +val kommVersion = "0.80.3" depensencies { implementation("com.ucasoft.komm:komm-annotations:$kommVersion") @@ -802,7 +841,7 @@ plugins { id("com.google.devtools.ksp") version "2.3.9" } -val kommVersion = "0.71.1" +val kommVersion = "0.80.3" //... diff --git a/build.gradle.kts b/build.gradle.kts index 874f74d..4baab11 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ tasks.wrapper { allprojects { group = "com.ucasoft.komm" - version = "0.71.1" + version = "0.80.3" repositories { mavenCentral() diff --git a/komm-annotations/src/main/kotlin/com/ucasoft/komm/annotations/MapTargetDefault.kt b/komm-annotations/src/main/kotlin/com/ucasoft/komm/annotations/MapTargetDefault.kt new file mode 100644 index 0000000..ad8ebd7 --- /dev/null +++ b/komm-annotations/src/main/kotlin/com/ucasoft/komm/annotations/MapTargetDefault.kt @@ -0,0 +1,12 @@ +package com.ucasoft.komm.annotations + +import kotlin.reflect.KClass + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +@Repeatable +annotation class MapTargetDefault( + val name: String, + val default: MapDefault<*>, + val `for`: Array> = [] +) 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 deleted file mode 100644 index 7d4622f..0000000 --- a/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/KOMMAnnotationFinder.kt +++ /dev/null @@ -1,202 +0,0 @@ -package com.ucasoft.komm.processor - -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.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.ksp.toClassName -import com.ucasoft.komm.abstractions.KOMMContextConverter -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) { - - private val namedAnnotations = - listOf( - MapName::class.simpleName, - MapConvert::class.simpleName, - NullSubstitute::class.simpleName - ) - - fun findResolver(member: KSPropertyDeclaration) = findMapAnnotation( - forClass.toClassName(), - member, - MapDefault::class.simpleName, - MapDefault<*>::resolver.name - ) - - fun findConverter(member: KSPropertyDeclaration) = - findMapAnnotation( - forClass.toClassName(), - member, - MapConvert::class.simpleName, - MapConvert<*, *, *>::converter.name - ) - - fun findFunction(member: KSPropertyDeclaration): Pair? { - 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): KSType? { - val annotations = member.annotations.filter { it.shortName.asString() == NullSubstitute::class.simpleName } - .associateWith(::associateWithFor) - - val annotation = filterAnnotationsByClass(forClass.toClassName(), annotations, member) - - 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 as? KSType - } - - return null - } - - private fun findMapAnnotation( - forClass: ClassName, - member: KSPropertyDeclaration, - annotationName: String?, - argumentName: String - ): KSType? { - val annotation = findAnnotation(forClass, member, annotationName) - - if (annotation != null) { - val resolverArgument = annotation.arguments.first { it.name?.asString() == argumentName } - 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) - .filter { it.value.isEmpty() || it.value.contains(forClass.toClassName()) } - .keys - .toList() - - 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) - - private fun associateWithFor(item: KSAnnotation): List { - val forArgument = item.arguments.firstOrNull { it.name?.asString() == MapName::`for`.name } - if (forArgument != null) { - return (forArgument.value as ArrayList<*>).filterIsInstance().map { it.toClassName() } - } - - if (item.annotationType.toString() == MapConvert::class.simpleName) { - return item.annotationType.element!!.typeArguments.take(2).mapNotNull { it.type?.resolve()?.toClassName() } - .ifEmpty { getMapConvertAssociationsFromConverter(item) } - } - - return emptyList() - } - - private fun getMapConvertAssociationsFromConverter(item: KSAnnotation): List { - val converterArgument = item.arguments - .firstOrNull { it.name?.asString() == MapConvert<*, *, *>::converter.name } - ?.value as? KSType - ?: return emptyList() - - val converterDeclaration = converterArgument.declaration as? KSClassDeclaration ?: return emptyList() - return getKOMMAssociations(converterDeclaration, KOMMConverter::class.qualifiedName!!, 0, 2) - .ifEmpty { getKOMMAssociations(converterDeclaration, KOMMContextConverter::class.qualifiedName!!, 0, 3) } - .map { it.toClassName() } - } - - private fun getKOMMAssociations( - declaration: KSClassDeclaration, - superClassName: String, - sourceIndex: Int, - destinationIndex: Int, - typeSubstitutions: Map = emptyMap() - ): List { - for (superTypeReference in declaration.superTypes) { - val superType = superTypeReference.resolve() - val superDeclaration = superType.declaration as? KSClassDeclaration ?: continue - - if (superDeclaration.qualifiedName?.asString() == superClassName) { - return listOfNotNull( - superType.arguments.getOrNull(sourceIndex).resolveTypeArgument(typeSubstitutions), - superType.arguments.getOrNull(destinationIndex).resolveTypeArgument(typeSubstitutions) - ) - } - - val superSubstitutions = superDeclaration.typeParameters - .zip(superType.arguments) - .mapNotNull { (parameter, argument) -> - argument.resolveTypeArgument(typeSubstitutions)?.let { parameter.name.asString() to it } - } - .toMap() - val associations = getKOMMAssociations( - superDeclaration, - superClassName, - sourceIndex, - destinationIndex, - superSubstitutions - ) - if (associations.isNotEmpty()) { - return associations - } - } - - return emptyList() - } - - private fun filterAnnotationsByClass( - forClass: ClassName, - annotationMap: Map>, - member: KSPropertyDeclaration - ): KSAnnotation? { - var annotations = annotationMap.filter { it.value.contains(forClass) } - if (annotations.isEmpty()) { - annotations = annotationMap.filter { it.value.isEmpty() } - } - if (annotations.count() > 1) { - val annotation = annotations.keys.first() - throw KOMMException("There are too many @${annotation.shortName.asString()} annotations for ${member.simpleName.asString()} property could be applied for ${forClass.simpleName}") - } - - return annotations.keys.firstOrNull() - } -} 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 a11025d..6d43550 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 @@ -10,6 +10,8 @@ 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.finders.annotation.KOMMClassAnnotationFinder +import com.ucasoft.komm.processor.finders.annotation.KOMMPropertyAnnotationFinder class KOMMPropertyMapper( source: KSType, @@ -21,18 +23,23 @@ class KOMMPropertyMapper( private val contextParameterName: String? ) { - private val annotationFinder = KOMMAnnotationFinder( - when (direction) { - KOMMVisitor.Direction.From -> source - KOMMVisitor.Direction.To -> destination - } - ) + private val forClass = when (direction) { + KOMMVisitor.Direction.From -> source + KOMMVisitor.Direction.To -> destination + } + private val annotationOwner = when (direction) { + KOMMVisitor.Direction.From -> destination.declaration + KOMMVisitor.Direction.To -> source.declaration + } as KSClassDeclaration + private val propertyAnnotationFinder = KOMMPropertyAnnotationFinder(forClass) + private val classAnnotationFinder = KOMMClassAnnotationFinder(forClass, annotationOwner) private val sourceProperties = getSourceProperties(source) - private val embeddedSourceProperties = getEmbeddedSourceProperties(source, destination) + private val embeddedSourceProperties = getEmbeddedSourceProperties(source) fun map(destination: KSPropertyDeclaration, mapTo: KOMMVisitor.MapTo): String? { - val resolver = annotationFinder.findResolver(destination) + val resolver = propertyAnnotationFinder.findResolver(destination) + ?: classAnnotationFinder.findTargetDefaultResolver(destination) if (!config.getConfigValue(MapConfiguration::mapDefaultAsFallback.name) && resolver != null) { return "$destination = ${mapResolver(resolver, mapTo)}" } @@ -43,21 +50,21 @@ class KOMMPropertyMapper( val sourceProperty = source.sourceProperty val converter = when (direction) { - KOMMVisitor.Direction.From -> annotationFinder.findConverter(destination) + KOMMVisitor.Direction.From -> propertyAnnotationFinder.findConverter(destination) KOMMVisitor.Direction.To -> (sourceProperty as? KSPropertyDeclaration)?.let { - annotationFinder.findConverter(it) + propertyAnnotationFinder.findConverter(it) } } val function = when (direction) { - KOMMVisitor.Direction.From -> annotationFinder.findFunction(destination) + KOMMVisitor.Direction.From -> propertyAnnotationFinder.findFunction(destination) KOMMVisitor.Direction.To -> (sourceProperty as? KSPropertyDeclaration)?.let { - annotationFinder.findFunction(it) + propertyAnnotationFinder.findFunction(it) } } val nullSubstituteResolver = when (direction) { - KOMMVisitor.Direction.From -> annotationFinder.findSubstituteResolver(destination) + KOMMVisitor.Direction.From -> propertyAnnotationFinder.findSubstituteResolver(destination) KOMMVisitor.Direction.To -> (sourceProperty as? KSPropertyDeclaration)?.let { - annotationFinder.findSubstituteResolver(it) + propertyAnnotationFinder.findSubstituteResolver(it) } } return when { @@ -137,7 +144,7 @@ class KOMMPropertyMapper( } private fun getMapNames(member: KSPropertyDeclaration): List { - val mapsFor = annotationFinder.getSuitedNamedAnnotations(member) + val mapsFor = propertyAnnotationFinder.getSuitedNamedAnnotations(member) val result = mutableListOf(member.toString()).apply { addAll(mapsFor.map { it.arguments.first { it.name?.asString() == MapName::name.name }.value.toString() } .filter { it.isNotEmpty() }.toMutableList()) @@ -147,7 +154,7 @@ class KOMMPropertyMapper( } private fun getMapName(member: KSPropertyDeclaration): String { - val mapFrom = annotationFinder.getSuitedNamedAnnotation(member) + val mapFrom = propertyAnnotationFinder.getSuitedNamedAnnotation(member) if (mapFrom != null) { val nameArgument = mapFrom.arguments.first { it.name?.asString() == MapName::name.name } @@ -199,15 +206,9 @@ class KOMMPropertyMapper( } private fun getEmbeddedSourceProperties( - source: KSType, - destination: KSType + source: KSType ): Map> { - val annotationOwner = when (direction) { - KOMMVisitor.Direction.From -> destination.declaration - KOMMVisitor.Direction.To -> source.declaration - } as KSClassDeclaration - - return annotationFinder.getSuitedEmbeddedAnnotations(annotationOwner) + return classAnnotationFinder.getSuitedEmbeddedAnnotations() .flatMap { annotation -> val embeddedName = annotation.arguments .first { it.name?.asString() == MapEmbedded::name.name } diff --git a/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/finders/annotation/KOMMAnnotationFinder.kt b/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/finders/annotation/KOMMAnnotationFinder.kt new file mode 100644 index 0000000..4520083 --- /dev/null +++ b/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/finders/annotation/KOMMAnnotationFinder.kt @@ -0,0 +1,106 @@ +package com.ucasoft.komm.processor.finders.annotation + +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +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.KOMMConverter +import com.ucasoft.komm.annotations.* +import com.ucasoft.komm.processor.exceptions.KOMMException +import com.ucasoft.komm.processor.extensions.resolveTypeArgument + +abstract class KOMMAnnotationFinder(forClass: KSType) { + + protected val forClassName: ClassName = forClass.toClassName() + + protected fun filterAnnotationsByClass( + annotationMap: Map>, + memberName: String + ): KSAnnotation? { + var annotations = annotationMap.filter { it.value.contains(forClassName) } + if (annotations.isEmpty()) { + annotations = annotationMap.filter { it.value.isEmpty() } + } + if (annotations.count() > 1) { + val annotation = annotations.keys.first() + throw KOMMException("There are too many @${annotation.shortName.asString()} annotations for $memberName property could be applied for ${forClassName.simpleName}") + } + + return annotations.keys.firstOrNull() + } + + protected fun associateWithFor(item: KSAnnotation): List { + val forArgument = item.arguments.firstOrNull { it.name?.asString() == MapName::`for`.name } + if (forArgument != null) { + return (forArgument.value as ArrayList<*>).filterIsInstance().map { it.toClassName() } + } + + if (item.annotationType.toString() == MapConvert::class.simpleName) { + return item.annotationType.element!!.typeArguments.take(2).mapNotNull { it.type?.resolve()?.toClassName() } + .ifEmpty { getMapConvertAssociationsFromConverter(item) } + } + + return emptyList() + } + + protected fun getMapDefaultResolver(item: KSAnnotation): KSType? = + item.arguments + .first { it.name?.asString() == MapDefault<*>::resolver.name } + .value as? KSType + + /* + Workaround for KSP issue - https://github.com/google/ksp/issues/2622 + */ + private fun getMapConvertAssociationsFromConverter(item: KSAnnotation): List { + val converterArgument = item.arguments + .firstOrNull { it.name?.asString() == MapConvert<*, *, *>::converter.name } + ?.value as? KSType + ?: return emptyList() + + val converterDeclaration = converterArgument.declaration as? KSClassDeclaration ?: return emptyList() + return getKOMMAssociations(converterDeclaration, KOMMConverter::class.qualifiedName!!, 0, 2) + .ifEmpty { getKOMMAssociations(converterDeclaration, KOMMContextConverter::class.qualifiedName!!, 0, 3) } + .map { it.toClassName() } + } + + private fun getKOMMAssociations( + declaration: KSClassDeclaration, + superClassName: String, + sourceIndex: Int, + destinationIndex: Int, + typeSubstitutions: Map = emptyMap() + ): List { + for (superTypeReference in declaration.superTypes) { + val superType = superTypeReference.resolve() + val superDeclaration = superType.declaration as? KSClassDeclaration ?: continue + + if (superDeclaration.qualifiedName?.asString() == superClassName) { + return listOfNotNull( + superType.arguments.getOrNull(sourceIndex).resolveTypeArgument(typeSubstitutions), + superType.arguments.getOrNull(destinationIndex).resolveTypeArgument(typeSubstitutions) + ) + } + + val superSubstitutions = superDeclaration.typeParameters + .zip(superType.arguments) + .mapNotNull { (parameter, argument) -> + argument.resolveTypeArgument(typeSubstitutions)?.let { parameter.name.asString() to it } + } + .toMap() + val associations = getKOMMAssociations( + superDeclaration, + superClassName, + sourceIndex, + destinationIndex, + superSubstitutions + ) + if (associations.isNotEmpty()) { + return associations + } + } + + return emptyList() + } +} \ No newline at end of file diff --git a/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/finders/annotation/KOMMClassAnnotationFinder.kt b/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/finders/annotation/KOMMClassAnnotationFinder.kt new file mode 100644 index 0000000..23581c0 --- /dev/null +++ b/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/finders/annotation/KOMMClassAnnotationFinder.kt @@ -0,0 +1,39 @@ +package com.ucasoft.komm.processor.finders.annotation + +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.ucasoft.komm.annotations.MapEmbedded +import com.ucasoft.komm.annotations.MapTargetDefault + +class KOMMClassAnnotationFinder( + forClass: KSType, + private val annotationOwner: KSClassDeclaration +) : KOMMAnnotationFinder(forClass) { + + fun findTargetDefaultResolver(member: KSPropertyDeclaration): KSType? { + val annotations = annotationOwner.annotations + .filter { it.shortName.asString() == MapTargetDefault::class.simpleName } + .filter { + it.arguments + .first { argument -> argument.name?.asString() == MapTargetDefault::name.name } + .value + .toString() == member.simpleName.asString() + } + .associateWith(::associateWithFor) + + val annotation = filterAnnotationsByClass(annotations, member.simpleName.asString()) ?: return null + val defaultArgument = + annotation.arguments.first { it.name?.asString() == MapTargetDefault::default.name }.value as KSAnnotation + return getMapDefaultResolver(defaultArgument) + } + + fun getSuitedEmbeddedAnnotations(): List = + annotationOwner.annotations + .filter { it.shortName.asString() == MapEmbedded::class.simpleName } + .associateWith(::associateWithFor) + .filter { it.value.isEmpty() || it.value.contains(forClassName) } + .keys + .toList() +} \ No newline at end of file diff --git a/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/finders/annotation/KOMMPropertyAnnotationFinder.kt b/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/finders/annotation/KOMMPropertyAnnotationFinder.kt new file mode 100644 index 0000000..32b0e05 --- /dev/null +++ b/komm-processor/src/main/kotlin/com/ucasoft/komm/processor/finders/annotation/KOMMPropertyAnnotationFinder.kt @@ -0,0 +1,100 @@ +package com.ucasoft.komm.processor.finders.annotation + +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.ucasoft.komm.annotations.MapConvert +import com.ucasoft.komm.annotations.MapDefault +import com.ucasoft.komm.annotations.MapFunction +import com.ucasoft.komm.annotations.MapName +import com.ucasoft.komm.annotations.NullSubstitute + +class KOMMPropertyAnnotationFinder(forClass: KSType) : KOMMAnnotationFinder(forClass) { + + private val namedAnnotations = + listOf( + MapName::class.simpleName, + MapConvert::class.simpleName, + NullSubstitute::class.simpleName + ) + + fun findResolver(member: KSPropertyDeclaration) = findMapAnnotation( + member, + MapDefault::class.simpleName, + MapDefault<*>::resolver.name + ) + + fun findConverter(member: KSPropertyDeclaration) = + findMapAnnotation( + member, + MapConvert::class.simpleName, + MapConvert<*, *, *>::converter.name + ) + + fun findFunction(member: KSPropertyDeclaration): Pair? { + val annotations = member.annotations.filter { it.shortName.asString() == MapFunction::class.simpleName } + .associateWith(::associateWithFor) + + val annotation = filterAnnotationsByClass(annotations, member.simpleName.asString()) + ?: 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): KSType? { + val annotations = member.annotations.filter { it.shortName.asString() == NullSubstitute::class.simpleName } + .associateWith(::associateWithFor) + + val annotation = filterAnnotationsByClass(annotations, member.simpleName.asString()) ?: return null + val defaultArgument = + annotation.arguments.first { it.name?.asString() == NullSubstitute::default.name }.value as KSAnnotation + return getMapDefaultResolver(defaultArgument) + } + + fun getSuitedNamedAnnotations(member: KSPropertyDeclaration) = + getSuitedNamedAnnotationsForClass(member) + .filter { it.value.isEmpty() || it.value.contains(forClassName) } + .keys + .toList() + + fun getSuitedNamedAnnotation(member: KSPropertyDeclaration) = + filterAnnotationsByClass(getSuitedNamedAnnotationsForClass(member), member.simpleName.asString()) + + private fun findMapAnnotation( + member: KSPropertyDeclaration, + annotationName: String?, + argumentName: String + ): KSType? { + val annotation = findAnnotation(member, annotationName) + + if (annotation != null) { + val resolverArgument = annotation.arguments.first { it.name?.asString() == argumentName } + return resolverArgument.value as? KSType + } + + return null + } + + private fun findAnnotation( + member: KSPropertyDeclaration, + annotationName: String? + ): KSAnnotation? { + val annotations = + member.annotations.filter { it.shortName.asString() == annotationName }.associateWith(::associateWithFor) + + return filterAnnotationsByClass(annotations, member.simpleName.asString()) + } + + private fun getSuitedNamedAnnotationsForClass(member: KSPropertyDeclaration) = + member.annotations.filter { it.shortName.asString() in namedAnnotations } + .associateWith(::associateWithFor) +} \ No newline at end of file diff --git a/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/CompilationTests.kt b/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/CompilationTests.kt index b30b4df..1492e7a 100644 --- a/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/CompilationTests.kt +++ b/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/CompilationTests.kt @@ -25,7 +25,7 @@ abstract class CompilationTests { `package`: String = packageName ) = FileSpec .builder(`package`, "$className.kt") - .addImport("com.ucasoft.komm.annotations", "MapConfiguration", "MapDefault") + .addImport("com.ucasoft.komm.annotations", "MapConfiguration", "MapDefault", "MapTargetDefault") .addImport("java.util", "Currency") .addType( TypeSpec diff --git a/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/ContextTests.kt b/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/ContextTests.kt index 605f4a2..9f5ed67 100644 --- a/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/ContextTests.kt +++ b/komm-processor/src/test/kotlin/com/ucasoft/komm/processor/ContextTests.kt @@ -11,6 +11,7 @@ import com.ucasoft.komm.annotations.KOMMMap import com.ucasoft.komm.annotations.MapDefault import com.ucasoft.komm.annotations.MapConvert import com.ucasoft.komm.annotations.MapConfiguration +import com.ucasoft.komm.annotations.MapTargetDefault import com.ucasoft.komm.processor.exceptions.KOMMException import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.reflection.shouldHaveMemberProperty @@ -269,6 +270,124 @@ internal class ContextTests : SatelliteTests() { } } + @Test + fun mapTargetDefaultWithContextResolverForMapTo() { + val sourceClassName = ClassName(packageName, "AccountCard") + val destinationClassName = ClassName(packageName, "DbAccountCard") + val contextSpec = buildFileSpec("AccountCardMapContext", mapOf("accountId" to PropertySpecInit(INT))) + val contextClassName = ClassName(packageName, "AccountCardMapContext") + val resolverSpec = buildContextResolver( + destinationClassName, + contextClassName, + INT, + "return context.accountId" + ) + val resolverClassName = ClassName(packageName, resolverSpec.typeSpecs.first().name!!) + val sourceSpec = buildFileSpec( + sourceClassName.simpleName, + mapOf( + "id" to PropertySpecInit(INT), + "type" to PropertySpecInit(STRING), + "number" to PropertySpecInit(STRING) + ), + listOf( + KOMMMap::class to mapOf( + "to = %L" to listOf("[${destinationClassName.simpleName}::class]"), + "context = %L" to listOf("${contextClassName.simpleName}::class") + ), + MapTargetDefault::class to mapOf( + "name = %S" to listOf("accountId"), + "default = %L" to listOf("${MapDefault::class.simpleName}(${resolverClassName.simpleName}::class)"), + "`for` = %L" to listOf("[${destinationClassName.simpleName}::class]") + ) + ) + ) + val destinationSpec = buildFileSpec( + destinationClassName.simpleName, + mapOf( + "id" to PropertySpecInit(INT), + "accountId" to PropertySpecInit(INT), + "type" to PropertySpecInit(STRING), + "number" to PropertySpecInit(STRING) + ) + ) + val generated = generate( + sourceSpec, + contextSpec, + resolverSpec, + destinationSpec + ) + + generated.exitCode.shouldBe(KotlinCompilation.ExitCode.OK) + + val mappingClass = generated.classLoader.loadClass("$packageName.MappingExtensionsKt") + val mappingMethod = mappingClass.declaredMethods.first() + val sourceClass = generated.classLoader.loadClass(sourceClassName.canonicalName) + val contextClass = generated.classLoader.loadClass(contextClassName.canonicalName) + val sourceInstance = sourceClass.constructors.first().newInstance(3, "visa", "4242") + val contextInstance = contextClass.constructors.first().newInstance(45) + val destinationInstance = mappingMethod.invoke(null, sourceInstance, contextInstance) + + destinationInstance.shouldNotBeNull() + destinationInstance::class.shouldHaveMemberProperty("id") { + it.getter.call(destinationInstance).shouldBe(3) + } + destinationInstance::class.shouldHaveMemberProperty("accountId") { + it.getter.call(destinationInstance).shouldBe(45) + } + destinationInstance::class.shouldHaveMemberProperty("type") { + it.getter.call(destinationInstance).shouldBe("visa") + } + destinationInstance::class.shouldHaveMemberProperty("number") { + it.getter.call(destinationInstance).shouldBe("4242") + } + } + + @Test + fun mapTargetDefaultWithResolverForMapFrom() { + val sourceSpec = buildFileSpec("SourceObject", mapOf("id" to PropertySpecInit(INT))) + val sourceClassName = ClassName(packageName, "SourceObject") + val destinationClassName = ClassName(packageName, "DestinationObject") + val resolverSpec = buildResolver(destinationClassName, STRING, "return \"fallback\"") + val resolverClassName = ClassName(packageName, resolverSpec.typeSpecs.first().name!!) + val generated = generate( + sourceSpec, + resolverSpec, + buildFileSpec( + destinationClassName.simpleName, + mapOf( + "id" to PropertySpecInit(INT), + "name" to PropertySpecInit(STRING) + ), + listOf( + KOMMMap::class to mapOf( + "from = %L" to listOf("[${sourceClassName.simpleName}::class]") + ), + MapTargetDefault::class to mapOf( + "name = %S" to listOf("name"), + "default = %L" to listOf("${MapDefault::class.simpleName}(${resolverClassName.simpleName}::class)") + ) + ) + ) + ) + + generated.exitCode.shouldBe(KotlinCompilation.ExitCode.OK) + + val mappingClass = generated.classLoader.loadClass("$packageName.MappingExtensionsKt") + val mappingMethod = mappingClass.declaredMethods.first() + val sourceClass = generated.classLoader.loadClass(sourceClassName.canonicalName) + val sourceInstance = sourceClass.constructors.first().newInstance(7) + val destinationInstance = mappingMethod.invoke(null, sourceInstance) + + destinationInstance.shouldNotBeNull() + destinationInstance::class.shouldHaveMemberProperty("id") { + it.getter.call(destinationInstance).shouldBe(7) + } + destinationInstance::class.shouldHaveMemberProperty("name") { + it.getter.call(destinationInstance).shouldBe("fallback") + } + } + @Test fun mapDefaultWithContextResolverWithNullableContext() { val sourceSpec = buildFileSpec("SourceObject", mapOf("accountId" to PropertySpecInit(INT)))