Repository: serpro69/kotlin-faker
Pull request #117 (2022-01-03 04:44:29)

Add namedParameterGenerator for RandomProvider#randomClassInstance
Faker provides the ability to create a random object instance for a specific type, and to override the generation of the object's fields by type as described in this example from the documentation:

```kotlin
class Baz(val id: Int, val uuid: UUID)

val baz: Baz = faker.randomProvider.randomClassInstance {
    typeGenerator<UUID> { UUID.fromString("00000000-0000-0000-0000-000000000000") }
}
```

This is great, but sometimes the objects we generate are required to be related in a way that makes overriding by type not granular enough. For example, if an object of one class must hold an ID of an object of another class, generation by type would not allow us to form a valid relationship.

```kotlin
class Foo(val id: Int, val bazId: Int)

val foo: Foo = faker.randomProvider.randomClassInstance {
    typeGenerator<Int> { 42 }
}

```

Here `bazId` should be set equal to `baz.id`, but instead both `id` and `bazId` will be `42` and (unless I'm mistaken) a mechanism doesn't exist to assign a specific yet different value to fields of the same type. 

One way to solve this problem would be to provide a way to override field generation not by type but by constructor parameter name, as in this example:

```kotlin
val foo: Foo = faker.randomProvider.randomClassInstance {
    namedParameterGenerator("bazId") { baz.id }
}
```

This is what is implemented in this PR. Looking forward to hearing your thoughts on whether this is a worthwhile addition, or if there is a better way to achieve my aims.



serpro69: Hi @humblehacker ,

This sounds like a great addition, thanks! I'll review the code later this week.

Patch:
diff --git a/README.md b/README.md
index 63e2bdd2..0fcf2b49 100644
--- a/README.md
+++ b/README.md
@@ -646,10 +646,10 @@ class Test {
 
 #### Pre-Configuring type generation for constructor params
 
-Some, or all, of the constructor params can be instantiated with values following some pre-configured logic using `typeGenerator` function. Consider the following example:
+Some, or all, of the constructor params can be instantiated with values following some pre-configured logic using `typeGenerator` or `namedParameterGenerator` functions. Consider the following example:
 
 ```kotlin
-class Baz(val id: Int, val uuid: UUID)
+class Baz(val id: Int, val uuid: UUID, val relatedUuid: UUID)
 
 class Test {
     @Test
@@ -659,17 +659,18 @@ class Test {
         val baz: Baz = faker.randomProvider.randomClassInstance {
             typeGenerator<UUID> { UUID.fromString("00000000-0000-0000-0000-000000000000") }
             typeGenerator<Int> { 0 }
+            namedParameterGenerator("relatedUuid") { UUID.fromString("11111111-1111-1111-1111-111111111111") }
         }
-
     }
 }
 ```
 
 For each instance of `Baz` the following will be true:
 
-```
+```kotlin
 baz.id == 0
 baz.uuid == UUID.fromString("00000000-0000-0000-0000-000000000000")
+baz.relatedUuid == UUID.fromString("11111111-1111-1111-1111-111111111111")
 ```
 
 The example itself does not make that much sense, since we're using "static" values, but we could also do something like:
diff --git a/core/src/integration/kotlin/io/github/serpro69/kfaker/docs/Extras.kt b/core/src/integration/kotlin/io/github/serpro69/kfaker/docs/Extras.kt
index f1a49f9f..76278073 100644
--- a/core/src/integration/kotlin/io/github/serpro69/kfaker/docs/Extras.kt
+++ b/core/src/integration/kotlin/io/github/serpro69/kfaker/docs/Extras.kt
@@ -24,16 +24,18 @@ class Extras : DescribeSpec({
         context("configurable constructor arg type generation") {
             it("should generate pre-configured constructor params") {
                 // START extras_random_instance_two
-                class Baz(val id: Int, val uuid: UUID)
+                class Baz(val id: Int, val uuid: UUID, val relatedUuid: UUID)
 
                 val baz: Baz = faker.randomProvider.randomClassInstance {
                     typeGenerator<UUID> { UUID.fromString("00000000-0000-0000-0000-000000000000") }
                     typeGenerator<Int> { 0 }
+                    namedParameterGenerator("relatedUuid") { UUID.fromString("11111111-1111-1111-1111-111111111111") }
                 }
+                // END extras_random_instance_two
 
                 assertEquals(baz.id, 0)
                 assertEquals(baz.uuid, UUID.fromString("00000000-0000-0000-0000-000000000000"))
-                // END extras_random_instance_two
+                assertEquals(baz.relatedUuid, UUID.fromString("11111111-1111-1111-1111-111111111111"))
             }
         }
 
diff --git a/core/src/main/kotlin/io/github/serpro69/kfaker/provider/RandomProvider.kt b/core/src/main/kotlin/io/github/serpro69/kfaker/provider/RandomProvider.kt
index 8c3ccbd4..ce2ece53 100644
--- a/core/src/main/kotlin/io/github/serpro69/kfaker/provider/RandomProvider.kt
+++ b/core/src/main/kotlin/io/github/serpro69/kfaker/provider/RandomProvider.kt
@@ -1,6 +1,5 @@
 package io.github.serpro69.kfaker.provider
 
-import io.github.serpro69.kfaker.Faker
 import io.github.serpro69.kfaker.FakerConfig
 import io.github.serpro69.kfaker.RandomService
 import kotlin.Boolean
@@ -78,19 +77,24 @@ class RandomProvider internal constructor(fakerConfig: FakerConfig) {
             } ?: throw NoSuchElementException("No suitable constructor found for $this")
 
             val params = constructor.parameters
-                .map { it.type }
                 .map {
-                    val klass = it.classifier as KClass<*>
-                    if (it.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, config)
-                            ?: klass.randomClassInstance(config)
+                    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()
@@ -177,12 +181,22 @@ class RandomProviderConfig @PublishedApi internal constructor() {
     var constructorFilterStrategy: ConstructorFilterStrategy = ConstructorFilterStrategy.NO_ARGS
     var fallbackStrategy: FallbackStrategy = FallbackStrategy.USE_MIN_NUM_OF_ARGS
 
+    @PublishedApi
+    internal val namedParameterGenerators = mutableMapOf<String, () -> Any?>()
+
     @PublishedApi
     internal val predefinedGenerators = mutableMapOf<KClass<*>, () -> Any>()
 
     @PublishedApi
     internal val nullableGenerators = mutableMapOf<KClass<*>, () -> Any?>()
 
+    /**
+     * Configures generation for a specific named parameter. Overrides all other generators
+     */
+    inline fun <reified K : Any> namedParameterGenerator(parameterName: String, noinline generator: () -> K?) {
+        namedParameterGenerators[parameterName] = generator
+    }
+
     /**
      * Configures generation for a specific type. It can override internal generators (for primitives, for example)
      */
diff --git a/docs/src/orchid/resources/wiki/extras.md b/docs/src/orchid/resources/wiki/extras.md
index fefa94da..24bd0738 100644
--- a/docs/src/orchid/resources/wiki/extras.md
+++ b/docs/src/orchid/resources/wiki/extras.md
@@ -44,7 +44,8 @@ Random instance generation is available through `Faker().randomProvider`:
 
 ### Pre-Configuring type generation for constructor arguments
 
-Some, or all, of the constructor params can be instantiated with values following some pre-configured logic using `typeGenerator` function. Consider the following example:
+Some, or all, of the constructor params can be instantiated with values following some pre-configured logic using `typeGenerator` or `namedParameterGenerator` functions. Consider the following example:
+
 
 {% tabs %}
 
@@ -61,9 +62,10 @@ Some, or all, of the constructor params can be instantiated with values followin
 <br>
 So for each instance of `Baz` the following will be true:
 
-```
+```kotlin
 baz.id == 0
 baz.uuid == UUID.fromString("00000000-0000-0000-0000-000000000000")
+baz.relatedUuid == UUID.fromString("11111111-1111-1111-1111-111111111111")
 ```
 
 This example itself does not make that much sense, since we're using "static" values, but we could also do something like:


Test patch:
diff --git a/core/src/test/kotlin/io/github/serpro69/kfaker/provider/RandomProviderTest.kt b/core/src/test/kotlin/io/github/serpro69/kfaker/provider/RandomProviderTest.kt
index df7cb703..76da322d 100644
--- a/core/src/test/kotlin/io/github/serpro69/kfaker/provider/RandomProviderTest.kt
+++ b/core/src/test/kotlin/io/github/serpro69/kfaker/provider/RandomProviderTest.kt
@@ -186,6 +186,38 @@ class RandomProviderTest : DescribeSpec({
         }
     }
 
+    describe("a TestClass with non-empty constructor for custom parameter generators") {
+        class TestClass(
+            val id: UUID,
+            val id2: UUID,
+            val nullableId: UUID?,
+            val nullableId2: UUID?
+        )
+
+        val typeGeneratedId = UUID.fromString("00000000-0000-0000-0000-000000000000")
+        val namedParameterGeneratedId = UUID.randomUUID()
+        val nullableNamedParameterGeneratedId = UUID.randomUUID()
+
+        context("creating a random instance of the class with custom parameter and type generators") {
+            val testClass: TestClass = randomProvider.randomClassInstance {
+                namedParameterGenerator("id") { namedParameterGeneratedId }
+                namedParameterGenerator("nullableId") { nullableNamedParameterGeneratedId }
+                namedParameterGenerator("nullableId2") { null }
+                typeGenerator<UUID> { typeGeneratedId }
+            }
+
+            it("should use predefined UUIDs with parameter generators taking precedence over type generators") {
+                assertSoftly {
+                    testClass shouldBe instanceOf(TestClass::class)
+                    testClass.id shouldBe namedParameterGeneratedId
+                    testClass.id2 shouldBe typeGeneratedId
+                    testClass.nullableId shouldBe nullableNamedParameterGeneratedId
+                    testClass.nullableId2 shouldBe null
+                }
+            }
+        }
+    }
+
     describe("a TestClass with non-empty constructor for nullable custom type generators") {
         class Foo(val int: Int?)
         class TestClass(