Skip to content

Commit

Permalink
feat: add auto mapping capabilities
Browse files Browse the repository at this point in the history
Closes #10
  • Loading branch information
Idane committed Aug 9, 2022
1 parent 141c06a commit 9d5fecb
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import dev.krud.shapeshift.condition.MappingCondition
import dev.krud.shapeshift.decorator.MappingDecorator
import dev.krud.shapeshift.dto.ResolvedMappedField
import dev.krud.shapeshift.dto.TransformerCoordinates
import dev.krud.shapeshift.enums.AutoMappingStrategy
import dev.krud.shapeshift.resolver.MappingDefinition
import dev.krud.shapeshift.transformer.base.MappingTransformer
import dev.krud.shapeshift.util.getAutoMappings
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.jvm.javaField
Expand All @@ -32,6 +34,17 @@ class KotlinDslMappingDefinitionBuilder<RootFrom : Any, RootTo : Any>(
) {
private val fieldMappings = mutableListOf<FieldMapping<*, *>>()
private val decoratorRegistrations: MutableSet<MappingDecoratorRegistration<RootFrom, RootTo>> = mutableSetOf()
private var autoMappingStrategy: AutoMappingStrategy = AutoMappingStrategy.NONE

/**
* Enable automapping with the given strategy
*/
fun autoMap(strategy: AutoMappingStrategy) {
if (strategy == AutoMappingStrategy.NONE) {
error("Auto mapping strategy cannot be NONE")
}
autoMappingStrategy = strategy
}

/**
* Helper operator function to access sub-fields of a field.
Expand Down Expand Up @@ -137,25 +150,34 @@ class KotlinDslMappingDefinitionBuilder<RootFrom : Any, RootTo : Any>(
}

fun build(): Result {
val resolvedMappedFields = fieldMappings.map { fieldMapping ->
ResolvedMappedField(
fieldMapping.fromField.fields.map { it.javaField!! },
fieldMapping.toField.fields.map { it.javaField!! },
if (fieldMapping.transformerClazz == null) {
TransformerCoordinates.NONE
} else {
TransformerCoordinates.ofType(fieldMapping.transformerClazz!!.java)
},
fieldMapping.transformer,
fieldMapping.conditionClazz?.java,
fieldMapping.condition,
fieldMapping.mappingStrategy
)
}
.toMutableList()

resolvedMappedFields += getAutoMappings(fromClazz, toClazz, autoMappingStrategy)
.filter { autoResolvedMappedField ->
resolvedMappedFields.none {
it.mapFromCoordinates.first() == autoResolvedMappedField.mapFromCoordinates.first() || it.mapToCoordinates.first() == autoResolvedMappedField.mapToCoordinates.first()
}
}
return Result(
MappingDefinition(
fromClazz,
toClazz,
fieldMappings.map { fieldMapping ->
ResolvedMappedField(
fieldMapping.fromField.fields.map { it.javaField!! },
fieldMapping.toField.fields.map { it.javaField!! },
if (fieldMapping.transformerClazz == null) {
TransformerCoordinates.NONE
} else {
TransformerCoordinates.ofType(fieldMapping.transformerClazz!!.java)
},
fieldMapping.transformer,
fieldMapping.conditionClazz?.java,
fieldMapping.condition,
fieldMapping.mappingStrategy
)
}
resolvedMappedFields
),
decoratorRegistrations
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright KRUD 2022
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package dev.krud.shapeshift.enums

/**
* Auto mapping strategy for supported resolvers
*/
enum class AutoMappingStrategy {
/**
* Do not automap, used as a default value
*/
NONE,

/**
* Automap by name irrespective of type
*/
BY_NAME,

/**
* Automap by name and type
*/
BY_NAME_AND_TYPE
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import dev.krud.shapeshift.dto.ResolvedMappedField
import dev.krud.shapeshift.dto.TransformerCoordinates
import dev.krud.shapeshift.resolver.MappingDefinition
import dev.krud.shapeshift.resolver.MappingDefinitionResolver
import dev.krud.shapeshift.util.getAutoMappings
import dev.krud.shapeshift.util.getDeclaredFieldRecursive
import dev.krud.shapeshift.util.splitIgnoreEmpty
import java.lang.reflect.Field
Expand Down Expand Up @@ -51,9 +52,26 @@ class AnnotationMappingDefinitionResolver : MappingDefinitionResolver {
mappedField.overrideMappingStrategy
)
}
resolvedMappedFields += generateAutoMappings(fromClazz, toClazz).filter { autoResolvedMappedField ->
resolvedMappedFields.none {
it.mapFromCoordinates.first() == autoResolvedMappedField.mapFromCoordinates.first() || it.mapToCoordinates.first() == autoResolvedMappedField.mapToCoordinates.first()
}
}
return MappingDefinition(fromClazz, toClazz, resolvedMappedFields)
}

