diff --git a/README.md b/README.md index 74c1bed9..fb2af95a 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ val user = some() - **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. - **Deterministic by choice** — Set a seed for reproducible test data across runs, or default to random for variation. ## Installation diff --git a/build.gradle.kts b/build.gradle.kts index 872e4967..62102fd9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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) @@ -22,6 +23,7 @@ dependencies { dokkaHtmlPlugin(libs.dokka.versioning) implementation(libs.kotlin.reflect) + implementation(libs.kermit) testImplementation(libs.kotlin.test) } diff --git a/docs/custom-factories.md b/docs/custom-factories.md index 02b7d583..0a7cd28b 100644 --- a/docs/custom-factories.md +++ b/docs/custom-factories.md @@ -146,3 +146,5 @@ val stillBase = someWithDefaults() ## Choosing the right override Prefer property factories for test readability when only one field matters. Prefer type factories when a type has domain rules, validation constraints, or a value format that should be consistent wherever that type appears. + +If you are building a library or need to provide resolver logic for internal application types, consider shipping a [custom resolver](custom-resolvers.md) instead. Custom resolvers are discovered automatically via `ServiceLoader` and don't require consumers to write any configuration. diff --git a/docs/custom-resolvers.md b/docs/custom-resolvers.md new file mode 100644 index 00000000..80df0af2 --- /dev/null +++ b/docs/custom-resolvers.md @@ -0,0 +1,269 @@ +--- +icon: lucide/puzzle +--- +# Custom Resolvers + +Some discovers custom `TypeResolver` implementations at runtime through Java's `ServiceLoader` mechanism. This lets you extend Some with support for domain-specific, third-party, or internal application types **without requiring every consumer to write configuration code**. + +If you only need to override how a single type is generated in your own tests, a [custom factory](custom-factories.md) is simpler. Use a custom resolver when you are **building a library** that others will depend on, **adding support for internal types in your own application**, or when the type needs its own resolution logic that delegates to the resolver chain. + +## How it works + +Some defines a service-provider interface called `TypeResolverProvider`. When fixture generation starts, Some calls `ServiceLoader.load(TypeResolverProvider::class.java)` to discover all implementations on the classpath. Each provider returns a list of [TypeResolver] instances that are inserted into the resolver chain. + +The resolver chain order is: + +1. **Custom type factories** — explicit user factories registered with `factory()`. +2. **Nullable resolver** — handles nullable wrappers before concrete types. +3. **Discovered resolvers** — contributed by `TypeResolverProvider` implementations. +4. **Built-in resolvers** — standard types like `String`, `Int`, `List`, etc. +5. **Class resolver** — fallback for data classes and other constructable types. + +Because discovered resolvers sit between nullable handling and built-in resolvers, a third-party resolver for `String` would take precedence over the built-in `StringResolver`, while still allowing a user-registered type factory to override everything. + +Misbehaving providers are silently skipped. If a provider throws during discovery or resolver creation, Some continues with the remaining providers and built-in chain. + +## Implementing a TypeResolverProvider + +### 1. Create a resolver + +A resolver implements the `TypeResolver` interface with two methods: + +```kotlin +import dev.appoutlet.some.core.ResolverChain +import dev.appoutlet.some.core.TypeResolver +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +class UrlResolver : TypeResolver { + + override fun canResolve(type: KType): Boolean = type == typeOf() + + override fun resolve(type: KType, chain: ResolverChain): Any { + return java.net.URL("https://example.com") + } +} +``` + +- Use `typeOf()` for exact type matching. Avoid `type.toString().contains(...)` — it can cause false positives. +- `resolve()` returns `Any?`. Delegate nested type resolution to `chain.resolve(type)` when needed. + +### 2. Access strategies + +Resolvers that need strategies receive a `StrategyProvider` via the provider. Retrieve a strategy with the reified `get()` extension and fall back to a sensible default: + +```kotlin +import dev.appoutlet.some.config.StringStrategy +import dev.appoutlet.some.core.StrategyProvider +import dev.appoutlet.some.core.TypeResolver +import dev.appoutlet.some.core.get +import kotlin.random.Random + +class UrlResolver( + strategyProvider: StrategyProvider, + private val random: Random, +) : TypeResolver { + private val stringStrategy = strategyProvider.get() ?: StringStrategy.default + + // ... +} +``` + +### 3. Create the provider + +The provider is the entry point that Some discovers via `ServiceLoader`. It receives the `StrategyProvider` and `Random` from the current configuration so that contributed resolvers stay consistent with the rest of the chain: + +```kotlin +import dev.appoutlet.some.core.StrategyProvider +import dev.appoutlet.some.core.TypeResolver +import dev.appoutlet.some.core.TypeResolverProvider +import kotlin.random.Random + +class UrlResolverProvider : TypeResolverProvider { + override fun createResolvers( + strategyProvider: StrategyProvider, + random: Random, + ): List = listOf(UrlResolver(strategyProvider, random)) +} +``` + +### 4. Register the provider + +Create a file at: + +``` +META-INF/services/dev.appoutlet.some.core.TypeResolverProvider +``` + +containing the fully qualified class name of your provider: + +``` +com.example.some.UrlResolverProvider +``` + +Place this file in your JAR's `resources/META-INF/services/` directory. Some will discover it automatically at runtime. + +#### Using @AutoService + +If you use the [autoservice-ir](https://github.com/joshfriend/autoservice-ir) compiler plugin, you can annotate your provider instead of maintaining the service file manually: + +```kotlin +import com.fueledbycaffeine.autoservice.AutoService +import dev.appoutlet.some.core.TypeResolverProvider + +@AutoService +class UrlResolverProvider : TypeResolverProvider { + // ... +} +``` + +Then apply the plugin in `build.gradle.kts`: + +```kotlin +plugins { + id("com.fueledbycaffeine.autoservice") version "0.1.5" +} +``` + +## Full example + +Here is a complete example that adds support for `java.net.URL`: + +```kotlin +// UrlResolver.kt +package com.example.some + +import dev.appoutlet.some.core.ResolverChain +import dev.appoutlet.some.core.StrategyProvider +import dev.appoutlet.some.core.TypeResolver +import dev.appoutlet.some.config.StringStrategy +import dev.appoutlet.some.core.get +import kotlin.random.Random +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +class UrlResolver( + private val strategyProvider: StrategyProvider, + private val random: Random, +) : TypeResolver { + private val stringStrategy = strategyProvider.get() ?: StringStrategy.default + + override fun canResolve(type: KType): Boolean = type == typeOf() + + override fun resolve(type: KType, chain: ResolverChain): Any { + val host = when (stringStrategy) { + is StringStrategy.Readable -> "example-${random.nextInt(1000)}.com" + else -> "example.com" + } + return java.net.URL("https://$host") + } +} +``` + +```kotlin +// UrlResolverProvider.kt +package com.example.some + +import dev.appoutlet.some.core.StrategyProvider +import dev.appoutlet.some.core.TypeResolver +import dev.appoutlet.some.core.TypeResolverProvider +import kotlin.random.Random + +class UrlResolverProvider : TypeResolverProvider { + override fun createResolvers( + strategyProvider: StrategyProvider, + random: Random, + ): List = listOf(UrlResolver(strategyProvider, random)) +} +``` + +With this provider on the classpath, any project that depends on your library — or any module in your own application — can generate `URL` values without additional configuration: + +```kotlin +val url: java.net.URL = some() +// https://example-42.com +``` + +## Delegating to the resolver chain + +Resolvers can delegate nested type resolution to `chain.resolve(type)`. This is useful when your custom type contains fields that Some should still generate automatically: + +```kotlin +data class Email(val address: String) + +class EmailResolver : TypeResolver { + override fun canResolve(type: KType): Boolean = type == typeOf() + + override fun resolve(type: KType, chain: ResolverChain): Any { + val address = chain.resolve(typeOf()) as String + return Email("$address@example.com") + } +} +``` + +## Custom strategies + +Your resolver can define its own strategy to let consumers control its behavior. A strategy is any class that implements the `Strategy` marker interface and provides a `key` property: + +```kotlin +import dev.appoutlet.some.config.Strategy +import kotlin.reflect.KClass + +sealed interface UrlStrategy : Strategy { + override val key get() = UrlStrategy::class + + data class Fixed(val url: String = "https://example.com") : UrlStrategy + data class RandomHost(val tld: String = "com") : UrlStrategy + + companion object { + val default: UrlStrategy get() = Fixed() + } +} +``` + +Register the strategy like any built-in one: + +```kotlin +val url = some { + strategy(UrlStrategy.RandomHost(tld = "org")) +} +``` + +Then retrieve it in your resolver through the `StrategyProvider`: + +```kotlin +class UrlResolver( + strategyProvider: StrategyProvider, + private val random: Random, +) : TypeResolver { + private val urlStrategy = strategyProvider.get() ?: UrlStrategy.default + + override fun canResolve(type: KType): Boolean = type == typeOf() + + override fun resolve(type: KType, chain: ResolverChain): Any = when (urlStrategy) { + is UrlStrategy.Fixed -> java.net.URL(urlStrategy.url) + is UrlStrategy.RandomHost -> { + val host = "host-${random.nextInt(1000)}.${urlStrategy.tld}" + java.net.URL("https://$host") + } + } +} +``` + +When no strategy is registered, the resolver falls back to `UrlStrategy.default` — the same pattern used by all built-in resolvers. + +## Custom factories vs. custom resolvers + +| | Custom factory | Custom resolver | +|---|---|---| +| **Registered by** | Test code via `factory()` or `property()` | `ServiceLoader` (library or app module) | +| **Priority** | Highest — always wins | Between nullable and built-in | +| **Access to chain** | No (direct value construction) | Yes (can delegate via `chain.resolve`) | +| **Access to strategies** | Via `FixtureContext.strategyProvider` | Via `StrategyProvider` parameter | +| **Best for** | One-off overrides in test suites | Reusable extensions, internal app types, libraries | + +## Error handling + +If a `TypeResolverProvider` throws during discovery or resolver creation, Some catches the error and continues with the remaining providers. The built-in resolver chain is always available as a fallback. This means your application will still work even if an extension fails to load. + +[TypeResolver]: ../reference/latest/index.html \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index bd63ab51..d68acc8c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -63,6 +63,7 @@ Writing tests means creating test data — lots of it. Constructing data classes - **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. - **Deterministic by choice** — Set a seed for reproducible test data across runs, or default to random for variation. ## Quick start diff --git a/docs/supported-types.md b/docs/supported-types.md index 485c2729..14b3461e 100644 --- a/docs/supported-types.md +++ b/docs/supported-types.md @@ -5,7 +5,7 @@ icon: lucide/file-type Some resolves common Kotlin and Java types with zero configuration. Nullable variants (`T?`) are supported everywhere — [NullableStrategy](configuration/nullable-strategy.md) controls whether `null` is emitted. -For types not listed here, register a [custom factory](custom-factories.md). +For types not listed here, register a [custom factory](custom-factories.md) or ship a [custom resolver](custom-resolvers.md). ## Reference @@ -64,3 +64,5 @@ someSetup { ``` See [Type and Property Factories](custom-factories.md) for more. + +For library authors or application developers who want to bundle resolver logic for a custom type, see [Custom Resolvers](custom-resolvers.md) to learn about `TypeResolverProvider`. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c6d7cb36..b34326bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" @@ -9,12 +10,14 @@ mavenPublish = "0.36.0" [libraries] detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } dokka-versioning = { module = "org.jetbrains.dokka:versioning-plugin", version.ref = "dokka" } +kermit = { module = "co.touchlab:kermit", version = "2.0.4" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } [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" } diff --git a/src/main/kotlin/dev/appoutlet/some/config/DefaultStrategyProvider.kt b/src/main/kotlin/dev/appoutlet/some/config/DefaultStrategyProvider.kt new file mode 100644 index 00000000..76e602f5 --- /dev/null +++ b/src/main/kotlin/dev/appoutlet/some/config/DefaultStrategyProvider.kt @@ -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, Strategy> = emptyMap() +) : StrategyProvider { + @Suppress("UNCHECKED_CAST") + override operator fun get(key: KClass): T? = strategies[key] as? T +} diff --git a/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt b/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt index 73ae3280..7d7c429e 100644 --- a/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt +++ b/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt @@ -3,6 +3,8 @@ 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.logging.logger import dev.appoutlet.some.resolver.ArrayResolver import dev.appoutlet.some.resolver.BigDecimalResolver import dev.appoutlet.some.resolver.BigIntegerResolver @@ -36,6 +38,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 @@ -52,11 +55,14 @@ import kotlin.reflect.KClass * @param propertyFactories Custom property factories keyed by class and property name. */ data class SomeConfig( - val strategies: Map, Strategy> = defaultStrategies(), + val strategies: Map, Strategy> = emptyMap(), val seed: Long? = null, val typeFactories: Map, FixtureContext.() -> Any?> = emptyMap(), val propertyFactories: Map, String>, FixtureContext.() -> Any?> = emptyMap(), ) : StrategyProvider { + private val strategyProvider: StrategyProvider = DefaultStrategyProvider(strategies) + private val logger by logger() + /** * Returns the strategy registered for [key]. * @@ -64,10 +70,7 @@ data class SomeConfig( * @return The registered strategy instance. * @throws NoSuchElementException when no strategy is registered for [key]. */ - @Suppress("UNCHECKED_CAST") - override fun get(key: KClass): T? { - return strategies[key] as? T - } + override fun get(key: KClass): T? = strategyProvider[key] /** * Creates a [SomeConfigBuilder] pre-populated with this configuration's values. @@ -87,22 +90,29 @@ 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 { - return listOf( - CustomTypeFactoryResolver(typeFactories, random, this), - NullableResolver(this[NullableStrategy::class], random), + val builtInResolvers = listOf( ObjectResolver(), EnumResolver(random), SealedClassResolver(random), ValueClassResolver(), - StringResolver(this[StringStrategy::class], random), + StringResolver(strategyProvider, random), IntResolver(random), LongResolver(random), DoubleResolver(random), @@ -119,17 +129,60 @@ 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. + */ + @Suppress("TooGenericExceptionCaught") + private fun discoverResolvers( + strategyProvider: StrategyProvider, + random: Random, + ): List { + val loader = ServiceLoader.load(TypeResolverProvider::class.java) + val providers = mutableListOf() + val iterator = loader.iterator() + + while (iterator.hasNext()) { + try { + providers.add(iterator.next()) + } catch (e: Throwable) { + logger.w(e) { "Failed to load a TypeResolverProvider implementation" } + } + } + + return providers.flatMap { provider -> + try { + provider.createResolvers(strategyProvider, random) + } catch (e: Throwable) { + logger.w(e) { "Failed to instantiate resolver provider: ${provider::class.simpleName}" } + emptyList() + } + } } /** @@ -141,16 +194,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, Strategy> = mapOf( - NullableStrategy::class to NullableStrategy.default, - StringStrategy::class to StringStrategy.default, - CollectionStrategy::class to CollectionStrategy.default, - DefaultValueStrategy::class to DefaultValueStrategy.default, - ) - } } diff --git a/src/main/kotlin/dev/appoutlet/some/config/SomeConfigBuilder.kt b/src/main/kotlin/dev/appoutlet/some/config/SomeConfigBuilder.kt index 332a6406..0f284038 100644 --- a/src/main/kotlin/dev/appoutlet/some/config/SomeConfigBuilder.kt +++ b/src/main/kotlin/dev/appoutlet/some/config/SomeConfigBuilder.kt @@ -50,8 +50,9 @@ class SomeConfigBuilder { * Registers a strategy for fixture generation. * * The strategy replaces any previous strategy registered under the same base type. - * Built-in strategies ([NullableStrategy], [StringStrategy], [CollectionStrategy], - * [DefaultValueStrategy]) are pre-populated with sensible defaults. + * When no strategy is explicitly registered, each resolver falls back to its own + * sensible default (e.g. [NullableStrategy.NullOnCircularReference], + * [StringStrategy.Random], [CollectionStrategy] with a range of 1..5). * * @param strategy The strategy instance to register. */ @@ -130,7 +131,7 @@ class SomeConfigBuilder { * @return A new [SomeConfig] instance with the configured values. */ fun build(): SomeConfig = SomeConfig( - strategies = SomeConfig.defaultStrategies() + _strategies, + strategies = _strategies.toMap(), seed = seed, typeFactories = _typeFactories.toMap(), propertyFactories = _propertyFactories.toMap() diff --git a/src/main/kotlin/dev/appoutlet/some/core/StrategyProvider.kt b/src/main/kotlin/dev/appoutlet/some/core/StrategyProvider.kt index 053f0e7b..f01c5efc 100644 --- a/src/main/kotlin/dev/appoutlet/some/core/StrategyProvider.kt +++ b/src/main/kotlin/dev/appoutlet/some/core/StrategyProvider.kt @@ -31,18 +31,17 @@ import kotlin.reflect.KClass */ interface StrategyProvider { /** - * Returns the strategy instance registered for [key]. + * Returns the strategy instance registered for [key], or `null` when no strategy is registered. * * @param key The [KClass] of the strategy to retrieve * (e.g. [NullableStrategy::class][dev.appoutlet.some.config.NullableStrategy]). - * @return The registered strategy instance. - * @throws NoSuchElementException when no strategy is registered for [key]. + * @return The registered strategy instance, or `null` when no strategy is registered for [key]. */ operator fun get(key: KClass): T? } /** - * Returns the strategy instance of type [T] registered for this provider. + * Returns the strategy instance of type [T] registered for this provider, or `null` when no strategy is registered. * * This is a convenience extension that allows idiomatic usage without explicitly passing a [KClass]: * @@ -51,7 +50,6 @@ interface StrategyProvider { * ``` * * @param T The type of the strategy to retrieve. - * @return The registered strategy instance. - * @throws NoSuchElementException when no strategy is registered for [T]. + * @return The registered strategy instance, or `null` when no strategy is registered for [T]. */ inline fun StrategyProvider.get(): T? = get(T::class) diff --git a/src/main/kotlin/dev/appoutlet/some/core/TypeResolver.kt b/src/main/kotlin/dev/appoutlet/some/core/TypeResolver.kt index 6703fbd7..ba026af7 100644 --- a/src/main/kotlin/dev/appoutlet/some/core/TypeResolver.kt +++ b/src/main/kotlin/dev/appoutlet/some/core/TypeResolver.kt @@ -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? } diff --git a/src/main/kotlin/dev/appoutlet/some/core/TypeResolverProvider.kt b/src/main/kotlin/dev/appoutlet/some/core/TypeResolverProvider.kt new file mode 100644 index 00000000..4ce0f58a --- /dev/null +++ b/src/main/kotlin/dev/appoutlet/some/core/TypeResolverProvider.kt @@ -0,0 +1,28 @@ +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. + * + * 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]) + * to take precedence. + */ +interface TypeResolverProvider { + /** + * Creates resolvers to be contributed to the fixture generation 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 +} diff --git a/src/main/kotlin/dev/appoutlet/some/logging/LoggerConfig.kt b/src/main/kotlin/dev/appoutlet/some/logging/LoggerConfig.kt new file mode 100644 index 00000000..fdd566bb --- /dev/null +++ b/src/main/kotlin/dev/appoutlet/some/logging/LoggerConfig.kt @@ -0,0 +1,19 @@ +@file:Suppress("ForbiddenImport") + +package dev.appoutlet.some.logging + +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity +import co.touchlab.kermit.mutableLoggerConfigInit +import co.touchlab.kermit.platformLogWriter + +private val loggerConfig = mutableLoggerConfigInit( + minSeverity = Severity.Warn, + logWriters = arrayOf( + platformLogWriter(), + ), +) + +fun getLogger(tag: String): Logger { + return Logger(loggerConfig).withTag(tag) +} diff --git a/src/main/kotlin/dev/appoutlet/some/logging/LoggerDelegate.kt b/src/main/kotlin/dev/appoutlet/some/logging/LoggerDelegate.kt new file mode 100644 index 00000000..ad8957c1 --- /dev/null +++ b/src/main/kotlin/dev/appoutlet/some/logging/LoggerDelegate.kt @@ -0,0 +1,16 @@ +@file:Suppress("ForbiddenImport") + +package dev.appoutlet.some.logging + +import co.touchlab.kermit.Logger +import kotlin.reflect.KProperty + +class LoggerDelegate { + operator fun getValue(thisRef: Any, property: KProperty<*>): Logger { + return getLogger( + thisRef::class.simpleName ?: thisRef::class.qualifiedName ?: "Unknown class" + ) + } +} + +fun logger(): LoggerDelegate = LoggerDelegate() diff --git a/src/main/kotlin/dev/appoutlet/some/resolver/ArrayResolver.kt b/src/main/kotlin/dev/appoutlet/some/resolver/ArrayResolver.kt index b4af6b27..e79d9357 100644 --- a/src/main/kotlin/dev/appoutlet/some/resolver/ArrayResolver.kt +++ b/src/main/kotlin/dev/appoutlet/some/resolver/ArrayResolver.kt @@ -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 @@ -10,14 +12,14 @@ 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.default override fun canResolve(type: KType): Boolean { val kClass = type.classifier as? KClass<*> ?: return false diff --git a/src/main/kotlin/dev/appoutlet/some/resolver/ClassResolver.kt b/src/main/kotlin/dev/appoutlet/some/resolver/ClassResolver.kt index d7c1b265..55087b8f 100644 --- a/src/main/kotlin/dev/appoutlet/some/resolver/ClassResolver.kt +++ b/src/main/kotlin/dev/appoutlet/some/resolver/ClassResolver.kt @@ -5,8 +5,10 @@ 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 dev.appoutlet.some.logging.logger import java.lang.reflect.Modifier import kotlin.random.Random import kotlin.reflect.KClass @@ -38,16 +40,17 @@ 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` 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( + private val strategyProvider: StrategyProvider, private val propertyFactories: Map, 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 logger by logger() + private val defaultValueStrategy = strategyProvider.get() ?: DefaultValueStrategy.default /** * Returns whether [type] can be instantiated by this resolver. @@ -117,12 +120,14 @@ class ClassResolver( } catch (e: SomeCircularReferenceException) { throw e } catch (e: Exception) { + logger.d(e) { "Constructor failed for ${kClass.simpleName}" } failures.add( "Constructor ${constructor.parameters.map { it.name }}: ${e.message ?: e::class.simpleName}" ) } } + logger.w { "All constructors failed for ${kClass.simpleName}" } throw SomeInstantiationException(kClass, failures) } diff --git a/src/main/kotlin/dev/appoutlet/some/resolver/CustomTypeFactoryResolver.kt b/src/main/kotlin/dev/appoutlet/some/resolver/CustomTypeFactoryResolver.kt index 2ba96903..e6d16ab2 100644 --- a/src/main/kotlin/dev/appoutlet/some/resolver/CustomTypeFactoryResolver.kt +++ b/src/main/kotlin/dev/appoutlet/some/resolver/CustomTypeFactoryResolver.kt @@ -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( + private val strategyProvider: StrategyProvider, private val typeFactories: Map, FixtureContext.() -> Any?>, private val random: Random, - private val strategyProvider: StrategyProvider, ) : TypeResolver { /** * Returns whether [type] has a registered type factory. diff --git a/src/main/kotlin/dev/appoutlet/some/resolver/ListResolver.kt b/src/main/kotlin/dev/appoutlet/some/resolver/ListResolver.kt index dd3e2819..da27e059 100644 --- a/src/main/kotlin/dev/appoutlet/some/resolver/ListResolver.kt +++ b/src/main/kotlin/dev/appoutlet/some/resolver/ListResolver.kt @@ -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 @@ -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?, - val random: Random + strategyProvider: StrategyProvider, + private val random: Random ) : TypeResolver { - private val collectionStrategy = collectionStrategy ?: CollectionStrategy.default + private val collectionStrategy = strategyProvider.get() ?: CollectionStrategy.default override fun canResolve(type: KType): Boolean { val kClass = type.classifier as? KClass<*> ?: return false diff --git a/src/main/kotlin/dev/appoutlet/some/resolver/MapResolver.kt b/src/main/kotlin/dev/appoutlet/some/resolver/MapResolver.kt index cc98e34b..009225f0 100644 --- a/src/main/kotlin/dev/appoutlet/some/resolver/MapResolver.kt +++ b/src/main/kotlin/dev/appoutlet/some/resolver/MapResolver.kt @@ -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 @@ -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.default override fun canResolve(type: KType): Boolean { val kClass = type.classifier as? KClass<*> ?: return false diff --git a/src/main/kotlin/dev/appoutlet/some/resolver/NullableResolver.kt b/src/main/kotlin/dev/appoutlet/some/resolver/NullableResolver.kt index 8a4e417f..a5c2294a 100644 --- a/src/main/kotlin/dev/appoutlet/some/resolver/NullableResolver.kt +++ b/src/main/kotlin/dev/appoutlet/some/resolver/NullableResolver.kt @@ -2,7 +2,9 @@ package dev.appoutlet.some.resolver import dev.appoutlet.some.config.NullableStrategy 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.KType import kotlin.reflect.full.createType @@ -15,14 +17,14 @@ import kotlin.reflect.full.createType * - **NeverNull** – always resolves a non-null value. * - **Random** – returns `null` based on the strategy's probability. * - * @param nullableStrategy Strategy for resolving nullable types. Defaults to [NullableStrategy.default] when null. + * @param strategyProvider Provider of all configured generation strategies. * @param random Random source used by [NullableStrategy.Random]. */ class NullableResolver( - nullableStrategy: NullableStrategy?, + strategyProvider: StrategyProvider, private val random: Random ) : TypeResolver { - private val nullableStrategy = nullableStrategy ?: NullableStrategy.default + private val nullableStrategy = strategyProvider.get() ?: NullableStrategy.default override fun canResolve(type: KType): Boolean = type.isMarkedNullable diff --git a/src/main/kotlin/dev/appoutlet/some/resolver/OptionalResolver.kt b/src/main/kotlin/dev/appoutlet/some/resolver/OptionalResolver.kt index 317a13b8..082b9aac 100644 --- a/src/main/kotlin/dev/appoutlet/some/resolver/OptionalResolver.kt +++ b/src/main/kotlin/dev/appoutlet/some/resolver/OptionalResolver.kt @@ -2,8 +2,11 @@ package dev.appoutlet.some.resolver import dev.appoutlet.some.config.NullableStrategy 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.logging.logger import java.util.Optional import kotlin.random.Random import kotlin.reflect.KClass @@ -18,19 +21,21 @@ import kotlin.reflect.full.isSubclassOf * - **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 strategyProvider Provider of all configured generation strategies. * @param random Random source used by [NullableStrategy.Random]. */ class OptionalResolver( - nullableStrategy: NullableStrategy?, + strategyProvider: StrategyProvider, private val random: Random ) : TypeResolver { + private val logger by logger() + /** * 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. + * Fetched from [strategyProvider], falling back to [NullableStrategy.default] when not registered. */ - private val nullableStrategy = nullableStrategy ?: NullableStrategy.default + private val nullableStrategy = strategyProvider.get() ?: NullableStrategy.default /** * Returns `true` when [type] is a subclass of [Optional]. @@ -76,7 +81,8 @@ class OptionalResolver( is NullableStrategy.NullOnCircularReference -> { try { Optional.ofNullable(chain.resolve(valueType)) - } catch (_: SomeCircularReferenceException) { + } catch (e: SomeCircularReferenceException) { + logger.d(e) { "Circular reference detected in Optional<$valueType>, returning empty" } Optional.empty() } } diff --git a/src/main/kotlin/dev/appoutlet/some/resolver/SetResolver.kt b/src/main/kotlin/dev/appoutlet/some/resolver/SetResolver.kt index b5dc7d3d..0ef81468 100644 --- a/src/main/kotlin/dev/appoutlet/some/resolver/SetResolver.kt +++ b/src/main/kotlin/dev/appoutlet/some/resolver/SetResolver.kt @@ -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 @@ -13,15 +15,14 @@ import kotlin.reflect.typeOf /** * Resolves [Set] and [MutableSet] 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 set size within the configured range. */ class SetResolver( - collectionStrategy: CollectionStrategy?, - val random: Random + strategyProvider: StrategyProvider, + private val random: Random ) : TypeResolver { - private val collectionStrategy = collectionStrategy ?: CollectionStrategy.default + private val collectionStrategy = strategyProvider.get() ?: CollectionStrategy.default override fun canResolve(type: KType): Boolean { val kClass = type.classifier as? KClass<*> ?: return false diff --git a/src/main/kotlin/dev/appoutlet/some/resolver/StringResolver.kt b/src/main/kotlin/dev/appoutlet/some/resolver/StringResolver.kt index 6167d509..42edabdd 100644 --- a/src/main/kotlin/dev/appoutlet/some/resolver/StringResolver.kt +++ b/src/main/kotlin/dev/appoutlet/some/resolver/StringResolver.kt @@ -2,7 +2,9 @@ package dev.appoutlet.some.resolver import dev.appoutlet.some.config.StringStrategy 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.KType import kotlin.reflect.typeOf @@ -12,15 +14,15 @@ import kotlin.uuid.Uuid /** * Resolves [String] types using the active [StringStrategy]. * - * @param stringStrategy Strategy for generating string values. Defaults to [StringStrategy.default] when null. + * @param strategyProvider Provider of all configured generation strategies. * @param random Random source used when generating random strings. */ @OptIn(ExperimentalUuidApi::class) class StringResolver( - stringStrategy: StringStrategy?, - val random: Random + strategyProvider: StrategyProvider, + private val random: Random ) : TypeResolver { - private val stringStrategy = stringStrategy ?: StringStrategy.default + private val stringStrategy = strategyProvider.get() ?: StringStrategy.default override fun canResolve(type: KType): Boolean = type == typeOf() diff --git a/src/test/kotlin/dev/appoutlet/some/ServiceLoaderDiscoveryTest.kt b/src/test/kotlin/dev/appoutlet/some/ServiceLoaderDiscoveryTest.kt new file mode 100644 index 00000000..f05c9b12 --- /dev/null +++ b/src/test/kotlin/dev/appoutlet/some/ServiceLoaderDiscoveryTest.kt @@ -0,0 +1,70 @@ +package dev.appoutlet.some + +import dev.appoutlet.some.config.SomeConfig +import dev.appoutlet.some.resolver.ClassResolver +import dev.appoutlet.some.resolver.CustomTypeFactoryResolver +import dev.appoutlet.some.resolver.NullableResolver +import dev.appoutlet.some.resolver.ObjectResolver +import dev.appoutlet.some.resolver.StringResolver +import dev.appoutlet.some.test.DiscoveredType +import dev.appoutlet.some.test.TestTypeResolver +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ServiceLoaderDiscoveryTest { + @Test + fun `top-level some discovers third-party resolver`() { + val result: DiscoveredType = some() + assertEquals("discovered", result.value) + } + + @Test + fun `someSetup discovers third-party resolver`() { + val generator = someSetup { } + val result: DiscoveredType = generator() + assertEquals("discovered", result.value) + } + + @Test + fun `custom factory takes precedence over discovered resolver`() { + val generator = someSetup { + factory(DiscoveredType::class) { DiscoveredType("custom") } + } + val result: DiscoveredType = generator() + assertEquals("custom", result.value) + } + + @Test + fun `discovered resolvers take precedence over built-in resolvers`() { + val resolvers = SomeConfig().buildResolvers() + val resolverClasses = resolvers.map { it::class } + + assertTrue(resolverClasses.first() == CustomTypeFactoryResolver::class) + assertTrue(resolverClasses.last() == ClassResolver::class) + assertTrue(TestTypeResolver::class in resolverClasses) + + val nullableResolverIndex = resolverClasses.indexOf(NullableResolver::class) + val objectResolverIndex = resolverClasses.indexOf(ObjectResolver::class) + val stringResolverIndex = resolverClasses.indexOf(StringResolver::class) + val classResolverIndex = resolverClasses.indexOf(ClassResolver::class) + val discoveredIndex = resolverClasses.indexOf(TestTypeResolver::class) + + assertTrue(nullableResolverIndex < discoveredIndex) + assertTrue(discoveredIndex < objectResolverIndex) + assertTrue(discoveredIndex < stringResolverIndex) + assertTrue(discoveredIndex < classResolverIndex) + } + + @Test + fun `misbehaving provider is skipped and built-in chain still works`() { + val result: Int = some() + assertTrue(result in Int.MIN_VALUE..Int.MAX_VALUE) + } + + @Test + fun `valid providers survive when another provider fails during loading`() { + val result: DiscoveredType = some() + assertEquals("discovered", result.value) + } +} diff --git a/src/test/kotlin/dev/appoutlet/some/config/DefaultStrategyProviderTest.kt b/src/test/kotlin/dev/appoutlet/some/config/DefaultStrategyProviderTest.kt new file mode 100644 index 00000000..908d82cc --- /dev/null +++ b/src/test/kotlin/dev/appoutlet/some/config/DefaultStrategyProviderTest.kt @@ -0,0 +1,38 @@ +package dev.appoutlet.some.config + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class DefaultStrategyProviderTest { + @Test + fun `DefaultStrategyProvider returns registered strategy`() { + val provider = DefaultStrategyProvider(mapOf(NullableStrategy::class to NullableStrategy.AlwaysNull)) + + assertEquals(NullableStrategy.AlwaysNull, provider[NullableStrategy::class]) + } + + @Test + fun `DefaultStrategyProvider returns null for unregistered strategy`() { + val provider = DefaultStrategyProvider() + + assertNull(provider[NullableStrategy::class]) + } + + @Test + fun `DefaultStrategyProvider returns strategies from the provided map`() { + val provider = DefaultStrategyProvider( + mapOf( + NullableStrategy::class to NullableStrategy.default, + StringStrategy::class to StringStrategy.default, + CollectionStrategy::class to CollectionStrategy.default, + DefaultValueStrategy::class to DefaultValueStrategy.default, + ) + ) + + assertEquals(NullableStrategy.default, provider[NullableStrategy::class]) + assertEquals(StringStrategy.default, provider[StringStrategy::class]) + assertEquals(CollectionStrategy.default, provider[CollectionStrategy::class]) + assertEquals(DefaultValueStrategy.default, provider[DefaultValueStrategy::class]) + } +} diff --git a/src/test/kotlin/dev/appoutlet/some/resolver/ArrayResolverTest.kt b/src/test/kotlin/dev/appoutlet/some/resolver/ArrayResolverTest.kt index 5fc15a8d..10165c92 100644 --- a/src/test/kotlin/dev/appoutlet/some/resolver/ArrayResolverTest.kt +++ b/src/test/kotlin/dev/appoutlet/some/resolver/ArrayResolverTest.kt @@ -1,9 +1,7 @@ package dev.appoutlet.some.resolver import dev.appoutlet.some.config.CollectionStrategy -import dev.appoutlet.some.config.NullableStrategy -import dev.appoutlet.some.config.buildSomeConfig -import dev.appoutlet.some.core.ResolverChain +import dev.appoutlet.some.config.DefaultStrategyProvider import dev.appoutlet.some.test.defaultTestChain import org.junit.jupiter.api.Assertions.assertFalse import kotlin.random.Random @@ -16,16 +14,12 @@ import kotlin.test.assertTrue class ArrayResolverTest { @Test fun `ArrayResolver generates array with correct size`() { - val config = buildSomeConfig { - strategy(CollectionStrategy(3..5)) - } - val resolvers = config.buildResolvers() - val resolver = ArrayResolver(CollectionStrategy(3..5), Random.Default) - - val result = resolver.resolve( - typeOf>(), - ResolverChain(resolvers, config[NullableStrategy::class]) + val strategyProvider = DefaultStrategyProvider( + mapOf(CollectionStrategy::class to CollectionStrategy(3..5)) ) + val resolver = ArrayResolver(strategyProvider, Random.Default) + + val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) assertTrue(result.size in 3..5) assertTrue(result.all { it is String }) @@ -33,7 +27,7 @@ class ArrayResolverTest { @Test fun `ArrayResolver generates array with Int elements`() { - val resolver = ArrayResolver(CollectionStrategy.default, Random.Default) + val resolver = ArrayResolver(DefaultStrategyProvider(), Random.Default) val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) @@ -44,7 +38,7 @@ class ArrayResolverTest { fun `ArrayResolver generates array with Class elements`() { data class User(val name: String, val age: Int) - val resolver = ArrayResolver(CollectionStrategy.default, Random.Default) + val resolver = ArrayResolver(DefaultStrategyProvider(), Random.Default) val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) @@ -53,14 +47,14 @@ class ArrayResolverTest { @Test fun `ArrayResolver canResolve detects Array types`() { - val resolver = ArrayResolver(CollectionStrategy.default, Random.Default) + val resolver = ArrayResolver(DefaultStrategyProvider(), Random.Default) assertTrue(resolver.canResolve(typeOf>())) assertTrue(resolver.canResolve(typeOf>())) } @Test fun `ArrayResolver rejects non-Array types`() { - val resolver = ArrayResolver(CollectionStrategy.default, Random.Default) + val resolver = ArrayResolver(DefaultStrategyProvider(), Random.Default) assertFalse(resolver.canResolve(typeOf())) assertFalse(resolver.canResolve(typeOf())) assertFalse(resolver.canResolve(typeOf>())) @@ -68,7 +62,7 @@ class ArrayResolverTest { @Test fun `ArrayResolver throws error on star projection`() { - val resolver = ArrayResolver(CollectionStrategy.default, Random.Default) + val resolver = ArrayResolver(DefaultStrategyProvider(), Random.Default) assertFailsWith { resolver.resolve(typeOf>(), defaultTestChain) diff --git a/src/test/kotlin/dev/appoutlet/some/resolver/ClassResolverTest.kt b/src/test/kotlin/dev/appoutlet/some/resolver/ClassResolverTest.kt index 99a1abe0..c4d7a1d4 100644 --- a/src/test/kotlin/dev/appoutlet/some/resolver/ClassResolverTest.kt +++ b/src/test/kotlin/dev/appoutlet/some/resolver/ClassResolverTest.kt @@ -1,10 +1,7 @@ package dev.appoutlet.some.resolver +import dev.appoutlet.some.config.DefaultStrategyProvider import dev.appoutlet.some.config.DefaultValueStrategy -import dev.appoutlet.some.config.NullableStrategy -import dev.appoutlet.some.config.SomeConfig -import dev.appoutlet.some.config.buildSomeConfig -import dev.appoutlet.some.core.ResolverChain import dev.appoutlet.some.exception.SomeInstantiationException import dev.appoutlet.some.test.defaultTestChain import kotlin.random.Random @@ -50,8 +47,8 @@ data class WithDefaultParams(val name: String, val count: Int = 42, val active: class ClassResolverTest { private val resolver = ClassResolver( - random = Random.Default, - strategyProvider = SomeConfig() + strategyProvider = DefaultStrategyProvider(), + random = Random.Default ) @Test @@ -139,12 +136,7 @@ class ClassResolverTest { @Test fun `ClassResolver with UseDefault strategy uses Kotlin default values for optional parameters`() { - val config = buildSomeConfig() - val resolver = ClassResolver(random = Random.Default, strategyProvider = config) - - val chain = ResolverChain(config.buildResolvers(), config[NullableStrategy::class]) - - val result = resolver.resolve(typeOf(), chain) as WithDefaultParams + val result = resolver.resolve(typeOf(), defaultTestChain) as WithDefaultParams assertTrue(result.name.isNotEmpty()) assertEquals(42, result.count) assertTrue(result.active) @@ -152,18 +144,16 @@ class ClassResolverTest { @Test fun `ClassResolver with Generate strategy generates values for optional parameters`() { - val config = buildSomeConfig { - strategy(DefaultValueStrategy.Generate) - } - - val resolver = ClassResolver( - random = Random.Default, - strategyProvider = config, + val generateResolver = ClassResolver( + strategyProvider = DefaultStrategyProvider( + mapOf(DefaultValueStrategy::class to DefaultValueStrategy.Generate) + ), + random = Random.Default ) - val chain = ResolverChain(config.buildResolvers(), config[NullableStrategy::class]) - - val results = List(100) { resolver.resolve(typeOf(), chain) as WithDefaultParams } + val results = List(100) { + generateResolver.resolve(typeOf(), defaultTestChain) as WithDefaultParams + } val hasNonDefaultCount = results.any { it.count != 42 } val hasNonDefaultActive = results.any { !it.active } assertTrue( diff --git a/src/test/kotlin/dev/appoutlet/some/resolver/ListResolverTest.kt b/src/test/kotlin/dev/appoutlet/some/resolver/ListResolverTest.kt index 7d787928..00cbc840 100644 --- a/src/test/kotlin/dev/appoutlet/some/resolver/ListResolverTest.kt +++ b/src/test/kotlin/dev/appoutlet/some/resolver/ListResolverTest.kt @@ -1,9 +1,7 @@ package dev.appoutlet.some.resolver import dev.appoutlet.some.config.CollectionStrategy -import dev.appoutlet.some.config.NullableStrategy -import dev.appoutlet.some.config.buildSomeConfig -import dev.appoutlet.some.core.ResolverChain +import dev.appoutlet.some.config.DefaultStrategyProvider import dev.appoutlet.some.test.defaultTestChain import kotlin.random.Random import kotlin.reflect.typeOf @@ -15,20 +13,19 @@ import kotlin.test.assertTrue class ListResolverTest { @Test fun `ListResolver generates list with correct size`() { - val config = buildSomeConfig { - strategy(CollectionStrategy(3..5)) - } - val resolvers = config.buildResolvers() - val resolver = ListResolver(CollectionStrategy(3..5), Random.Default) + val strategyProvider = DefaultStrategyProvider( + mapOf(CollectionStrategy::class to CollectionStrategy(3..5)) + ) + val resolver = ListResolver(strategyProvider, Random.Default) - val result = resolver.resolve(typeOf>(), ResolverChain(resolvers, config[NullableStrategy::class])) + val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) assertTrue(result.size in 3..5) } @Test fun `ListResolver generates MutableList when requested`() { - val resolver = ListResolver(CollectionStrategy.default, Random.Default) + val resolver = ListResolver(DefaultStrategyProvider(), Random.Default) val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) @@ -36,13 +33,13 @@ class ListResolverTest { @Test fun `ListResolver canResolve detects List types`() { - val resolver = ListResolver(CollectionStrategy.default, Random.Default) + val resolver = ListResolver(DefaultStrategyProvider(), Random.Default) assertTrue(resolver.canResolve(typeOf>())) } @Test fun `ListResolver rejects non-List types`() { - val resolver = ListResolver(CollectionStrategy.default, Random.Default) + val resolver = ListResolver(DefaultStrategyProvider(), Random.Default) assertFalse(resolver.canResolve(typeOf())) assertFalse(resolver.canResolve(typeOf())) } diff --git a/src/test/kotlin/dev/appoutlet/some/resolver/MapResolverTest.kt b/src/test/kotlin/dev/appoutlet/some/resolver/MapResolverTest.kt index 9099ac24..84d25ac5 100644 --- a/src/test/kotlin/dev/appoutlet/some/resolver/MapResolverTest.kt +++ b/src/test/kotlin/dev/appoutlet/some/resolver/MapResolverTest.kt @@ -1,9 +1,7 @@ package dev.appoutlet.some.resolver import dev.appoutlet.some.config.CollectionStrategy -import dev.appoutlet.some.config.NullableStrategy -import dev.appoutlet.some.config.buildSomeConfig -import dev.appoutlet.some.core.ResolverChain +import dev.appoutlet.some.config.DefaultStrategyProvider import dev.appoutlet.some.test.defaultTestChain import kotlin.random.Random import kotlin.reflect.typeOf @@ -15,23 +13,19 @@ import kotlin.test.assertTrue class MapResolverTest { @Test fun `MapResolver generates map with correct size`() { - val config = buildSomeConfig { - strategy(CollectionStrategy(2..4)) - } - val resolvers = config.buildResolvers() - val resolver = MapResolver(CollectionStrategy(2..4), Random.Default) - - val result = resolver.resolve( - typeOf>(), - ResolverChain(resolvers, config[NullableStrategy::class]) + val strategyProvider = DefaultStrategyProvider( + mapOf(CollectionStrategy::class to CollectionStrategy(2..4)) ) + val resolver = MapResolver(strategyProvider, Random.Default) + + val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) assertTrue(result.size in 2..4) } @Test fun `MapResolver generates MutableMap when requested`() { - val resolver = MapResolver(CollectionStrategy.default, Random.Default) + val resolver = MapResolver(DefaultStrategyProvider(), Random.Default) val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) @@ -39,13 +33,13 @@ class MapResolverTest { @Test fun `MapResolver canResolve detects Map types`() { - val resolver = MapResolver(CollectionStrategy.default, Random.Default) + val resolver = MapResolver(DefaultStrategyProvider(), Random.Default) assertTrue(resolver.canResolve(typeOf>())) } @Test fun `MapResolver rejects non-Map types`() { - val resolver = MapResolver(CollectionStrategy.default, Random.Default) + val resolver = MapResolver(DefaultStrategyProvider(), Random.Default) assertFalse(resolver.canResolve(typeOf())) assertFalse(resolver.canResolve(typeOf())) } diff --git a/src/test/kotlin/dev/appoutlet/some/resolver/NullableResolverTest.kt b/src/test/kotlin/dev/appoutlet/some/resolver/NullableResolverTest.kt index 04662ab6..c0b9ef84 100644 --- a/src/test/kotlin/dev/appoutlet/some/resolver/NullableResolverTest.kt +++ b/src/test/kotlin/dev/appoutlet/some/resolver/NullableResolverTest.kt @@ -1,5 +1,6 @@ package dev.appoutlet.some.resolver +import dev.appoutlet.some.config.DefaultStrategyProvider import dev.appoutlet.some.config.NullableStrategy import dev.appoutlet.some.config.buildSomeConfig import dev.appoutlet.some.core.ResolverChain @@ -16,7 +17,10 @@ import kotlin.test.assertTrue class NullableResolverTest { @Test fun `NullableResolver with AlwaysNull strategy returns null`() { - val resolver = NullableResolver(NullableStrategy.AlwaysNull, Random.Default) + val resolver = NullableResolver( + DefaultStrategyProvider(mapOf(NullableStrategy::class to NullableStrategy.AlwaysNull)), + Random.Default + ) repeat(1000) { val result = resolver.resolve(typeOf(), defaultTestChain) @@ -85,13 +89,13 @@ class NullableResolverTest { @Test fun `NullableResolver canResolve detects nullable types`() { - val resolver = NullableResolver(NullableStrategy.default, Random.Default) + val resolver = NullableResolver(DefaultStrategyProvider(), Random.Default) assertTrue(resolver.canResolve(typeOf())) } @Test fun `NullableResolver rejects non-nullable types`() { - val resolver = NullableResolver(NullableStrategy.default, Random.Default) + val resolver = NullableResolver(DefaultStrategyProvider(), Random.Default) assertFalse(resolver.canResolve(typeOf())) } } diff --git a/src/test/kotlin/dev/appoutlet/some/resolver/OptionalResolverTest.kt b/src/test/kotlin/dev/appoutlet/some/resolver/OptionalResolverTest.kt index 88852228..e2cfa6a5 100644 --- a/src/test/kotlin/dev/appoutlet/some/resolver/OptionalResolverTest.kt +++ b/src/test/kotlin/dev/appoutlet/some/resolver/OptionalResolverTest.kt @@ -1,8 +1,10 @@ package dev.appoutlet.some.resolver +import dev.appoutlet.some.config.DefaultStrategyProvider import dev.appoutlet.some.config.NullableStrategy import dev.appoutlet.some.some import java.util.Optional +import kotlin.random.Random import kotlin.reflect.typeOf import kotlin.test.Test import kotlin.test.assertFalse @@ -63,14 +65,14 @@ class OptionalResolverTest { @Test fun `JavaOptionalResolver canResolve detects Optional types`() { - val resolver = OptionalResolver(NullableStrategy.default, kotlin.random.Random.Default) + val resolver = OptionalResolver(DefaultStrategyProvider(), Random.Default) assertTrue(resolver.canResolve(typeOf>())) assertTrue(resolver.canResolve(typeOf>())) } @Test fun `JavaOptionalResolver rejects non-Optional types`() { - val resolver = OptionalResolver(NullableStrategy.default, kotlin.random.Random.Default) + val resolver = OptionalResolver(DefaultStrategyProvider(), Random.Default) assertFalse(resolver.canResolve(typeOf())) assertFalse(resolver.canResolve(typeOf>())) } diff --git a/src/test/kotlin/dev/appoutlet/some/resolver/SetResolverTest.kt b/src/test/kotlin/dev/appoutlet/some/resolver/SetResolverTest.kt index 68a9e819..3b129847 100644 --- a/src/test/kotlin/dev/appoutlet/some/resolver/SetResolverTest.kt +++ b/src/test/kotlin/dev/appoutlet/some/resolver/SetResolverTest.kt @@ -1,9 +1,7 @@ package dev.appoutlet.some.resolver import dev.appoutlet.some.config.CollectionStrategy -import dev.appoutlet.some.config.NullableStrategy -import dev.appoutlet.some.config.buildSomeConfig -import dev.appoutlet.some.core.ResolverChain +import dev.appoutlet.some.config.DefaultStrategyProvider import dev.appoutlet.some.test.defaultTestChain import kotlin.random.Random import kotlin.reflect.typeOf @@ -16,20 +14,19 @@ import kotlin.test.assertTrue class SetResolverTest { @Test fun `SetResolver generates set with correct size`() { - val config = buildSomeConfig { - strategy(CollectionStrategy(3..5)) - } - val resolvers = config.buildResolvers() - val resolver = SetResolver(CollectionStrategy(3..5), Random.Default) + val strategyProvider = DefaultStrategyProvider( + mapOf(CollectionStrategy::class to CollectionStrategy(3..5)) + ) + val resolver = SetResolver(strategyProvider, Random.Default) - val result = resolver.resolve(typeOf>(), ResolverChain(resolvers, config[NullableStrategy::class])) + val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) assertTrue(result.size in 3..5) } @Test fun `SetResolver generates set with elements of correct type`() { - val resolver = SetResolver(CollectionStrategy.default, Random.Default) + val resolver = SetResolver(DefaultStrategyProvider(), Random.Default) val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) @@ -38,7 +35,7 @@ class SetResolverTest { @Test fun `SetResolver generates set with Int elements`() { - val resolver = SetResolver(CollectionStrategy.default, Random.Default) + val resolver = SetResolver(DefaultStrategyProvider(), Random.Default) val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) @@ -48,7 +45,7 @@ class SetResolverTest { @Test fun `SetResolver generates set of data class`() { data class DataClass(val a: Int, val b: String) - val resolver = SetResolver(CollectionStrategy.default, Random.Default) + val resolver = SetResolver(DefaultStrategyProvider(), Random.Default) val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) @@ -56,7 +53,7 @@ class SetResolverTest { @Test fun `SetResolver generates MutableSet when requested`() { - val resolver = SetResolver(CollectionStrategy.default, Random.Default) + val resolver = SetResolver(DefaultStrategyProvider(), Random.Default) val result = resolver.resolve(typeOf>(), defaultTestChain) assertIs>(result) @@ -64,7 +61,7 @@ class SetResolverTest { @Test fun `SetResolver canResolve detects Set types`() { - val resolver = SetResolver(CollectionStrategy.default, Random.Default) + val resolver = SetResolver(DefaultStrategyProvider(), Random.Default) assertTrue(resolver.canResolve(typeOf>())) assertTrue(resolver.canResolve(typeOf>())) assertTrue(resolver.canResolve(typeOf>())) @@ -72,7 +69,7 @@ class SetResolverTest { @Test fun `SetResolver rejects non-Set types`() { - val resolver = SetResolver(CollectionStrategy.default, Random.Default) + val resolver = SetResolver(DefaultStrategyProvider(), Random.Default) assertFalse(resolver.canResolve(typeOf())) assertFalse(resolver.canResolve(typeOf())) assertFalse(resolver.canResolve(typeOf>())) @@ -80,7 +77,7 @@ class SetResolverTest { @Test fun `SetResolver throws error on star projection`() { - val resolver = SetResolver(CollectionStrategy.default, Random.Default) + val resolver = SetResolver(DefaultStrategyProvider(), Random.Default) assertFailsWith { resolver.resolve(typeOf>(), defaultTestChain) diff --git a/src/test/kotlin/dev/appoutlet/some/resolver/StringResolverTest.kt b/src/test/kotlin/dev/appoutlet/some/resolver/StringResolverTest.kt index 8913b6cc..3e874ecb 100644 --- a/src/test/kotlin/dev/appoutlet/some/resolver/StringResolverTest.kt +++ b/src/test/kotlin/dev/appoutlet/some/resolver/StringResolverTest.kt @@ -1,5 +1,6 @@ package dev.appoutlet.some.resolver +import dev.appoutlet.some.config.DefaultStrategyProvider import dev.appoutlet.some.config.NullableStrategy import dev.appoutlet.some.config.StringStrategy import dev.appoutlet.some.config.buildSomeConfig @@ -19,18 +20,23 @@ import kotlin.uuid.Uuid class StringResolverTest { @Test fun `StringResolver respects the default lenght`() { - val strategy = StringStrategy.Random() - val resolver = StringResolver(strategy, Random.Default) + val resolver = StringResolver( + DefaultStrategyProvider(), + Random.Default + ) val result = resolver.resolve(typeOf(), defaultTestChain) assertIs(result) - assertEquals(result.length, strategy.length) + assertEquals(result.length, StringStrategy.Random().length) } @Test fun `StringResolver generates random string with custom length`() { val customLength = 16 - val resolver = StringResolver(StringStrategy.Random(length = customLength), Random.Default) + val resolver = StringResolver( + DefaultStrategyProvider(mapOf(StringStrategy::class to StringStrategy.Random(length = customLength))), + Random.Default + ) val result = resolver.resolve(typeOf(), defaultTestChain) assertIs(result) @@ -40,7 +46,10 @@ class StringResolverTest { @OptIn(ExperimentalUuidApi::class) @Test fun `StringResolver generates UUID when configured`() { - val resolver = StringResolver(StringStrategy.Uuid, Random.Default) + val resolver = StringResolver( + DefaultStrategyProvider(mapOf(StringStrategy::class to StringStrategy.Uuid)), + Random.Default + ) val result = resolver.resolve(typeOf(), defaultTestChain) as String val uuid = Uuid.parse(result) @@ -50,7 +59,10 @@ class StringResolverTest { @Test fun `StringResolver generates readable strings`() { - val resolver = StringResolver(StringStrategy.Readable, Random.Default) + val resolver = StringResolver( + DefaultStrategyProvider(mapOf(StringStrategy::class to StringStrategy.Readable)), + Random.Default + ) val result = resolver.resolve(typeOf(), defaultTestChain) assertIs(result) @@ -59,13 +71,13 @@ class StringResolverTest { @Test fun `StringResolver canResolve detects String type`() { - val resolver = StringResolver(StringStrategy.default, Random.Default) + val resolver = StringResolver(DefaultStrategyProvider(), Random.Default) assertTrue(resolver.canResolve(typeOf())) } @Test fun `StringResolver rejects non-String types`() { - val resolver = StringResolver(StringStrategy.default, Random.Default) + val resolver = StringResolver(DefaultStrategyProvider(), Random.Default) assertFalse(resolver.canResolve(typeOf())) assertFalse(resolver.canResolve(typeOf())) } diff --git a/src/test/kotlin/dev/appoutlet/some/test/FailingLoadTypeResolverProvider.kt b/src/test/kotlin/dev/appoutlet/some/test/FailingLoadTypeResolverProvider.kt new file mode 100644 index 00000000..2807971e --- /dev/null +++ b/src/test/kotlin/dev/appoutlet/some/test/FailingLoadTypeResolverProvider.kt @@ -0,0 +1,26 @@ +package dev.appoutlet.some.test + +import com.fueledbycaffeine.autoservice.AutoService +import dev.appoutlet.some.core.StrategyProvider +import dev.appoutlet.some.core.TypeResolver +import dev.appoutlet.some.core.TypeResolverProvider +import kotlin.random.Random + +/** + * Test [TypeResolverProvider] that fails during class instantiation (init block). + * + * Simulates a class-loading failure (e.g., NoClassDefFoundError from a missing optional dependency) + * that would cause [java.util.ServiceLoader.iterator.next] to throw. Used to verify that a faulty + * provider does not discard other valid providers discovered in the same iteration. + */ +@AutoService +class FailingLoadTypeResolverProvider : TypeResolverProvider { + init { + error("Simulated class loading failure") + } + + override fun createResolvers( + strategyProvider: StrategyProvider, + random: Random, + ): List = throw UnsupportedOperationException() +} diff --git a/src/test/kotlin/dev/appoutlet/some/test/FailingTypeResolverProvider.kt b/src/test/kotlin/dev/appoutlet/some/test/FailingTypeResolverProvider.kt new file mode 100644 index 00000000..b9cd644e --- /dev/null +++ b/src/test/kotlin/dev/appoutlet/some/test/FailingTypeResolverProvider.kt @@ -0,0 +1,20 @@ +package dev.appoutlet.some.test + +import com.fueledbycaffeine.autoservice.AutoService +import dev.appoutlet.some.core.StrategyProvider +import dev.appoutlet.some.core.TypeResolver +import dev.appoutlet.some.core.TypeResolverProvider +import kotlin.random.Random + +/** + * Test [TypeResolverProvider] that throws during resolver creation. + * + * Used to verify that `SomeConfig.buildResolvers()` gracefully skips misbehaving providers. + */ +@AutoService +class FailingTypeResolverProvider : TypeResolverProvider { + override fun createResolvers( + strategyProvider: StrategyProvider, + random: Random, + ): List = error("Simulated provider failure") +} diff --git a/src/test/kotlin/dev/appoutlet/some/test/TestTypeResolverProvider.kt b/src/test/kotlin/dev/appoutlet/some/test/TestTypeResolverProvider.kt new file mode 100644 index 00000000..b76dbf63 --- /dev/null +++ b/src/test/kotlin/dev/appoutlet/some/test/TestTypeResolverProvider.kt @@ -0,0 +1,35 @@ +package dev.appoutlet.some.test + +import com.fueledbycaffeine.autoservice.AutoService +import dev.appoutlet.some.core.ResolverChain +import dev.appoutlet.some.core.StrategyProvider +import dev.appoutlet.some.core.TypeResolver +import dev.appoutlet.some.core.TypeResolverProvider +import kotlin.random.Random +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * Test type handled by the discovered [TestTypeResolverProvider]. + */ +data class DiscoveredType(val value: String) + +/** + * Test [TypeResolverProvider] registered via `autoservice-ir` to verify ServiceLoader discovery. + */ +@AutoService +class TestTypeResolverProvider : TypeResolverProvider { + override fun createResolvers( + strategyProvider: StrategyProvider, + random: Random, + ): List = listOf(TestTypeResolver()) +} + +/** + * Resolver that handles [DiscoveredType] for discovery tests. + */ +class TestTypeResolver : TypeResolver { + override fun canResolve(type: KType): Boolean = type == typeOf() + + override fun resolve(type: KType, chain: ResolverChain): Any? = DiscoveredType("discovered") +} diff --git a/zensical.toml b/zensical.toml index 418c8b8b..74433c6e 100644 --- a/zensical.toml +++ b/zensical.toml @@ -16,7 +16,8 @@ nav = [ "configuration/collection-strategy.md" ] }, "supported-types.md", - "custom-factories.md" , + "custom-factories.md", + "custom-resolvers.md", { "API Reference" = "https://some.appoutlet.dev/reference/latest/" } ]