Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ val user = some<User>()

## Installation

Some is published as four artifacts:
Some is published as five artifacts:

- `some-core` for Java and Kotlin/JVM projects
- `some-android` for Android projects. It re-exports the core API, so you do not need to add `some-core` separately.
- `some-kotest` for Kotest `Arb` integration. Add it alongside either `some-core` or `some-android`.
- `some-retrofit` for Retrofit response fixtures. Add it alongside `some-core` and Retrofit.
- `some-kotlin-fixture` for Appmattus KotlinFixture compatibility and migration.

```kotlin
dependencies {
Expand All @@ -61,6 +62,9 @@ dependencies {
// Optional: Retrofit response integration.
// Retrofit must be declared directly by the consuming project.
testImplementation("dev.appoutlet:some-retrofit:{version}")

// Optional: Appmattus KotlinFixture compatibility. For better migration
testImplementation("dev.appoutlet:some-kotlin-fixture:{version}")
}
```

Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies {
dokka(projects.core)
dokka(projects.kotest)
dokka(projects.retrofit)
dokka(projects.kotlinFixture)
}

tasks.named("prepareKotlinBuildScriptModel") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,31 @@ private const val DEFAULT_SIZE_RANGE_START = 1
private const val DEFAULT_SIZE_RANGE_END = 5
private val defaultSizeRange = DEFAULT_SIZE_RANGE_START..DEFAULT_SIZE_RANGE_END

