From 498fc9d3a238bbf05e952beb83b6b4417850fc06 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Mon, 6 Jul 2026 17:30:32 +0100 Subject: [PATCH 1/3] Color, Pair, UserHandle --- .../some/android/resolver/ColorResolver.kt | 27 ++++++++ .../some/android/resolver/PairResolver.kt | 30 ++++++++ .../android/resolver/UserHandleResolver.kt | 23 +++++++ .../android/resolver/ColorResolverTest.kt | 45 ++++++++++++ .../some/android/resolver/PairResolverTest.kt | 68 +++++++++++++++++++ .../resolver/UserHandleResolverTest.kt | 45 ++++++++++++ docs/android/index.md | 5 +- docs/getting-started.md | 2 +- 8 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 android/src/main/java/dev/appoutlet/some/android/resolver/ColorResolver.kt create mode 100644 android/src/main/java/dev/appoutlet/some/android/resolver/PairResolver.kt create mode 100644 android/src/main/java/dev/appoutlet/some/android/resolver/UserHandleResolver.kt create mode 100644 android/src/test/java/dev/appoutlet/some/android/resolver/ColorResolverTest.kt create mode 100644 android/src/test/java/dev/appoutlet/some/android/resolver/PairResolverTest.kt create mode 100644 android/src/test/java/dev/appoutlet/some/android/resolver/UserHandleResolverTest.kt 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..04c64e4b --- /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.nextInt(0, 255).toFloat(), + random.nextInt(0, 255).toFloat(), + random.nextInt(0, 255).toFloat(), + ) + } +} 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/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 From bb099dbfd1948bf80c03447780c1fdc9d19d3e80 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Mon, 6 Jul 2026 17:31:53 +0100 Subject: [PATCH 2/3] Color max value --- .../dev/appoutlet/some/android/resolver/ColorResolver.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 index 04c64e4b..c47b6550 100644 --- a/android/src/main/java/dev/appoutlet/some/android/resolver/ColorResolver.kt +++ b/android/src/main/java/dev/appoutlet/some/android/resolver/ColorResolver.kt @@ -7,6 +7,8 @@ import kotlin.random.Random import kotlin.reflect.KType import kotlin.reflect.typeOf +private const val COLOR_MAX_VALUE = 255 + /** * Resolves [Color] values when the runtime API level supports [Color.valueOf]. * @@ -19,9 +21,9 @@ class ColorResolver( override fun resolve(type: KType, chain: ResolverChain): Color { return Color.valueOf( - random.nextInt(0, 255).toFloat(), - random.nextInt(0, 255).toFloat(), - random.nextInt(0, 255).toFloat(), + random.nextInt(0, COLOR_MAX_VALUE).toFloat(), + random.nextInt(0, COLOR_MAX_VALUE).toFloat(), + random.nextInt(0, COLOR_MAX_VALUE).toFloat(), ) } } From f2c57d14c8de7ca6d8cea5e02a43c4f47d8ee61f Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Mon, 6 Jul 2026 17:45:41 +0100 Subject: [PATCH 3/3] fix stuff --- .../some/android/AndroidResolverProvider.kt | 37 ++++++++++++++++++- .../some/android/resolver/ColorResolver.kt | 8 ++-- .../some/android/AndroidServiceLoaderTest.kt | 7 ++++ 3 files changed, 46 insertions(+), 6 deletions(-) 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 index c47b6550..7d7f6281 100644 --- a/android/src/main/java/dev/appoutlet/some/android/resolver/ColorResolver.kt +++ b/android/src/main/java/dev/appoutlet/some/android/resolver/ColorResolver.kt @@ -7,8 +7,6 @@ import kotlin.random.Random import kotlin.reflect.KType import kotlin.reflect.typeOf -private const val COLOR_MAX_VALUE = 255 - /** * Resolves [Color] values when the runtime API level supports [Color.valueOf]. * @@ -21,9 +19,9 @@ class ColorResolver( override fun resolve(type: KType, chain: ResolverChain): Color { return Color.valueOf( - random.nextInt(0, COLOR_MAX_VALUE).toFloat(), - random.nextInt(0, COLOR_MAX_VALUE).toFloat(), - random.nextInt(0, COLOR_MAX_VALUE).toFloat(), + random.nextFloat(), + random.nextFloat(), + random.nextFloat(), ) } } 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) + } }