diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/generation/IrTransformer.kt b/compiler-plugin/src/main/kotlin/tech/mappie/generation/IrTransformer.kt index bf91f9ad..1dace33e 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/generation/IrTransformer.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/generation/IrTransformer.kt @@ -105,7 +105,9 @@ fun ExpressionSource.toIr(builder: IrBuilderWithScope): IrExpression { } fun PropertySource.toIr(builder: IrBuilderWithScope): IrExpression { - val getter = builder.irCall(property).apply { dispatchReceiver = builder.irGet(type, dispatchReceiverSymbol) } + val getter = builder.irCall(property).apply { + dispatchReceiver = this@toIr.dispatchReceiver + } return transformation?.let { builder.irCall(context.referenceLetFunction()).apply { extensionReceiver = getter diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingBodyCollector.kt b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingBodyCollector.kt index 8717b71c..f1184581 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingBodyCollector.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingBodyCollector.kt @@ -91,10 +91,8 @@ private class ObjectBodyStatementCollector( IDENTIFIER_VIA -> { val mapping = expression.dispatchReceiver!!.accept(data)!! val transformation = expression.valueArguments.first()!!.accept(MapperReferenceCollector(), Unit) - val type = transformation.type mapping.first to (mapping.second as PropertySource).copy( - transformation = transformation, - type = type, + transformation = transformation ) } else -> { @@ -202,10 +200,10 @@ private class SourceValueCollector( ) : BaseVisitor() { override fun visitPropertyReference(expression: IrPropertyReference, data: Unit): ObjectMappingSource { + val dispatchReceiver = expression.dispatchReceiver ?: irGet(expression.type, dispatchReceiverSymbol) return PropertySource( property = expression.getter!!, - type = (expression.type as IrSimpleType).arguments[1].typeOrFail, - dispatchReceiverSymbol = dispatchReceiverSymbol, + dispatchReceiver = dispatchReceiver, transformation = null, origin = expression, isResolvedAutomatically = false diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingSource.kt b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingSource.kt index e6635d94..98237630 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingSource.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingSource.kt @@ -10,26 +10,26 @@ import org.jetbrains.kotlin.ir.types.typeOrFail import org.jetbrains.kotlin.ir.util.isFunction sealed interface ObjectMappingSource { - fun resolveType(): IrType + val type: IrType } data class PropertySource( val property: IrSimpleFunctionSymbol, - val type: IrType, - val dispatchReceiverSymbol: IrValueSymbol, + val dispatchReceiver: IrExpression, +// val dispatchReceiverSymbol: IrValueSymbol, val isResolvedAutomatically: Boolean, val transformation: IrFunctionExpression? = null, val origin: IrExpression? = null, ) : ObjectMappingSource { - override fun resolveType(): IrType { - return if (transformation == null) { - type + + override val type: IrType + get() = if (transformation == null) { + property.owner.returnType } else if (transformation.type.isFunction()) { (transformation.type as IrSimpleType).arguments[1].typeOrFail } else { transformation.type } - } } data class ExpressionSource( @@ -37,11 +37,11 @@ data class ExpressionSource( val expression: IrFunctionExpression, val origin: IrExpression?, ) : ObjectMappingSource { - override fun resolveType() = expression.function.returnType + override val type = expression.function.returnType } data class ValueSource( val value: IrExpression, ) : ObjectMappingSource { - override fun resolveType() = value.type + override val type = value.type } diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingsConstructor.kt b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingsConstructor.kt index 502e1533..a4f76b13 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingsConstructor.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingsConstructor.kt @@ -8,6 +8,7 @@ import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.util.hasDefaultValue import org.jetbrains.kotlin.name.Name +import tech.mappie.util.irGet class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParameter) { @@ -31,7 +32,7 @@ class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParam getter.name == getterName(target.name) } if (getter != null) { - listOf(PropertySource(getter.symbol, getter.returnType, source.symbol, true)) + listOf(PropertySource(getter.symbol, irGet(source), true)) } else if (target.hasDefaultValue()) { listOf(ValueSource(target.defaultValue!!.expression)) } else { diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt b/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt index 2ac46bf3..ea2ee3ab 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt @@ -54,9 +54,9 @@ interface MappingValidation { addAll( mapping.mappings .filter { (_, sources) -> sources.size == 1 } - .filter { (target, sources) -> !target.type.isAssignableFrom(sources.single().resolveType()) } + .filter { (target, sources) -> !target.type.isAssignableFrom(sources.single().type) } .map { (target, sources) -> Problem.error( - "Target ${mapping.targetType.getClass()!!.name.asString()}.${target.name.asString()} has type ${target.type.pretty()} which cannot be assigned from type ${sources.single().resolveType().pretty()}", + "Target ${mapping.targetType.getClass()!!.name.asString()}.${target.name.asString()} has type ${target.type.pretty()} which cannot be assigned from type ${sources.single().type.pretty()}", when (val source = sources.single()) { is PropertySource -> source.origin?.let { location(file, it) } is ExpressionSource -> source.origin?.let { location(file, it) } diff --git a/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie.kt b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie.kt index 424c8076..1b79045c 100644 --- a/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie.kt +++ b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie.kt @@ -2,6 +2,7 @@ package tech.mappie.api +import kotlin.reflect.KProperty0 import kotlin.reflect.KProperty1 /** @@ -33,6 +34,19 @@ public abstract class ObjectMappie : Mappie() { public val forSet: SetMappie get() = error("The mapper forSet should only be used in the context of 'via'. Use mapSet instead.") + /** + * Explicitly construct a mapping to [TO] from property source [source]. + * + * For example + * ```kotlin + * Person::name fromProperty PersonDto::fullName + * ``` + * will generate an explicit mapping, setting constructor parameter `Person.name` to `PersonDto.fullName`. + */ + protected infix fun KProperty1.fromProperty(source: KProperty0): TransformableValue = + generated() + + /** * Explicitly construct a mapping to [TO] from property source [source]. * diff --git a/testing/src/main/kotlin/testing/MultipleConstructor.kt b/testing/src/main/kotlin/testing/MultipleConstructor.kt index 08737144..31671403 100644 --- a/testing/src/main/kotlin/testing/MultipleConstructor.kt +++ b/testing/src/main/kotlin/testing/MultipleConstructor.kt @@ -14,6 +14,6 @@ object MultipleConstructorsWithoutIntMapper : ObjectMappie() { override fun map(from: MultipleConstructors): MultipleConstructorsDto = mapping { - MultipleConstructorsDto::int fromConstant 2 + MultipleConstructorsDto::int fromValue 2 } } \ No newline at end of file diff --git a/testing/src/main/kotlin/testing/NestedPropertyMapper.kt b/testing/src/main/kotlin/testing/NestedPropertyMapper.kt new file mode 100644 index 00000000..b6583efa --- /dev/null +++ b/testing/src/main/kotlin/testing/NestedPropertyMapper.kt @@ -0,0 +1,15 @@ +package testing + +import tech.mappie.api.ObjectMappie + +data class NestedInput(val input: NestedInputValue) + +data class NestedInputValue(val value: String) + +data class NestedOutput(val value: String) + +object NestedPropertyMapper : ObjectMappie() { + override fun map(from: NestedInput): NestedOutput = mapping { + NestedOutput::value fromProperty from.input::value + } +} \ No newline at end of file diff --git a/testing/src/test/kotlin/testing/NestedPropertyMapperTest.kt b/testing/src/test/kotlin/testing/NestedPropertyMapperTest.kt new file mode 100644 index 00000000..e3410ab6 --- /dev/null +++ b/testing/src/test/kotlin/testing/NestedPropertyMapperTest.kt @@ -0,0 +1,15 @@ +package testing + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class NestedPropertyMapperTest { + + @Test + fun `map NestedInput to NestedOutput via NestedPropertyMapper`() { + assertEquals( + NestedOutput("Test"), + NestedPropertyMapper.map(NestedInput(NestedInputValue("Test"))) + ) + } +} \ No newline at end of file diff --git a/website/src/changelog.md b/website/src/changelog.md index d0fea82c..2e5cc9d7 100644 --- a/website/src/changelog.md +++ b/website/src/changelog.md @@ -9,6 +9,8 @@ changelog: - "[#16](https://github.com/Mr-Mappie/mappie/issues/16) improved resolution of explicit parameter names." - "[#21](https://github.com/Mr-Mappie/mappie/issues/21) added global configuration option to report all warnings as errors." - "[#24](https://github.com/Mr-Mappie/mappie/issues/24) added explicit mapping fromValue as a replacement of the much more restricting fromConstant." + - "[#26](https://github.com/Mr-Mappie/mappie/issues/26) support for selecting nested properties." + - "Several other bug fixes." - date: "2024-06-22" title: "v0.1.0" items: diff --git a/website/src/posts/object-mapping/posts/resolving.md b/website/src/posts/object-mapping/posts/resolving.md index ff9a59b3..ec3c19a1 100644 --- a/website/src/posts/object-mapping/posts/resolving.md +++ b/website/src/posts/object-mapping/posts/resolving.md @@ -38,6 +38,17 @@ object PersonMapper : ObjectMappie() { ``` will set `PersonDto.description` to `Person.name`. +We can also select a nested property using `fromProperty`. This can be done by selecting the property from the mapping +parameter, usually named `from`. For example +```kotlin +object PersonMapper : ObjectMappie() { + override fun map(from: Person): PersonDto = mapping { + PersonDto::streetname fromProperty from.address::street + } +} +``` +will construct an explicit mapping from the property `street` from the `address` of `from` to `streetname` of `PersonDto`. + Sometimes, you want to map from a source property, but tweak the value, handle nullability, or transform the source in some other way. See [Transforming](/object-mapping/transforming/) for some guidelines.