Skip to content

Commit

Permalink
Support for selecting nested properties (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
stefankoppier authored Jun 26, 2024
1 parent 5402e6d commit 9efe4cc
Show file tree
Hide file tree
Showing 11 changed files with 77 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
Expand Down Expand Up @@ -202,10 +200,10 @@ private class SourceValueCollector(
) : BaseVisitor<ObjectMappingSource, Unit>() {

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,38 @@ 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(
val extensionReceiverSymbol: IrValueSymbol,
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
14 changes: 14 additions & 0 deletions mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package tech.mappie.api

import kotlin.reflect.KProperty0
import kotlin.reflect.KProperty1

/**
Expand Down Expand Up @@ -33,6 +34,19 @@ public abstract class ObjectMappie<FROM, TO> : Mappie<FROM, TO>() {
public val forSet: SetMappie<FROM, TO> 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 <TO_TYPE, FROM_TYPE> KProperty1<TO, TO_TYPE>.fromProperty(source: KProperty0<FROM_TYPE>): TransformableValue<FROM_TYPE, TO_TYPE> =
generated()


/**
* Explicitly construct a mapping to [TO] from property source [source].
*
Expand Down
2 changes: 1 addition & 1 deletion testing/src/main/kotlin/testing/MultipleConstructor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ object MultipleConstructorsWithoutIntMapper : ObjectMappie<MultipleConstructors,

object MultipleConstructorsWitIntMapper : ObjectMappie<MultipleConstructors, MultipleConstructorsDto>() {
override fun map(from: MultipleConstructors): MultipleConstructorsDto = mapping {
MultipleConstructorsDto::int fromConstant 2
MultipleConstructorsDto::int fromValue 2
}
}
15 changes: 15 additions & 0 deletions testing/src/main/kotlin/testing/NestedPropertyMapper.kt
Original file line number Diff line number Diff line change
@@ -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<NestedInput, NestedOutput>() {
override fun map(from: NestedInput): NestedOutput = mapping {
NestedOutput::value fromProperty from.input::value
}
}
15 changes: 15 additions & 0 deletions testing/src/test/kotlin/testing/NestedPropertyMapperTest.kt
Original file line number Diff line number Diff line change
@@ -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")))
)
}
}
2 changes: 2 additions & 0 deletions website/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions website/src/posts/object-mapping/posts/resolving.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ object PersonMapper : ObjectMappie<Person, PersonDto>() {
```
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<Person, PersonDto>() {
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.

Expand Down

0 comments on commit 9efe4cc

Please sign in to comment.