Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import dev.appoutlet.some.resolver.FloatResolver
import dev.appoutlet.some.resolver.IntResolver
import dev.appoutlet.some.resolver.JavaDurationResolver
import dev.appoutlet.some.resolver.JavaInstantResolver
import dev.appoutlet.some.resolver.JavaOptionalResolver
import dev.appoutlet.some.resolver.JavaUuidResolver
import dev.appoutlet.some.resolver.JavaZonedDateTimeResolver
import dev.appoutlet.some.resolver.KotlinDurationResolver
Expand Down Expand Up @@ -116,6 +117,7 @@ data class SomeConfig(
JavaInstantResolver(random),
JavaDurationResolver(random),
JavaZonedDateTimeResolver(random),
JavaOptionalResolver(this[NullableStrategy::class], random),
BigDecimalResolver(random),
BigIntegerResolver(random),
LocalDateResolver(random),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.config.NullableStrategy
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.TypeResolver
import java.util.Optional
import kotlin.random.Random
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.isSubclassOf

/**
* Resolves [Optional] types according to the configured [NullableStrategy].
*
* - **AlwaysNull** – always returns [Optional.empty].
* - **NeverNull** – always resolves a present [Optional].
* - **Random** – returns [Optional.empty] based on the strategy's probability.
* - **NullOnCircularReference** – returns [Optional.empty] if a circular reference is detected for the optional field.
*
* @param nullableStrategy Strategy for resolving optional types. Defaults to [NullableStrategy.default] when null.
* @param random Random source used by [NullableStrategy.Random].
*/
class JavaOptionalResolver(
nullableStrategy: NullableStrategy?,
private val random: Random
) : TypeResolver {
private val nullableStrategy = nullableStrategy ?: NullableStrategy.default

override fun canResolve(type: KType): Boolean {
val kClass = type.classifier as? KClass<*> ?: return false
return kClass.isSubclassOf(Optional::class)
}

override fun resolve(type: KType, chain: ResolverChain): Any {
val valueType = requireNotNull(type.arguments.firstOrNull()?.type) {
"Star projection not supported in Optional"
}

return when (nullableStrategy) {
is NullableStrategy.AlwaysNull -> Optional.empty<Any>()
is NullableStrategy.NeverNull -> Optional.ofNullable(chain.resolve(valueType))
is NullableStrategy.Random -> {
if (random.nextDouble() < nullableStrategy.probability) {
Optional.empty<Any>()
} else {
Optional.ofNullable(chain.resolve(valueType))
}
}
is NullableStrategy.NullOnCircularReference -> {
val nullableValueType = (valueType.classifier as? KClass<*>)?.createType(
arguments = valueType.arguments,
nullable = true
) ?: valueType
Comment thread
MessiasLima marked this conversation as resolved.
Outdated

Optional.ofNullable(chain.resolve(nullableValueType))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.config.NullableStrategy
import dev.appoutlet.some.some
import java.util.Optional
import kotlin.reflect.typeOf
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class JavaOptionalResolverTest {
@Test
fun `JavaOptionalResolver generates Optional values`() {
val result: Optional<String> = some()
assertNotNull(result)
assertTrue(result.isPresent)
assertIs<String>(result.get())
}

@Test
fun `JavaOptionalResolver with AlwaysNull strategy returns empty Optional`() {
val result: Optional<String> = some {
strategy(NullableStrategy.AlwaysNull)
}
assertNotNull(result)
assertFalse(result.isPresent)
}

@Test
fun `JavaOptionalResolver with NeverNull strategy always returns present Optional`() {
repeat(100) {
val result: Optional<String> = some {
strategy(NullableStrategy.NeverNull)
}
assertNotNull(result)
assertTrue(result.isPresent)
}
}

@Test
fun `JavaOptionalResolver with Random strategy can return empty or present Optional`() {
val results = (1..100).map {
some<Optional<String>> {
strategy(NullableStrategy.Random(probability = 0.5))
}
}
assertTrue(results.any { !it.isPresent })
assertTrue(results.any { it.isPresent })
}

data class OptionalCircular(val optional: Optional<OptionalCircular>)

@Test
fun `JavaOptionalResolver handles circular references with empty Optional`() {
val result: OptionalCircular = some {
strategy(NullableStrategy.NullOnCircularReference)
}

assertNotNull(result)
// Since Optional<OptionalCircular> is treated as OptionalCircular?,
// and we are at the first level of circularity for OptionalCircular,
// it should resolve to a non-null OptionalCircular, but wrapped in Optional.

// Wait, if it's the first time it sees OptionalCircular, it should resolve it.
// If it's a circular reference, it should be null.

// Let's re-examine how ResolverChain works.
// resolve(OptionalCircular)
// -> ClassResolver resolves OptionalCircular
// -> resolve(Optional<OptionalCircular>)
// -> JavaOptionalResolver
// -> resolve(OptionalCircular?)
// -> NullableResolver
// -> resolve(OptionalCircular)
// -> CIRCULAR REFERENCE!

// If NullableStrategy is NullOnCircularReference, resolve(OptionalCircular?) returns null.
// So Optional.ofNullable(null) is Optional.empty().

assertFalse(result.optional.isPresent, "Circular reference should result in empty optional")
}

@Test
fun `JavaOptionalResolver canResolve detects Optional types`() {
val resolver = JavaOptionalResolver(NullableStrategy.default, kotlin.random.Random.Default)
assertTrue(resolver.canResolve(typeOf<Optional<String>>()))
assertTrue(resolver.canResolve(typeOf<Optional<Int>>()))
}

@Test
fun `JavaOptionalResolver rejects non-Optional types`() {
val resolver = JavaOptionalResolver(NullableStrategy.default, kotlin.random.Random.Default)
assertFalse(resolver.canResolve(typeOf<String>()))
assertFalse(resolver.canResolve(typeOf<List<String>>()))
}
}