Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

"some" is a Kotlin test data generation library that creates random instances of data classes,
sealed classes/interfaces, collections, and primitive types. It uses a resolver chain pattern
where each TypeResolver handles specific types.
where each Resolver handles specific types.

**Tech Stack:**
- Kotlin (JVM)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ val user = some<User>()
- **Universal type support** — Works with data classes, sealed classes/interfaces, object singletons, value classes, generics, and all standard collections. If Kotlin can represent it, Some can generate it.
- **Nested and recursive structures** — Handles deeply nested data classes, circular references, and recursive sealed class hierarchies without infinite loops.
- **Fine-grained control** — Override how specific fields are generated: control nullable probability, string format, collection sizes, register custom type factories for types, or use property factories for individual fields.
- **Extensible** — Ship custom `TypeResolverProvider` implementations discovered via `ServiceLoader` to add support for domain-specific, third-party, or internal application types — with custom strategies and no consumer configuration required.
- **Extensible** — Ship custom `ResolverProvider` implementations discovered via `ServiceLoader` to add support for domain-specific, third-party, or internal application types — with custom strategies and no consumer configuration required.
- **Deterministic by choice** — Set a seed for reproducible test data across runs, or default to random for variation.

## Installation
Expand Down
4 changes: 3 additions & 1 deletion android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ detekt { autoCorrect = true }
dependencies {
implementation(projects.core)

testImplementation(libs.androidx.compose.ui)
testImplementation(libs.junit)
testImplementation(libs.kotlin.test)

detektPlugins(libs.detekt.formatting)
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package dev.appoutlet.some.android

import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverProvider
import dev.appoutlet.some.core.StrategyProvider
import dev.appoutlet.some.core.TypeResolver
import dev.appoutlet.some.core.TypeResolverProvider
import kotlin.random.Random

class AndroidResolverProvider : TypeResolverProvider {
class AndroidResolverProvider : ResolverProvider {
override fun createResolvers(
strategyProvider: StrategyProvider,
random: Random
): List<TypeResolver> {
): List<Resolver> {
return emptyList()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dev.appoutlet.some.android.AndroidResolverProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.appoutlet.some.android

import dev.appoutlet.some.some
import org.junit.Assert.assertTrue
import org.junit.Test

class AndroidServiceLoaderTest {
@Test
fun `top-level some also works with AndroidTypeResolverProvider`() {
val result: String = some()
assertTrue("Generated string should not be empty", result.isNotEmpty())
}
}
6 changes: 3 additions & 3 deletions core/src/main/kotlin/dev/appoutlet/some/Some.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import dev.appoutlet.some.config.NullableStrategy
import dev.appoutlet.some.config.SomeConfig
import dev.appoutlet.some.config.SomeConfigBuilder
import dev.appoutlet.some.config.buildSomeConfig
import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.TypeResolver
import kotlin.random.Random
import kotlin.reflect.typeOf

Expand All @@ -19,7 +19,7 @@ import kotlin.reflect.typeOf
* @param config Configuration used by this generator.
*/
class Some(
val resolvers: List<TypeResolver>,
val resolvers: List<Resolver>,
val random: Random,
val config: SomeConfig
) {
Expand Down Expand Up @@ -83,7 +83,7 @@ val defaultConfig: SomeConfig by lazy { SomeConfig() }
/**
* Lazily-created default resolver chain used by top-level [some].
*/
val defaultResolvers: List<TypeResolver> by lazy { defaultConfig.buildResolvers() }
val defaultResolvers: List<Resolver> by lazy { defaultConfig.buildResolvers() }

/**
* Generates a fixture value using the default configuration.
Expand Down
22 changes: 11 additions & 11 deletions core/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package dev.appoutlet.some.config

import dev.appoutlet.some.core.FixtureContext
import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverProvider
import dev.appoutlet.some.core.StrategyProvider
import dev.appoutlet.some.core.TypeResolver
import dev.appoutlet.some.core.TypeResolverProvider
import dev.appoutlet.some.logging.logger
import dev.appoutlet.some.resolver.ArrayResolver
import dev.appoutlet.some.resolver.BigDecimalResolver
Expand All @@ -12,7 +12,7 @@ import dev.appoutlet.some.resolver.BooleanResolver
import dev.appoutlet.some.resolver.ByteResolver
import dev.appoutlet.some.resolver.CharResolver
import dev.appoutlet.some.resolver.ClassResolver
import dev.appoutlet.some.resolver.CustomTypeFactoryResolver
import dev.appoutlet.some.resolver.CustomFactoryResolver
import dev.appoutlet.some.resolver.DoubleResolver
import dev.appoutlet.some.resolver.EnumResolver
import dev.appoutlet.some.resolver.FloatResolver
Expand Down Expand Up @@ -92,7 +92,7 @@ data class SomeConfig(
*
* Resolver order defines precedence: the first resolver that supports a type is used.
*
* 1. [CustomTypeFactoryResolver] is first so explicit user factories override everything.
* 1. [CustomFactoryResolver] is first so explicit user factories override everything.
* 2. [NullableResolver] handles nullable wrappers before any concrete type resolver.
* 3. Third-party resolvers discovered via [java.util.ServiceLoader] come next, allowing external libraries to
* override built-in behavior for basic types.
Expand All @@ -104,9 +104,9 @@ data class SomeConfig(
* This keeps resolver construction free of strategy-specific knowledge.
*
* @param random Random source shared by resolvers that generate randomized values.
* @return The ordered [TypeResolver] list for this configuration.
* @return The ordered [Resolver] list for this configuration.
*/
fun buildResolvers(random: Random = buildRandom()): List<TypeResolver> {
fun buildResolvers(random: Random = buildRandom()): List<Resolver> {
val builtInResolvers = listOf(
ObjectResolver(),
EnumResolver(random),
Expand Down Expand Up @@ -143,13 +143,13 @@ data class SomeConfig(
val discoveredResolvers = discoverResolvers(strategyProvider, random)

return listOf(
CustomTypeFactoryResolver(strategyProvider, typeFactories, random),
CustomFactoryResolver(strategyProvider, typeFactories, random),
NullableResolver(strategyProvider, random),
) + discoveredResolvers + builtInResolvers + ClassResolver(strategyProvider, propertyFactories, random)
}

/**
* Discovers additional [TypeResolver]s from third-party libraries via [java.util.ServiceLoader].
* Discovers additional [Resolver]s from third-party libraries via [java.util.ServiceLoader].
*
* Failures during discovery or provider instantiation are swallowed so the library always falls back to the
* built-in resolver chain.
Expand All @@ -162,9 +162,9 @@ data class SomeConfig(
private fun discoverResolvers(
strategyProvider: StrategyProvider,
random: Random,
): List<TypeResolver> {
val loader = ServiceLoader.load(TypeResolverProvider::class.java)
val providers = mutableListOf<TypeResolverProvider>()
): List<Resolver> {
val loader = ServiceLoader.load(ResolverProvider::class.java)
val providers = mutableListOf<ResolverProvider>()
val iterator = loader.iterator()

while (iterator.hasNext()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import kotlin.reflect.KType
/**
* Resolves a requested [KType] into a generated value.
*/
interface TypeResolver {
interface Resolver {
/**
* Returns whether this resolver can generate a value for [type].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import kotlin.reflect.KType
* @param nullableStrategy Strategy used when a circular reference is detected for a nullable type.
*/
class ResolverChain(
val resolvers: List<TypeResolver>,
val resolvers: List<Resolver>,
nullableStrategy: NullableStrategy?,
) {
private val nullableStrategy = nullableStrategy ?: NullableStrategy.default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ package dev.appoutlet.some.core
import kotlin.random.Random

/**
* Service-provider interface for contributing [TypeResolver]s to the fixture generation chain.
* Service-provider interface for contributing [Resolver]s to the fixture generation chain.
*
* Implementations are discovered at runtime via [java.util.ServiceLoader]. Third-party libraries
* can ship their own provider to extend `Some` without requiring users to write configuration.
*
* Contributed resolvers are inserted between [NullableResolver][dev.appoutlet.some.resolver.NullableResolver]
* and the built-in resolvers, giving them priority over built-in type handling while still allowing
* user-registered type factories ([CustomTypeFactoryResolver][dev.appoutlet.some.resolver.CustomTypeFactoryResolver])
* user-registered type factories ([CustomTypeFactoryResolver][dev.appoutlet.some.resolver.CustomFactoryResolver])
* to take precedence.
*/
interface TypeResolverProvider {
interface ResolverProvider {
/**
* Creates resolvers to be contributed to the fixture generation chain.
*
Expand All @@ -24,5 +24,5 @@ interface TypeResolverProvider {
fun createResolvers(
strategyProvider: StrategyProvider,
random: Random,
): List<TypeResolver>
): List<Resolver>
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.config.CollectionStrategy
import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.StrategyProvider
import dev.appoutlet.some.core.TypeResolver
import dev.appoutlet.some.core.get
import kotlin.random.Random
import kotlin.reflect.KClass
Expand All @@ -18,7 +18,7 @@ import kotlin.reflect.KType
class ArrayResolver(
strategyProvider: StrategyProvider,
private val random: Random
) : TypeResolver {
) : Resolver {
private val collectionStrategy = strategyProvider.get<CollectionStrategy>() ?: CollectionStrategy.default

override fun canResolve(type: KType): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.TypeResolver
import java.math.BigDecimal
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.typeOf

class BigDecimalResolver(val random: Random) : TypeResolver {
class BigDecimalResolver(val random: Random) : Resolver {
override fun canResolve(type: KType): Boolean {
return type == typeOf<BigDecimal>()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.TypeResolver
import java.math.BigInteger
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.typeOf

class BigIntegerResolver(val random: Random) : TypeResolver {
class BigIntegerResolver(val random: Random) : Resolver {
override fun canResolve(type: KType): Boolean {
return type == typeOf<BigInteger>()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.TypeResolver
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.typeOf

class BooleanResolver(val random: Random) : TypeResolver {
class BooleanResolver(val random: Random) : Resolver {
override fun canResolve(type: KType): Boolean = type == typeOf<Boolean>()

override fun resolve(type: KType, chain: ResolverChain): Any {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.TypeResolver
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.typeOf

class ByteResolver(val random: Random) : TypeResolver {
class ByteResolver(val random: Random) : Resolver {
override fun canResolve(type: KType): Boolean = type == typeOf<Byte>()

override fun resolve(type: KType, chain: ResolverChain): Any {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.TypeResolver
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.typeOf

class CharResolver(val random: Random) : TypeResolver {
class CharResolver(val random: Random) : Resolver {
override fun canResolve(type: KType): Boolean = type == typeOf<Char>()

override fun resolve(type: KType, chain: ResolverChain): Any {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package dev.appoutlet.some.resolver

import dev.appoutlet.some.config.DefaultValueStrategy
import dev.appoutlet.some.core.FixtureContext
import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.StrategyProvider
import dev.appoutlet.some.core.TypeResolver
import dev.appoutlet.some.core.get
import dev.appoutlet.some.exception.SomeCircularReferenceException
import dev.appoutlet.some.exception.SomeInstantiationException
Expand Down Expand Up @@ -48,7 +48,7 @@ class ClassResolver(
private val strategyProvider: StrategyProvider,
private val propertyFactories: Map<Pair<KClass<*>, String>, FixtureContext.() -> Any?> = emptyMap(),
private val random: Random = Random.Default,
) : TypeResolver {
) : Resolver {
private val logger by logger()
private val defaultValueStrategy = strategyProvider.get<DefaultValueStrategy>() ?: DefaultValueStrategy.default

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.core.FixtureContext
import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.StrategyProvider
import dev.appoutlet.some.core.TypeResolver
import kotlin.random.Random
import kotlin.reflect.KClass
import kotlin.reflect.KType
Expand All @@ -22,11 +22,11 @@ import kotlin.reflect.KType
* @param typeFactories Map of classes to user-provided type factory functions.
* @param random Random source exposed to type factories through [FixtureContext].
*/
class CustomTypeFactoryResolver(
class CustomFactoryResolver(
private val strategyProvider: StrategyProvider,
private val typeFactories: Map<KClass<*>, FixtureContext.() -> Any?>,
private val random: Random,
) : TypeResolver {
) : Resolver {
/**
* Returns whether [type] has a registered type factory.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.TypeResolver
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.typeOf

class DoubleResolver(val random: Random) : TypeResolver {
class DoubleResolver(val random: Random) : Resolver {
override fun canResolve(type: KType): Boolean = type == typeOf<Double>()

override fun resolve(type: KType, chain: ResolverChain): Any {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.TypeResolver
import kotlin.random.Random
import kotlin.reflect.KClass
import kotlin.reflect.KType

class EnumResolver(val random: Random) : TypeResolver {
class EnumResolver(val random: Random) : Resolver {
override fun canResolve(type: KType): Boolean {
val kClass = type.classifier as? KClass<*> ?: return false
return kClass.java.isEnum
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.TypeResolver
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.typeOf

class FloatResolver(val random: Random) : TypeResolver {
class FloatResolver(val random: Random) : Resolver {
override fun canResolve(type: KType): Boolean = type == typeOf<Float>()

override fun resolve(type: KType, chain: ResolverChain): Any {
Expand Down
Loading