Kotlin DSL
Kotlin DSL based usage of the ShapeShift library.
We start by defining two classes, our source class
SimpleEntity
and our destination class SimpleEntityDisplay
.data class SimpleEntity(
val name: String,
val description: String,
val privateData: String
)
data class SimpleEntityDisplay(
val name: String = "",
val description: String = ""
)
We can now create a simple mapper. In this example, we want to map the
name
and description
fields of SimpleEntity
to the name
and description
fields of the SimpleEntityDisplay
class, but not the privateData
field.val mapper = mapper<SimpleEntity, SimpleEntityDisplay> {
SimpleEntity::name mappedTo SimpleEntityDisplay::name
SimpleEntity::description mappedTo SimpleEntityDisplay::description
}
To instantiate
ShapeShift
we use the ShapeShiftBuilder
and register our mapper:val shapeShift = ShapeShiftBuilder()
.withMapping(mapper)
.build()
All that's left is to map the
SimpleEntity
instance to the SimpleEntityDisplay
class.val simpleEntity = SimpleEntity("test", "test description", "private data")
val simpleEntityDisplay = shapeShift.map<SimpleEntityDisplay>(simpleEntity)
In the example above we did basic mapping between fields. But what if we want to map from/to deep fields of child classes?
In order to access child classes we can use the
..
operator. Let's look at the following example:class From {
var child: Child = Child()
class Child {
var value: String?
}
}
class To {
var childValue: String?
}
We want to map the
value
field in Child
class inside the From
class to the childValue
field in the To
class. We will create a mapper with the ..
operator.val mapper = mapper<From, To> {
From::child..From.Child::value mappedTo To::childValue
}
The
..
operator is supported in both source and destination fields, it also supports multi level depth.class From {
var grandChildValue: String?
}
class To {
var child: Child = Child()
class Child {
var grandChild: GrandChild = GrandChild()
}
class GrandChild {
var value: String?
}
}
To access the grand child field we just use the
..
operator twice.val mapper = mapper<From, To> {
From::grandChildValue mappedTo To::child..To.Child::grandChild..To.GrandChild::value
}
Field transformers are a way to transform a field from one type to another when mapping it to a destination class. More about the ins-and-outs of transformers is available here:
The
withTransformer
function has 2 options to use transformers. Let's look at the following classes.data class SimpleEntity(
val commaDelimitedString: String
)
data class SimpleEntityDisplay(
val stringList: List<String> = emptyList()
)
We want to map the
commaDelimitedString
field to the stringList
field and change the field type from String
to List<String>
while doing so. To accomplish that we will use a transformer.Our first option is to create a transformer class,
StringToListMappingTransformer
;class StringToListMappingTransformer : MappingTransformer<String, List<String>> {
override fun transform(context: MappingTransformerContext<out String>): List<String>? {
return context.originalValue?.split(",")
}
}
All we need to do to use our transformer is to pass it to the
withTransformer
function.val mapper = mapper<SimpleEntity, SimpleEntityDisplay> {
SimpleEntity::commaDelimitedString mappedTo SimpleEntityDisplay::stringList withTransformer StringToListMappingTransformer::class
}
Transformers must be registered to the
ShapeShift
instance in order to be used. More info about registering transformers is available in the transformers page.Our second option is to use an inline transformer. When we don't need to reuse a transformer we can just add its logic to the DSL.
val mapper = mapper<SimpleEntity, SimpleEntityDisplay> {
SimpleEntity::commaDelimitedString mappedTo SimpleEntityDisplay::stringList withTransformer {
it.originalValue?.split(",")
}
}
Auto mapping is used to reduce the amount of boiler-place code required to configure mapping between two classes. More info about auto mapping is available here:
Auto mapping can be added using the
autoMap
function. val mapper = mapper<SimpleEntity, SimpleEntityDisplay> {
autoMap(AutoMappingStrategy.BY_NAME)
SimpleEntity::name mappedTo SimpleEntityDisplay::fullName
}
autoMap
function receives the desired auto mapping strategy. It is possible to add any manual mapping to add/change mapping behavior.Conditions are used to determine wether a field should be mapped according to certain logic. More info about conditions is available here:
Let's look at the following classes.
data class SimpleEntity(
val name: String
)
data class SimpleEntityDisplay(
val name: String = ""
)
We want to map the
name
field only if it's not null or blank. The withCondition
function has 2 options to add conditions. Our first option is to create a condition class. The condition receives context with the original value of the field and checks that it is not null or blank.
class NotBlankStringCondition : MappingCondition<String> {
override fun isValid(context: MappingConditionContext<String>): Boolean {
return !context.originalValue.isNullOrBlank()
}
}
We will create our mapper and add the condition.
val mapper = mapper<SimpleEntity, SimpleEntityDisplay> {
SimpleEntity::name mappedTo SimpleEntityDisplay::name withCondition NotBlankStringCondition::class
}
Our second option is to use an inline condition. When we don't need to reuse a condition we can just add its logic to the DSL.
val mapper = mapper<SimpleEntity, SimpleEntityDisplay> {
SimpleEntity::name mappedTo SimpleEntityDisplay::name withCondition {
!it.originalValue.isNullOrBlank()
}
}
Decorators allow to add additional logic to the mapping operation. More info about conditions is available here:
Let's look at the following classes.
data class User(
var firstName: String,
var lastName: String
)
data class UserDisplay(
var firstName: String,
var lastName: String,
var fullName: String
)
We want to merge the
firstName
and lastName
fields to the fullName
field in addition to mapping them to their respectable fields.Decorators can be added inline or as a separate class.
To create a decorator class implement the
MappingDecorator
interface.class UserUserDisplayDecorator : MappingDecorator<User, UserDisplay> {
override fun decorate(context: MappingDecoratorContext<User, UserDisplay>) {
val (from, to) = context
to.fullName = "${from.firstName} ${from.lastName}"
}
}
And register it to the
ShapeShift
instance.val shapeShift = ShapeShiftBuilder()
.withMapping<User, UserDisplay> {
User::firstName mappedTo UserDisplay::firstName
User::lastName mappedTo UserDisplay::lastName
decorate(UserUserDisplayDecorator())
}
.build()
It is also possible to add the decorator logic inline.
val shapeShift = ShapeShiftBuilder()
.withMapping<User, UserDisplay> {
User::firstName mappedTo UserDisplay::firstName
User::lastName mappedTo UserDisplay::lastName
decorate {
val (from, to) = it
to.fullName = "${from.firstName} ${from.lastName}"
}
}
.build()
Due to the fact that ShapeShift uses reflection behind the scenes, destination classes need a no arg constructor. But in some cases you have no control over the destination classes and cannot modify them to add a no arg constructor. This is where Object Suppliers comes into play, you can register object suppliers to the ShapeShift instance to add your own logic for instance generation.
More info about Object Suppliers is available here:
The
overrideStrategy
function allows you to override the default mapping strategy configured on the ShapeShift
instance. val mapper = mapper<SimpleEntity, SimpleEntityDisplay> {
SimpleEntity::name mappedTo SimpleEntityDisplay::name overrideStrategy MappingStrategy.MAP_ALL
}
More info about mapping strategy is available here:
Last modified 10mo ago