diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/generation/MappieIrTransformer.kt b/compiler-plugin/src/main/kotlin/tech/mappie/generation/MappieIrTransformer.kt index 097f982b..47c79fea 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/generation/MappieIrTransformer.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/generation/MappieIrTransformer.kt @@ -7,14 +7,23 @@ import tech.mappie.util.* import tech.mappie.validation.MappingValidation import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext import org.jetbrains.kotlin.backend.common.lower.irThrow +import org.jetbrains.kotlin.ir.IrFileEntry import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.builders.* import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrFunctionAccessExpression +import org.jetbrains.kotlin.ir.expressions.IrPropertyReference +import org.jetbrains.kotlin.ir.types.IrSimpleType +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.classOrFail +import org.jetbrains.kotlin.ir.types.typeOrFail import org.jetbrains.kotlin.ir.util.* +import tech.mappie.MappieIrRegistrar import tech.mappie.resolving.enums.ExplicitEnumMappingTarget import tech.mappie.resolving.enums.ResolvedEnumMappingTarget import tech.mappie.resolving.enums.ThrowingEnumMappingTarget +import java.beans.Expression class MappieIrTransformer(private val symbols: List) : IrElementTransformerVoidWithContext() { @@ -60,8 +69,9 @@ class MappieIrTransformer(private val symbols: List) : IrEleme context.blockBody(scope) { +irReturn(irCallConstructor(mapping.symbol, emptyList()).apply { mapping.mappings.map { (target, source) -> + val file = declaration.fileEntry val index = mapping.symbol.owner.valueParameters.indexOf(target) - putValueArgument(index, source.single().toIr(this@blockBody)) + putValueArgument(index, generateValueArgument(file, source.single(), declaration.valueParameters)) } }) } @@ -104,41 +114,53 @@ class MappieIrTransformer(private val symbols: List) : IrEleme } } -fun ObjectMappingSource.toIr(builder: IrBuilderWithScope): IrExpression = - when (this) { - is ResolvedSource -> toIr(builder) - is PropertySource -> toIr(builder) - is ExpressionSource -> toIr(builder) - is ValueSource -> value - } - -fun ExpressionSource.toIr(builder: IrBuilderWithScope): IrExpression { - return builder.irCall(context.referenceLetFunction()).apply { - extensionReceiver = builder.irGet(type, extensionReceiverSymbol) - putValueArgument(0, expression) +fun IrBuilderWithScope.generateValueArgument(file: IrFileEntry, source: ObjectMappingSource, parameters: List): IrExpression { + return when (source) { + is ResolvedSource -> generateResolvedValueArgument(source) + is PropertySource -> generatePropertyValueArgument(file, source, parameters) + is ExpressionSource -> generateExpressionValueArgument(source, parameters) + is ValueSource -> source.value } } -fun ResolvedSource.toIr(builder: IrBuilderWithScope): IrExpression { - val getter = builder.irCall(property.function).apply { - dispatchReceiver = this@toIr.dispatchReceiver +fun IrBuilderWithScope.generateResolvedValueArgument(source: ResolvedSource): IrFunctionAccessExpression { + val getter = irCall(source.property.function).apply { + dispatchReceiver = irGet(source.property.holder) } - return via?.let { - builder.irCall(via).apply { - dispatchReceiver = viaDispatchReceiver + return source.via?.let { + irCall(source.via).apply { + dispatchReceiver = source.viaDispatchReceiver putValueArgument(0, getter) } } ?: getter } -fun PropertySource.toIr(builder: IrBuilderWithScope): IrExpression { - val getter = builder.irCall(property).apply { - dispatchReceiver = this@toIr.dispatchReceiver +fun IrPropertyReference.classType(): IrType { + return when (type.classOrFail) { + context.irBuiltIns.kProperty0Class -> dispatchReceiver!!.type + context.irBuiltIns.kProperty1Class -> (this.type as IrSimpleType).arguments.first().typeOrFail + else -> error("Unknown KProperty ${dumpKotlinLike()}") + } +} + +fun IrBuilderWithScope.generatePropertyValueArgument(file: IrFileEntry, source: PropertySource, parameters: List): IrFunctionAccessExpression { + val getter = irCall(source.getter).apply { + dispatchReceiver = source.property.dispatchReceiver + ?: irGet(parameters.singleOrNull { it.type == source.property.classType() } ?: run { + logError("Could not determine value parameters for property reference. Please use a property reference of an object instead of the class", location(file, source.property)) + error("Mappie resolving failed") + }) } - return transformation?.let { - builder.irCall(context.referenceLetFunction()).apply { + return source.transformation?.let { + irCall(MappieIrRegistrar.context.referenceLetFunction()).apply { extensionReceiver = getter - putValueArgument(0, transformation) + putValueArgument(0, source.transformation) } } ?: getter } +fun IrBuilderWithScope.generateExpressionValueArgument(source: ExpressionSource, parameters: List): IrFunctionAccessExpression { + return irCall(MappieIrRegistrar.context.referenceLetFunction()).apply { + extensionReceiver = irGet(parameters.single()) + putValueArgument(0, source.expression) + } +} \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/MappingResolver.kt b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/MappingResolver.kt index 489ebe73..cf80232f 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/MappingResolver.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/MappingResolver.kt @@ -22,7 +22,7 @@ sealed interface Mapping data class ConstructorCallMapping( val targetType: IrType, - val sourceType: IrType, + val sourceTypes: List, val symbol: IrConstructorSymbol, val mappings: Map>, val unknowns: Map>, diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ClassResolver.kt b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ClassResolver.kt index 7ffc0f22..2e88fdb9 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ClassResolver.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ClassResolver.kt @@ -8,17 +8,17 @@ import org.jetbrains.kotlin.ir.util.isClass class ClassResolver(private val declaration: IrFunction, private val symbols: List) { - private val sourceParameter = declaration.valueParameters.first() + private val sourceParameters = declaration.valueParameters init { require(declaration.returnType.getClass()!!.isClass) } fun resolve(): List { - val constructor = ObjectMappingsConstructor.of(declaration.returnType, sourceParameter) - .apply { getters.addAll(sourceParameter.accept(GettersCollector(), Unit)) } + val constructor = ObjectMappingsConstructor.of(declaration.returnType, sourceParameters) + .apply { getters.addAll(sourceParameters.flatMap { it.accept(GettersCollector(), it) }) } - declaration.body?.accept(ObjectMappingBodyCollector(declaration.fileEntry, sourceParameter.symbol), constructor) + declaration.body?.accept(ObjectMappingBodyCollector(declaration.fileEntry, sourceParameters), constructor) return declaration.accept(ConstructorsCollector(), Unit).map { ObjectMappingsConstructor.of(constructor).apply { diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/GettersCollector.kt b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/GettersCollector.kt index fb370d4d..8923c4e9 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/GettersCollector.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/GettersCollector.kt @@ -8,21 +8,23 @@ import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.types.getClass import org.jetbrains.kotlin.ir.util.properties -class GettersCollector : BaseVisitor, Unit>() { +class GettersCollector : BaseVisitor, IrValueParameter>() { - override fun visitValueParameter(declaration: IrValueParameter, data: Unit): List { - return declaration.type.getClass()!!.properties.flatMap { it.accept(data) }.toList() + - declaration.type.getClass()!!.declarations.filterIsInstance().flatMap { it.accept(data) } + override fun visitValueParameter(declaration: IrValueParameter, data: IrValueParameter): List { + val clazz = declaration.type.getClass()!! + val properties = clazz.properties.flatMap { it.accept(data) }.toList() + val methods = clazz.declarations.filterIsInstance().flatMap { it.accept(data) } + return properties + methods } - override fun visitFunction(declaration: IrFunction, data: Unit): List { + override fun visitFunction(declaration: IrFunction, data: IrValueParameter): List { if (declaration.name.asString().startsWith("get") && declaration.symbol.owner.valueParameters.isEmpty()) { - return listOf(MappieFunctionGetter(declaration)) + return listOf(MappieFunctionGetter(declaration, data)) } return emptyList() } - override fun visitProperty(declaration: IrProperty, data: Unit): List { - return declaration.getter?.let { listOf(MappiePropertyGetter(it)) } ?: emptyList() + override fun visitProperty(declaration: IrProperty, data: IrValueParameter): List { + return declaration.getter?.let { listOf(MappiePropertyGetter(it, data)) } ?: emptyList() } } \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/MappieGetter.kt b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/MappieGetter.kt index fbff8502..a162e22e 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/MappieGetter.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/MappieGetter.kt @@ -2,6 +2,7 @@ package tech.mappie.resolving.classes import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.name.Name import tech.mappie.util.dumpKotlinLike @@ -11,18 +12,19 @@ sealed interface MappieGetter { val name: Name val type: IrType val function: IrFunction + val holder: IrValueParameter fun dumpKotlinLike(): String } -data class MappiePropertyGetter(override val function: IrSimpleFunction) : MappieGetter { +data class MappiePropertyGetter(override val function: IrSimpleFunction, override val holder: IrValueParameter) : MappieGetter { override val name = function.name override val type = function.returnType override fun dumpKotlinLike(): String = function.symbol.dumpKotlinLike() } -data class MappieFunctionGetter(override val function: IrFunction) : MappieGetter { +data class MappieFunctionGetter(override val function: IrFunction, override val holder: IrValueParameter) : MappieGetter { override val name = getterName(function.name.asString().removePrefix("get").replaceFirstChar { it.lowercaseChar() }) override val type = function.returnType 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 fcd1e668..8730b00d 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 @@ -13,6 +13,7 @@ import org.jetbrains.kotlin.ir.backend.js.utils.valueArguments import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter import org.jetbrains.kotlin.ir.builders.declarations.buildFun import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.declarations.createExpressionBody import org.jetbrains.kotlin.ir.expressions.* import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl @@ -25,7 +26,7 @@ import org.jetbrains.kotlin.name.Name class ObjectMappingBodyCollector( file: IrFileEntry, - private val dispatchReceiverSymbol: IrValueSymbol, + private val dispatchReceiverSymbols: List, ) : BaseVisitor(file) { override fun visitBlockBody(body: IrBlockBody, data: ObjectMappingsConstructor): ObjectMappingsConstructor { @@ -49,21 +50,20 @@ class ObjectMappingBodyCollector( override fun visitFunctionExpression(expression: IrFunctionExpression, data: ObjectMappingsConstructor): ObjectMappingsConstructor { return expression.function.body?.statements?.fold(data) { acc, current -> - acc.let { current.accept(ObjectBodyStatementCollector(file!!, dispatchReceiverSymbol), Unit)?.let { acc.explicit(it) } ?: it } + acc.let { current.accept(ObjectBodyStatementCollector(file!!), Unit)?.let { acc.explicit(it) } ?: it } } ?: data } } private class ObjectBodyStatementCollector( file: IrFileEntry, - private val dispatchReceiverSymbol: IrValueSymbol, ) : BaseVisitor?, Unit>(file) { override fun visitCall(expression: IrCall, data: Unit): Pair? { return when (expression.symbol.owner.name) { IDENTIFIER_FROM_PROPERTY, IDENTIFIER_FROM_CONSTANT -> { val target = expression.extensionReceiver!!.accept(TargetValueCollector(file!!), data) - val source = expression.valueArguments.first()!!.accept(SourceValueCollector(dispatchReceiverSymbol), Unit) + val source = expression.valueArguments.first()!!.accept(SourceValueCollector(), Unit) target to source } @@ -78,7 +78,6 @@ private class ObjectBodyStatementCollector( val source = expression.valueArguments.first() as IrFunctionExpression target to ExpressionSource( - dispatchReceiverSymbol, source, expression, ) @@ -195,15 +194,11 @@ private class MapperReferenceCollector : BaseVisitor ) } -private class SourceValueCollector( - private val dispatchReceiverSymbol: IrValueSymbol, -) : BaseVisitor() { +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!!, - dispatchReceiver = dispatchReceiver, + property = expression, transformation = null, origin = expression, ) 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 81105f66..a1165ec1 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 @@ -1,8 +1,11 @@ package tech.mappie.resolving.classes import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrFunctionExpression +import org.jetbrains.kotlin.ir.expressions.IrPropertyReference import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol import org.jetbrains.kotlin.ir.symbols.IrValueSymbol import org.jetbrains.kotlin.ir.types.IrSimpleType @@ -16,7 +19,6 @@ sealed interface ObjectMappingSource { data class ResolvedSource( val property: MappieGetter, - val dispatchReceiver: IrExpression, val via: IrSimpleFunction? = null, val viaDispatchReceiver: IrExpression? = null, ) : ObjectMappingSource { @@ -25,15 +27,16 @@ data class ResolvedSource( } data class PropertySource( - val property: IrSimpleFunctionSymbol, - val dispatchReceiver: IrExpression, + val property: IrPropertyReference, val transformation: IrFunctionExpression? = null, val origin: IrExpression, ) : ObjectMappingSource { + val getter = property.getter!! + override val type: IrType get() = if (transformation == null) { - property.owner.returnType + getter.owner.returnType } else if (transformation.type.isFunction()) { (transformation.type as IrSimpleType).arguments[1].typeOrFail } else { @@ -42,7 +45,6 @@ data class PropertySource( } data class ExpressionSource( - val extensionReceiverSymbol: IrValueSymbol, val expression: IrFunctionExpression, val origin: IrExpression, ) : ObjectMappingSource { @@ -54,4 +56,4 @@ data class ValueSource( val origin: IrExpression?, ) : ObjectMappingSource { override val type = value.type -} +} \ No newline at end of file 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 77362f84..4288ffa5 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 @@ -12,7 +12,7 @@ import tech.mappie.MappieIrRegistrar.Companion.context import tech.mappie.resolving.* import tech.mappie.util.* -class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParameter) { +class ObjectMappingsConstructor(val targetType: IrType, val sources: List) { var symbols = listOf() @@ -26,31 +26,39 @@ class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParam get() = constructor?.valueParameters ?: emptyList() fun construct(): ConstructorCallMapping { - val mappings = targets.associateWith { target -> + val mappings: Map> = targets.associateWith { target -> val concreteSource = explicit[target.name] if (concreteSource != null) { concreteSource } else { - val getter = getters.firstOrNull { getter -> - getter.name == getterName(target.name) - } - if (getter != null) { - val clazz = symbols.singleOrNull { it.fits(getter.type, target.type) }?.clazz - val via = when { - clazz == null -> null - getter.type.isList() && target.type.isList() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_LIST } - getter.type.isSet() && target.type.isSet() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_SET } - getter.type.isNullable() && target.type.isNullable() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_NULLABLE } - else -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP } - } - val viaDispatchReceiver = when { - clazz == null -> null - clazz.isObject -> IrGetObjectValueImpl(SYNTHETIC_OFFSET, SYNTHETIC_OFFSET, clazz.symbol.defaultType, clazz.symbol) - else -> clazz.constructors.firstOrNull { it.valueParameters.isEmpty() }?.let { irConstructorCall(it) } + val mappings = getters.filter { getter -> getter.name == getterName(target.name) } + .flatMap { getter -> + val clazz = symbols.singleOrNull { it.fits(getter.type, target.type) }?.clazz + val via = when { + clazz == null -> null + getter.type.isList() && target.type.isList() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_LIST } + getter.type.isSet() && target.type.isSet() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_SET } + getter.type.isNullable() && target.type.isNullable() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_NULLABLE } + else -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP } + } + val viaDispatchReceiver = when { + clazz == null -> null + clazz.isObject -> IrGetObjectValueImpl( + SYNTHETIC_OFFSET, + SYNTHETIC_OFFSET, + clazz.symbol.defaultType, + clazz.symbol + ) + else -> clazz.constructors.firstOrNull { it.valueParameters.isEmpty() } + ?.let { irConstructorCall(it) } + } + listOf(ResolvedSource(getter, via, viaDispatchReceiver)) } - listOf(ResolvedSource(getter, irGet(source), via, viaDispatchReceiver)) - } else if (target.hasDefaultValue() && context.configuration.useDefaultArguments) { + + if (mappings.isNotEmpty()) { + mappings + } else if (target.hasDefaultValue() && context.configuration.useDefaultArguments) { listOf(ValueSource(target.defaultValue!!.expression, null)) } else { emptyList() @@ -63,7 +71,7 @@ class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParam return ConstructorCallMapping( targetType = targetType, - sourceType = source.type, + sourceTypes = sources.map { it.type }, symbol = constructor!!.symbol, mappings = mappings, unknowns = unknowns, @@ -75,12 +83,12 @@ class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParam companion object { fun of(constructor: ObjectMappingsConstructor) = - ObjectMappingsConstructor(constructor.targetType, constructor.source).apply { + ObjectMappingsConstructor(constructor.targetType, constructor.sources).apply { getters = constructor.getters explicit = constructor.explicit } - fun of(targetType: IrType, source: IrValueParameter) = - ObjectMappingsConstructor(targetType, source) + fun of(targetType: IrType, sources: List) = + ObjectMappingsConstructor(targetType, sources) } } \ No newline at end of file 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 acd9dea4..4a2e0d0c 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt @@ -4,17 +4,13 @@ import tech.mappie.MappieIrRegistrar.Companion.context import tech.mappie.resolving.ConstructorCallMapping import tech.mappie.resolving.EnumMapping import tech.mappie.resolving.Mapping -import tech.mappie.resolving.classes.PropertySource import tech.mappie.util.isAssignableFrom import tech.mappie.util.location import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation import org.jetbrains.kotlin.ir.IrFileEntry import org.jetbrains.kotlin.ir.types.removeAnnotations import org.jetbrains.kotlin.ir.util.dumpKotlinLike -import tech.mappie.resolving.classes.ExpressionSource -import tech.mappie.resolving.classes.ResolvedSource -import tech.mappie.resolving.classes.ValueSource -import tech.mappie.util.dumpKotlinLike +import tech.mappie.resolving.classes.* data class Problem( val description: String, diff --git a/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/ObjectWithSameValuesTest.kt b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/ObjectWithSameValuesTest.kt index 8e9a53a6..5d71ec9d 100644 --- a/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/ObjectWithSameValuesTest.kt +++ b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/ObjectWithSameValuesTest.kt @@ -50,7 +50,7 @@ class ObjectWithSameValuesTest { @Test @Disabled("Not implemented yet") - fun `map identical data classes with an explicit mapping should succeed`() { + fun `map identical data classes with an explicit mapping should warn`() { KotlinCompilation(directory).apply { sources = buildList { add( diff --git a/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/WrongTypeAssignmentTest.kt b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/WrongTypeAssignmentTest.kt index 45e0b779..5c6a3455 100644 --- a/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/WrongTypeAssignmentTest.kt +++ b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/WrongTypeAssignmentTest.kt @@ -39,7 +39,7 @@ class WrongTypeAssignmentTest { }.compile { assertThat(exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) assertThat(messages) - .containsError("Target Output::value of type Int cannot be assigned from Input::value of type String") + .containsError("Target Output::value of type Int cannot be assigned from from::value of type String") } } diff --git a/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects2/Object2WithOverlappingValuesTest.kt b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects2/Object2WithOverlappingValuesTest.kt new file mode 100644 index 00000000..9d1bf5d2 --- /dev/null +++ b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects2/Object2WithOverlappingValuesTest.kt @@ -0,0 +1,78 @@ +package tech.mappie.testing.objects2 + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import tech.mappie.testing.compilation.KotlinCompilation +import tech.mappie.testing.compilation.KotlinCompilation.ExitCode +import tech.mappie.testing.compilation.SourceFile.Companion.kotlin +import tech.mappie.testing.containsError +import tech.mappie.testing.loadObjectMappie2Class +import java.io.File + +class Object2WithOverlappingValuesTest { + + data class Input1(val value: String, val age: Int) + data class Input2(val age: Int) + data class Output(val value: String, val age: Int) + + @TempDir + private lateinit var directory: File + + @Test + fun `map identical data classes should fail`() { + KotlinCompilation(directory).apply { + sources = buildList { + add( + kotlin("Test.kt", + """ + import tech.mappie.api.ObjectMappie2 + import tech.mappie.testing.objects2.Object2WithOverlappingValuesTest.* + + class Mapper : ObjectMappie2() + """ + ) + ) + } + }.compile { + assertThat(exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) + assertThat(messages) + .containsError("Target Output::age has multiple sources defined") + } + } + + @Test + fun `map identical data classes should with one specified succeed`() { + KotlinCompilation(directory).apply { + sources = buildList { + add( + kotlin("Test.kt", + """ + import tech.mappie.api.ObjectMappie2 + import tech.mappie.testing.objects2.Object2WithOverlappingValuesTest.* + + class Mapper : ObjectMappie2() { + override fun map(first: Input1, second: Input2) = mapping { + to::age fromProperty second::age + } + } + """ + ) + ) + } + }.compile { + assertThat(exitCode).isEqualTo(ExitCode.OK) + assertThat(messages).isEmpty() + + val mapper = classLoader + .loadObjectMappie2Class("Mapper") + .constructors + .first() + .call() + + assertThat(mapper.map(Input1("value", 10), Input2(20))) + .isEqualTo(Output("value", 20) + ) + } + } +} \ No newline at end of file diff --git a/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects2/Object2WithSameValuesTest.kt b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects2/Object2WithSameValuesTest.kt new file mode 100644 index 00000000..daded3af --- /dev/null +++ b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects2/Object2WithSameValuesTest.kt @@ -0,0 +1,49 @@ +package tech.mappie.testing.objects2 + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import tech.mappie.testing.compilation.KotlinCompilation +import tech.mappie.testing.compilation.KotlinCompilation.ExitCode +import tech.mappie.testing.compilation.SourceFile.Companion.kotlin +import tech.mappie.testing.loadObjectMappie2Class +import java.io.File + +class Object2WithSameValuesTest { + + data class Input1(val value: String) + data class Input2(val age: Int) + data class Output(val value: String, val age: Int) + + @TempDir + private lateinit var directory: File + + @Test + fun `map identical data classes should succeed`() { + KotlinCompilation(directory).apply { + sources = buildList { + add( + kotlin("Test.kt", + """ + import tech.mappie.api.ObjectMappie2 + import tech.mappie.testing.objects2.Object2WithSameValuesTest.* + + class Mapper : ObjectMappie2() + """ + ) + ) + } + }.compile { + assertThat(exitCode).isEqualTo(ExitCode.OK) + assertThat(messages).isEmpty() + + val mapper = classLoader + .loadObjectMappie2Class("Mapper") + .constructors + .first() + .call() + + assertThat(mapper.map(Input1("value"), Input2(10))).isEqualTo(Output("value", 10)) + } + } +} \ No newline at end of file diff --git a/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects3/Object3WithOverlappingValuesTest.kt b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects3/Object3WithOverlappingValuesTest.kt new file mode 100644 index 00000000..ac8e6720 --- /dev/null +++ b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects3/Object3WithOverlappingValuesTest.kt @@ -0,0 +1,81 @@ +package tech.mappie.testing.objects3 + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import tech.mappie.testing.compilation.KotlinCompilation +import tech.mappie.testing.compilation.KotlinCompilation.ExitCode +import tech.mappie.testing.compilation.SourceFile.Companion.kotlin +import tech.mappie.testing.containsError +import tech.mappie.testing.loadObjectMappie2Class +import tech.mappie.testing.loadObjectMappie3Class +import java.io.File + +class Object3WithOverlappingValuesTest { + + data class Input1(val value: String, val char: Char) + data class Input2(val value: String, val age: Int) + data class Input3(val age: Int) + data class Output(val value: String, val age: Int, val char: Char) + + @TempDir + private lateinit var directory: File + + @Test + fun `map identical data classes should fail`() { + KotlinCompilation(directory).apply { + sources = buildList { + add( + kotlin("Test.kt", + """ + import tech.mappie.api.ObjectMappie3 + import tech.mappie.testing.objects3.Object3WithOverlappingValuesTest.* + + class Mapper : ObjectMappie3() + """ + ) + ) + } + }.compile { + assertThat(exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) + assertThat(messages) + .containsError("Target Output::age has multiple sources defined") + } + } + + @Test + fun `map identical data classes should with one specified succeed`() { + KotlinCompilation(directory).apply { + sources = buildList { + add( + kotlin("Test.kt", + """ + import tech.mappie.api.ObjectMappie3 + import tech.mappie.testing.objects3.Object3WithOverlappingValuesTest.* + + class Mapper : ObjectMappie3() { + override fun map(first: Input1, second: Input2, third: Input3) = mapping { + to::value fromProperty first::value + to::age fromProperty Input3::age + } + } + """ + ) + ) + } + }.compile { + assertThat(exitCode).isEqualTo(ExitCode.OK) + assertThat(messages).isEmpty() + + val mapper = classLoader + .loadObjectMappie3Class("Mapper") + .constructors + .first() + .call() + + assertThat(mapper.map(Input1("value", 'q'), Input2("second", 10), Input3(20))) + .isEqualTo(Output("value", 20, 'q') + ) + } + } +} \ No newline at end of file diff --git a/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects3/Object3WithSameValuesTest.kt b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects3/Object3WithSameValuesTest.kt new file mode 100644 index 00000000..7dc002d8 --- /dev/null +++ b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects3/Object3WithSameValuesTest.kt @@ -0,0 +1,52 @@ +package tech.mappie.testing.objects3 + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import tech.mappie.testing.compilation.KotlinCompilation +import tech.mappie.testing.compilation.KotlinCompilation.ExitCode +import tech.mappie.testing.compilation.SourceFile.Companion.kotlin +import tech.mappie.testing.loadObjectMappie2Class +import tech.mappie.testing.loadObjectMappie3Class +import java.io.File + +class Object3WithSameValuesTest { + + data class Input1(val value: String) + data class Input2(val age: Int) + data class Input3(val char: Char) + data class Output(val value: String, val age: Int, val char: Char) + + @TempDir + private lateinit var directory: File + + @Test + fun `map identical data classes should succeed`() { + KotlinCompilation(directory).apply { + sources = buildList { + add( + kotlin("Test.kt", + """ + import tech.mappie.api.ObjectMappie3 + import tech.mappie.testing.objects3.Object3WithSameValuesTest.* + + class Mapper : ObjectMappie3() + """ + ) + ) + } + }.compile { + assertThat(exitCode).isEqualTo(ExitCode.OK) + assertThat(messages).isEmpty() + + val mapper = classLoader + .loadObjectMappie3Class("Mapper") + .constructors + .first() + .call() + + assertThat(mapper.map(Input1("value"), Input2(10), Input3('c'))) + .isEqualTo(Output("value", 10, 'c')) + } + } +} \ No newline at end of file diff --git a/compiler-plugin/src/testFixtures/kotlin/tech/mappie/testing/ClassLoading.kt b/compiler-plugin/src/testFixtures/kotlin/tech/mappie/testing/ClassLoading.kt index 1f7dd95d..6d667b1e 100644 --- a/compiler-plugin/src/testFixtures/kotlin/tech/mappie/testing/ClassLoading.kt +++ b/compiler-plugin/src/testFixtures/kotlin/tech/mappie/testing/ClassLoading.kt @@ -1,13 +1,25 @@ +@file:Suppress("UNCHECKED_CAST") + package tech.mappie.testing -import tech.mappie.api.EnumMappie -import tech.mappie.api.ObjectMappie +import tech.mappie.api.* import java.net.URLClassLoader import kotlin.reflect.KClass fun , TO : Enum<*>> URLClassLoader.loadEnumMappieClass(name: String) = loadClass(name).kotlin as KClass> - fun URLClassLoader.loadObjectMappieClass(name: String) = - loadClass(name).kotlin as KClass> \ No newline at end of file + loadClass(name).kotlin as KClass> + +fun URLClassLoader.loadObjectMappie2Class(name: String) = + loadClass(name).kotlin as KClass> + +fun URLClassLoader.loadObjectMappie3Class(name: String) = + loadClass(name).kotlin as KClass> + +fun URLClassLoader.loadObjectMappie4Class(name: String) = + loadClass(name).kotlin as KClass> + +fun URLClassLoader.loadObjectMappie5Class(name: String) = + loadClass(name).kotlin as KClass> \ No newline at end of file diff --git a/mappie-api/src/commonMain/kotlin/tech/mappie/api/EnumMappie.kt b/mappie-api/src/commonMain/kotlin/tech/mappie/api/EnumMappie.kt index 6c52628e..b91871ef 100644 --- a/mappie-api/src/commonMain/kotlin/tech/mappie/api/EnumMappie.kt +++ b/mappie-api/src/commonMain/kotlin/tech/mappie/api/EnumMappie.kt @@ -17,7 +17,53 @@ package tech.mappie.api * @param FROM the source type to map from. * @param TO the target type to map to. */ -public abstract class EnumMappie, TO : Enum<*>> : Mappie() { +public abstract class EnumMappie, TO : Enum<*>> : Mappie { + + /** + * Map [from] to an instance of [TO]. + * + * @param from the source value. + * @return [from] mapped to an instance of [TO]. + */ + public open fun map(from: FROM): TO = generated() + + /** + * Map nullable [from] to an instance of [TO]. + * + * @param from the source value. + * @return [from] mapped to an instance of [TO]. + */ + public fun mapNullable(from: FROM?): TO? = + if (from == null) null else map(from) + + /** + * Map each element in [from] to an instance of [TO]. + * + * @param from the source values. + * @return [from] mapped to a list of instances of [TO]. + */ + public fun mapList(from: List): List = + ArrayList(from.size).apply { from.forEach { add(map(it)) } } + + /** + * Map each element in [from] to an instance of [TO]. + * + * @param from the source values. + * @return [from] mapped to a set of instances of [TO]. + */ + public fun mapSet(from: Set): Set = + HashSet(from.size).apply { from.forEach { add(map(it)) } } + + /** + * Mapping function which instructs Mappie to generate code for this implementation. + * + * @param builder the configuration for the generation of this mapping. + * @return An instance of the mapped value at runtime. + */ + protected fun mapping(builder: EnumMappingConstructor.() -> Unit = { }): TO = generated() +} + +public class EnumMappingConstructor { /** * Explicitly construct a mapping to [TO] from source entry [source]. @@ -28,7 +74,7 @@ public abstract class EnumMappie, TO : Enum<*>> : Mappie * ``` * will generate an explicit mapping, mapping `Colour.ORANGE` to `Color.UNKNOWN`. */ - protected infix fun TO.fromEnumEntry(source: FROM): Unit = generated() + public infix fun TO.fromEnumEntry(source: FROM): Unit = generated() /** * Explicitly construct a mapping to throw an exception from source entry [source]. @@ -39,5 +85,5 @@ public abstract class EnumMappie, TO : Enum<*>> : Mappie * ``` * will generate an explicit mapping, mapping `Colour.ORANGE` to an [IllegalStateException] being thrown. */ - protected infix fun Throwable.thrownByEnumEntry(source: FROM): Unit = generated() + public infix fun Throwable.thrownByEnumEntry(source: FROM): Unit = generated() } \ No newline at end of file diff --git a/mappie-api/src/commonMain/kotlin/tech/mappie/api/ListMappie.kt b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ListMappie.kt index 2bea81ba..d7afba10 100644 --- a/mappie-api/src/commonMain/kotlin/tech/mappie/api/ListMappie.kt +++ b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ListMappie.kt @@ -3,4 +3,4 @@ package tech.mappie.api /** * Base mapper class for list mappers. Cannot be instantiated, but can be created by using the field [ObjectMappie.forList]. */ -public sealed class ListMappie : Mappie, List>() \ No newline at end of file +public sealed class ListMappie : Mappie> \ No newline at end of file diff --git a/mappie-api/src/commonMain/kotlin/tech/mappie/api/Mappie.kt b/mappie-api/src/commonMain/kotlin/tech/mappie/api/Mappie.kt index 4bdf2345..0a518224 100644 --- a/mappie-api/src/commonMain/kotlin/tech/mappie/api/Mappie.kt +++ b/mappie-api/src/commonMain/kotlin/tech/mappie/api/Mappie.kt @@ -1,52 +1,6 @@ -@file:Suppress("UNUSED_PARAMETER") - package tech.mappie.api -public abstract class Mappie { - - /** - * Map [from] to an instance of [TO]. - * - * @param from the source value. - * @return [from] mapped to an instance of [TO]. - */ - public open fun map(from: FROM): TO = generated() - - /** - * Map nullable [from] to an instance of [TO]. - * - * @param from the source value. - * @return [from] mapped to an instance of [TO]. - */ - public fun mapNullable(from: FROM?): TO? = - if (from == null) null else map(from) - - /** - * Map each element in [from] to an instance of [TO]. - * - * @param from the source values. - * @return [from] mapped to a list of instances of [TO]. - */ - public fun mapList(from: List): List = - ArrayList(from.size).apply { from.forEach { add(map(it)) } } - - /** - * Map each element in [from] to an instance of [TO]. - * - * @param from the source values. - * @return [from] mapped to a set of instances of [TO]. - */ - public fun mapSet(from: Set): Set = - HashSet(from.size).apply { from.forEach { add(map(it)) } } - - /** - * Mapping function which instructs Mappie to generate code for this implementation. - * - * @param builder the configuration for the generation of this mapping. - * @return An instance of the mapped value at runtime. - */ - protected fun mapping(builder: Mappie.() -> Unit = { }): TO = generated() -} +public interface Mappie internal fun generated(): Nothing = error("Will be generated") \ No newline at end of file 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 ce987e3f..21fe32f4 100644 --- a/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie.kt +++ b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie.kt @@ -21,67 +21,88 @@ import kotlin.reflect.KProperty1 * @param FROM the source type to map from. * @param TO the target type to map to. */ -public abstract class ObjectMappie : Mappie() { - - /** - * Alias for the target type [TO] to simply property references. - * - * For example, suppose we are constructing a mapper with target type `Person` - * ```kotlin - * to::name fromProperty PersonDto::fullName - * ``` - * is equivalent to `Person::name fromProperty PersonDto::fullName`. - */ - protected val to: TO - get() = error("The to property should only be used in the context of `to::property fromX y`.") +public abstract class ObjectMappie : Mappie { /** * A mapper for [List] to be used in [TransformableValue.via]. */ - public val forList: ListMappie get() = + public val forList: ListMappie get() = error("The mapper forList should only be used in the context of 'via'. Use mapList instead.") /** * A mapper for [Set] to be used in [TransformableValue.via]. */ - public val forSet: SetMappie get() = + 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]. + * Map [from] to an instance of [TO]. * - * For example - * ```kotlin - * Person::name fromProperty PersonDto::fullName - * ``` - * will generate an explicit mapping, setting constructor parameter `Person.name` to `PersonDto.fullName`. + * @param from the source value. + * @return [from] mapped to an instance of [TO]. */ - protected infix fun KProperty.fromProperty(source: KProperty0): TransformableValue = - generated() + public open fun map(from: FROM): TO = generated() /** - * Explicitly construct a mapping to [TO] from property source [source]. + * Map nullable [from] to an instance of [TO]. * - * For example + * @param from the source value. + * @return [from] mapped to an instance of [TO]. + */ + public fun mapNullable(from: FROM?): TO? = + if (from == null) null else map(from) + + /** + * Map each element in [from] to an instance of [TO]. + * + * @param from the source values. + * @return [from] mapped to a list of instances of [TO]. + */ + public fun mapList(from: List): List = + ArrayList(from.size).apply { from.forEach { add(map(it)) } } + + /** + * Map each element in [from] to an instance of [TO]. + * + * @param from the source values. + * @return [from] mapped to a set of instances of [TO]. + */ + public fun mapSet(from: Set): Set = + HashSet(from.size).apply { from.forEach { add(map(it)) } } + + /** + * Mapping function which instructs Mappie to generate code for this implementation. + * + * @param builder the configuration for the generation of this mapping. + * @return An instance of the mapped value at runtime. + */ + protected fun mapping(builder: ObjectMappingConstructor.() -> Unit = { }): TO = generated() +} + +public class ObjectMappingConstructor { + + /** + * Alias for the target type [TO] to simply property references. + * + * For example, suppose we are constructing a mapper with target type `Person` * ```kotlin - * Person::name fromProperty PersonDto::fullName + * to::name fromProperty PersonDto::fullName * ``` - * will generate an explicit mapping, setting constructor parameter `Person.name` to `PersonDto.fullName`. + * is equivalent to `Person::name fromProperty PersonDto::fullName`. */ - protected infix fun KProperty.fromProperty(source: KProperty1): TransformableValue = - generated() + public val to: TO + get() = error("The to property should only be used in the context of `to::property fromX y`.") /** - * Explicitly construct a mapping to [TO] from constant source [value]. + * Explicitly construct a mapping to [TO] from property source [source]. * * For example * ```kotlin - * Person::name fromConstant "John Doe" + * Person::name fromProperty PersonDto::fullName * ``` - * will generate an explicit mapping, setting constructor parameter `Person.name` to `"John Doe"`. + * will generate an explicit mapping, setting constructor parameter `Person.name` to `PersonDto.fullName`. */ - @Deprecated("This function is unnecessarily limiting.", replaceWith = ReplaceWith("this fromValue value")) - protected infix fun KProperty.fromConstant(value: TO_TYPE): Unit = + public infix fun KProperty.fromProperty(source: KProperty): TransformableValue = generated() /** @@ -93,7 +114,7 @@ public abstract class ObjectMappie : Mappie() { * ``` * will generate an explicit mapping, setting constructor parameter `Person.name` to `"John Doe"`. */ - protected infix fun KProperty.fromValue(value: TO_TYPE): Unit = + public infix fun KProperty.fromValue(value: TO_TYPE): Unit = generated() /** @@ -106,7 +127,7 @@ public abstract class ObjectMappie : Mappie() { * will generate an explicit mapping, setting constructor parameter `Person.name` to `"John Doe (full)"`, * assuming `personDto.fullName == "John Doe"`. */ - protected infix fun KProperty.fromExpression(function: (FROM) -> FROM_TYPE): Unit = + public infix fun KProperty.fromExpression(function: (FROM) -> FROM_TYPE): Unit = generated() /** @@ -118,6 +139,6 @@ public abstract class ObjectMappie : Mappie() { * ``` * will generate an explicit mapping, setting constructor parameter `name` to `PersonDto.fullName`. */ - protected fun parameter(name: String): KProperty1 = + public fun parameter(name: String): KProperty<*> = generated() } \ No newline at end of file diff --git a/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie2.kt b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie2.kt new file mode 100644 index 00000000..ba2e7615 --- /dev/null +++ b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie2.kt @@ -0,0 +1,124 @@ +@file:Suppress("UNUSED_PARAMETER", "SameParameterValue") + +package tech.mappie.api + +import kotlin.reflect.KProperty + +/** + * Base class for object mappers. See the [documentation](https://mappie.tech/object-mapping/object-mapping-overview/) + * for a complete overview of how to generate object mappers. + * + * For example + * ```kotlin + * class PersonMapper : ObjectMappie() { + * override fun map(from: PersonDto) = mapping() + * } + * ``` + * will generate a mapper from `PersonDto` to `Person`, assuming both `PersonDto` and `Person` have a resolvable mapping. + * + * @param FROM the source type to map from. + * @param TO the target type to map to. + */ +public abstract class ObjectMappie2 : Mappie { + + /** + * A mapper for [List] to be used in [TransformableValue.via]. + */ + public val forList: ListMappie get() = + error("The mapper forList should only be used in the context of 'via'. Use mapList instead.") + + /** + * A mapper for [Set] to be used in [TransformableValue.via]. + */ + public val forSet: SetMappie get() = + error("The mapper forSet should only be used in the context of 'via'. Use mapSet instead.") + + /** + * Map [from] to an instance of [TO]. + * + * @param from the source value. + * @return [from] mapped to an instance of [TO]. + */ + public open fun map(first: FROM1, second: FROM2): TO = generated() + + /** + * Map nullable [from] to an instance of [TO]. + * + * @param from the source value. + * @return [from] mapped to an instance of [TO]. + */ + public fun mapNullable(first: FROM1?, second: FROM2?): TO? = + if (first == null || second == null) null else map(first, second) + + /** + * Mapping function which instructs Mappie to generate code for this implementation. + * + * @param builder the configuration for the generation of this mapping. + * @return An instance of the mapped value at runtime. + */ + protected fun mapping(builder: ObjectMappingConstructor2.() -> Unit = { }): TO = generated() +} + +public class ObjectMappingConstructor2 { + + /** + * Alias for the target type [TO] to simply property references. + * + * For example, suppose we are constructing a mapper with target type `Person` + * ```kotlin + * to::name fromProperty PersonDto::fullName + * ``` + * is equivalent to `Person::name fromProperty PersonDto::fullName`. + */ + public val to: TO + get() = error("The to property should only be used in the context of `to::property fromX y`.") + + /** + * 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`. + */ + public infix fun KProperty.fromProperty(source: KProperty): TransformableValue = + generated() + + /** + * Explicitly construct a mapping to [TO] from constant source [value]. + * + * For example + * ```kotlin + * Person::name fromConstant "John Doe" + * ``` + * will generate an explicit mapping, setting constructor parameter `Person.name` to `"John Doe"`. + */ + @Deprecated("This function is unnecessarily limiting.", replaceWith = ReplaceWith("this fromValue value")) + public infix fun KProperty.fromConstant(value: TO_TYPE): Unit = + generated() + + /** + * Explicitly construct a mapping to [TO] from a value source [value]. + * + * For example + * ```kotlin + * Person::name fromValue "John Doe" + * ``` + * will generate an explicit mapping, setting constructor parameter `Person.name` to `"John Doe"`. + */ + public infix fun KProperty.fromValue(value: TO_TYPE): Unit = + generated() + + /** + * Reference a constructor parameter in lieu of a property reference, if it not exists as a property. + * + * For example + * ```kotlin + * parameter("name") fromProperty PersonDto::fullName + * ``` + * will generate an explicit mapping, setting constructor parameter `name` to `PersonDto.fullName`. + */ + public fun parameter(name: String): KProperty<*> = + generated() +} \ No newline at end of file diff --git a/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie3.kt b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie3.kt new file mode 100644 index 00000000..19321588 --- /dev/null +++ b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie3.kt @@ -0,0 +1,124 @@ +@file:Suppress("UNUSED_PARAMETER", "SameParameterValue") + +package tech.mappie.api + +import kotlin.reflect.KProperty + +/** + * Base class for object mappers. See the [documentation](https://mappie.tech/object-mapping/object-mapping-overview/) + * for a complete overview of how to generate object mappers. + * + * For example + * ```kotlin + * class PersonMapper : ObjectMappie() { + * override fun map(from: PersonDto) = mapping() + * } + * ``` + * will generate a mapper from `PersonDto` to `Person`, assuming both `PersonDto` and `Person` have a resolvable mapping. + * + * @param FROM the source type to map from. + * @param TO the target type to map to. + */ +public abstract class ObjectMappie3 : Mappie { + + /** + * A mapper for [List] to be used in [TransformableValue.via]. + */ + public val forList: ListMappie get() = + error("The mapper forList should only be used in the context of 'via'. Use mapList instead.") + + /** + * A mapper for [Set] to be used in [TransformableValue.via]. + */ + public val forSet: SetMappie get() = + error("The mapper forSet should only be used in the context of 'via'. Use mapSet instead.") + + /** + * Map [from] to an instance of [TO]. + * + * @param from the source value. + * @return [from] mapped to an instance of [TO]. + */ + public open fun map(first: FROM1, second: FROM2, third: FROM3): TO = generated() + + /** + * Map nullable [from] to an instance of [TO]. + * + * @param from the source value. + * @return [from] mapped to an instance of [TO]. + */ + public fun mapNullable(first: FROM1?, second: FROM2?, third: FROM3?): TO? = + if (first == null || second == null || third == null) null else map(first, second, third) + + /** + * Mapping function which instructs Mappie to generate code for this implementation. + * + * @param builder the configuration for the generation of this mapping. + * @return An instance of the mapped value at runtime. + */ + protected fun mapping(builder: ObjectMappingConstructor2.() -> Unit = { }): TO = generated() +} + +public class ObjectMappingConstructor3 { + + /** + * Alias for the target type [TO] to simply property references. + * + * For example, suppose we are constructing a mapper with target type `Person` + * ```kotlin + * to::name fromProperty PersonDto::fullName + * ``` + * is equivalent to `Person::name fromProperty PersonDto::fullName`. + */ + public val to: TO + get() = error("The to property should only be used in the context of `to::property fromX y`.") + + /** + * 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`. + */ + public infix fun KProperty.fromProperty(source: KProperty): TransformableValue = + generated() + + /** + * Explicitly construct a mapping to [TO] from constant source [value]. + * + * For example + * ```kotlin + * Person::name fromConstant "John Doe" + * ``` + * will generate an explicit mapping, setting constructor parameter `Person.name` to `"John Doe"`. + */ + @Deprecated("This function is unnecessarily limiting.", replaceWith = ReplaceWith("this fromValue value")) + public infix fun KProperty.fromConstant(value: TO_TYPE): Unit = + generated() + + /** + * Explicitly construct a mapping to [TO] from a value source [value]. + * + * For example + * ```kotlin + * Person::name fromValue "John Doe" + * ``` + * will generate an explicit mapping, setting constructor parameter `Person.name` to `"John Doe"`. + */ + public infix fun KProperty.fromValue(value: TO_TYPE): Unit = + generated() + + /** + * Reference a constructor parameter in lieu of a property reference, if it not exists as a property. + * + * For example + * ```kotlin + * parameter("name") fromProperty PersonDto::fullName + * ``` + * will generate an explicit mapping, setting constructor parameter `name` to `PersonDto.fullName`. + */ + public fun parameter(name: String): KProperty<*> = + generated() +} \ No newline at end of file diff --git a/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie4.kt b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie4.kt new file mode 100644 index 00000000..6e0c72bb --- /dev/null +++ b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie4.kt @@ -0,0 +1,124 @@ +@file:Suppress("UNUSED_PARAMETER", "SameParameterValue") + +package tech.mappie.api + +import kotlin.reflect.KProperty + +/** + * Base class for object mappers. See the [documentation](https://mappie.tech/object-mapping/object-mapping-overview/) + * for a complete overview of how to generate object mappers. + * + * For example + * ```kotlin + * class PersonMapper : ObjectMappie() { + * override fun map(from: PersonDto) = mapping() + * } + * ``` + * will generate a mapper from `PersonDto` to `Person`, assuming both `PersonDto` and `Person` have a resolvable mapping. + * + * @param FROM the source type to map from. + * @param TO the target type to map to. + */ +public abstract class ObjectMappie4 : Mappie { + + /** + * A mapper for [List] to be used in [TransformableValue.via]. + */ + public val forList: ListMappie get() = + error("The mapper forList should only be used in the context of 'via'. Use mapList instead.") + + /** + * A mapper for [Set] to be used in [TransformableValue.via]. + */ + public val forSet: SetMappie get() = + error("The mapper forSet should only be used in the context of 'via'. Use mapSet instead.") + + /** + * Map [from] to an instance of [TO]. + * + * @param from the source value. + * @return [from] mapped to an instance of [TO]. + */ + public open fun map(first: FROM1, second: FROM2, third: FROM3, fourth: FROM4): TO = generated() + + /** + * Map nullable [from] to an instance of [TO]. + * + * @param from the source value. + * @return [from] mapped to an instance of [TO]. + */ + public fun mapNullable(first: FROM1?, second: FROM2?, third: FROM3?, fourth: FROM4?): TO? = + if (first == null || second == null || third == null || fourth == null ) null else map(first, second, third, fourth) + + /** + * Mapping function which instructs Mappie to generate code for this implementation. + * + * @param builder the configuration for the generation of this mapping. + * @return An instance of the mapped value at runtime. + */ + protected fun mapping(builder: ObjectMappingConstructor2.() -> Unit = { }): TO = generated() +} + +public class ObjectMappingConstructor4 { + + /** + * Alias for the target type [TO] to simply property references. + * + * For example, suppose we are constructing a mapper with target type `Person` + * ```kotlin + * to::name fromProperty PersonDto::fullName + * ``` + * is equivalent to `Person::name fromProperty PersonDto::fullName`. + */ + public val to: TO + get() = error("The to property should only be used in the context of `to::property fromX y`.") + + /** + * 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`. + */ + public infix fun KProperty.fromProperty(source: KProperty): TransformableValue = + generated() + + /** + * Explicitly construct a mapping to [TO] from constant source [value]. + * + * For example + * ```kotlin + * Person::name fromConstant "John Doe" + * ``` + * will generate an explicit mapping, setting constructor parameter `Person.name` to `"John Doe"`. + */ + @Deprecated("This function is unnecessarily limiting.", replaceWith = ReplaceWith("this fromValue value")) + public infix fun KProperty.fromConstant(value: TO_TYPE): Unit = + generated() + + /** + * Explicitly construct a mapping to [TO] from a value source [value]. + * + * For example + * ```kotlin + * Person::name fromValue "John Doe" + * ``` + * will generate an explicit mapping, setting constructor parameter `Person.name` to `"John Doe"`. + */ + public infix fun KProperty.fromValue(value: TO_TYPE): Unit = + generated() + + /** + * Reference a constructor parameter in lieu of a property reference, if it not exists as a property. + * + * For example + * ```kotlin + * parameter("name") fromProperty PersonDto::fullName + * ``` + * will generate an explicit mapping, setting constructor parameter `name` to `PersonDto.fullName`. + */ + public fun parameter(name: String): KProperty<*> = + generated() +} \ No newline at end of file diff --git a/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie5.kt b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie5.kt new file mode 100644 index 00000000..c2c89e88 --- /dev/null +++ b/mappie-api/src/commonMain/kotlin/tech/mappie/api/ObjectMappie5.kt @@ -0,0 +1,124 @@ +@file:Suppress("UNUSED_PARAMETER", "SameParameterValue") + +package tech.mappie.api + +import kotlin.reflect.KProperty + +/** + * Base class for object mappers. See the [documentation](https://mappie.tech/object-mapping/object-mapping-overview/) + * for a complete overview of how to generate object mappers. + * + * For example + * ```kotlin + * class PersonMapper : ObjectMappie() { + * override fun map(from: PersonDto) = mapping() + * } + * ``` + * will generate a mapper from `PersonDto` to `Person`, assuming both `PersonDto` and `Person` have a resolvable mapping. + * + * @param FROM the source type to map from. + * @param TO the target type to map to. + */ +public abstract class ObjectMappie5 : Mappie { + + /** + * A mapper for [List] to be used in [TransformableValue.via]. + */ + public val forList: ListMappie get() = + error("The mapper forList should only be used in the context of 'via'. Use mapList instead.") + + /** + * A mapper for [Set] to be used in [TransformableValue.via]. + */ + public val forSet: SetMappie get() = + error("The mapper forSet should only be used in the context of 'via'. Use mapSet instead.") + + /** + * Map [from] to an instance of [TO]. + * + * @param from the source value. + * @return [from] mapped to an instance of [TO]. + */ + public open fun map(first: FROM1, second: FROM2, third: FROM3, fourth: FROM4, fifth: FROM5): TO = generated() + + /** + * Map nullable [from] to an instance of [TO]. + * + * @param from the source value. + * @return [from] mapped to an instance of [TO]. + */ + public fun mapNullable(first: FROM1?, second: FROM2?, third: FROM3?, fourth: FROM4?, fifth: FROM5?): TO? = + if (first == null || second == null || third == null || fourth == null || fifth == null) null else map(first, second, third, fourth, fifth) + + /** + * Mapping function which instructs Mappie to generate code for this implementation. + * + * @param builder the configuration for the generation of this mapping. + * @return An instance of the mapped value at runtime. + */ + protected fun mapping(builder: ObjectMappingConstructor2.() -> Unit = { }): TO = generated() +} + +public class ObjectMappingConstructor5 { + + /** + * Alias for the target type [TO] to simply property references. + * + * For example, suppose we are constructing a mapper with target type `Person` + * ```kotlin + * to::name fromProperty PersonDto::fullName + * ``` + * is equivalent to `Person::name fromProperty PersonDto::fullName`. + */ + public val to: TO + get() = error("The to property should only be used in the context of `to::property fromX y`.") + + /** + * 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`. + */ + public infix fun KProperty.fromProperty(source: KProperty): TransformableValue = + generated() + + /** + * Explicitly construct a mapping to [TO] from constant source [value]. + * + * For example + * ```kotlin + * Person::name fromConstant "John Doe" + * ``` + * will generate an explicit mapping, setting constructor parameter `Person.name` to `"John Doe"`. + */ + @Deprecated("This function is unnecessarily limiting.", replaceWith = ReplaceWith("this fromValue value")) + public infix fun KProperty.fromConstant(value: TO_TYPE): Unit = + generated() + + /** + * Explicitly construct a mapping to [TO] from a value source [value]. + * + * For example + * ```kotlin + * Person::name fromValue "John Doe" + * ``` + * will generate an explicit mapping, setting constructor parameter `Person.name` to `"John Doe"`. + */ + public infix fun KProperty.fromValue(value: TO_TYPE): Unit = + generated() + + /** + * Reference a constructor parameter in lieu of a property reference, if it not exists as a property. + * + * For example + * ```kotlin + * parameter("name") fromProperty PersonDto::fullName + * ``` + * will generate an explicit mapping, setting constructor parameter `name` to `PersonDto.fullName`. + */ + public fun parameter(name: String): KProperty<*> = + generated() +} \ No newline at end of file diff --git a/mappie-api/src/commonMain/kotlin/tech/mappie/api/SetMappie.kt b/mappie-api/src/commonMain/kotlin/tech/mappie/api/SetMappie.kt index 556edb6b..179aed93 100644 --- a/mappie-api/src/commonMain/kotlin/tech/mappie/api/SetMappie.kt +++ b/mappie-api/src/commonMain/kotlin/tech/mappie/api/SetMappie.kt @@ -3,4 +3,4 @@ package tech.mappie.api /** * Base mapper class for set mappers. Cannot be instantiated, but can be created by using the field [ObjectMappie.forSet]. */ -public sealed class SetMappie : Mappie, Set>() \ No newline at end of file +public sealed class SetMappie : Mappie> \ No newline at end of file diff --git a/mappie-api/src/commonMain/kotlin/tech/mappie/api/TransformableValue.kt b/mappie-api/src/commonMain/kotlin/tech/mappie/api/TransformableValue.kt index 7ad3fc8f..2e34ea12 100644 --- a/mappie-api/src/commonMain/kotlin/tech/mappie/api/TransformableValue.kt +++ b/mappie-api/src/commonMain/kotlin/tech/mappie/api/TransformableValue.kt @@ -27,5 +27,5 @@ public class TransformableValue { * * @param mapper the mapper to transform the value with. */ - public infix fun > via(mapper: M): M = generated() + public infix fun > via(mapper: M): M = generated() }