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

Support merging with mapper annotation #11330

Open
wants to merge 2 commits into
base: 4.8.x
Choose a base branch
from
Open
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
633 changes: 412 additions & 221 deletions context/src/main/java/io/micronaut/runtime/beans/MapperIntroduction.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import io.micronaut.context.annotation.Mapper
import io.micronaut.core.annotation.AccessorsStyle
import io.micronaut.core.annotation.Introspected
import io.micronaut.core.convert.ConversionService
import jakarta.inject.Inject
import jakarta.inject.Singleton
import spock.lang.AutoCleanup
import spock.lang.Shared
Expand Down Expand Up @@ -142,6 +141,23 @@ class MapperAnnotationSpec extends Specification {
result.companyId == 'rab'
result.parts == 10
}

void "list mapper test"() {
when:
VacuumCleanersEntity result = testBean.toEntities(
new VacuumCleanerCollection([
new VacuumCleaner("first"),
new VacuumCleaner("second"),
new VacuumCleaner("third")
])
)

then:
result.cleaners[0].name == 'first'
result.cleaners[1].name == 'second'
result.cleaners[2].name == 'third'
}

}

@Singleton
Expand Down Expand Up @@ -172,6 +188,12 @@ abstract class Test {
String calcCompanyId(CreateRobot createRobot) {
return createRobot.companyId.reverse()
}

@Mapper
abstract VacuumCleanersEntity toEntity(VacuumCleaner cleaner)

@Mapper
abstract VacuumCleanersEntity toEntities(VacuumCleanerCollection cleaner)
}

@Introspected
Expand Down Expand Up @@ -292,3 +314,39 @@ class SimpleRobotEntity {
}

}

@Introspected
final class VacuumCleaner {
final String name

VacuumCleaner(String name) {
this.name = name
}
}

@Introspected
final class VacuumCleanerEntity {
final String name

VacuumCleanerEntity(String name) {
this.name = name
}
}

@Introspected
final class VacuumCleanerCollection {
final List<VacuumCleaner> cleaners

VacuumCleanerCollection(List<VacuumCleaner> cleaners) {
this.cleaners = cleaners
}
}

