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
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import kotlinx.kover.gradle.plugin.dsl.CoverageUnit

plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.autoservice.ir)
alias(libs.plugins.detekt)
alias(libs.plugins.dokka)
alias(libs.plugins.gitHooks)
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[versions]
kotlin = "2.3.21"
autoserviceIr = "0.1.5"
detekt = "1.23.8"
dokka = "2.2.0"
gitHooksPlugin = "1.1.1"
Expand All @@ -15,6 +16,7 @@ kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
autoservice-ir = { id = "com.fueledbycaffeine.autoservice", version.ref = "autoserviceIr" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
gitHooks = { id = "eu.bambooapps.gradle.plugin.githook", version.ref = "gitHooksPlugin" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.appoutlet.some.config

import dev.appoutlet.some.core.StrategyProvider
import kotlin.reflect.KClass

/**
* Default [StrategyProvider] implementation backed by a map of registered strategy instances.
*
* The provider is intentionally narrow: it exposes only strategy lookup and hides all other configuration
* details from resolvers.
*
* @param strategies Map of strategy instances keyed by their base type.
*/
class DefaultStrategyProvider(
private val strategies: Map<KClass<out Strategy>, Strategy> = emptyMap()
) : StrategyProvider {
@Suppress("UNCHECKED_CAST")
override operator fun <T : Strategy> get(key: KClass<T>): T? = strategies[key] as? T
}
93 changes: 63 additions & 30 deletions src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dev.appoutlet.some.config
import dev.appoutlet.some.core.FixtureContext
import dev.appoutlet.some.core.StrategyProvider
import dev.appoutlet.some.core.TypeResolver
import dev.appoutlet.some.core.TypeResolverProvider
import dev.appoutlet.some.resolver.ArrayResolver
import dev.appoutlet.some.resolver.BigDecimalResolver
import dev.appoutlet.some.resolver.BigIntegerResolver
Expand Down Expand Up @@ -36,6 +37,7 @@ import dev.appoutlet.some.resolver.SetResolver
import dev.appoutlet.some.resolver.ShortResolver
import dev.appoutlet.some.resolver.StringResolver
import dev.appoutlet.some.resolver.ValueClassResolver
import java.util.ServiceLoader
import kotlin.random.Random
import kotlin.reflect.KClass

Expand All @@ -52,22 +54,21 @@ import kotlin.reflect.KClass
* @param propertyFactories Custom property factories keyed by class and property name.
*/
data class SomeConfig(
val strategies: Map<KClass<out Strategy>, Strategy> = defaultStrategies(),
val strategies: Map<KClass<out Strategy>, Strategy> = emptyMap(),
val seed: Long? = null,
val typeFactories: Map<KClass<*>, FixtureContext.() -> Any?> = emptyMap(),
val propertyFactories: Map<Pair<KClass<*>, String>, FixtureContext.() -> Any?> = emptyMap(),
) : StrategyProvider {
private val strategyProvider: StrategyProvider = DefaultStrategyProvider(strategies)

/**
* Returns the strategy registered for [key].
*
* @param key The [KClass] of the strategy to retrieve (e.g. [NullableStrategy::class]).
* @return The registered strategy instance.
* @throws NoSuchElementException when no strategy is registered for [key].
*/
@Suppress("UNCHECKED_CAST")
override fun <T : Strategy> get(key: KClass<T>): T? {
return strategies[key] as? T
}
override fun <T : Strategy> get(key: KClass<T>): T? = strategyProvider[key]

/**
* Creates a [SomeConfigBuilder] pre-populated with this configuration's values.
Expand All @@ -87,22 +88,31 @@ data class SomeConfig(
/**
* Creates the resolver chain used to generate fixture values.
*
* Resolver order defines precedence: the first resolver that supports a type is used. Type factories are first so
* explicit user configuration overrides built-in behavior. [ClassResolver] is last because it is the
* fallback for constructable model classes and is also where property factories are applied.
* Resolver order defines precedence: the first resolver that supports a type is used.
*
* 1. [CustomTypeFactoryResolver] 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.
* 4. Built-in resolvers handle remaining standard types.
* 5. [ClassResolver] is last because it is the fallback for constructable model classes and is also where
* property factories are applied.
*
* Resolvers that need strategies receive a [StrategyProvider]; the rest receive only the shared [Random] source.
* 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.
*/
fun buildResolvers(random: Random = buildRandom()): List<TypeResolver> {
return listOf(
CustomTypeFactoryResolver(typeFactories, random, this),
NullableResolver(this[NullableStrategy::class], random),
val strategyProvider = DefaultStrategyProvider(strategies)
Comment thread
MessiasLima marked this conversation as resolved.
Outdated

val builtInResolvers = listOf(
ObjectResolver(),
EnumResolver(random),
SealedClassResolver(random),
ValueClassResolver(),
StringResolver(this[StringStrategy::class], random),
StringResolver(strategyProvider, random),
IntResolver(random),
LongResolver(random),
DoubleResolver(random),
Expand All @@ -119,17 +129,52 @@ data class SomeConfig(
JavaInstantResolver(random),
JavaDurationResolver(random),
JavaZonedDateTimeResolver(random),
OptionalResolver(this[NullableStrategy::class], random),
OptionalResolver(strategyProvider, random),
BigDecimalResolver(random),
BigIntegerResolver(random),
LocalDateResolver(random),
LocalDateTimeResolver(random),
ListResolver(this[CollectionStrategy::class], random),
SetResolver(this[CollectionStrategy::class], random),
MapResolver(this[CollectionStrategy::class], random),
ArrayResolver(this[CollectionStrategy::class], random),
ClassResolver(propertyFactories, random, this),
ListResolver(strategyProvider, random),
SetResolver(strategyProvider, random),
MapResolver(strategyProvider, random),
ArrayResolver(strategyProvider, random),
)

val discoveredResolvers = discoverResolvers(strategyProvider, random)

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

/**
* Discovers additional [TypeResolver]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.
*
* @param strategyProvider Provider of all configured generation strategies.
* @param random Random source shared by discovered resolvers.
* @return Ordered list of discovered resolvers, or an empty list if discovery fails.
*/
private fun discoverResolvers(
strategyProvider: StrategyProvider,
random: Random,
): List<TypeResolver> {
val providers = try {
ServiceLoader.load(TypeResolverProvider::class.java).toList()
} catch (_: Throwable) {
return emptyList()
}

return providers.flatMap { provider ->
try {
provider.createResolvers(strategyProvider, random)
} catch (_: Throwable) {
emptyList()
}
}
Comment thread
MessiasLima marked this conversation as resolved.
Outdated
}

/**
Expand All @@ -141,16 +186,4 @@ data class SomeConfig(
* @return [Random] seeded with [seed] if set, or [Random.Default] otherwise.
*/
internal fun buildRandom(): Random = seed?.let { Random(it) } ?: Random.Default

companion object {
/**
* Returns the default strategy map with sensible defaults for all built-in strategies.
*/
fun defaultStrategies(): Map<KClass<out Strategy>, Strategy> = mapOf(
NullableStrategy::class to NullableStrategy.default,
StringStrategy::class to StringStrategy.default,
CollectionStrategy::class to CollectionStrategy.default,
DefaultValueStrategy::class to DefaultValueStrategy.default,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class SomeConfigBuilder {
* @return A new [SomeConfig] instance with the configured values.
*/
fun build(): SomeConfig = SomeConfig(
strategies = SomeConfig.defaultStrategies() + _strategies,
strategies = _strategies,
Comment thread
MessiasLima marked this conversation as resolved.
Outdated
seed = seed,
typeFactories = _typeFactories.toMap(),
propertyFactories = _propertyFactories.toMap()
Expand Down
10 changes: 10 additions & 0 deletions src/main/kotlin/dev/appoutlet/some/core/TypeResolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ package dev.appoutlet.some.core

import kotlin.reflect.KType

/**
* Resolves a requested [KType] into a generated value.
*/
interface TypeResolver {
/**
* Returns whether this resolver can generate a value for [type].
*/
fun canResolve(type: KType): Boolean

/**
* Generates a value for [type], delegating to [chain] for nested types when needed.
*/
fun resolve(type: KType, chain: ResolverChain): Any?
}
23 changes: 23 additions & 0 deletions src/main/kotlin/dev/appoutlet/some/core/TypeResolverProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.appoutlet.some.core

import kotlin.random.Random

/**
* Service-provider interface for contributing [TypeResolver]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.
*/
interface TypeResolverProvider {
/**
* Creates resolvers to be appended to the built-in resolver chain.
*
* @param strategyProvider Provider of all configured generation strategies.
* @param random Shared random source.
* @return Ordered list of resolvers contributed by this provider.
*/
fun createResolvers(
strategyProvider: StrategyProvider,
random: Random,
): List<TypeResolver>
}
10 changes: 6 additions & 4 deletions src/main/kotlin/dev/appoutlet/some/resolver/ArrayResolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ package dev.appoutlet.some.resolver

import dev.appoutlet.some.config.CollectionStrategy
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
import kotlin.reflect.KType

/**
* Resolves [Array] types using the active [CollectionStrategy].
*
* @param collectionStrategy Strategy for determining array sizes. Defaults to [CollectionStrategy.default] when null.
* @param strategyProvider Provider of all configured generation strategies.
* @param random Random source used for determining array size within the configured range.
*/
class ArrayResolver(
collectionStrategy: CollectionStrategy?,
val random: Random
strategyProvider: StrategyProvider,
private val random: Random
) : TypeResolver {
private val collectionStrategy = collectionStrategy ?: CollectionStrategy.default
private val collectionStrategy = strategyProvider.get<CollectionStrategy>() ?: CollectionStrategy.default

override fun canResolve(type: KType): Boolean {
val kClass = type.classifier as? KClass<*> ?: return false
Expand Down
7 changes: 4 additions & 3 deletions src/main/kotlin/dev/appoutlet/some/resolver/ClassResolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dev.appoutlet.some.core.FixtureContext
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
import java.lang.reflect.Modifier
Expand Down Expand Up @@ -38,16 +39,16 @@ import kotlin.reflect.jvm.isAccessible
* Generic type arguments from the requested [KType] are mapped onto constructor parameter types before delegation,
* allowing declarations such as `Box<String>` to resolve constructor parameters declared as `T`.
*
* @param strategyProvider Provides all registered generation strategies to property factories through [FixtureContext].
* @param propertyFactories Per-property factories keyed by owning class and constructor parameter name.
* @param random Random source exposed to property factories through [FixtureContext].
* @param strategyProvider Provides all registered generation strategies to property factories through [FixtureContext].
*/
class ClassResolver(
val strategyProvider: StrategyProvider,
Comment thread
MessiasLima marked this conversation as resolved.
Outdated
private val propertyFactories: Map<Pair<KClass<*>, String>, FixtureContext.() -> Any?> = emptyMap(),
private val random: Random = Random.Default,
private val strategyProvider: StrategyProvider,
) : TypeResolver {
private val defaultValueStrategy = strategyProvider[DefaultValueStrategy::class] ?: DefaultValueStrategy.default
private val defaultValueStrategy = strategyProvider.get<DefaultValueStrategy>() ?: DefaultValueStrategy.default

/**
* Returns whether [type] can be instantiated by this resolver.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import kotlin.reflect.KType
* Type factories receive a [FixtureContext] containing the active random source, resolution stack, and strategy
* provider. This lets user code produce values that still respect the current fixture configuration.
*
* @param strategyProvider Provides all registered generation strategies to type factories through [FixtureContext].
* @param typeFactories Map of classes to user-provided type factory functions.
* @param random Random source exposed to type factories through [FixtureContext].
* @param strategyProvider Provides all registered generation strategies to type factories through [FixtureContext].
*/
class CustomTypeFactoryResolver(
val strategyProvider: StrategyProvider,
Comment thread
MessiasLima marked this conversation as resolved.
Outdated
private val typeFactories: Map<KClass<*>, FixtureContext.() -> Any?>,
private val random: Random,
private val strategyProvider: StrategyProvider,
) : TypeResolver {
/**
* Returns whether [type] has a registered type factory.
Expand Down
9 changes: 5 additions & 4 deletions src/main/kotlin/dev/appoutlet/some/resolver/ListResolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package dev.appoutlet.some.resolver

import dev.appoutlet.some.config.CollectionStrategy
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
import kotlin.reflect.KType
Expand All @@ -13,15 +15,14 @@ import kotlin.reflect.typeOf
/**
* Resolves [List] and [MutableList] types using the active [CollectionStrategy].
*
* @param collectionStrategy Strategy for determining collection sizes. Defaults to
* [CollectionStrategy.default] when null.
* @param strategyProvider Provider of all configured generation strategies.
* @param random Random source used for determining list size within the configured range.
*/
class ListResolver(
collectionStrategy: CollectionStrategy?,
strategyProvider: StrategyProvider,
val random: Random
Comment thread
MessiasLima marked this conversation as resolved.
Outdated
) : TypeResolver {
private val collectionStrategy = collectionStrategy ?: CollectionStrategy.default
private val collectionStrategy = strategyProvider.get<CollectionStrategy>() ?: CollectionStrategy.default

override fun canResolve(type: KType): Boolean {
val kClass = type.classifier as? KClass<*> ?: return false
Expand Down
11 changes: 6 additions & 5 deletions src/main/kotlin/dev/appoutlet/some/resolver/MapResolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package dev.appoutlet.some.resolver

import dev.appoutlet.some.config.CollectionStrategy
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
import kotlin.reflect.KType
Expand All @@ -13,15 +15,14 @@ import kotlin.reflect.typeOf
/**
* Resolves [Map] and [MutableMap] types using the active [CollectionStrategy].
*
* @param collectionStrategy Strategy for determining collection sizes.
* Defaults to [CollectionStrategy.default] when null.
* @param strategyProvider Provider of all configured generation strategies.
* @param random Random source used for determining map size within the configured range.
*/
class MapResolver(
collectionStrategy: CollectionStrategy?,
val random: Random
strategyProvider: StrategyProvider,
private val random: Random
) : TypeResolver {
private val collectionStrategy = collectionStrategy ?: CollectionStrategy.default
private val collectionStrategy = strategyProvider.get<CollectionStrategy>() ?: CollectionStrategy.default

override fun canResolve(type: KType): Boolean {
val kClass = type.classifier as? KClass<*> ?: return false
Expand Down
Loading