-
Notifications
You must be signed in to change notification settings - Fork 0
Implement StrategyProvider - Jules #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
348bf95
06030c6
fb81323
1252d34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,19 +17,25 @@ class SomeConfigBuilder { | |
| * Strategy for handling nullable type resolution. | ||
| * Defaults to [NullableStrategy.NullOnCircularReference]. | ||
| */ | ||
| var nullableStrategy: NullableStrategy = NullableStrategy.NullOnCircularReference | ||
| var nullableStrategy: NullableStrategy | ||
| get() = strategiesMap[NullableStrategy::class] as NullableStrategy | ||
| set(value) { strategiesMap[NullableStrategy::class] = value } | ||
|
|
||
| /** | ||
| * Strategy for generating string values. | ||
| * Defaults to [StringStrategy.Random]. | ||
| */ | ||
| var stringStrategy: StringStrategy = StringStrategy.Random() | ||
| var stringStrategy: StringStrategy | ||
| get() = strategiesMap[StringStrategy::class] as StringStrategy | ||
| set(value) { strategiesMap[StringStrategy::class] = value } | ||
|
|
||
| /** | ||
| * Strategy for generating collection sizes. | ||
| * Defaults to [CollectionStrategy] with a range of 1..5. | ||
| */ | ||
| var collectionStrategy: CollectionStrategy = CollectionStrategy() | ||
| var collectionStrategy: CollectionStrategy | ||
| get() = strategiesMap[CollectionStrategy::class] as CollectionStrategy | ||
| set(value) { strategiesMap[CollectionStrategy::class] = value } | ||
|
|
||
| /** | ||
| * Strategy for handling data class constructor defaults. | ||
|
|
@@ -43,9 +49,25 @@ class SomeConfigBuilder { | |
| */ | ||
| var seed: Long? = null | ||
|
|
||
| @PublishedApi | ||
| internal val strategiesMap: MutableMap<KClass<out Strategy>, Strategy> = mutableMapOf( | ||
| NullableStrategy::class to NullableStrategy.NullOnCircularReference, | ||
| StringStrategy::class to StringStrategy.Random(), | ||
| CollectionStrategy::class to CollectionStrategy(), | ||
| ) | ||
| private val _typeFactories: MutableMap<KClass<*>, FixtureContext.() -> Any?> = mutableMapOf() | ||
| private val _propertyFactories: MutableMap<Pair<KClass<*>, String>, FixtureContext.() -> Any?> = mutableMapOf() | ||
|
|
||
| /** | ||
| * Registers a strategy instance. | ||
| * | ||
| * @param T The strategy type. | ||
| * @param strategy The strategy instance to register. | ||
| */ | ||
| inline fun <reified T : Strategy> strategy(strategy: T) { | ||
| strategiesMap[T::class] = strategy | ||
| } | ||
|
Comment on lines
+67
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When registering a strategy using However, the built-in resolvers (like To fix this, we should also update any keys in inline fun <reified T : Strategy> strategy(strategy: T) {
_strategies[T::class] = strategy
_strategies.keys.toList().forEach { key ->
if (key.isInstance(strategy)) {
_strategies[key] = strategy
}
}
} |
||
|
|
||
| /** | ||
| * Registers a custom type factory function for type [T]. | ||
| * | ||
|
|
@@ -55,10 +77,18 @@ class SomeConfigBuilder { | |
| * @param kClass The [KClass] of the type to override. | ||
| * @param typeFactory Lambda receiving a [FixtureContext] and returning a value of type [T]. | ||
| */ | ||
| fun <T : Any> register(kClass: KClass<T>, typeFactory: FixtureContext.() -> T) { | ||
| fun <T : Any> factory(kClass: KClass<T>, typeFactory: FixtureContext.() -> T) { | ||
| _typeFactories[kClass] = typeFactory | ||
| } | ||
|
|
||
| /** | ||
| * Deprecated: Use [factory] instead. | ||
| */ | ||
| @Deprecated("Use factory() instead", ReplaceWith("factory(kClass, typeFactory)")) | ||
| fun <T : Any> register(kClass: KClass<T>, typeFactory: FixtureContext.() -> T) { | ||
| factory(kClass, typeFactory) | ||
| } | ||
|
|
||
| /** | ||
| * Registers a custom property factory function for a specific property of class [T]. | ||
| * | ||
|
|
@@ -73,6 +103,17 @@ class SomeConfigBuilder { | |
| _propertyFactories[kClass to property.name] = factory | ||
| } | ||
|
|
||
| /** | ||
| * Populates the builder's strategy map with entries from an existing map. | ||
| * | ||
| * Used internally by [SomeConfig.toBuilder] to transfer strategy registrations. | ||
| * | ||
| * @param strategies Map of strategy registrations to copy into this builder. | ||
| */ | ||
| internal fun populateStrategies(strategies: Map<KClass<out Strategy>, Strategy>) { | ||
| strategiesMap.putAll(strategies) | ||
| } | ||
|
|
||
| /** | ||
| * Populates the builder's type factory map with entries from an existing map. | ||
| * | ||
|
|
@@ -103,9 +144,7 @@ class SomeConfigBuilder { | |
| * @return A new [SomeConfig] instance with the configured values. | ||
| */ | ||
| fun build(): SomeConfig = SomeConfig( | ||
| nullableStrategy = nullableStrategy, | ||
| stringStrategy = stringStrategy, | ||
| collectionStrategy = collectionStrategy, | ||
| strategies = strategiesMap.toMap(), | ||
| defaultValueStrategy = defaultValueStrategy, | ||
| seed = seed, | ||
| typeFactories = _typeFactories.toMap(), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package dev.appoutlet.some.config | ||
|
|
||
| /** | ||
| * Marker interface for all generation strategies. | ||
| */ | ||
| interface Strategy |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,27 @@ | ||
| package dev.appoutlet.some.core | ||
|
|
||
| import dev.appoutlet.some.config.CollectionStrategy | ||
| import dev.appoutlet.some.config.DefaultValueStrategy | ||
| import dev.appoutlet.some.config.NullableStrategy | ||
| import dev.appoutlet.some.config.StringStrategy | ||
| import kotlin.random.Random | ||
| import kotlin.reflect.KType | ||
|
|
||
| /** | ||
| * Runtime context provided as the receiver for custom factory functions. | ||
| * | ||
| * Both type factories registered with `register` and property factories registered with `property` receive this | ||
| * context. It exposes the same random source and generation strategies used by the resolver chain so custom values | ||
| * Both type factories registered with `factory` and property factories registered with `property` receive this | ||
| * context. It exposes the same random source and strategy provider used by the resolver chain so custom values | ||
| * can stay consistent with the active [dev.appoutlet.some.config.SomeConfig]. | ||
| * | ||
| * The context is a snapshot for the current factory invocation. In particular, [resolutionStack] is immutable from the | ||
| * factory's perspective and should be used only for inspection or debugging, not for controlling resolver state. | ||
| * | ||
| * @property random Random source for factory-generated values. This respects the configured seed when one is set. | ||
| * @property resolutionStack Types currently being resolved, ordered from the outermost request to the current type. | ||
| * @property nullableStrategy Strategy currently used for nullable type handling. | ||
| * @property stringStrategy Strategy currently used for generated string values. | ||
| * @property collectionStrategy Strategy currently used for generated collection sizes. | ||
| * @property strategyProvider Provider for accessing configured generation strategies. | ||
| * @property defaultValueStrategy Strategy currently used for handling constructor defaults. | ||
| */ | ||
| data class FixtureContext( | ||
| val random: Random, | ||
| val resolutionStack: List<KType>, | ||
| val nullableStrategy: NullableStrategy, | ||
| val stringStrategy: StringStrategy, | ||
| val collectionStrategy: CollectionStrategy, | ||
| val strategyProvider: StrategyProvider, | ||
| val defaultValueStrategy: DefaultValueStrategy, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package dev.appoutlet.some.core | ||
|
|
||
| import dev.appoutlet.some.config.Strategy | ||
| import kotlin.reflect.KClass | ||
|
|
||
| /** | ||
| * Provides access to configured [Strategy] instances. | ||
| */ | ||
| interface StrategyProvider { | ||
| /** | ||
| * Retrieves the [Strategy] of type [key]. | ||
| * | ||
| * @throws IllegalArgumentException if the strategy is not registered. | ||
| */ | ||
| operator fun <T : Strategy> get(key: KClass<T>): T | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
as? Tand throwing a "not registered" exception can be misleading if the strategy is registered but the cast fails due to a type mismatch. Performing an explicit null check first allows us to throw the correctIllegalArgumentExceptionfor missing strategies, while letting the cast throw aClassCastExceptionif there is an unexpected type mismatch.