private fun generateAutoMappings(fromClazz: Class<*>, toClazz: Class<*>): List<ResolvedMappedField> {
val autoMappingAnnotations = fromClazz.getDeclaredAnnotationsByType(AutoMapping::class.java)

if (autoMappingAnnotations.isEmpty()) {
return emptyList()
}

val effectiveAnnotation = autoMappingAnnotations.firstOrNull { it.target.java == toClazz }
?: (autoMappingAnnotations.firstOrNull { it.target.java == Nothing::class.java } ?: return emptyList())
return getAutoMappings(fromClazz, toClazz, effectiveAnnotation.strategy)
}

/**
* Get true from field from path like "user.address.city" delimtied by [NODE_DELIMITER]
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright KRUD 2022
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package dev.krud.shapeshift.resolver.annotation

import dev.krud.shapeshift.enums.AutoMappingStrategy
import kotlin.reflect.KClass

@Repeatable
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
annotation class AutoMapping(val target: KClass<*> = Nothing::class, val strategy: AutoMappingStrategy)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright KRUD 2022
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package dev.krud.shapeshift.util

import dev.krud.shapeshift.dto.ResolvedMappedField
import dev.krud.shapeshift.dto.TransformerCoordinates
import dev.krud.shapeshift.enums.AutoMappingStrategy

internal fun <From, To> getAutoMappings(fromClazz: Class<From>, toClazz: Class<To>, strategy: AutoMappingStrategy): List<ResolvedMappedField> {
val resolvedMappedFields = mutableListOf<ResolvedMappedField>()
if (strategy != AutoMappingStrategy.NONE) {
val fromFields = fromClazz.getDeclaredFieldsRecursive()
val toFields = toClazz.getDeclaredFieldsRecursive()
for (fromField in fromFields) {
val toField = toFields.find {
when (strategy) {
AutoMappingStrategy.BY_NAME -> it.name == fromField.name
AutoMappingStrategy.BY_NAME_AND_TYPE -> it.name == fromField.name && it.type.kotlin.javaObjectType == fromField.type.kotlin.javaObjectType
else -> error("Unsupported auto mapping strategy")
}
} ?: continue
resolvedMappedFields += ResolvedMappedField(
listOf(fromField),
listOf(toField),
TransformerCoordinates.NONE,
null,
null,
null,
null
)
}
}
return resolvedMappedFields
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ internal fun Field.setValue(target: Any, value: Any?) {
this.set(target, value)
}

internal fun Class<*>.getDeclaredFieldsRecursive(): List<Field> {
var clazz: Class<*>? = this
val fields = mutableListOf<Field>()
while (clazz != null) {
fields += clazz.declaredFields
clazz = clazz.superclass
}

return fields
}

internal fun Class<*>.getDeclaredFieldRecursive(name: String): Field {
var clazz: Class<*>? = this
while (clazz != null) {
Expand Down
41 changes: 41 additions & 0 deletions shapeshift/src/test/kotlin/dev/krud/shapeshift/ShapeShiftTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,36 @@ internal class ShapeShiftTests {

@Nested
inner class Scenarios {
@Test
internal fun `annotation automatic mapping with implicit target`() {
val shapeShift = ShapeShiftBuilder()
.build()
val from = SameTypeAutomaticMappingFromImplicit()
val result = shapeShift.map<SameTypeAutomaticMappingFromImplicit, GenericTo>(from)
expectThat(result.long)
.isEqualTo(1L)
}

@Test
internal fun `annotation automatic mapping with explicit target`() {
val shapeShift = ShapeShiftBuilder()
.build()
val from = SameTypeAutomaticMappingFromExplicit()
val result = shapeShift.map<SameTypeAutomaticMappingFromExplicit, GenericTo>(from)
expectThat(result.long)
.isEqualTo(1L)
}

@Test
internal fun `annotation automatic mapping with explicit wrong target`() {
val shapeShift = ShapeShiftBuilder()
.build()
val from = SameTypeAutomaticMappingFromExplicitWrongTarget()
val result = shapeShift.map<SameTypeAutomaticMappingFromExplicitWrongTarget, GenericTo>(from)
expectThat(result.long)
.isNull()
}

@Test
internal fun `mapCollection with set of objects`() {
val shapeShift = ShapeShiftBuilder()
Expand Down Expand Up @@ -206,6 +236,17 @@ internal class ShapeShiftTests {
}
}

// @Test
internal fun `annotation automatic mapping with type mismatch should throw exception`() {
val shapeShift = ShapeShiftBuilder()
.excludeDefaultTransformers()
.build()
val from = NameOnlyTypeAutomaticMappingFromExplicitWrongTarget()
expectThrows<IllegalStateException> {
shapeShift.map<NameOnlyTypeAutomaticMappingFromExplicitWrongTarget, GenericTo>(from)
}
}

@Test
internal fun `mismatch between from and to types should throw exception`() {
expectThrows<IllegalStateException> {
Expand Down
32 changes: 32 additions & 0 deletions shapeshift/src/test/kotlin/dev/krud/shapeshift/_fixtures.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ package dev.krud.shapeshift

import dev.krud.shapeshift.condition.MappingCondition
import dev.krud.shapeshift.condition.MappingConditionContext
import dev.krud.shapeshift.enums.AutoMappingStrategy
import dev.krud.shapeshift.resolver.annotation.AutoMapping
import dev.krud.shapeshift.resolver.annotation.DefaultMappingTarget
import dev.krud.shapeshift.resolver.annotation.MappedField
import dev.krud.shapeshift.transformer.base.MappingTransformer
Expand Down Expand Up @@ -245,6 +247,36 @@ internal class ToWithComplexPath {
}
}

@AutoMapping(strategy = AutoMappingStrategy.BY_NAME_AND_TYPE)
internal class SameTypeAutomaticMappingFromImplicit {
val long: Long = 1L
}

@AutoMapping(strategy = AutoMappingStrategy.BY_NAME_AND_TYPE)
internal class NameOnlyAutomaticMappingFromImplicit {
val long: Long = 1L
}

@AutoMapping(GenericTo::class, strategy = AutoMappingStrategy.BY_NAME_AND_TYPE)
internal class SameTypeAutomaticMappingFromExplicit {
val long: Long = 1L
}

@AutoMapping(GenericTo::class, strategy = AutoMappingStrategy.BY_NAME_AND_TYPE)
internal class NameOnlyAutomaticMappingFromExplicit {
val long: Long = 1L
}

@AutoMapping(StringTo::class, strategy = AutoMappingStrategy.BY_NAME_AND_TYPE)
internal class SameTypeAutomaticMappingFromExplicitWrongTarget {
val long: Long = 1L
}

@AutoMapping(StringTo::class, strategy = AutoMappingStrategy.BY_NAME_AND_TYPE)
internal class NameOnlyTypeAutomaticMappingFromExplicitWrongTarget {
val long: Long = 1L
}

internal abstract class BaseTo {
val baseLong: Long? = null
}
Expand Down
Loading

0 comments on commit 9d5fecb

Please sign in to comment.