Skip to content

Commit 46dbc79

Browse files
mcarleioMilan Machain
andauthored
feat(124): provide options how to handle properties which are not part of the (used) constructor and how to treat invalid mappings
* feat: add non-constructor property mapping modes Adds support for configurable mapping of target properties not present in constructor. Includes new options: - konvert.non-constructor-properties-mapping - konvert.invalid-mapping-strategy --------- Co-authored-by: Milan Machain <milan.machain@homecredit.cz>
1 parent 2a01a66 commit 46dbc79

File tree

25 files changed

+1655
-271
lines changed

25 files changed

+1655
-271
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### New features
8+
9+
* New option `konvert.non-constructor-properties-mapping` to define how non-constructor target properties should be mapped: [#124](https://github.com/mcarleio/konvert/issues/124) (thanks to [@kosoj](https://github.com/kosoj) for idea and initial work [#138](https://github.com/mcarleio/konvert/pull/138))
10+
* `auto` (default): Behaves like `implicit` if no explicit mappings are present, otherwise behaves like `explicit`.
11+
* `explicit`: Only non-constructor target properties explicitly declared in mappings are mapped.
12+
* `implicit`: Maps all non-constructor target properties with a matching source property or explicit mapping.
13+
* `all`: All non-constructor target properties must be mapped, otherwise an exception is thrown.
14+
15+
* New option `konvert.invalid-mapping-strategy` to define how invalid mappings should be handled:
16+
* `warn` (default): Logs a warning and ignores invalid mappings.
17+
* `fail`: Throws an exception when an invalid mapping is encountered.
18+
19+
720
## [4.1.2]
821

922
### Bug fixes

converter-api/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,11 @@ dependencies {
88
api(project(":annotations"))
99
api(symbolProcessingApi)
1010
api(kotlinPoet)
11+
12+
testImplementation(kotlinTest)
13+
testImplementation("org.junit.jupiter:junit-jupiter-params:${Versions.jUnit}")
14+
}
15+
16+
tasks.test {
17+
useJUnitPlatform()
1118
}

converter-api/src/main/kotlin/io/mcarle/konvert/converter/api/config/Configuration.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ fun <T> withIsolatedConfiguration(environment: SymbolProcessorEnvironment, exec:
2222
return withIsolatedConfiguration(environment.options, exec)
2323
}
2424

25-
private inline fun <T> withIsolatedConfiguration(options: Map<String, String>, exec: () -> T): T {
25+
internal inline fun <T> withIsolatedConfiguration(options: Map<String, String>, exec: () -> T): T {
2626
val previous = Configuration.CURRENT
2727
Configuration.CURRENT = Configuration(options.toMutableMap())
2828
try {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.mcarle.konvert.converter.api.config
2+
3+
/**
4+
* Defines how Konvert should handle invalid mappings.
5+
*
6+
* @see WARN
7+
* @see FAIL
8+
*/
9+
enum class InvalidMappingStrategy {
10+
11+
/**
12+
* Logs a warning when an invalid mapping is encountered. Ignores that mapping and continues execution.
13+
*/
14+
WARN,
15+
16+
/**
17+
*
18+
* Fail with an exception when an invalid mapping is encountered.
19+
*/
20+
FAIL
21+
}

converter-api/src/main/kotlin/io/mcarle/konvert/converter/api/config/KonvertOptions.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,40 @@ object GENERATED_MODULE_SUFFIX_OPTION : Option<String>("konvert.generatedModuleS
108108
* Default: false
109109
*/
110110
object PARSE_DEPRECATED_META_INF_FILES_OPTION : Option<Boolean>("konvert.parseDeprecatedMetaInfFiles", false)
111+
112+
/**
113+
* Controls how properties outside the constructor (non-constructor properties and setters) are handled during mapping.
114+
*
115+
* Possible values:
116+
* - "auto" (default)
117+
* - "implicit"
118+
* - "explicit"
119+
* - "all"
120+
*
121+
* @see NonConstructorPropertiesMapping
122+
* @since 4.2.0
123+
*/
124+
object NON_CONSTRUCTOR_PROPERTIES_MAPPING_OPTION : Option<NonConstructorPropertiesMapping>(
125+
key = "konvert.non-constructor-properties-mapping",
126+
defaultValue = NonConstructorPropertiesMapping.AUTO
127+
)
128+
129+
/**
130+
* Controls how Konvert reacts when it encounters an invalid mapping.
131+
* A mapping is invalid when:
132+
* - it defines a source property that is not present
133+
* - it defines a target property that is not present
134+
* - it defines incompatible parameters (e.g. source and ignore=true)
135+
* - there are multiple mappings for the same target
136+
*
137+
* Possible values:
138+
* - "warn" (default)
139+
* - "fail"
140+
*
141+
* @see InvalidMappingStrategy
142+
* @since 4.2.0
143+
*/
144+
object INVALID_MAPPING_STRATEGY_OPTION : Option<InvalidMappingStrategy>(
145+
key = "konvert.invalid-mapping-strategy",
146+
defaultValue = InvalidMappingStrategy.WARN
147+
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.mcarle.konvert.converter.api.config
2+
3+
/**
4+
* Defines how non-constructor target properties should be handled during mapping.
5+
*
6+
* @see AUTO
7+
* @see EXPLICIT
8+
* @see IMPLICIT
9+
* @see ALL
10+
*/
11+
enum class NonConstructorPropertiesMapping {
12+
13+
/**
14+
* Behaves like [IMPLICIT] if no [io.mcarle.konvert.api.Mapping]s are present
15+
* (other than those with [io.mcarle.konvert.api.Mapping.ignore]`=true`).
16+
* Otherwise, behaves like [EXPLICIT].
17+
*/
18+
AUTO,
19+
20+
/**
21+
* Only non-constructor target properties that are explicitly declared within [io.mcarle.konvert.api.Mapping]s will be mapped.
22+
* Ignores the rest, even if they have matching source properties.
23+
*/
24+
EXPLICIT,
25+
26+
/**
27+
* Generates mappings for every non-constructor target property for which a matching source property exist or a
28+
* [io.mcarle.konvert.api.Mapping] is defined.
29+
* Ignores the rest.
30+
*/
31+
IMPLICIT,
32+
33+
/**
34+
* All non-constructor target properties must be mapped. Throws exceptions,
35+
* if no matching source property/[io.mcarle.konvert.api.Mapping] is found/defined.
36+
*/
37+
ALL,
38+
}

converter-api/src/main/kotlin/io/mcarle/konvert/converter/api/config/extensions.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@ val Configuration.Companion.generatedModuleSuffix: String
5050
val Configuration.Companion.parseDeprecatedMetaInfFiles: Boolean
5151
get() = PARSE_DEPRECATED_META_INF_FILES_OPTION.get(CURRENT, String::toBoolean)
5252

53+
/**
54+
* @see NON_CONSTRUCTOR_PROPERTIES_MAPPING_OPTION
55+
*/
56+
val Configuration.Companion.nonConstructorPropertiesMapping: NonConstructorPropertiesMapping
57+
get() = NON_CONSTRUCTOR_PROPERTIES_MAPPING_OPTION.get(CURRENT) { configString ->
58+
NonConstructorPropertiesMapping.entries.firstOrNull { it.name.equals(configString, ignoreCase = true) }
59+
?: NON_CONSTRUCTOR_PROPERTIES_MAPPING_OPTION.defaultValue
60+
}
61+
62+
/**
63+
* @see INVALID_MAPPING_STRATEGY_OPTION
64+
*/
65+
val Configuration.Companion.invalidMappingStrategy: InvalidMappingStrategy
66+
get() = INVALID_MAPPING_STRATEGY_OPTION.get(CURRENT) { configString ->
67+
InvalidMappingStrategy.entries.firstOrNull { it.name.equals(configString, ignoreCase = true) }
68+
?: INVALID_MAPPING_STRATEGY_OPTION.defaultValue
69+
}
70+
5371
/**
5472
* Reads the value for [Option.key] from the provided `options` or fallbacks to the [Option.defaultValue].
5573
*/
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package io.mcarle.konvert.converter.api.config
2+
3+
import org.junit.jupiter.api.Test
4+
import org.junit.jupiter.params.ParameterizedTest
5+
import org.junit.jupiter.params.provider.Arguments
6+
import org.junit.jupiter.params.provider.MethodSource
7+
import org.junit.jupiter.params.provider.ValueSource
8+
import kotlin.test.assertEquals
9+
10+
class ConfigurationExtensionsTest {
11+
12+
companion object {
13+
14+
@JvmStatic
15+
fun validValuesForNonConstructorPropertiesMapping(): List<Arguments> {
16+
return listOf(
17+
Arguments.of("auto", NonConstructorPropertiesMapping.AUTO),
18+
Arguments.of("all", NonConstructorPropertiesMapping.ALL),
19+
Arguments.of("explicit", NonConstructorPropertiesMapping.EXPLICIT),
20+
Arguments.of("implicit", NonConstructorPropertiesMapping.IMPLICIT),
21+
Arguments.of("AUTO", NonConstructorPropertiesMapping.AUTO),
22+
Arguments.of("ALL", NonConstructorPropertiesMapping.ALL),
23+
Arguments.of("EXPLICIT", NonConstructorPropertiesMapping.EXPLICIT),
24+
Arguments.of("IMPLICIT", NonConstructorPropertiesMapping.IMPLICIT),
25+
)
26+
}
27+
28+
@JvmStatic
29+
fun validValuesForInvalidMappingStrategy(): List<Arguments> {
30+
return listOf(
31+
Arguments.of("warn", InvalidMappingStrategy.WARN),
32+
Arguments.of("fail", InvalidMappingStrategy.FAIL),
33+
Arguments.of("WARN", InvalidMappingStrategy.WARN),
34+
Arguments.of("FAIL", InvalidMappingStrategy.FAIL)
35+
)
36+
}
37+
}
38+
39+
@ParameterizedTest
40+
@MethodSource("validValuesForNonConstructorPropertiesMapping")
41+
fun `valid values for nonConstructorPropertiesMapping are correctly mapped to enum`(
42+
configValue: String,
43+
expectedResult: NonConstructorPropertiesMapping
44+
) {
45+
withIsolatedConfiguration(
46+
mapOf(
47+
NON_CONSTRUCTOR_PROPERTIES_MAPPING_OPTION.key to configValue
48+
)
49+
) {
50+
val parsedValue = Configuration.nonConstructorPropertiesMapping
51+
52+
assertEquals(expectedResult, parsedValue)
53+
}
54+
}
55+
56+
@ParameterizedTest
57+
@ValueSource(strings = ["invalid", "_", ""])
58+
fun `invalid values for nonConstructorPropertiesMapping are mapped to default`(configValue: String) {
59+
withIsolatedConfiguration(
60+
mapOf(
61+
NON_CONSTRUCTOR_PROPERTIES_MAPPING_OPTION.key to configValue
62+
)
63+
) {
64+
val parsedValue = Configuration.nonConstructorPropertiesMapping
65+
66+
assertEquals(NON_CONSTRUCTOR_PROPERTIES_MAPPING_OPTION.defaultValue, parsedValue)
67+
}
68+
}
69+
70+
@Test
71+
fun `default value for nonConstructorPropertiesMapping is AUTO`() {
72+
assertEquals(NonConstructorPropertiesMapping.AUTO, NON_CONSTRUCTOR_PROPERTIES_MAPPING_OPTION.defaultValue)
73+
}
74+
75+
@ParameterizedTest
76+
@MethodSource("validValuesForInvalidMappingStrategy")
77+
fun `valid values for invalidMappingStrategy are correctly mapped to enum`(
78+
configValue: String,
79+
expectedResult: InvalidMappingStrategy
80+
) {
81+
withIsolatedConfiguration(
82+
mapOf(
83+
INVALID_MAPPING_STRATEGY_OPTION.key to configValue
84+
)
85+
) {
86+
val parsedValue = Configuration.invalidMappingStrategy
87+
88+
assertEquals(expectedResult, parsedValue)
89+
}
90+
}
91+
92+
@ParameterizedTest
93+
@ValueSource(strings = ["invalid", "_", ""])
94+
fun `invalid values for invalidMappingStrategy are mapped to default`(configValue: String) {
95+
withIsolatedConfiguration(
96+
mapOf(
97+
INVALID_MAPPING_STRATEGY_OPTION.key to configValue
98+
)
99+
) {
100+
val parsedValue = Configuration.invalidMappingStrategy
101+
102+
assertEquals(INVALID_MAPPING_STRATEGY_OPTION.defaultValue, parsedValue)
103+
}
104+
}
105+
106+
@Test
107+
fun `default value for invalidMappingStrategy is WARN`() {
108+
assertEquals(InvalidMappingStrategy.WARN, INVALID_MAPPING_STRATEGY_OPTION.defaultValue)
109+
}
110+
}

docs/options/index.adoc

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,82 @@ a|Only effective if set globally. This setting defines if the deprecated META-IN
212212

213213
Will be removed in one of the next releases.
214214

215+
216+
a|`*konvert.non-constructor-properties-mapping*`
217+
a|`auto`, `explicit`, `implicit`, `all`
218+
a|`auto`
219+
a|Controls how properties of the target class that are *not* part of the constructor (i.e., mutable properties, properties with setters, or properties defined outside the primary constructor) are mapped.
220+
221+
* `auto` (default): If there are no `@Mapping` annotations (except those with `ignore = true`), behaves like `implicit`. If there are any `@Mapping` annotations, behaves like `explicit`.
222+
* `explicit`: Only non-constructor target properties that are explicitly declared via `@Mapping` will be mapped. All others are ignored, even if a matching source property exists.
223+
* `implicit`: All non-constructor target properties for which a matching source property exists (by name), or for which a `@Mapping` is defined, will be mapped. All others are ignored.
224+
* `all`: All non-constructor target properties must be mapped. If a property cannot be mapped (no matching source property and no `@Mapping`), an exception is thrown.
225+
226+
4+a|
227+
[.pl-6]
228+
.Example
229+
[%collapsible]
230+
====
231+
[source,kotlin]
232+
----
233+
class Source(val id: String) {
234+
var description: String? = null
235+
var additional: String? = null
236+
}
237+
238+
class Target(val id: String) {
239+
var description: String? = null
240+
var extra: String? = null
241+
}
242+
243+
@Konverter
244+
interface MapperAuto {
245+
fun map(source: Source): Target
246+
}
247+
248+
@Konverter(options=[
249+
Konfig(key="konvert.non-constructor-properties-mapping", value="implicit")
250+
])
251+
interface MapperImplicit {
252+
fun map(source: Source): Target
253+
}
254+
255+
@Konverter(options=[
256+
Konfig(key="konvert.non-constructor-properties-mapping", value="explicit")
257+
])
258+
interface MapperExplicit {
259+
fun map(source: Source): Target
260+
}
261+
262+
@Konverter(options=[
263+
Konfig(key="konvert.non-constructor-properties-mapping", value="all")
264+
])
265+
interface MapperAll {
266+
fun map(source: Source): Target
267+
}
268+
----
269+
270+
Will result in:
271+
272+
- `MapperAuto` and `MapperImplicit` will generate the same code, which only maps `id` and `description` properties
273+
- `MapperExplicit` will only map `id`, as there are no `@Mapping` annotations defined
274+
- `MapperAll` will throw an exception, as `extra` has no matching source property and no `@Mapping` defined
275+
276+
277+
a|`*konvert.invalid-mapping-strategy*`
278+
a|`warn`, `fail`
279+
a|`warn`
280+
a|Determines how Konvert handles invalid mapping definitions.
281+
282+
- `warn` (default): Konvert logs a warning for each invalid mapping, ignores it, and continues code generation.
283+
- `fail`: Konvert will throw an exception and fail the build
284+
285+
An invalid mapping occurs when:
286+
287+
- A mapping references a source property that does not exist in the source type.
288+
- A mapping references a target property that does not exist in the target type.
289+
- A mapping defines incompatible parameters (e.g., both `source` and `ignore = true`).
290+
- There are multiple mappings for the same target property.
215291
|===
216292
217293
=== `@Konverter` Options

0 commit comments

Comments
 (0)