io.github.serpro69.kfaker.provider.misc.RandomClassProvider.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kotlin-faker Show documentation
Show all versions of kotlin-faker Show documentation
https://github.com/serpro69/kotlin-faker
package io.github.serpro69.kfaker.provider.misc
import io.github.serpro69.kfaker.FakerConfig
import io.github.serpro69.kfaker.RandomService
import kotlin.Boolean
import kotlin.Char
import kotlin.Double
import kotlin.Float
import kotlin.Int
import kotlin.Long
import kotlin.Short
import kotlin.String
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.KVisibility
/**
* Provider functionality for generating random class instances.
*
* Inspired by [Creating a random instance of any class in Kotlin blog post](https://blog.kotlin-academy.com/creating-a-random-instance-of-any-class-in-kotlin-b6168655b64a).
*
* @property config configuration for this [RandomClassProvider]
*/
@Suppress("unused")
class RandomClassProvider {
private val fakerConfig: FakerConfig
private val randomService: RandomService
@PublishedApi
@JvmSynthetic
internal val config: RandomProviderConfig
/**
* Creates an instance of this [RandomClassProvider] with the given [fakerConfig].
*/
internal constructor(fakerConfig: FakerConfig) {
this.fakerConfig = fakerConfig
randomService = RandomService(fakerConfig)
config = fakerConfig.randomProviderConfig?.copy() ?: RandomProviderConfig()
}
/**
* Private constructor that is only used for [new] and [copy] functions.
*/
private constructor(fakerConfig: FakerConfig, config: RandomProviderConfig) {
this.fakerConfig = fakerConfig
this.config = config.copy()
randomService = RandomService(fakerConfig)
}
/**
* Applies [configurator] to this [RandomClassProvider].
*/
fun configure(configurator: RandomProviderConfig.() -> Unit) {
config.apply(configurator)
}
/**
* Creates a new instance of this [RandomClassProvider].
*
* IF [FakerConfig.randomProviderConfig] was configured
* THEN new instance will be created with a copy of that configuration,
* ELSE a new instance is created with a new instance of default configuration as defined in [RandomProviderConfig].
*/
fun new(): RandomClassProvider = RandomClassProvider(
fakerConfig,
fakerConfig.randomProviderConfig ?: RandomProviderConfig()
)
/**
* Creates a copy of this [RandomClassProvider] instance with a copy of its [config].
*/
fun copy(): RandomClassProvider = RandomClassProvider(fakerConfig, config)
/**
* Resets [config] to defaults for this [RandomClassProvider] instance.
*/
fun reset() = config.reset()
/**
* Creates an instance of [T]. If [T] has a parameterless public constructor then it will be used to create an instance of this class,
* otherwise a constructor with minimal number of parameters will be used with randomly-generated values.
*
* @throws NoSuchElementException if [T] has no public constructor.
*/
inline fun randomClassInstance() = T::class.randomClassInstance(config)
/**
* Creates an instance of [T]. If [T] has a parameterless public constructor then it will be used to create an instance of this class,
* otherwise a constructor with minimal number of parameters will be used with randomly-generated values.
*
* @param configurator configure instance creation.
*
* @throws NoSuchElementException if [T] has no public constructor.
*/
inline fun randomClassInstance(configurator: RandomProviderConfig.() -> Unit): T {
return T::class.randomClassInstance(RandomProviderConfig().apply(configurator))
}
@Suppress("UNCHECKED_CAST")
@JvmSynthetic
@PublishedApi
internal fun KClass.randomClassInstance(config: RandomProviderConfig): T {
val defaultInstance: T? = if (
config.constructorParamSize == -1
&& config.constructorFilterStrategy == ConstructorFilterStrategy.NO_ARGS
) {
randomPrimitiveOrNull() as T?
?: constructors.firstOrNull { it.parameters.isEmpty() && it.visibility == KVisibility.PUBLIC }?.call()
} else null
return defaultInstance ?: objectInstance ?: run {
val constructors = constructors
.filter { it.visibility == KVisibility.PUBLIC }
val constructor = constructors.firstOrNull {
it.parameters.size == config.constructorParamSize
} ?: when (config.constructorFilterStrategy) {
ConstructorFilterStrategy.MIN_NUM_OF_ARGS -> constructors.minByOrNull { it.parameters.size }
ConstructorFilterStrategy.MAX_NUM_OF_ARGS -> constructors.maxByOrNull { it.parameters.size }
else -> {
when (config.fallbackStrategy) {
FallbackStrategy.FAIL_IF_NOT_FOUND -> {
throw NoSuchElementException("Constructor with 'parameters.size == ${config.constructorParamSize}' not found for $this")
}
FallbackStrategy.USE_MIN_NUM_OF_ARGS -> constructors.minByOrNull { it.parameters.size }
FallbackStrategy.USE_MAX_NUM_OF_ARGS -> constructors.maxByOrNull { it.parameters.size }
}
}
} ?: throw NoSuchElementException("No suitable constructor found for $this")
val params = constructor.parameters
.map {
val klass = it.type.classifier as KClass<*>
when {
config.namedParameterGenerators.containsKey(it.name) -> {
config.namedParameterGenerators[it.name]?.invoke()
}
it.type.isMarkedNullable && config.nullableGenerators.containsKey(klass) -> {
config.nullableGenerators[klass]?.invoke()
}
else -> {
klass.predefinedTypeOrNull(config)
?: klass.randomPrimitiveOrNull()
?: klass.objectInstance
?: klass.randomEnumOrNull()
?: klass.randomSealedClassOrNull(config)
?: klass.randomCollectionOrNull(it.type, config)
?: klass.randomClassInstance(config)
}
}
}
.toTypedArray()
constructor.call(*params)
}
}
private fun KClass.predefinedTypeOrNull(config: RandomProviderConfig): Any? {
return config.predefinedGenerators[this]?.invoke()
}
/**
* Handles generation of primitive types since they do not have a public constructor.
*/
private fun KClass<*>.randomPrimitiveOrNull(): Any? = when (this) {
Double::class -> randomService.nextDouble()
Float::class -> randomService.nextFloat()
Long::class -> randomService.nextLong()
Int::class -> randomService.nextInt()
Short::class -> randomService.nextInt().toShort()
Byte::class -> randomService.nextInt().toByte()
String::class -> randomService.randomString()
Char::class -> randomService.nextChar()
Boolean::class -> randomService.nextBoolean()
else -> null
}
/**
* Handles generation of enums types since they do not have a public constructor.
*/
private fun KClass<*>.randomEnumOrNull(): Any? {
return if (this.java.isEnum) randomService.randomValue(this.java.enumConstants) else null
}
private fun KClass<*>.randomSealedClassOrNull(config: RandomProviderConfig): Any? {
return if (isSealed) randomService.randomValue(sealedSubclasses).randomClassInstance(config) else null
}
private fun KClass<*>.randomCollectionOrNull(kType: KType, config: RandomProviderConfig): Any? {
return when (this) {
List::class -> {
val elementType = kType.arguments[0].type?.classifier as KClass<*>
List(config.collectionsSize) { elementType.randomClassInstance(config) }
}
Set::class -> {
val elementType = kType.arguments[0].type?.classifier as KClass<*>
List(config.collectionsSize) { elementType.randomClassInstance(config) }.toSet()
}
Map::class -> {
val keyElementType = kType.arguments[0].type?.classifier as KClass<*>
val valElementType = kType.arguments[1].type?.classifier as KClass<*>
val keys = List(config.collectionsSize) { keyElementType.randomClassInstance(config) }
val values = List(config.collectionsSize) { valElementType.randomClassInstance(config) }
keys.zip(values).associate { (k, v) -> k to v }
}
else -> null
}
}
}
/**
* Configuration for [RandomClassProvider.randomClassInstance].
*
* @property collectionsSize the size of the generated [Collection] type arguments.
* Defaults to `1`.
*
* @property constructorParamSize will try to look up the constructor with specified number of arguments,
* and use that to create the instance of the class.
* Defaults to `-1`, which ignores this configuration property.
* This takes precedence over [constructorFilterStrategy] configuration.
*
* @property constructorFilterStrategy default strategy for looking up a constructor
* that is used to create the instance of a class.
* By default, a zero-args constructor will be used.
*
* @property fallbackStrategy fallback strategy that is used to look up a constructor
* if no constructor with [constructorParamSize] or [constructorFilterStrategy] was found.
*/
class RandomProviderConfig @PublishedApi internal constructor() {
var collectionsSize: Int = 1
var constructorParamSize: Int = -1
var constructorFilterStrategy: ConstructorFilterStrategy = ConstructorFilterStrategy.NO_ARGS
var fallbackStrategy: FallbackStrategy = FallbackStrategy.USE_MIN_NUM_OF_ARGS
@PublishedApi
internal val namedParameterGenerators = mutableMapOf Any?>()
@PublishedApi
internal val predefinedGenerators = mutableMapOf, () -> Any>()
@PublishedApi
internal val nullableGenerators = mutableMapOf, () -> Any?>()
/**
* Configures generation for a specific named parameter. Overrides all other generators
*/
inline fun namedParameterGenerator(parameterName: String, noinline generator: () -> K?) {
namedParameterGenerators[parameterName] = generator
}
/**
* Configures generation for a specific type. It can override internal generators (for primitives, for example)
*/
inline fun typeGenerator(noinline generator: () -> K) {
predefinedGenerators[K::class] = generator
}
/**
* Configures generation for a specific nullable type. It can override internal generators (for primitives, for example)
*/
inline fun nullableTypeGenerator(noinline generator: () -> K?) {
nullableGenerators[K::class] = generator
}
}
private fun RandomProviderConfig.reset() {
collectionsSize = 1
constructorParamSize = -1
constructorFilterStrategy = ConstructorFilterStrategy.NO_ARGS
fallbackStrategy = FallbackStrategy.USE_MIN_NUM_OF_ARGS
namedParameterGenerators.clear()
predefinedGenerators.clear()
nullableGenerators.clear()
}
private fun RandomProviderConfig.copy(
collectionsSize: Int? = null,
constructorParamSize: Int? = null,
constructorFilterStrategy: ConstructorFilterStrategy? = null,
fallbackStrategy: FallbackStrategy? = null,
namedParameterGenerators: Map Any?>? = null,
predefinedGenerators: Map, () -> Any>? = null,
nullableGenerators: Map, () -> Any?>? = null
): RandomProviderConfig = RandomProviderConfig().apply {
[email protected] = collectionsSize ?: [email protected]
[email protected] = constructorParamSize ?: [email protected]
[email protected] = constructorFilterStrategy ?: [email protected]
[email protected] = fallbackStrategy ?: [email protected]
[email protected](namedParameterGenerators ?: [email protected])
[email protected](predefinedGenerators ?: [email protected])
[email protected](nullableGenerators ?: [email protected])
}
enum class FallbackStrategy {
USE_MIN_NUM_OF_ARGS,
USE_MAX_NUM_OF_ARGS,
FAIL_IF_NOT_FOUND
}
enum class ConstructorFilterStrategy {
NO_ARGS,
MIN_NUM_OF_ARGS,
MAX_NUM_OF_ARGS
}