Repository: serpro69/kotlin-faker (kotlin)

Issue:
Add support for Collection types in RandomProvider#randomClassInstance
Since constructors are essentially functions (and consequentially - java methods), it is possible to get the element types from List/Map/... (and other Collection types) properties of a constructor.

Here's a simple example on how to get the containing element types from a List and Map properties of a class constructor:

```
fun main() {
    class Test(
        val list: List<String>,
        val map: Map<String, Int>
    )

    val test = Test(listOf(), mapOf())

    test::class.primaryConstructor?.let {
        it.javaConstructor?.genericParameterTypes?.get(0)?.let { t ->
            if (t is ParameterizedType) {
                val elementType: Type = t.actualTypeArguments[0]
                println("List element type: $elementType") // class java.lang.String
            }
        }
        it.javaConstructor?.genericParameterTypes?.get(1)?.let { t ->
            if (t is ParameterizedType) {
                val keyType: Type = t.actualTypeArguments[0]
                val valType: Type = t.actualTypeArguments[1]
                println("Map key element type: $keyType") // class java.lang.String
                println("Map val element type: $valType") // class java.lang.Integer
            }
        }
    }
}
```

Pull request description:
todo:

- [x] update documentation
- [x] configurable collection size?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4de424de8d..e0f45c2a88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,13 +6,14 @@ and the project versioning adheres to [Semantic Versioning](https://semver.org/s
 
 ## [1.9.0] - UNRELEASED
 ### Added
+- [#103](https://github.com/serpro69/kotlin-faker/issues/103) [core] Add support for `Collection` types in `RandomProvider#randomClassInstance`
 - [#96](https://github.com/serpro69/kotlin-faker/issues/96) [core] Add `randomSubset` and `randomSublist` to `RandomService`
 - [#92](https://github.com/serpro69/kotlin-faker/issues/92) [core] Add `randomString` function to `RandomService`
 - [#86](https://github.com/serpro69/kotlin-faker/issues/86) [core] Generate birth-date based on the age
 
 ### Changed
-- [#97](https://github.com/serpro69/kotlin-faker/issues/97) [core] Change `RandomService#nextString` to generate strings only within given locale
 - [#100](https://github.com/serpro69/kotlin-faker/issues/100) [core] Add deprecation warning for `RandomService#nextString` since it's going to be replaced with `RandomService#randomString`
+- [#97](https://github.com/serpro69/kotlin-faker/issues/97) [core] Change `RandomService#nextString` to generate strings only within given locale
 
 ### Fixed
 - [#104](https://github.com/serpro69/kotlin-faker/issues/104) [core] `RandomProvider#randomClassInstance` : 'No suitable constructor found' for primitive classes
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 719403f03a..f1a49f9f47 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
@@ -86,6 +86,77 @@ class Extras : DescribeSpec({
                 assertNotEquals(fooBarBaz.baz, null)
                 // END extras_random_instance_five
             }
+
+            it("should generate collections with size 1 be default") {
+                // START extras_random_instance_six
+                class Foo(
+                    val list: List<String>,
+                    val set: Set<String>,
+                    val map: Map<String, Int>
+                )
+
+                val foo = faker.randomProvider.randomClassInstance<Foo>()
+
+                assertEquals(foo.list.size, 1)
+                assertEquals(foo.set.size, 1)
+                assertEquals(foo.map.size, 1)
+                // END extras_random_instance_six
+            }
+
+            it("should generate collections with configured size") {
+                // START extras_random_instance_seven
+                class Foo(
+                    val list: List<String>,
+                    val set: Set<String>,
+                    val map: Map<String, Int>
+                )
+
+                val foo = faker.randomProvider.randomClassInstance<Foo> {
+                    collectionsSize = 6
+                }
+
+                assertEquals(foo.list.size, 6)
+                assertEquals(foo.set.size, 6)
+                assertEquals(foo.map.size, 6)
+                // END extras_random_instance_seven
+            }
+
+            it("should generate a set of size 10") {
+                // START extras_random_instance_eight
+                class TestClass(
+                    val string: String,
+                    val set: Set<String>
+                )
+
+                val testClass = faker.randomProvider.randomClassInstance<TestClass> {
+                    typeGenerator { "a string" }
+                    collectionsSize = 10
+                }
+
+                assertEquals(testClass.string, "a string")
+                assertEquals(testClass.set.size, 10)
+                // END extras_random_instance_eight
+            }
+
+            it("should generate Collections with pre-configured type generation") {
+                // START extras_random_instance_nine
+                class Foo
+                class Bar(
+                    val list: List<Foo>,
+                    val set: Set<String>,
+                    val map: Map<String, Int>
+                )
+
+                val bar = faker.randomProvider.randomClassInstance<Bar> {
+                    typeGenerator { emptyList<Foo>() }
+                    typeGenerator { setOf("one", "two", "fortytwo") }
+                    typeGenerator { mapOf("pwd" to 12177) }
+                }
+                assertEquals(bar.list, emptyList<Foo>())
+                assertEquals(bar.set, setOf("one", "two", "fortytwo"))
+                assertEquals(bar.map, mapOf("pwd" to 12177))
+                // END extras_random_instance_nine
+            }
         }
     }
 
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 0b8cace036..8c3ccbd4cb 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,5 +1,6 @@
 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
@@ -8,10 +9,10 @@ import kotlin.Double
 import kotlin.Float
 import kotlin.Int
 import kotlin.Long
-import kotlin.NoSuchElementException
 import kotlin.Short
 import kotlin.String
 import kotlin.reflect.KClass
+import kotlin.reflect.KType
 import kotlin.reflect.KVisibility
 
 /**
@@ -88,6 +89,7 @@ class RandomProvider internal constructor(fakerConfig: FakerConfig) {
                             ?: klass.objectInstance
                             ?: klass.randomEnumOrNull()
                             ?: klass.randomSealedClassOrNull(config)
+                            ?: klass.randomCollectionOrNull(it, config)
                             ?: klass.randomClassInstance(config)
                     }
                 }
@@ -114,7 +116,6 @@ class RandomProvider internal constructor(fakerConfig: FakerConfig) {
         String::class -> randomService.randomString()
         Char::class -> randomService.nextChar()
         Boolean::class -> randomService.nextBoolean()
-        // TODO: 16.06.19 Arrays
         else -> null
     }
 
@@ -128,11 +129,36 @@ class RandomProvider internal constructor(fakerConfig: FakerConfig) {
     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 [RandomProvider.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.
@@ -140,12 +166,13 @@ class RandomProvider internal constructor(fakerConfig: FakerConfig) {
  *
  * @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.
+ * 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
diff --git a/docs/src/orchid/resources/wiki/extras.md b/docs/src/orchid/resources/wiki/extras.md
index 4922d067ed..fefa94da01 100644
--- a/docs/src/orchid/resources/wiki/extras.md
+++ b/docs/src/orchid/resources/wiki/extras.md
@@ -10,6 +10,7 @@
 * [Random instance of any class](#random-instance-of-any-class)
   * [Pre-configuring type generation for constructor arguments](#pre-configuring-type-generation-for-constructor-arguments)
   * [Deterministic constructor selection](#deterministic-constructor-selection)
+  * [Configuring the size of generated Collections](#configuring-the-size-of-generated-collections)
 * [Random Everything](#random-everything)
 
 <br>
@@ -21,7 +22,7 @@ It is possible to create a random instance of (almost) any class.
 There are some rules to keep in mind:
 
 - By default, the constructor with the least number of arguments is used (This can be configured - read on.)
-- `kotlin.collection.*` and `kolin.Array` types in the constructor are not supported at the moment
+- `kolin.Array` type in the constructor is not supported at the moment
 
 Random instance generation is available through `Faker().randomProvider`:
 
@@ -162,6 +163,88 @@ The above has the following rules:
 
 <br>
 
+### Configuring the size of generated Collections
+
+Support for `kotlin.collections.Collection` parameter types - `List`, `Set` and `Map` has been added in version `1.9.0` and with that - a new configuration parameter to configure the size of the generated collection.
+
+By default, all collections will be generated with only 1 element:
+
+{% tabs %}
+
+{% kotlin "Kotlin" %}
+{% filter compileAs('md') %}
+```kotlin
+{% snippet 'extras_random_instance_six' %}
+```
+{% endfilter %}
+{% endkotlin %}
+
+{% endtabs %}
+
+<br>
+
+This can be configured using `collectionsSize` parameter:
+
+{% tabs %}
+
+{% kotlin "Kotlin" %}
+{% filter compileAs('md') %}
+```kotlin
+{% snippet 'extras_random_instance_seven' %}
+```
+{% endfilter %}
+{% endkotlin %}
+
+{% endtabs %}
+
+{% info %}
+{% filter compileAs('md') %}
+Note that the `collectionsSize` configuration parameter affects all 3 types of Collections.
+{% endfilter %}
+{% endinfo %}
+
+<br>
+
+{% warn %}
+{% filter compileAs('md') %}
+It is also worth noting that `typeGenerator<Foo> { ... }` configuration, which was covered above, will not affect `Foo` typed elements in a generated collection.
+{% endfilter %}
+{% endwarn %}
+
+Consider the following example. If `typeGenerator<String> { "a string" }` would affect `String` typed elements of `Set`, the resulting generated set would be of size `1`:
+
+{% tabs %}
+
+{% kotlin "Kotlin" %}
+{% filter compileAs('md') %}
+```kotlin
+{% snippet 'extras_random_instance_eight' %}
+```
+{% endfilter %}
+{% endkotlin %}
+
+{% endtabs %}
+
+<br>
+
+At the same time, `typeGenerator` configurator itself can be used with collections as well:
+
+{% tabs %}
+
+{% kotlin "Kotlin" %}
+{% filter compileAs('md') %}
+```kotlin
+{% snippet 'extras_random_instance_nine' %}
+```
+{% endfilter %}
+{% endkotlin %}
+
+{% endtabs %}
+
+{% btc %}{% endbtc %}
+
+<br>
+
 ## Random Everything
 
 Faker provides its wrapper functions around `java.util.Random` (with some additional functionality that is not covered by `java.util.Random`) through `Faker().random` property.


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 d63fc0ddc9..df7cb7032c 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
@@ -5,6 +5,8 @@ import io.kotest.assertions.assertSoftly
 import io.kotest.assertions.throwables.shouldNotThrow
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.core.spec.style.DescribeSpec
+import io.kotest.matchers.collections.shouldHaveSize
+import io.kotest.matchers.maps.shouldHaveSize
 import io.kotest.matchers.shouldBe
 import io.kotest.matchers.shouldNotBe
 import io.kotest.matchers.string.shouldHaveLength
@@ -326,6 +328,47 @@ class RandomProviderTest : DescribeSpec({
         }
     }
 
+    describe("a TestClass with non-empty constructor with Collection-type parameters") {
+        class Foo
+        class Bar(val int: Int)
+        class Baz(val foo: Foo, val string: String)
+        class TestClass(
+            val list: List<Foo>,
+            val set: Set<Bar>,
+            val map: Map<String, Baz>
+        )
+
+        it("should generate Collections with default size 1") {
+            val testClass = randomProvider.randomClassInstance<TestClass>()
+            assertSoftly {
+                testClass.list shouldHaveSize 1
+                testClass.set shouldHaveSize 1
+                testClass.map shouldHaveSize 1
+            }
+        }
+
+        it("should generate Collections with pre-configured size") {
+            val testClass = randomProvider.randomClassInstance<TestClass> {
+                collectionsSize = 10
+            }
+            assertSoftly {
+                testClass.list shouldHaveSize 10
+                testClass.set shouldHaveSize 10
+                testClass.map shouldHaveSize 10
+            }
+        }
+        it("should generate Collections with pre-configured type generation") {
+            val testClass = randomProvider.randomClassInstance<TestClass> {
+                typeGenerator<List<Foo>> { listOf() }
+            }
+            assertSoftly {
+                testClass.list shouldHaveSize 0
+                testClass.set shouldHaveSize 1
+                testClass.map shouldHaveSize 1
+            }
+        }
+    }
+
     describe("a primitive type") {
         it("should not throw an exception when no suitable constructor exists") {
             assertSoftly {