From b97874a3eecc0aeb345bac8ce575a7347c004217 Mon Sep 17 00:00:00 2001 From: stefankoppier Date: Mon, 17 Jun 2024 16:22:04 +0200 Subject: [PATCH] Renamed DataClassMapper to ObjectMapper and added support for constructor parameters without a backing field --- .../{DataClassMapper.kt => ObjectMapper.kt} | 11 +++---- .../io/github/mappie/resolving/Identifiers.kt | 2 ++ .../mappie/resolving/MappingResolver.kt | 2 ++ .../classes/ObjectMappingBodyCollector.kt | 31 ++++++++++++++----- .../classes/ObjectMappingsConstructor.kt | 8 +++-- .../mappie/validation/MappingValidation.kt | 5 +++ .../src/main/kotlin/testing/ClassMapper.kt | 4 +-- .../ConstructorParameterWhichIsNotAField.kt | 26 ++++++++++++++++ .../src/main/kotlin/testing/DefaultValue.kt | 4 +-- .../main/kotlin/testing/ExpressionMapper.kt | 4 +-- testing/src/main/kotlin/testing/GameMapper.kt | 4 +-- testing/src/main/kotlin/testing/ListMapper.kt | 6 ++-- .../kotlin/testing/MultipleConstructor.kt | 6 ++-- .../src/main/kotlin/testing/NestedMapper.kt | 6 ++-- .../src/main/kotlin/testing/PersonMapper.kt | 6 ++-- .../main/kotlin/testing/PrimitiveMapper.kt | 6 ++-- .../main/kotlin/testing/PrivateConstructor.kt | 4 +-- ...ructParameterWhichIsNotAFieldMapperTest.kt | 16 ++++++++++ .../posts/object-mapping/posts/overview.md | 8 ++--- 19 files changed, 115 insertions(+), 44 deletions(-) rename api/src/main/kotlin/io/github/mappie/api/{DataClassMapper.kt => ObjectMapper.kt} (65%) create mode 100644 testing/src/main/kotlin/testing/ConstructorParameterWhichIsNotAField.kt create mode 100644 testing/src/test/kotlin/testing/ConstructParameterWhichIsNotAFieldMapperTest.kt diff --git a/api/src/main/kotlin/io/github/mappie/api/DataClassMapper.kt b/api/src/main/kotlin/io/github/mappie/api/ObjectMapper.kt similarity index 65% rename from api/src/main/kotlin/io/github/mappie/api/DataClassMapper.kt rename to api/src/main/kotlin/io/github/mappie/api/ObjectMapper.kt index c37c2092..4b271050 100644 --- a/api/src/main/kotlin/io/github/mappie/api/DataClassMapper.kt +++ b/api/src/main/kotlin/io/github/mappie/api/ObjectMapper.kt @@ -9,8 +9,7 @@ abstract class CollectionMapper : Mapper, List>() { abstract infix fun filteredBy(predicate: (FROM) -> Boolean): CollectionMapper } -// TODO: choose between DataClassMapper and ClassMapper vs just ObjectMapper -abstract class DataClassMapper : Mapper() { +abstract class ObjectMapper : Mapper() { val forList: CollectionMapper get() = error("The mapper forList should only be used in the context of 'via'. Use mapList instead.") @@ -18,14 +17,14 @@ abstract class DataClassMapper : Mapper() { protected infix fun KProperty1.mappedFromProperty(source: KProperty1): TransformableValue = generated() - protected infix fun KProperty1.mappedFromConstant(value: TO_TYPE): DataClassMapper = + protected infix fun KProperty1.mappedFromConstant(value: TO_TYPE): ObjectMapper = generated() - protected infix fun KProperty1.mappedFromExpression(function: (FROM) -> TO_TYPE): DataClassMapper = + protected infix fun KProperty1.mappedFromExpression(function: (FROM) -> TO_TYPE): ObjectMapper = generated() - fun expression(function: (FROM) -> TO): Mapper = + protected fun parameter(name: String): KProperty1 = generated() - protected fun result(source: TO): DataClassMapper = generated() + protected fun result(source: TO): ObjectMapper = generated() } \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/Identifiers.kt b/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/Identifiers.kt index 7a81261a..f9ae5c8c 100644 --- a/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/Identifiers.kt +++ b/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/Identifiers.kt @@ -18,6 +18,8 @@ val IDENTIFIER_MAPPED_FROM_EXPRESSION = Name.identifier("mappedFromExpression") val IDENTIFIER_RESULT = Name.identifier("result") +val IDENTIFIER_PARAMETER = Name.identifier("parameter") + val IDENTIFIER_TRANFORM = Name.identifier("transform") val IDENTIFIER_VIA = Name.identifier("via") diff --git a/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/MappingResolver.kt b/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/MappingResolver.kt index 6ce4bea1..e04c93f8 100644 --- a/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/MappingResolver.kt +++ b/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/MappingResolver.kt @@ -15,6 +15,7 @@ import org.jetbrains.kotlin.ir.types.getClass import org.jetbrains.kotlin.ir.types.isPrimitiveType import org.jetbrains.kotlin.ir.types.isString import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.name.Name sealed interface Mapping @@ -23,6 +24,7 @@ data class ConstructorCallMapping( val sourceType: IrType, val symbol: IrConstructorSymbol, val mappings: Map>, + val unknowns: List>, ) : Mapping data class EnumMapping( diff --git a/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/classes/ObjectMappingBodyCollector.kt b/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/classes/ObjectMappingBodyCollector.kt index 7f30622a..c5666f4d 100644 --- a/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/classes/ObjectMappingBodyCollector.kt +++ b/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/classes/ObjectMappingBodyCollector.kt @@ -2,7 +2,7 @@ package io.github.mappie.resolving.classes import io.github.mappie.BaseVisitor import io.github.mappie.MappieIrRegistrar.Companion.context -import io.github.mappie.api.DataClassMapper +import io.github.mappie.api.ObjectMapper import io.github.mappie.resolving.* import io.github.mappie.util.getterName import io.github.mappie.util.irGet @@ -49,26 +49,26 @@ 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!!, dispatchReceiverSymbol), Unit)?.let { acc.explicit(it) } ?: it } } ?: data } } private class ObjectBodyStatementCollector( - file: IrFileEntry?, + file: IrFileEntry, private val dispatchReceiverSymbol: IrValueSymbol, ) : BaseVisitor?, Unit>(file) { override fun visitCall(expression: IrCall, data: Unit): Pair? { return when (expression.symbol.owner.name) { IDENTIFIER_MAPPED_FROM_PROPERTY, IDENTIFIER_MAPPED_FROM_CONSTANT -> { - val target = expression.extensionReceiver!!.accept(TargetValueCollector(), data) + val target = expression.extensionReceiver!!.accept(TargetValueCollector(file!!), data) val source = expression.valueArguments.first()!!.accept(SourceValueCollector(dispatchReceiverSymbol), Unit) target to source } IDENTIFIER_MAPPED_FROM_EXPRESSION -> { - val target = expression.extensionReceiver!!.accept(TargetValueCollector(), data) + val target = expression.extensionReceiver!!.accept(TargetValueCollector(file!!), data) val source = expression.valueArguments.first() as IrFunctionExpression target to ExpressionSource( @@ -135,7 +135,7 @@ private class MapperReferenceCollector : BaseVisitor require(expression.origin == IrStatementOrigin.GET_PROPERTY) return when (expression.symbol.owner.name) { - getterName(DataClassMapper<*, *>::forList.name) -> { + getterName(ObjectMapper<*, *>::forList.name) -> { val mapper = expression.symbol.owner.parent as IrClassImpl val function = mapper.functions @@ -201,9 +201,26 @@ private class SourceValueCollector( } } -private class TargetValueCollector : BaseVisitor() { +private class TargetValueCollector(file: IrFileEntry) : BaseVisitor(file) { override fun visitPropertyReference(expression: IrPropertyReference, data: Unit): Name { return expression.symbol.owner.name } + + override fun visitCall(expression: IrCall, data: Unit): Name { + return when (expression.symbol.owner.name) { + IDENTIFIER_PARAMETER -> { + val value = expression.valueArguments.first()!! + return if (value is IrConst<*>) { + Name.identifier(value.value as String) + } else { + logError("Parameter name must be a String literal", file?.let { location(it, expression) }) + throw AssertionError() + } + } + else -> { + super.visitCall(expression, data) + } + } + } } \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/classes/ObjectMappingsConstructor.kt b/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/classes/ObjectMappingsConstructor.kt index c87363e1..ba36061f 100644 --- a/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/classes/ObjectMappingsConstructor.kt +++ b/compiler-plugin/src/main/kotlin/io/github/mappie/resolving/classes/ObjectMappingsConstructor.kt @@ -18,7 +18,7 @@ class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParam var constructor: IrConstructor? = null - private val targets + val targets get() = constructor?.valueParameters ?: emptyList() fun construct(): ConstructorCallMapping { @@ -41,11 +41,15 @@ class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParam } } + val unknowns = explicit + .filter { it.first !in mappings.map { it.key.name } } + return ConstructorCallMapping( targetType = targetType, sourceType = source.type, symbol = constructor!!.symbol, - mappings = mappings + mappings = mappings, + unknowns = unknowns, ) } diff --git a/compiler-plugin/src/main/kotlin/io/github/mappie/validation/MappingValidation.kt b/compiler-plugin/src/main/kotlin/io/github/mappie/validation/MappingValidation.kt index 9d0d745c..875c50bd 100644 --- a/compiler-plugin/src/main/kotlin/io/github/mappie/validation/MappingValidation.kt +++ b/compiler-plugin/src/main/kotlin/io/github/mappie/validation/MappingValidation.kt @@ -60,6 +60,11 @@ interface MappingValidation { ) } ) + addAll( + mapping.unknowns + .map { Problem.error("Parameter ${it.first.asString()} does not occur as a parameter in constructor") } + ) + // TODO: make optional via configuration if (!mapping.symbol.owner.visibility.isPublicAPI) { add(Problem.error("Constructor is not public", location(mapping.symbol.owner))) diff --git a/testing/src/main/kotlin/testing/ClassMapper.kt b/testing/src/main/kotlin/testing/ClassMapper.kt index 90d52fc3..29b403b0 100644 --- a/testing/src/main/kotlin/testing/ClassMapper.kt +++ b/testing/src/main/kotlin/testing/ClassMapper.kt @@ -1,6 +1,6 @@ package testing -import io.github.mappie.api.DataClassMapper +import io.github.mappie.api.ObjectMapper class Class( private val field: String, @@ -22,7 +22,7 @@ class ClassDto( argument.hashCode() } -object ClassMapper : DataClassMapper() { +object ClassMapper : ObjectMapper() { override fun map(from: Class) = mapping { ClassDto::argument mappedFromConstant 1 } diff --git a/testing/src/main/kotlin/testing/ConstructorParameterWhichIsNotAField.kt b/testing/src/main/kotlin/testing/ConstructorParameterWhichIsNotAField.kt new file mode 100644 index 00000000..25fae806 --- /dev/null +++ b/testing/src/main/kotlin/testing/ConstructorParameterWhichIsNotAField.kt @@ -0,0 +1,26 @@ +package testing + +import io.github.mappie.api.ObjectMapper + +class ConstructorParameterWhichIsNotAField( + val parameter: String +) + +class ConstructorParameterWhichIsNotAFieldDto( + value: String +) { + val property = value + + override fun equals(other: Any?): Boolean { + if (other is ConstructorParameterWhichIsNotAFieldDto) { + return property == other.property + } + return false + } +} + +object ConstructorParameterWhichIsNotAFieldMapper : ObjectMapper() { + override fun map(from: ConstructorParameterWhichIsNotAField): ConstructorParameterWhichIsNotAFieldDto = mapping { + parameter("value") mappedFromProperty ConstructorParameterWhichIsNotAField::parameter + } +} diff --git a/testing/src/main/kotlin/testing/DefaultValue.kt b/testing/src/main/kotlin/testing/DefaultValue.kt index a71987ba..b0185029 100644 --- a/testing/src/main/kotlin/testing/DefaultValue.kt +++ b/testing/src/main/kotlin/testing/DefaultValue.kt @@ -1,11 +1,11 @@ package testing -import io.github.mappie.api.DataClassMapper +import io.github.mappie.api.ObjectMapper data class DefaultValue(val string: String) data class DefaultValueDto(val string: String, val int: Int = 10) -object DefaultValueMapper : DataClassMapper() { +object DefaultValueMapper : ObjectMapper() { override fun map(from: DefaultValue): DefaultValueDto = mapping() } diff --git a/testing/src/main/kotlin/testing/ExpressionMapper.kt b/testing/src/main/kotlin/testing/ExpressionMapper.kt index 3b09bac3..03262480 100644 --- a/testing/src/main/kotlin/testing/ExpressionMapper.kt +++ b/testing/src/main/kotlin/testing/ExpressionMapper.kt @@ -1,8 +1,8 @@ package testing -import io.github.mappie.api.DataClassMapper +import io.github.mappie.api.ObjectMapper -object ExpressionMapper : DataClassMapper() { +object ExpressionMapper : ObjectMapper() { override fun map(from: Person): PersonDto = mapping { PersonDto::age mappedFromConstant 10 PersonDto::description mappedFromExpression { it::class.simpleName!! } diff --git a/testing/src/main/kotlin/testing/GameMapper.kt b/testing/src/main/kotlin/testing/GameMapper.kt index a8ed9465..6ed49c5d 100644 --- a/testing/src/main/kotlin/testing/GameMapper.kt +++ b/testing/src/main/kotlin/testing/GameMapper.kt @@ -1,6 +1,6 @@ package testing -import io.github.mappie.api.DataClassMapper +import io.github.mappie.api.ObjectMapper data class Game( val name: String, @@ -12,7 +12,7 @@ data class GameDto( val description: String, ) -object GameMapper : DataClassMapper() { +object GameMapper : ObjectMapper() { override fun map(from: Game): GameDto = mapping { GameDto::description mappedFromProperty Game::description transform { it ?: "default" } } diff --git a/testing/src/main/kotlin/testing/ListMapper.kt b/testing/src/main/kotlin/testing/ListMapper.kt index e76445c3..7fbad4c8 100644 --- a/testing/src/main/kotlin/testing/ListMapper.kt +++ b/testing/src/main/kotlin/testing/ListMapper.kt @@ -1,6 +1,6 @@ package testing -import io.github.mappie.api.DataClassMapper +import io.github.mappie.api.ObjectMapper data class Book(val pages: List) @@ -8,13 +8,13 @@ data class Page(val text: String) data class BookDto(val pages: List) -object BookMapper : DataClassMapper() { +object BookMapper : ObjectMapper() { override fun map(from: Book): BookDto = mapping { BookDto::pages mappedFromProperty Book::pages via PageMapper.forList } } -object PageMapper : DataClassMapper() { +object PageMapper : ObjectMapper() { override fun map(from: Page): String = mapping { result(from.text) } diff --git a/testing/src/main/kotlin/testing/MultipleConstructor.kt b/testing/src/main/kotlin/testing/MultipleConstructor.kt index 19e87a5f..0f9c2a58 100644 --- a/testing/src/main/kotlin/testing/MultipleConstructor.kt +++ b/testing/src/main/kotlin/testing/MultipleConstructor.kt @@ -1,6 +1,6 @@ package testing -import io.github.mappie.api.DataClassMapper +import io.github.mappie.api.ObjectMapper data class MultipleConstructors(val string: String) @@ -8,11 +8,11 @@ data class MultipleConstructorsDto constructor(val string: String, val int: Int) constructor(string: String) : this(string, 1) } -object MultipleConstructorsWithoutIntMapper : DataClassMapper() { +object MultipleConstructorsWithoutIntMapper : ObjectMapper() { override fun map(from: MultipleConstructors): MultipleConstructorsDto = mapping() } -object MultipleConstructorsWitIntMapper : DataClassMapper() { +object MultipleConstructorsWitIntMapper : ObjectMapper() { override fun map(from: MultipleConstructors): MultipleConstructorsDto = mapping { MultipleConstructorsDto::int mappedFromConstant 2 } diff --git a/testing/src/main/kotlin/testing/NestedMapper.kt b/testing/src/main/kotlin/testing/NestedMapper.kt index 570cbcba..fe9c4bd9 100644 --- a/testing/src/main/kotlin/testing/NestedMapper.kt +++ b/testing/src/main/kotlin/testing/NestedMapper.kt @@ -1,6 +1,6 @@ package testing -import io.github.mappie.api.DataClassMapper +import io.github.mappie.api.ObjectMapper import io.github.mappie.api.EnumMapper enum class BooleanEnum { @@ -21,14 +21,14 @@ data class ThingDto(val inner: ThangDto, val boolean: BooleanDto) data class ThangDto(val description: String) -object ThingMapper : DataClassMapper() { +object ThingMapper : ObjectMapper() { override fun map(from: Thing): ThingDto = mapping { ThingDto::inner mappedFromProperty Thing::inner via ThangMapper ThingDto::boolean mappedFromProperty Thing::boolean via BooleanMapper() } } -object ThangMapper : DataClassMapper() { +object ThangMapper : ObjectMapper() { override fun map(from: Thang): ThangDto = mapping() } diff --git a/testing/src/main/kotlin/testing/PersonMapper.kt b/testing/src/main/kotlin/testing/PersonMapper.kt index a16a944f..0de51c39 100644 --- a/testing/src/main/kotlin/testing/PersonMapper.kt +++ b/testing/src/main/kotlin/testing/PersonMapper.kt @@ -1,13 +1,13 @@ package testing -import io.github.mappie.api.DataClassMapper +import io.github.mappie.api.ObjectMapper import io.github.mappie.api.Mapper data class Person(val name: String) data class PersonDto(val name: String, val description: String, val age: Int) -object PersonMapper : DataClassMapper() { +object PersonMapper : ObjectMapper() { override fun map(from: Person): PersonDto = mapping { PersonDto::description mappedFromProperty Person::name @@ -23,7 +23,7 @@ object ConstructorCallPersonMapper : Mapper() { } } -object TransformingPersonMapper : DataClassMapper() { +object TransformingPersonMapper : ObjectMapper() { override fun map(from: Person): PersonDto = mapping { PersonDto::description mappedFromProperty Person::name transform { "$it Surname" } diff --git a/testing/src/main/kotlin/testing/PrimitiveMapper.kt b/testing/src/main/kotlin/testing/PrimitiveMapper.kt index 95283025..b6519135 100644 --- a/testing/src/main/kotlin/testing/PrimitiveMapper.kt +++ b/testing/src/main/kotlin/testing/PrimitiveMapper.kt @@ -1,15 +1,15 @@ package testing -import io.github.mappie.api.DataClassMapper +import io.github.mappie.api.ObjectMapper -object IntMapper : DataClassMapper() { +object IntMapper : ObjectMapper() { override fun map(from: Int) = mapping { result(from.toString()) } } -object StringMapper : DataClassMapper() { +object StringMapper : ObjectMapper() { override fun map(from: String) = mapping { result(from.toInt()) diff --git a/testing/src/main/kotlin/testing/PrivateConstructor.kt b/testing/src/main/kotlin/testing/PrivateConstructor.kt index a7243a93..75d0e1ab 100644 --- a/testing/src/main/kotlin/testing/PrivateConstructor.kt +++ b/testing/src/main/kotlin/testing/PrivateConstructor.kt @@ -1,6 +1,6 @@ package testing -import io.github.mappie.api.DataClassMapper +import io.github.mappie.api.ObjectMapper data class PrivateConstructor(val string: String) @@ -8,7 +8,7 @@ data class PrivateConstructorDto constructor(val string: String, val int: Int) { private constructor(string: String) : this(string, 1) } -object PrivateConstructorMapper : DataClassMapper() { +object PrivateConstructorMapper : ObjectMapper() { override fun map(from: PrivateConstructor): PrivateConstructorDto = mapping { PrivateConstructorDto::int mappedFromConstant 1 } diff --git a/testing/src/test/kotlin/testing/ConstructParameterWhichIsNotAFieldMapperTest.kt b/testing/src/test/kotlin/testing/ConstructParameterWhichIsNotAFieldMapperTest.kt new file mode 100644 index 00000000..ffe9456e --- /dev/null +++ b/testing/src/test/kotlin/testing/ConstructParameterWhichIsNotAFieldMapperTest.kt @@ -0,0 +1,16 @@ +package testing + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class ConstructParameterWhichIsNotAFieldMapperTest { + + @Test + fun `map ConstructorParameterWhichIsNotAField to ConstructorParameterWhichIsNotAFieldDto via ConstructorParameterWhichIsNotAFieldMapper`() { + assertEquals( + ConstructorParameterWhichIsNotAFieldDto("test"), + ConstructorParameterWhichIsNotAFieldMapper.map(ConstructorParameterWhichIsNotAField("test")), + ) + } + +} \ No newline at end of file diff --git a/website/src/posts/object-mapping/posts/overview.md b/website/src/posts/object-mapping/posts/overview.md index 3e1b8e41..6ac28d48 100644 --- a/website/src/posts/object-mapping/posts/overview.md +++ b/website/src/posts/object-mapping/posts/overview.md @@ -7,7 +7,7 @@ eleventyNavigation: order: 3 --- -Mappie supports creating data object mappers via the base class `DataClassMapper`. +Mappie supports creating object mappers via the base class `ObjectMapper`. Suppose we have a data class `Person` ```kotlin @@ -19,7 +19,7 @@ data class PersonDto(val name: String, val age: Int) ``` The fields of `Person` match those of `PersonDto`, and as such, not mappings have to be defined, for example ```kotlin -object PersonMapper : DataClassMapper() { +object PersonMapper : ObjectMapper() { override fun map(from: Person): PersonDto = mapping() } ``` @@ -42,7 +42,7 @@ This can be addressed in multiple ways. A possibility is to map `description` from another property, e.g. via `name` ```kotlin -object PersonMapper : DataClassMapper() { +object PersonMapper : ObjectMapper() { override fun map(from: Person): PersonDto = mapping { PersonDto::description mappedFromProperty Person::name } @@ -53,7 +53,7 @@ object PersonMapper : DataClassMapper() { A possibility is to map `description` from an expression, e.g. setting it to the constant `"unknown"` ```kotlin -object PersonMapper : DataClassMapper() { +object PersonMapper : ObjectMapper() { override fun map(from: Person): PersonDto = mapping { PersonDto::description mappedFromExpression { source -> "unknown" } }