Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add optional support #24

Merged
merged 3 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions shapeshift/src/main/kotlin/dev/krud/shapeshift/ShapeShift.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import dev.krud.shapeshift.MappingDecoratorRegistration.Companion.id
import dev.krud.shapeshift.MappingTransformerRegistration.Companion.id
import dev.krud.shapeshift.condition.MappingCondition
import dev.krud.shapeshift.condition.MappingConditionContext
import dev.krud.shapeshift.container.ContainerAdapter
import dev.krud.shapeshift.decorator.MappingDecorator
import dev.krud.shapeshift.decorator.MappingDecoratorContext
import dev.krud.shapeshift.dto.MappingStructure
Expand All @@ -35,7 +36,8 @@ class ShapeShift internal constructor(
val mappingDefinitionResolvers: Set<MappingDefinitionResolver>,
val defaultMappingStrategy: MappingStrategy,
val decoratorRegistrations: Set<MappingDecoratorRegistration<out Any, out Any>>,
val objectSuppliers: Map<Class<*>, Supplier<*>>
val objectSuppliers: Map<Class<*>, Supplier<*>>,
val containerAdapters: Map<Class<*>, ContainerAdapter<out Any>>
) {
val transformerRegistrations: MutableList<MappingTransformerRegistration<out Any, out Any>> = mutableListOf()
internal val transformersByTypeCache: MutableMap<Class<out MappingTransformer<out Any?, out Any?>>, MappingTransformerRegistration<out Any?, out Any?>> =
Expand Down Expand Up @@ -116,14 +118,15 @@ class ShapeShift internal constructor(
toPair.field.isAccessible = true

val mappingStrategy = resolvedMappedField.effectiveMappingStrategy(defaultMappingStrategy)
val fromValue = fromPair.field.getValue(fromPair.target)
var fromValue = fromPair.field.getValue(fromPair.target)
val shouldMapValue = when (mappingStrategy) {
MappingStrategy.NONE -> error("Mapping strategy is set to NONE")
MappingStrategy.MAP_ALL -> true
MappingStrategy.MAP_NOT_NULL -> fromValue != null
}

if (shouldMapValue) {
fromValue = fromPair.field.getEffectiveValue(fromPair.target)
try {
if (!resolvedMappedField.conditionMatches(fromValue)) {
return
Expand All @@ -142,12 +145,12 @@ class ShapeShift internal constructor(
}

if (valueToSet == null) {
toPair.field.setValue(toPair.target, null)
toPair.field.setEffectiveValue(toPair.target, null)
} else {
if (!toPair.type.isAssignableFrom(valueToSet::class.java)) {
error("Type mismatch: Expected ${toPair.type} but got ${valueToSet::class.java}")
}
toPair.field.setValue(toPair.target, valueToSet)
toPair.field.setEffectiveValue(toPair.target, valueToSet)
}
} catch (e: Exception) {
val newException =
Expand Down Expand Up @@ -199,7 +202,7 @@ class ShapeShift internal constructor(
val fieldType = field.type.kotlin.javaObjectType

if (nodes.size == 1) {
return ObjectFieldTrio(target, field, fieldType)
return ObjectFieldTrio(target, field, field.getTrueType())
}
field.isAccessible = true
var subTarget = field.get(target)
Expand Down Expand Up @@ -290,6 +293,33 @@ class ShapeShift internal constructor(
error("Could not find a no-arg constructor or object supplier for class $clazz")
}

private val Field.isContainer: Boolean get() = type in containerAdapters

private fun Field.getEffectiveValue(target: Any): Any? {
val value = getValue(target)
if (isContainer) {
return (containerAdapters[type] as ContainerAdapter<Any?>).unwrapValue(value)
}
return value
}

private fun Field.setEffectiveValue(target: Any, value: Any?) {
val value = if (isContainer) {
(containerAdapters[type] as ContainerAdapter<Any?>).wrapValue(value)
} else {
value
}
setValue(target, value)
}

private fun Field.getTrueType(): Class<*> {
return if (isContainer) {
(containerAdapters[type] as ContainerAdapter<Any?>).getTrueType(this)
} else {
type.kotlin.javaObjectType
}
}

companion object {
enum class SourceType {
FROM,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ package dev.krud.shapeshift

import dev.krud.shapeshift.MappingDecoratorRegistration.Companion.toRegistration
import dev.krud.shapeshift.MappingTransformerRegistration.Companion.toRegistration
import dev.krud.shapeshift.container.ContainerAdapter
import dev.krud.shapeshift.container.OptionalContainerAdapter
import dev.krud.shapeshift.decorator.MappingDecorator
import dev.krud.shapeshift.dsl.KotlinDslMappingDefinitionBuilder
import dev.krud.shapeshift.resolver.MappingDefinition
Expand All @@ -37,7 +39,9 @@ import dev.krud.shapeshift.transformer.StringToIntMappingTransformer
import dev.krud.shapeshift.transformer.StringToLongMappingTransformer
import dev.krud.shapeshift.transformer.StringToShortMappingTransformer
import dev.krud.shapeshift.transformer.base.MappingTransformer
import java.util.*
import java.util.function.Supplier
import javax.swing.text.html.Option

/**
* A builder used to create a new ShapeShift instance.
Expand All @@ -51,6 +55,7 @@ class ShapeShiftBuilder {
private var defaultMappingStrategy: MappingStrategy = MappingStrategy.MAP_NOT_NULL
private val mappingDefinitions: MutableList<MappingDefinition> = mutableListOf()
private val objectSuppliers: MutableMap<Class<*>, Supplier<*>> = mutableMapOf()
private val containerAdapters: MutableMap<Class<*>, ContainerAdapter<out Any>> = mutableMapOf()

init {
// Add default annotation resolver
Expand All @@ -60,6 +65,9 @@ class ShapeShiftBuilder {
DEFAULT_TRANSFORMERS.forEach {
withTransformer(it)
}

// Default container adapters
containerAdapters[Optional::class.java] = OptionalContainerAdapter()
}

/**
Expand Down Expand Up @@ -174,7 +182,7 @@ class ShapeShiftBuilder {
resolvers += StaticMappingDefinitionResolver(mappingDefinitions)
}

return ShapeShift(transformerRegistrations, resolvers, defaultMappingStrategy, decoratorRegistrations, objectSuppliers)
return ShapeShift(transformerRegistrations, resolvers, defaultMappingStrategy, decoratorRegistrations, objectSuppliers, containerAdapters)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright KRUD 2024
*
* 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.container

import java.lang.reflect.Field

interface ContainerAdapter<ContainerType> {

val containerClazz: Class<ContainerType>

fun getTrueType(field: Field): Class<*>

fun unwrapValue(container: ContainerType): Any?

fun wrapValue(value: Any?): ContainerType
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright KRUD 2024
*
* 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.container

import dev.krud.shapeshift.util.getGenericAtPosition
import java.lang.reflect.Field
import java.util.*

class OptionalContainerAdapter: ContainerAdapter<Optional<*>> {
override val containerClazz: Class<Optional<*>> = Optional::class.java

override fun getTrueType(field: Field): Class<*> {
return field.getGenericAtPosition(0)
}

override fun unwrapValue(container: Optional<*>): Any? {
return container.orElse(null)
}

override fun wrapValue(value: Any?): Optional<*> {
return Optional.ofNullable(value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
package dev.krud.shapeshift.util

import java.lang.reflect.Field
import java.lang.reflect.ParameterizedType


data class ClassPair<From, To>(val from: Class<out From>, val to: Class<out To>)

Expand Down Expand Up @@ -45,4 +47,12 @@ internal fun Class<*>.getDeclaredFieldRecursive(name: String): Field {
}
}
throw NoSuchFieldException(name)
}

internal fun Field.getGenericAtPosition(position: Int): Class<*> {
if (genericType !is ParameterizedType) {
error("Type ${this.type} is not parameterized")
}
val typeArgument = (genericType as ParameterizedType).actualTypeArguments[position] as Class<*>
return typeArgument
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package dev.krud.shapeshift

import dev.krud.shapeshift.resolver.annotation.DefaultMappingTarget
import dev.krud.shapeshift.resolver.annotation.MappedField
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import strikt.api.expect
import strikt.assertions.isEqualTo
import java.util.*
import javax.swing.text.html.Option

class ValueContainerTest {

internal lateinit var shapeShift: ShapeShift

@BeforeEach
internal fun setUp() {
shapeShift = ShapeShiftBuilder().build()
}

@Test
fun `contained values should be unwrapped, transformed and rewrapped`() {
val toWrapped = shapeShift.map<FromWrapped, ToWrapped>(FromWrapped())
expect {
that(toWrapped.someField).isEqualTo(Optional.of("ABCD"))
that(toWrapped.someField2).isEqualTo(Optional.of(123))
}
}

@Test
fun `empty but non-null container should set null`() {
val toNullable = shapeShift.map<FromNullable, ToNullable>(FromNullable())
println()
}


@DefaultMappingTarget(ToWrapped::class)
class FromWrapped {
@MappedField
var someField: Optional<String>? = Optional.of("ABCD")

@MappedField
var someField2: Optional<String>? = Optional.of("123")
}

class ToWrapped {
var someField: Optional<String>? = null
var someField2: Optional<Int>? = null
}

@DefaultMappingTarget(ToNullable::class)
class FromNullable {
@MappedField
var someField: Optional<String>? = Optional.empty()
@MappedField
var someField2: Optional<String>? = null
}

class ToNullable {
var someField: String? = "ABCD"
var someField2: String? = "ABCD"
}
}
Loading