Skip to content

Commit be9816e

Browse files
feat: add Java Optional resolver
Implemented JavaOptionalResolver to provide first-class support for java.util.Optional<T> in fixture generation. The resolver respects the configured NullableStrategy: - AlwaysNull: returns Optional.empty() - NeverNull: returns Optional.of(value) - Random: returns Optional.empty() based on probability - NullOnCircularReference: returns Optional.empty() when a circular reference is detected The resolver is registered in SomeConfig and includes comprehensive unit tests. Closes # [issue number if provided] Co-authored-by: MessiasLima <10220064+MessiasLima@users.noreply.github.com>
1 parent 685faf4 commit be9816e

3 files changed

Lines changed: 160 additions & 0 deletions

File tree

src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import dev.appoutlet.some.resolver.FloatResolver
1717
import dev.appoutlet.some.resolver.IntResolver
1818
import dev.appoutlet.some.resolver.JavaDurationResolver
1919
import dev.appoutlet.some.resolver.JavaInstantResolver
20+
import dev.appoutlet.some.resolver.JavaOptionalResolver
2021
import dev.appoutlet.some.resolver.JavaUuidResolver
2122
import dev.appoutlet.some.resolver.JavaZonedDateTimeResolver
2223
import dev.appoutlet.some.resolver.KotlinDurationResolver
@@ -116,6 +117,7 @@ data class SomeConfig(
116117
JavaInstantResolver(random),
117118
JavaDurationResolver(random),
118119
JavaZonedDateTimeResolver(random),
120+
JavaOptionalResolver(this[NullableStrategy::class], random),
119121
BigDecimalResolver(random),
120122
BigIntegerResolver(random),
121123
LocalDateResolver(random),
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package dev.appoutlet.some.resolver
2+
3+
import dev.appoutlet.some.config.NullableStrategy
4+
import dev.appoutlet.some.core.ResolverChain
5+
import dev.appoutlet.some.core.TypeResolver
6+
import java.util.Optional
7+
import kotlin.random.Random
8+
import kotlin.reflect.KClass
9+
import kotlin.reflect.KType
10+
import kotlin.reflect.full.createType
11+
import kotlin.reflect.full.isSubclassOf
12+
13+
/**
14+
* Resolves [Optional] types according to the configured [NullableStrategy].
15+
*
16+
* - **AlwaysNull** – always returns [Optional.empty].
17+
* - **NeverNull** – always resolves a present [Optional].
18+
* - **Random** – returns [Optional.empty] based on the strategy's probability.
19+
* - **NullOnCircularReference** – returns [Optional.empty] if a circular reference is detected for the optional field.
20+
*
21+
* @param nullableStrategy Strategy for resolving optional types. Defaults to [NullableStrategy.default] when null.
22+
* @param random Random source used by [NullableStrategy.Random].
23+
*/
24+
class JavaOptionalResolver(
25+
nullableStrategy: NullableStrategy?,
26+
private val random: Random
27+
) : TypeResolver {
28+
private val nullableStrategy = nullableStrategy ?: NullableStrategy.default
29+
30+
override fun canResolve(type: KType): Boolean {
31+
val kClass = type.classifier as? KClass<*> ?: return false
32+
return kClass.isSubclassOf(Optional::class)
33+
}
34+
35+
override fun resolve(type: KType, chain: ResolverChain): Any {
36+
val valueType = requireNotNull(type.arguments.firstOrNull()?.type) {
37+
"Star projection not supported in Optional"
38+
}
39+
40+
return when (nullableStrategy) {
41+
is NullableStrategy.AlwaysNull -> Optional.empty<Any>()
42+
is NullableStrategy.NeverNull -> Optional.ofNullable(chain.resolve(valueType))
43+
is NullableStrategy.Random -> {
44+
if (random.nextDouble() < nullableStrategy.probability) {
45+
Optional.empty<Any>()
46+
} else {
47+
Optional.ofNullable(chain.resolve(valueType))
48+
}
49+
}
50+
is NullableStrategy.NullOnCircularReference -> {
51+
val nullableValueType = (valueType.classifier as? KClass<*>)?.createType(
52+
arguments = valueType.arguments,
53+
nullable = true
54+
) ?: valueType
55+
56+
Optional.ofNullable(chain.resolve(nullableValueType))
57+
}
58+
}
59+
}
60+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package dev.appoutlet.some.resolver
2+
3+
import dev.appoutlet.some.config.NullableStrategy
4+
import dev.appoutlet.some.some
5+
import java.util.Optional
6+
import kotlin.reflect.typeOf
7+
import kotlin.test.Test
8+
import kotlin.test.assertFalse
9+
import kotlin.test.assertIs
10+
import kotlin.test.assertNotNull
11+
import kotlin.test.assertTrue
12+
13+
class JavaOptionalResolverTest {
14+
@Test
15+
fun `JavaOptionalResolver generates Optional values`() {
16+
val result: Optional<String> = some()
17+
assertNotNull(result)
18+
assertTrue(result.isPresent)
19+
assertIs<String>(result.get())
20+
}
21+
22+
@Test
23+
fun `JavaOptionalResolver with AlwaysNull strategy returns empty Optional`() {
24+
val result: Optional<String> = some {
25+
strategy(NullableStrategy.AlwaysNull)
26+
}
27+
assertNotNull(result)
28+
assertFalse(result.isPresent)
29+
}
30+
31+
@Test
32+
fun `JavaOptionalResolver with NeverNull strategy always returns present Optional`() {
33+
repeat(100) {
34+
val result: Optional<String> = some {
35+
strategy(NullableStrategy.NeverNull)
36+
}
37+
assertNotNull(result)
38+
assertTrue(result.isPresent)
39+
}
40+
}
41+
42+
@Test
43+
fun `JavaOptionalResolver with Random strategy can return empty or present Optional`() {
44+
val results = (1..100).map {
45+
some<Optional<String>> {
46+
strategy(NullableStrategy.Random(probability = 0.5))
47+
}
48+
}
49+
assertTrue(results.any { !it.isPresent })
50+
assertTrue(results.any { it.isPresent })
51+
}
52+
53+
data class OptionalCircular(val optional: Optional<OptionalCircular>)
54+
55+
@Test
56+
fun `JavaOptionalResolver handles circular references with empty Optional`() {
57+
val result: OptionalCircular = some {
58+
strategy(NullableStrategy.NullOnCircularReference)
59+
}
60+
61+
assertNotNull(result)
62+
// Since Optional<OptionalCircular> is treated as OptionalCircular?,
63+
// and we are at the first level of circularity for OptionalCircular,
64+
// it should resolve to a non-null OptionalCircular, but wrapped in Optional.
65+
66+
// Wait, if it's the first time it sees OptionalCircular, it should resolve it.
67+
// If it's a circular reference, it should be null.
68+
69+
// Let's re-examine how ResolverChain works.
70+
// resolve(OptionalCircular)
71+
// -> ClassResolver resolves OptionalCircular
72+
// -> resolve(Optional<OptionalCircular>)
73+
// -> JavaOptionalResolver
74+
// -> resolve(OptionalCircular?)
75+
// -> NullableResolver
76+
// -> resolve(OptionalCircular)
77+
// -> CIRCULAR REFERENCE!
78+
79+
// If NullableStrategy is NullOnCircularReference, resolve(OptionalCircular?) returns null.
80+
// So Optional.ofNullable(null) is Optional.empty().
81+
82+
assertFalse(result.optional.isPresent, "Circular reference should result in empty optional")
83+
}
84+
85+
@Test
86+
fun `JavaOptionalResolver canResolve detects Optional types`() {
87+
val resolver = JavaOptionalResolver(NullableStrategy.default, kotlin.random.Random.Default)
88+
assertTrue(resolver.canResolve(typeOf<Optional<String>>()))
89+
assertTrue(resolver.canResolve(typeOf<Optional<Int>>()))
90+
}
91+
92+
@Test
93+
fun `JavaOptionalResolver rejects non-Optional types`() {
94+
val resolver = JavaOptionalResolver(NullableStrategy.default, kotlin.random.Random.Default)
95+
assertFalse(resolver.canResolve(typeOf<String>()))
96+
assertFalse(resolver.canResolve(typeOf<List<String>>()))
97+
}
98+
}

0 commit comments

Comments
 (0)