/**
* Strategy controlling the number of values generated for collections and maps.
*
* The configured range is inclusive at both ends. The default generates between one and five values.
*
* @property sizeRange Inclusive range of collection sizes. Both bounds must be non-negative and the end must not be
* less than the start.
*/
data class CollectionStrategy(
val sizeRange: IntRange = defaultSizeRange
) : Strategy {
override val key = CollectionStrategy::class

init {
require(sizeRange.first > -1) { "sizeRange.start must be positive" }
require(sizeRange.last > sizeRange.first) { "sizeRange.end must be greater than or equal to sizeRange.start" }
require(sizeRange.last >= sizeRange.first) { "sizeRange.end must be greater than or equal to sizeRange.start" }
}

/**
* Creates a strategy that always generates collections with [size] values.
*
* @param size Fixed non-negative collection size.
*/
constructor(size: Int) : this(size..size)

companion object {
/**
* The default collection strategy.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package dev.appoutlet.some.config
*
* - [UseDefault] – (Default) Uses the Kotlin default value for optional parameters.
* - [Generate] – Generates a value for optional parameters through the resolver chain.
* - [Random] – Randomly uses the Kotlin default or generates a value according to a probability.
*
* ## Example Usage
*
Expand Down Expand Up @@ -38,6 +39,17 @@ sealed interface DefaultValueStrategy : Strategy {
*/
data object Generate : DefaultValueStrategy

/**
* Generates a value for optional parameters with a specified probability using the shared random source.
*
* @property probability The probability (between 0.0 and 1.0) that an optional parameter will be generated.
*/
data class Random(val probability: Float = 0.5f) : DefaultValueStrategy {
init {
require(probability in 0f..1f) { "Probability must be between 0.0 and 1.0" }
}
}

companion object {
/**
* The default default-value strategy.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,13 @@ class ClassResolver(
): Any? {
val args = constructor.parameters.mapNotNull { param ->
val propertyFactory = propertyFactories[kClass to param.name]
val shouldGenerate = !param.isOptional ||
defaultValueStrategy == DefaultValueStrategy.Generate
val shouldGenerate = when {
!param.isOptional -> true
defaultValueStrategy == DefaultValueStrategy.Generate -> true
defaultValueStrategy is DefaultValueStrategy.Random ->
random.nextFloat() < defaultValueStrategy.probability
else -> false
}

when {
propertyFactory != null -> resolveByPropertyFactory(chain, param, propertyFactory)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,18 @@ class CollectionStrategyTest {
}

@Test
fun `CollectionStrategy rejects range where end equals start`() {
val exception = assertFailsWith<IllegalArgumentException> {
CollectionStrategy(5..5)
}
assertEquals("sizeRange.end must be greater than or equal to sizeRange.start", exception.message)
fun `CollectionStrategy accepts range where end equals start`() {
val strategy = CollectionStrategy(5..5)

assertEquals(5, strategy.sizeRange.first)
assertEquals(5, strategy.sizeRange.last)
}

@Test
fun `CollectionStrategy accepts a fixed size`() {
val strategy = CollectionStrategy(5)

assertEquals(5..5, strategy.sizeRange)
}

@Suppress("InvalidRange")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ class DefaultValueStrategyIntegrationTest {
assertNotEquals("default value", result.optional)
}

@Test
fun `should use default value when strategy is Random with probability 0_0`() {
val result: OptionalData = some {
strategy(DefaultValueStrategy.Random(probability = 0.0f))
}

assertEquals("default value", result.optional)
}

@Test
fun `should generate value when strategy is Random with probability 1_0`() {
val result: OptionalData = some {
strategy(DefaultValueStrategy.Random(probability = 1.0f))
}

assertNotEquals("default value", result.optional)
}

@Test
fun `property factory should take precedence over UseDefault`() {
val result: OptionalData = some {
Expand Down
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,13 @@ Writing tests means creating test data — lots of it. Constructing data classes

## Modules

Some is published as four user-facing artifacts:
Some is published as five user-facing artifacts:

- `some-core` for Java and Kotlin/JVM projects.
- `some-android` for Android projects. It re-exports the shared core API, so you do not need to add `some-core` separately.
- `some-kotest` for Kotest `Arb` integration. Add it alongside either `some-core` or `some-android`.
- `some-retrofit` for Retrofit response fixtures. Add it alongside `some-core` and Retrofit.
- `some-kotlin-fixture` for Appmattus KotlinFixture compatibility and migration.

Use [Getting Started](getting-started.md) to install Some and learn the shared API.

Expand Down
1 change: 1 addition & 0 deletions docs/migration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Use the guide that matches the exact version you are upgrading from.
## Available migrations

- [0.2.1 to 0.2.2](0.2.1-to-0.2.2.md) - moves the old `some` dependency to `some-core` and introduces `some-android`
- [Appmattus KotlinFixture Migration](kotlin-fixture-to-some.md) - migrate from Appmattus KotlinFixture to Some using `some-kotlin-fixture`
193 changes: 193 additions & 0 deletions docs/migration/kotlin-fixture-to-some.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Migrating from Appmattus KotlinFixture to Some

`some-kotlin-fixture` provides a small compatibility API for projects migrating from Appmattus KotlinFixture. It keeps the `kotlinFixture()` entry point and fixture invocation style, while delegating configuration and generation to Some's `SomeConfigBuilder`.

The compatibility module is intentionally thin. Configure factories, properties, strategies, and seeds with the core Some API rather than the removed KotlinFixture-specific configuration types.

## Dependency

Replace the Appmattus dependency with `some-kotlin-fixture`:

=== "Gradle Kotlin DSL"

```kotlin
dependencies {
// Remove:
// implementation("com.appmattus.fixture:fixture:1.2.0")

implementation("dev.appoutlet:some-kotlin-fixture:0.4.0")
}
```

=== "Gradle Groovy DSL"

```groovy
dependencies {
// Remove:
// implementation 'com.appmattus.fixture:fixture:1.2.0'

implementation 'dev.appoutlet:some-kotlin-fixture:0.4.0'
}
```

=== "Maven"

```xml
<dependency>
<groupId>dev.appoutlet</groupId>
<artifactId>some-kotlin-fixture</artifactId>
<version>0.4.0</version>
</dependency>
```

## Imports

Replace the Appmattus import with the compatibility entry point and sequence strategy:

```kotlin
// Remove:
import com.appmattus.kotlinfixture.kotlinFixture

// Add:
import dev.appoutlet.some.compat.kotlinfixture.SequenceStrategy
import dev.appoutlet.some.compat.kotlinfixture.kotlinFixture
```

Some configuration types come from the core module:

```kotlin
import dev.appoutlet.some.config.DefaultValueStrategy
import dev.appoutlet.some.config.NullableStrategy
import dev.appoutlet.some.config.StringStrategy
```

## Basic Generation

The fixture remains callable and supports selecting from a non-empty range:

```kotlin
val fixture = kotlinFixture()

val user: User = fixture()
val age: Int = fixture(18..65)
val name: String = fixture(listOf("Alice", "Bob", "Charlie"))
```

An empty range falls back to generated data.

## Configuration

The configuration lambda is a `SomeConfigBuilder` lambda. Type factories receive a `KClass` explicitly:

```kotlin
val fixture = kotlinFixture {
factory(String::class) { "fixed-value" }
factory(User::class) {
User(
id = random.nextInt(1, 101),
name = "generated-by-factory"
)
}
}
```

Factory lambdas receive a `FixtureContext`. It exposes `random`, `resolutionStack`, and `strategyProvider`.

Property factories use a property reference and override constructor property values:

```kotlin
val fixture = kotlinFixture {
property(User::name) { "OverriddenName" }
property(User::age) { 30 }
}
```

## Per-Call Overrides

Pass a builder lambda to an individual fixture invocation. The override applies only to that call:

```kotlin
val fixture = kotlinFixture {
factory(String::class) { "default" }
}

val overridden: String = fixture {
factory(String::class) { "override" }
}

val unchanged: String = fixture()
```

`Fixture.create` also accepts a one-off builder lambda:

```kotlin
val value: Int = fixture.create {
factory(Int::class) { 42 }
}
```

## New Fixtures

`Fixture.new` creates a new fixture from the supplied builder lambda:

```kotlin
val base = kotlinFixture {
factory(String::class) { "base" }
}

val other = base.new {
factory(Int::class) { 42 }
}
```

The new fixture uses the configuration passed to `new`; it does not merge the original fixture's configuration.

## Sequences

Use `SequenceStrategy.Bounded` for a finite sequence or `SequenceStrategy.Unbounded` for a lazy infinite sequence:

```kotlin
val batch = fixture
.asSequence<User>(SequenceStrategy.Bounded(10))
.toList()

val infinite = fixture
.asSequence<User>(SequenceStrategy.Unbounded)
.take(10)
.toList()
```

`SequenceStrategy.Bounded(0)` produces an empty sequence. Negative bounds are rejected.

## Seeds and Strategies

Use `seed` for reproducible generation:

```kotlin
val fixture = kotlinFixture {
seed = 12345L
}
```

Register Some strategies directly with `strategy`:

```kotlin
val fixture = kotlinFixture {
strategy(StringStrategy.Readable)
strategy(NullableStrategy.NeverNull)
strategy(DefaultValueStrategy.Generate)
}
```

## Unsupported KotlinFixture APIs

The simplified compatibility module does not provide the former KotlinFixture-specific APIs:

- `Configuration` and `ConfigurationBuilder`
- `NullabilityStrategy`, `OptionalStrategy`, and `RecursionStrategy` aliases
- `filter` and distinct-value configuration
- `subType` mappings
- the `range` helper inside factories
- KotlinFixture plugin integrations such as Java Faker, Generex, Kotest, and Android

Use the corresponding core Some API where one exists. For unsupported behavior, configure a `factory` or generate the value directly in a test-specific helper.
Loading
Loading