From be9816e8281a47a6a24875cdf8bcf4eaabebdb94 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:05:27 +0000 Subject: [PATCH 1/2] feat: add Java Optional resolver Implemented JavaOptionalResolver to provide first-class support for java.util.Optional 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> --- .../dev/appoutlet/some/config/SomeConfig.kt | 2 + .../some/resolver/JavaOptionalResolver.kt | 60 ++++++++++++ .../some/resolver/JavaOptionalResolverTest.kt | 98 +++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 src/main/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolver.kt create mode 100644 src/test/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolverTest.kt diff --git a/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt b/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt index afb9310c..9b27a62f 100644 --- a/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt +++ b/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt @@ -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 @@ -116,6 +117,7 @@ data class SomeConfig( JavaInstantResolver(random), JavaDurationResolver(random), JavaZonedDateTimeResolver(random), + JavaOptionalResolver(this[NullableStrategy::class], random), BigDecimalResolver(random), BigIntegerResolver(random), LocalDateResolver(random), diff --git a/src/main/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolver.kt b/src/main/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolver.kt new file mode 100644 index 00000000..8bdaa815 --- /dev/null +++ b/src/main/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolver.kt @@ -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() + is NullableStrategy.NeverNull -> Optional.ofNullable(chain.resolve(valueType)) + is NullableStrategy.Random -> { + if (random.nextDouble() < nullableStrategy.probability) { + Optional.empty() + } else { + Optional.ofNullable(chain.resolve(valueType)) + } + } + is NullableStrategy.NullOnCircularReference -> { + val nullableValueType = (valueType.classifier as? KClass<*>)?.createType( + arguments = valueType.arguments, + nullable = true + ) ?: valueType + + Optional.ofNullable(chain.resolve(nullableValueType)) + } + } + } +} diff --git a/src/test/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolverTest.kt b/src/test/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolverTest.kt new file mode 100644 index 00000000..3c7a8a04 --- /dev/null +++ b/src/test/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolverTest.kt @@ -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 = some() + assertNotNull(result) + assertTrue(result.isPresent) + assertIs(result.get()) + } + + @Test + fun `JavaOptionalResolver with AlwaysNull strategy returns empty Optional`() { + val result: Optional = some { + strategy(NullableStrategy.AlwaysNull) + } + assertNotNull(result) + assertFalse(result.isPresent) + } + + @Test + fun `JavaOptionalResolver with NeverNull strategy always returns present Optional`() { + repeat(100) { + val result: Optional = 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> { + strategy(NullableStrategy.Random(probability = 0.5)) + } + } + assertTrue(results.any { !it.isPresent }) + assertTrue(results.any { it.isPresent }) + } + + data class OptionalCircular(val optional: Optional) + + @Test + fun `JavaOptionalResolver handles circular references with empty Optional`() { + val result: OptionalCircular = some { + strategy(NullableStrategy.NullOnCircularReference) + } + + assertNotNull(result) + // Since Optional 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) + // -> 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>())) + assertTrue(resolver.canResolve(typeOf>())) + } + + @Test + fun `JavaOptionalResolver rejects non-Optional types`() { + val resolver = JavaOptionalResolver(NullableStrategy.default, kotlin.random.Random.Default) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf>())) + } +} From 03eb60bbd98229758e3f5095e86f135efa3dfd94 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Mon, 15 Jun 2026 15:15:02 +0100 Subject: [PATCH 2/2] rename class --- docs/supported-types.md | 1 + .../dev/appoutlet/some/config/SomeConfig.kt | 4 +- ...ptionalResolver.kt => OptionalResolver.kt} | 41 +++++++++++++++---- ...esolverTest.kt => OptionalResolverTest.kt} | 27 ++---------- 4 files changed, 39 insertions(+), 34 deletions(-) rename src/main/kotlin/dev/appoutlet/some/resolver/{JavaOptionalResolver.kt => OptionalResolver.kt} (53%) rename src/test/kotlin/dev/appoutlet/some/resolver/{JavaOptionalResolverTest.kt => OptionalResolverTest.kt} (66%) diff --git a/docs/supported-types.md b/docs/supported-types.md index 88555a7a..485c2729 100644 --- a/docs/supported-types.md +++ b/docs/supported-types.md @@ -25,6 +25,7 @@ For types not listed here, register a [custom factory](custom-factories.md). | **Standard Library** | | | | `kotlin.uuid.Uuid` | `some()` | Requires `@OptIn(ExperimentalUuidApi::class)` | | `java.util.UUID` | `some()` | | +| `java.util.Optional` | `some>()` | See [NullableStrategy](configuration/nullable-strategy.md) | | `kotlin.time.Duration` | `some()` | | | `java.time.Duration` | `some()` | | | `kotlin.time.Instant` | `some()` | | diff --git a/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt b/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt index ba06bee3..73ae3280 100644 --- a/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt +++ b/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt @@ -17,7 +17,6 @@ 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 @@ -31,6 +30,7 @@ import dev.appoutlet.some.resolver.MapResolver import dev.appoutlet.some.resolver.NullableResolver import dev.appoutlet.some.resolver.NumberResolver import dev.appoutlet.some.resolver.ObjectResolver +import dev.appoutlet.some.resolver.OptionalResolver import dev.appoutlet.some.resolver.SealedClassResolver import dev.appoutlet.some.resolver.SetResolver import dev.appoutlet.some.resolver.ShortResolver @@ -119,7 +119,7 @@ data class SomeConfig( JavaInstantResolver(random), JavaDurationResolver(random), JavaZonedDateTimeResolver(random), - JavaOptionalResolver(this[NullableStrategy::class], random), + OptionalResolver(this[NullableStrategy::class], random), BigDecimalResolver(random), BigIntegerResolver(random), LocalDateResolver(random), diff --git a/src/main/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolver.kt b/src/main/kotlin/dev/appoutlet/some/resolver/OptionalResolver.kt similarity index 53% rename from src/main/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolver.kt rename to src/main/kotlin/dev/appoutlet/some/resolver/OptionalResolver.kt index 8bdaa815..317a13b8 100644 --- a/src/main/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolver.kt +++ b/src/main/kotlin/dev/appoutlet/some/resolver/OptionalResolver.kt @@ -3,11 +3,11 @@ package dev.appoutlet.some.resolver import dev.appoutlet.some.config.NullableStrategy import dev.appoutlet.some.core.ResolverChain import dev.appoutlet.some.core.TypeResolver +import dev.appoutlet.some.exception.SomeCircularReferenceException 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 /** @@ -21,17 +21,43 @@ import kotlin.reflect.full.isSubclassOf * @param nullableStrategy Strategy for resolving optional types. Defaults to [NullableStrategy.default] when null. * @param random Random source used by [NullableStrategy.Random]. */ -class JavaOptionalResolver( +class OptionalResolver( nullableStrategy: NullableStrategy?, private val random: Random ) : TypeResolver { + /** + * Resolved nullable strategy used to decide whether the resulting [Optional] is empty or present. + * + * Initialized from the constructor parameter, falling back to [NullableStrategy.default] when null. + */ private val nullableStrategy = nullableStrategy ?: NullableStrategy.default + /** + * Returns `true` when [type] is a subclass of [Optional]. + * + * @param type The type to inspect. + * @return `true` if the type's classifier is a [KClass] that extends [Optional], `false` otherwise. + */ override fun canResolve(type: KType): Boolean { val kClass = type.classifier as? KClass<*> ?: return false return kClass.isSubclassOf(Optional::class) } + /** + * Resolves an [Optional] of the wrapped type described by [type]. + * + * The behavior depends on the configured [nullableStrategy]: + * - [NullableStrategy.AlwaysNull] – always returns [Optional.empty]. + * - [NullableStrategy.NeverNull] – always returns a present [Optional] containing the resolved value. + * - [NullableStrategy.Random] – returns [Optional.empty] with probability defined by the strategy. + * - [NullableStrategy.NullOnCircularReference] – resolves the wrapped type and returns it inside a + * present [Optional]. If the chain throws [SomeCircularReferenceException], returns [Optional.empty]. + * + * @param type The [Optional] type to resolve. Its first type argument is used as the wrapped value type. + * @param chain Resolver chain used to generate the wrapped value. + * @return An [Optional] that may be empty or present depending on the active strategy. + * @throws IllegalArgumentException If the [Optional] type uses a star projection. + */ override fun resolve(type: KType, chain: ResolverChain): Any { val valueType = requireNotNull(type.arguments.firstOrNull()?.type) { "Star projection not supported in Optional" @@ -48,12 +74,11 @@ class JavaOptionalResolver( } } is NullableStrategy.NullOnCircularReference -> { - val nullableValueType = (valueType.classifier as? KClass<*>)?.createType( - arguments = valueType.arguments, - nullable = true - ) ?: valueType - - Optional.ofNullable(chain.resolve(nullableValueType)) + try { + Optional.ofNullable(chain.resolve(valueType)) + } catch (_: SomeCircularReferenceException) { + Optional.empty() + } } } } diff --git a/src/test/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolverTest.kt b/src/test/kotlin/dev/appoutlet/some/resolver/OptionalResolverTest.kt similarity index 66% rename from src/test/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolverTest.kt rename to src/test/kotlin/dev/appoutlet/some/resolver/OptionalResolverTest.kt index 3c7a8a04..88852228 100644 --- a/src/test/kotlin/dev/appoutlet/some/resolver/JavaOptionalResolverTest.kt +++ b/src/test/kotlin/dev/appoutlet/some/resolver/OptionalResolverTest.kt @@ -10,7 +10,7 @@ import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertTrue -class JavaOptionalResolverTest { +class OptionalResolverTest { @Test fun `JavaOptionalResolver generates Optional values`() { val result: Optional = some() @@ -57,41 +57,20 @@ class JavaOptionalResolverTest { val result: OptionalCircular = some { strategy(NullableStrategy.NullOnCircularReference) } - assertNotNull(result) - // Since Optional 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) - // -> 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) + val resolver = OptionalResolver(NullableStrategy.default, kotlin.random.Random.Default) assertTrue(resolver.canResolve(typeOf>())) assertTrue(resolver.canResolve(typeOf>())) } @Test fun `JavaOptionalResolver rejects non-Optional types`() { - val resolver = JavaOptionalResolver(NullableStrategy.default, kotlin.random.Random.Default) + val resolver = OptionalResolver(NullableStrategy.default, kotlin.random.Random.Default) assertFalse(resolver.canResolve(typeOf())) assertFalse(resolver.canResolve(typeOf>())) }