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 b83c90205..b320202a1 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 @@ -6,7 +6,12 @@ import io.github.serpro69.kfaker.provider.misc.ConstructorFilterStrategy import io.github.serpro69.kfaker.provider.misc.FallbackStrategy import io.github.serpro69.kfaker.provider.misc.RandomProvider import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.collections.containOnly +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.collections.shouldContainAnyOf +import io.kotest.matchers.collections.shouldNotContain import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNot import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals import java.util.* @@ -87,6 +92,36 @@ class Extras : DescribeSpec({ assertEquals(baz.map.keys.all { it == "key" }, true) assertEquals(baz.map.values.all { it == "value" }, true) } + + it("should allow nullable element types in collections") { + // START extras_random_instance_eighteen + data class Nullable(val ints: List, val longs: Set, val map: Map) + val nullable = faker.randomClass.randomClassInstance { + collectionsSize = 10 + collectionElementTypeGenerator { if (faker.random.nextBoolean()) null else 42 } + collectionElementTypeGenerator { if (!faker.random.nextBoolean()) 0L else null } + mapEntryKeyTypeGenerator { faker.random.randomValue(listOf('a', 'b', 'c', 'd', 'e', 'f')) } + mapEntryValueTypeGenerator { if (faker.random.nextBoolean()) null else "foo" } + } + nullable.ints shouldContain 42 + // we allow nullable values, but `null` as a value will never be returned + nullable.ints shouldNotContain null + // with above config, if nextBoolean returns false, we say "return null", + // but since nulls are never returned as value, all nulls will be returned as random instance, + // hence we won't have all 42's + nullable.ints shouldNot containOnly(42) + + nullable.longs shouldContain 0L + nullable.longs shouldNotContain null + nullable.longs shouldNot containOnly(0L) + + nullable.map.keys shouldNotContain null + nullable.map.keys shouldContainAnyOf listOf('a', 'b', 'c', 'd', 'e', 'f') + nullable.map.values shouldNotContain null + nullable.map.values shouldContain "foo" + nullable.map.values shouldNot containOnly("foo") + // END extras_random_instance_eighteen + } } } diff --git a/core/src/main/kotlin/io/github/serpro69/kfaker/provider/misc/RandomClassProvider.kt b/core/src/main/kotlin/io/github/serpro69/kfaker/provider/misc/RandomClassProvider.kt index 23d8c7884..75feb1a65 100644 --- a/core/src/main/kotlin/io/github/serpro69/kfaker/provider/misc/RandomClassProvider.kt +++ b/core/src/main/kotlin/io/github/serpro69/kfaker/provider/misc/RandomClassProvider.kt @@ -1,5 +1,6 @@ package io.github.serpro69.kfaker.provider.misc +import com.fasterxml.jackson.databind.deser.impl.CreatorCandidate.Param import io.github.serpro69.kfaker.FakerConfig import io.github.serpro69.kfaker.RandomService import io.github.serpro69.kfaker.provider.misc.ConstructorFilterStrategy.MAX_NUM_OF_ARGS @@ -233,13 +234,13 @@ class RandomClassProvider { config: RandomProviderConfig, pInfo: ParameterInfo ): Any? { + val instance: (gen: Map, (ParameterInfo) -> Any?>, el: KClass<*>) -> Any = { gen, el -> + gen[el]?.invoke(pInfo) ?: el.randomClassInstance(config) + } return when (this) { List::class, Set::class -> { val elementType = kType.arguments[0].type?.classifier as KClass<*> - val r = List(config.collectionsSize) { - config.collectionElementTypeGenerators[elementType]?.invoke(pInfo) - ?: elementType.randomClassInstance(config) - } + val r = List(config.collectionsSize) { instance(config.collectionElementTypeGenerators, elementType) } when (this) { List::class -> r Set::class -> r.toSet() @@ -249,14 +250,8 @@ class RandomClassProvider { 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) { - config.mapEntriesTypeGenerators.first[keyElementType]?.invoke(pInfo) - ?: keyElementType.randomClassInstance(config) - } - keys.associateWith { - config.mapEntriesTypeGenerators.second[valElementType]?.invoke(pInfo) - ?: valElementType.randomClassInstance(config) - } + val keys = List(config.collectionsSize) { instance(config.mapEntriesTypeGenerators.first, keyElementType) } + keys.associateWith { instance(config.mapEntriesTypeGenerators.second, valElementType) } } else -> null } diff --git a/core/src/test/kotlin/io/github/serpro69/kfaker/provider/misc/RandomClassProviderTest.kt b/core/src/test/kotlin/io/github/serpro69/kfaker/provider/misc/RandomClassProviderTest.kt index 0551c5c19..9ff8d7f01 100644 --- a/core/src/test/kotlin/io/github/serpro69/kfaker/provider/misc/RandomClassProviderTest.kt +++ b/core/src/test/kotlin/io/github/serpro69/kfaker/provider/misc/RandomClassProviderTest.kt @@ -2,13 +2,21 @@ package io.github.serpro69.kfaker.provider.misc import io.github.serpro69.kfaker.FakerConfig import io.github.serpro69.kfaker.fakerConfig +import io.github.serpro69.kfaker.random import io.kotest.assertions.throwables.shouldNotThrow import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.collections.containOnly +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.collections.shouldContainAnyOf +import io.kotest.matchers.collections.shouldContainOnly import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.collections.shouldNotContain +import io.kotest.matchers.collections.shouldNotContainOnly import io.kotest.matchers.ints.shouldBeInRange import io.kotest.matchers.maps.shouldHaveSize import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNot import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldHaveLength import io.kotest.matchers.types.instanceOf @@ -503,6 +511,34 @@ class RandomClassProviderTest : DescribeSpec({ testClass.enumSet shouldBe emptySet() testClass.enumMap shouldBe mapOf(TestEnum.JAVA to TestEnum.KOTLIN) } + + it("should generate null elements in collections") { + data class Nullable(val ints: List, val longs: Set, val map: Map) + val nullable = randomProvider.randomClassInstance { + collectionsSize = 10 + collectionElementTypeGenerator { if (random.nextBoolean()) null else 42 } + collectionElementTypeGenerator { if (!random.nextBoolean()) 0L else null } + mapEntryKeyTypeGenerator { listOf('a', 'b', 'c', 'd', 'e', 'f').random() } + mapEntryValueTypeGenerator { if (random.nextBoolean()) null else "foo" } + } + nullable.ints shouldContain 42 + // we allow nullable values, but `null` as a value will never be returned + nullable.ints shouldNotContain null + // with above config, if nextBoolean returns false, we say "return null", + // but since nulls are never returned as value, all nulls will be returned as random instance, + // hence we won't have all 42's + nullable.ints shouldNot containOnly(42) + + nullable.longs shouldContain 0L + nullable.longs shouldNotContain null + nullable.longs shouldNot containOnly(0L) + + nullable.map.keys shouldNotContain null + nullable.map.keys shouldContainAnyOf listOf('a', 'b', 'c', 'd', 'e', 'f') + nullable.map.values shouldNotContain null + nullable.map.values shouldContain "foo" + nullable.map.values shouldNot containOnly("foo") + } } describe("a TestClass with with abstract type constructor parameter") { @@ -801,6 +837,7 @@ class RandomClassProviderTest : DescribeSpec({ copy.config shouldNotBeSameInstanceAs c.randomProviderConfig } } + it("should have same configuration as defined on fakerConfig level if not defined on RandomClassProvider level") { val c = cfg() with(RandomClassProvider(c)) { diff --git a/docs/src/orchid/resources/wiki/extras.md b/docs/src/orchid/resources/wiki/extras.md index 03d5674ae..690073157 100644 --- a/docs/src/orchid/resources/wiki/extras.md +++ b/docs/src/orchid/resources/wiki/extras.md @@ -299,6 +299,20 @@ There are also two similar methods for map entries: `mapEntryKeyTypeGenerator` a {% endtabs %} +Nullable collection element types are also supported (but note that `null`s as values are never returned): + +{% tabs %} + +{% kotlin "Kotlin" %} +{% filter compileAs('md') %} +```kotlin +{% snippet 'extras_random_instance_eighteen' %} +``` +{% endfilter %} +{% endkotlin %} + +{% endtabs %} +
### Deterministic constructor selection