diff --git a/android/src/main/java/dev/appoutlet/some/android/AndroidResolverProvider.kt b/android/src/main/java/dev/appoutlet/some/android/AndroidResolverProvider.kt index dff7f432..1d2a49ba 100644 --- a/android/src/main/java/dev/appoutlet/some/android/AndroidResolverProvider.kt +++ b/android/src/main/java/dev/appoutlet/some/android/AndroidResolverProvider.kt @@ -1,15 +1,50 @@ package dev.appoutlet.some.android +import dev.appoutlet.some.android.resolver.BundleResolver +import dev.appoutlet.some.android.resolver.ColorResolver +import dev.appoutlet.some.android.resolver.PairResolver +import dev.appoutlet.some.android.resolver.PointFResolver +import dev.appoutlet.some.android.resolver.PointResolver +import dev.appoutlet.some.android.resolver.RectFResolver +import dev.appoutlet.some.android.resolver.RectResolver +import dev.appoutlet.some.android.resolver.SizeFResolver +import dev.appoutlet.some.android.resolver.SizeResolver +import dev.appoutlet.some.android.resolver.UriResolver +import dev.appoutlet.some.android.resolver.UserHandleResolver import dev.appoutlet.some.core.Resolver import dev.appoutlet.some.core.ResolverProvider import dev.appoutlet.some.core.StrategyProvider import kotlin.random.Random +/** + * Contributes Android-specific resolvers discovered through `ServiceLoader`. + * + * The returned resolvers extend the shared `some-core` chain with support for common Android framework + * value types such as `Uri`, geometry classes, `Bundle`, `Pair`, `UserHandle`, and `Color`. + */ class AndroidResolverProvider : ResolverProvider { + /** + * Creates the Android resolver set using the shared [strategyProvider] and [random] source. + * + * Resolvers that depend on strategies receive [strategyProvider]; all others share the same [random] + * instance so generated Android values stay consistent with the rest of the fixture session. + */ override fun createResolvers( strategyProvider: StrategyProvider, random: Random ): List { - return emptyList() + return listOf( + UriResolver(strategyProvider, random), + BundleResolver(random), + RectResolver(random), + RectFResolver(random), + PointResolver(random), + PointFResolver(random), + SizeResolver(random), + SizeFResolver(random), + PairResolver(random), + UserHandleResolver(random), + ColorResolver(random) + ) } } diff --git a/android/src/main/java/dev/appoutlet/some/android/resolver/ColorResolver.kt b/android/src/main/java/dev/appoutlet/some/android/resolver/ColorResolver.kt new file mode 100644 index 00000000..7d7f6281 --- /dev/null +++ b/android/src/main/java/dev/appoutlet/some/android/resolver/ColorResolver.kt @@ -0,0 +1,27 @@ +package dev.appoutlet.some.android.resolver + +import android.graphics.Color +import dev.appoutlet.some.core.Resolver +import dev.appoutlet.some.core.ResolverChain +import kotlin.random.Random +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * Resolves [Color] values when the runtime API level supports [Color.valueOf]. + * + * @param random Random source used when generating RGB channel values. + */ +class ColorResolver( + private val random: Random +) : Resolver { + override fun canResolve(type: KType): Boolean = type == typeOf() + + override fun resolve(type: KType, chain: ResolverChain): Color { + return Color.valueOf( + random.nextFloat(), + random.nextFloat(), + random.nextFloat(), + ) + } +} diff --git a/android/src/main/java/dev/appoutlet/some/android/resolver/PairResolver.kt b/android/src/main/java/dev/appoutlet/some/android/resolver/PairResolver.kt new file mode 100644 index 00000000..7d384344 --- /dev/null +++ b/android/src/main/java/dev/appoutlet/some/android/resolver/PairResolver.kt @@ -0,0 +1,30 @@ +package dev.appoutlet.some.android.resolver + +import android.util.Pair +import dev.appoutlet.some.core.Resolver +import dev.appoutlet.some.core.ResolverChain +import kotlin.random.Random +import kotlin.reflect.KType + +/** + * Resolves [Pair] types by delegating both generic type arguments back through the resolver chain. + * + * @param random Shared random source accepted for consistency with the resolver provider API. + */ +class PairResolver( + @Suppress("UNUSED_PARAMETER") + random: Random +) : Resolver { + override fun canResolve(type: KType): Boolean = type.classifier == Pair::class + + override fun resolve(type: KType, chain: ResolverChain): Any { + val firstType = requireNotNull(type.arguments.getOrNull(0)?.type) { + "Star projection not supported in Pair first type" + } + val secondType = requireNotNull(type.arguments.getOrNull(1)?.type) { + "Star projection not supported in Pair second type" + } + + return Pair(chain.resolve(firstType), chain.resolve(secondType)) + } +} diff --git a/android/src/main/java/dev/appoutlet/some/android/resolver/UserHandleResolver.kt b/android/src/main/java/dev/appoutlet/some/android/resolver/UserHandleResolver.kt new file mode 100644 index 00000000..1b37d395 --- /dev/null +++ b/android/src/main/java/dev/appoutlet/some/android/resolver/UserHandleResolver.kt @@ -0,0 +1,23 @@ +package dev.appoutlet.some.android.resolver + +import android.os.UserHandle +import dev.appoutlet.some.core.Resolver +import dev.appoutlet.some.core.ResolverChain +import kotlin.random.Random +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * Resolves [UserHandle] values when the runtime API level supports [UserHandle.getUserHandleForUid]. + * + * @param random Random source used when generating a non-system application UID. + */ +class UserHandleResolver( + private val random: Random +) : Resolver { + override fun canResolve(type: KType): Boolean = type == typeOf() + + override fun resolve(type: KType, chain: ResolverChain): Any? { + return UserHandle.getUserHandleForUid(random.nextInt()) + } +} diff --git a/android/src/test/java/dev/appoutlet/some/android/AndroidServiceLoaderTest.kt b/android/src/test/java/dev/appoutlet/some/android/AndroidServiceLoaderTest.kt index 773a4bde..72a2b5c2 100644 --- a/android/src/test/java/dev/appoutlet/some/android/AndroidServiceLoaderTest.kt +++ b/android/src/test/java/dev/appoutlet/some/android/AndroidServiceLoaderTest.kt @@ -1,5 +1,6 @@ package dev.appoutlet.some.android +import android.graphics.Point import dev.appoutlet.some.some import org.junit.Assert.assertTrue import org.junit.Test @@ -10,4 +11,10 @@ class AndroidServiceLoaderTest { val result: String = some() assertTrue("Generated string should not be empty", result.isNotEmpty()) } + + @Test + fun `top-level some resolves Android types through AndroidResolverProvider`() { + val result: Any = some() + assertTrue(result is Point) + } } diff --git a/android/src/test/java/dev/appoutlet/some/android/resolver/ColorResolverTest.kt b/android/src/test/java/dev/appoutlet/some/android/resolver/ColorResolverTest.kt new file mode 100644 index 00000000..66a91b69 --- /dev/null +++ b/android/src/test/java/dev/appoutlet/some/android/resolver/ColorResolverTest.kt @@ -0,0 +1,45 @@ +package dev.appoutlet.some.android.resolver + +import android.graphics.Color +import android.os.UserHandle +import android.util.Pair +import dev.appoutlet.some.config.NullableStrategy +import dev.appoutlet.some.core.ResolverChain +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.random.Random +import kotlin.reflect.typeOf + +@RunWith(RobolectricTestRunner::class) +class ColorResolverTest { + @Test + @Config(sdk = [26]) + fun `ColorResolver generates Color values`() { + val resolver = ColorResolver(Random.Default) + val result = resolver.resolve(typeOf(), testChain) + assertTrue(result is Color) + } + + @Test + fun `ColorResolver canResolve detects Color type`() { + val resolver = ColorResolver(Random.Default) + assertTrue(resolver.canResolve(typeOf())) + } + + @Test + fun `ColorResolver rejects non-Color types`() { + val resolver = ColorResolver(Random.Default) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf>())) + } + + companion object { + private val testChain = ResolverChain(emptyList(), NullableStrategy.NullOnCircularReference) + } +} diff --git a/android/src/test/java/dev/appoutlet/some/android/resolver/PairResolverTest.kt b/android/src/test/java/dev/appoutlet/some/android/resolver/PairResolverTest.kt new file mode 100644 index 00000000..71d63f44 --- /dev/null +++ b/android/src/test/java/dev/appoutlet/some/android/resolver/PairResolverTest.kt @@ -0,0 +1,68 @@ +package dev.appoutlet.some.android.resolver + +import android.graphics.Color +import android.os.UserHandle +import android.util.Pair +import dev.appoutlet.some.config.NullableStrategy +import dev.appoutlet.some.core.Resolver +import dev.appoutlet.some.core.ResolverChain +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.random.Random +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +@RunWith(RobolectricTestRunner::class) +class PairResolverTest { + @Test + fun `PairResolver generates Pair values by delegating both generic types to the chain`() { + val resolver = PairResolver(Random.Default) + val resolvedTypes = mutableListOf() + val chain = ResolverChain( + listOf( + TrackingResolver(typeOf(), "first-value", resolvedTypes), + TrackingResolver(typeOf(), 42, resolvedTypes) + ), + NullableStrategy.NullOnCircularReference + ) + + val result = resolver.resolve(typeOf>(), chain) as Pair<*, *> + + assertEquals("first-value", result.first) + assertEquals(42, result.second) + assertEquals(listOf(typeOf(), typeOf()), resolvedTypes) + } + + @Test + fun `PairResolver canResolve detects Pair type`() { + val resolver = PairResolver(Random.Default) + assertTrue(resolver.canResolve(typeOf>())) + } + + @Test + fun `PairResolver rejects non-Pair types`() { + val resolver = PairResolver(Random.Default) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf>())) + } + + private class TrackingResolver( + private val supportedType: KType, + private val value: Any, + private val resolvedTypes: MutableList + ) : Resolver { + override fun canResolve(type: KType): Boolean = type == supportedType + + override fun resolve(type: KType, chain: ResolverChain): Any { + resolvedTypes += type + return value + } + } +} diff --git a/android/src/test/java/dev/appoutlet/some/android/resolver/UserHandleResolverTest.kt b/android/src/test/java/dev/appoutlet/some/android/resolver/UserHandleResolverTest.kt new file mode 100644 index 00000000..81a01f6b --- /dev/null +++ b/android/src/test/java/dev/appoutlet/some/android/resolver/UserHandleResolverTest.kt @@ -0,0 +1,45 @@ +package dev.appoutlet.some.android.resolver + +import android.graphics.Color +import android.os.UserHandle +import android.util.Pair +import dev.appoutlet.some.config.NullableStrategy +import dev.appoutlet.some.core.ResolverChain +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.random.Random +import kotlin.reflect.typeOf + +@RunWith(RobolectricTestRunner::class) +class UserHandleResolverTest { + @Test + @Config(sdk = [26]) + fun `UserHandleResolver generates UserHandle values when API level is supported`() { + val resolver = UserHandleResolver(Random.Default) + val result = resolver.resolve(typeOf(), testChain) + assertTrue(result is UserHandle) + } + + @Test + fun `UserHandleResolver canResolve detects UserHandle type`() { + val resolver = UserHandleResolver(Random.Default) + assertTrue(resolver.canResolve(typeOf())) + } + + @Test + fun `UserHandleResolver rejects non-UserHandle types`() { + val resolver = UserHandleResolver(Random.Default) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf>())) + } + + companion object { + private val testChain = ResolverChain(emptyList(), NullableStrategy.NullOnCircularReference) + } +} diff --git a/docs/android/index.md b/docs/android/index.md index 1934bfe6..554d6a4a 100644 --- a/docs/android/index.md +++ b/docs/android/index.md @@ -47,9 +47,12 @@ Today, the Android module adds these Android-specific types: | `android.graphics.PointF` | `some()` | Random float `x` and `y` | | `android.util.Size` | `some()` | Random positive integer `width` and `height` | | `android.util.SizeF` | `some()` | Random positive float `width` and `height` | +| `android.util.Pair` | `some>()` | Resolves `first` and `second` by delegating both generic type arguments through the resolver chain | | `android.os.Bundle` | `some()` | One random value each for `String`, `Int`, `Long`, `Float`, `Double`, and `Boolean` | +| `android.os.UserHandle` | `some()` | API 24+; generated from a random non-system UID | +| `android.graphics.Color` | `some()` | API 26+; generated from random RGB channel values | ## Platform constraints -- Android `minSdk 24` +- Android `minSdk 26` - Shared API behavior follows the same Kotlin/JVM constraints as `some-core` diff --git a/docs/getting-started.md b/docs/getting-started.md index ef565e59..dbffbb7d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -108,4 +108,4 @@ For more configuration use cases and details, see the [Configuration overview](c - **Kotlin 2.3.0+** — required for `typeOf()` and other type introspection APIs - **JVM 17+** — `some-core` targets JVM 17+ -- **Android minSdk 24** — use `some-android` for Android projects +- **Android minSdk 26** — use `some-android` for Android projects