@Introspected
final class VacuumCleanersEntity {
final ArrayList<VacuumCleanerEntity> cleaners

VacuumCleanersEntity(ArrayList<VacuumCleanerEntity> cleaners) {
this.cleaners = cleaners
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package io.micronaut.runtime.beans

import groovy.transform.EqualsAndHashCode
import io.micronaut.context.ApplicationContext
import io.micronaut.context.annotation.Mapper
import io.micronaut.core.annotation.Introspected
import jakarta.inject.Named
import jakarta.inject.Singleton
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class MapperMergingAnnotationSpec extends Specification {

@Shared @AutoCleanup ApplicationContext context = ApplicationContext.run()
@Shared TestMerge testMerge = context.getBean(TestMerge)

void "test merge beans"() {
given:
var a = new SkeletonPartA(description: "Spooky", age: 120)
var b = new SkeletonPartB(numBones: 130, height: 184)
var c = new SkeletonPartC(numToes: 10)

when:
Skeleton result = testMerge.merge(a, b, c)

then:
result.description == 'Spooky'
result.age == 120
result.numBones == 130
result.height == 184
result.numToes == 10
}

void "test merge beans with mapping"() {
given:
var a = new CustomPart(halloweenDescription: "Spooky and gloomy", aliveAge: 100, deadAge: 100)
var b = new SkeletonPartB(numBones: 130, height: 184)
var c = new SkeletonPartC(numToes: 10)

when:
Skeleton result = testMerge.merge(a, b, c)

then:
result.description == 'Spooky and gloomy'
result.age == 200
result.numBones == 130
result.height == 184
result.numToes == 10
}

void "test update from map"() {
given:
var s = new Skeleton(description: "Spooky", age: 102)
var update = ["description": "Boo!!!", "numBones": 200]

when:
Skeleton result = testMerge.update(s, update)

then:
result.description == 'Boo!!!'
result.age == 102
result.numBones == 200
}

void "test merge from maps with mapping"() {
given:
var a = ["description": "Spooky", deadAge: 100]
var b = ["halloweenDescription": "Boo!!!", "numBones": 200]

when:
Skeleton result = testMerge.merge(a, b)

then:
result.description == 'Boo!!!'
result.age == 100
result.numBones == 200
}

void "test update default merge strategy"() {
when:
var skeleton = new Skeleton(description: "Spooky", age: 100, numBones: 12, height: 100, numToes: 2)
var update = new SkeletonUpdater(halloweenDescription: "Very spooky")

then:
testMerge.updateNotNullOverride(skeleton, update) == new Skeleton(description: "Very spooky", age: 100, numBones: 12, height: 100, numToes: 2)
testMerge.updateAlwaysOverride(skeleton, update) == new Skeleton(description: "Very spooky", height: 100, numToes: 2)
}

void "test update custom merge strategy"() {
when:
var skeleton = new Skeleton(description: "Spooky", age: 100, numBones: 12, height: 100, numToes: 2)
var update1 = new SkeletonUpdater(halloweenDescription: "Very spooky", explicitlySet: [])
var update2 = new SkeletonUpdater(halloweenDescription: "Very spooky", explicitlySet: ["halloweenDescription"])
var update3 = new SkeletonUpdater(halloweenDescription: "Very spooky", explicitlySet: ["halloweenDescription", "age", "numBones"])

then:
// Nothing explicitly set
testMerge.updateExplicitlySet(skeleton, update1) == skeleton
// Item was explicitly set
testMerge.updateExplicitlySet(skeleton, update2) == new Skeleton(description: "Very spooky", age: 100, numBones: 12, height: 100, numToes: 2)
// Item was explicitly null-ed
testMerge.updateExplicitlySet(skeleton, update3) == new Skeleton(description: "Very spooky", height: 100, numToes: 2)
}

}

@Singleton
@Mapper
abstract class TestMerge {

@Mapper
abstract Skeleton merge(SkeletonPartA a, SkeletonPartB b, SkeletonPartC c);

@Mapper.Mapping(from = "a.halloweenDescription", to = "description")
@Mapper.Mapping(from = "#{a.deadAge + a.aliveAge}", to = "age")
abstract Skeleton merge(CustomPart a, SkeletonPartB b, SkeletonPartC c);

@Mapper
abstract Skeleton update(Skeleton skeleton, Map<String, Object> values);

@Mapper.Mapping(from = "values.halloweenDescription", to = "description")
@Mapper.Mapping(from = "#{skeleton.get('deadAge')}", to = "age")
abstract Skeleton merge(Map<String, Object> skeleton, Map<String, Object> values);

@Mapper(mergeStrategy = "EXPLICITLY_SET")
@Mapper.Mapping(from = "updater.halloweenDescription", to = "description")
abstract Skeleton updateExplicitlySet(Skeleton current, SkeletonUpdater updater)

@Mapper(mergeStrategy = Mapper.MERGE_STRATEGY_ALWAYS_OVERRIDE)
@Mapper.Mapping(from = "updater.halloweenDescription", to = "description")
abstract Skeleton updateAlwaysOverride(Skeleton current, SkeletonUpdater updater)

@Mapper(mergeStrategy = Mapper.MERGE_STRATEGY_NOT_NULL_OVERRIDE)
@Mapper.Mapping(from = "updater.halloweenDescription", to = "description")
abstract Skeleton updateNotNullOverride(Skeleton current, SkeletonUpdater updater)

}

@Introspected
@EqualsAndHashCode
final class Skeleton {
String description
Integer age
Integer numBones
Float height
Integer numToes
}

@Introspected
final class SkeletonUpdater {
String halloweenDescription
Integer age
Integer numBones
Set<String> explicitlySet
}

@Introspected
final class SkeletonPartA {
String description
Integer age
}

@Introspected
final class SkeletonPartB {
Integer numBones
double height
}

@Introspected
final class SkeletonPartC {
Integer numToes
}

@Introspected
final class CustomPart {
String halloweenDescription
Integer deadAge
Integer aliveAge
}

@Singleton
@Named("EXPLICITLY_SET")
final class ExplicitlySetMergeStrategy implements Mapper.MergeStrategy {

@Override
Object merge(Object currentValue, Object value, Object valueOwner, String propertyName, String mappedPropertyName) {
if (valueOwner instanceof SkeletonUpdater) {
return valueOwner.explicitlySet.contains(mappedPropertyName) ? value : currentValue
} else {
value
}
}
}
Loading
Loading