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
22 changes: 22 additions & 0 deletions core/src/main/kotlin/dev/appoutlet/some/config/FloatStrategy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dev.appoutlet.some.config

data class FloatStrategy(
val range: ClosedFloatingPointRange<Float> = 0.0f..1.0f
) : Strategy {
override val key = FloatStrategy::class

constructor(fixed: Float) : this(fixed..fixed)

init {
require(range.start <= range.endInclusive) {
"range.start must be less than or equal to range.endInclusive"
}
}

companion object {
/**
* The default float strategy.
*/
val default = FloatStrategy()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ data class SomeConfig(
IntResolver(random),
LongResolver(random),
DoubleResolver(random),
FloatResolver(random),
FloatResolver(strategyProvider, random),
BooleanResolver(random),
CharResolver(random),
ByteResolver(random),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import kotlin.reflect.full.instanceParameter
* strategy(NullableStrategy.NeverNull)
* strategy(StringStrategy.Uuid)
* strategy(CollectionStrategy(5..10))
* strategy(FloatStrategy(0.0f..10.0f))
* }
* ```
*
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/kotlin/dev/appoutlet/some/config/Strategy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import kotlin.reflect.KClass
* Marker interface for strategy objects that configure fixture generation behavior.
*
* All built-in strategies ([NullableStrategy], [StringStrategy], [CollectionStrategy],
* and [DefaultValueStrategy]) implement this interface. Custom strategies can also be
* [FloatStrategy], and [DefaultValueStrategy]) implement this interface. Custom strategies can also be
* created by implementing [Strategy] and registering them via [SomeConfigBuilder.strategy].
*
* The [key] property determines the registration bucket for the strategy. For sealed interface
Expand Down
20 changes: 18 additions & 2 deletions core/src/main/kotlin/dev/appoutlet/some/resolver/FloatResolver.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.config.FloatStrategy
import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.StrategyProvider
import dev.appoutlet.some.core.get
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.typeOf

class FloatResolver(val random: Random) : Resolver {
class FloatResolver(
strategyProvider: StrategyProvider,
private val random: Random
) : Resolver {
private val floatStrategy = strategyProvider.get<FloatStrategy>() ?: FloatStrategy.default

override fun canResolve(type: KType): Boolean = type == typeOf<Float>()

override fun resolve(type: KType, chain: ResolverChain): Any {
return random.nextFloat()
val range = floatStrategy.range
return if (range.start == range.endInclusive) {
range.start
} else {
random.nextDouble(
from = range.start.toDouble(),
until = range.endInclusive.toDouble(),
).toFloat()
}
Comment thread
MessiasLima marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package dev.appoutlet.some.config

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertSame

class FloatStrategyTest {
@Test
fun `FloatStrategy has default range 0_0 to 1_0`() {
val strategy = FloatStrategy()
assertEquals(0.0f, strategy.range.start)
assertEquals(1.0f, strategy.range.endInclusive)
}

@Test
fun `FloatStrategy default companion is no-arg instance`() {
val strategy = FloatStrategy.default
assertEquals(0.0f, strategy.range.start)
assertEquals(1.0f, strategy.range.endInclusive)
}

@Test
fun `FloatStrategy accepts valid range`() {
val strategy = FloatStrategy(1.0f..5.0f)
assertEquals(1.0f, strategy.range.start)
assertEquals(5.0f, strategy.range.endInclusive)
}

@Test
fun `FloatStrategy accepts zero-width range`() {
val strategy = FloatStrategy(3.0f..3.0f)
assertEquals(3.0f, strategy.range.start)
assertEquals(3.0f, strategy.range.endInclusive)
}

@Test
fun `FloatStrategy accepts negative range`() {
val strategy = FloatStrategy(-5.0f..-1.0f)
assertEquals(-5.0f, strategy.range.start)
assertEquals(-1.0f, strategy.range.endInclusive)
}

@Test
fun `FloatStrategy rejects inverted range`() {
val exception = assertFailsWith<IllegalArgumentException> {
FloatStrategy(5.0f..2.0f)
}
assertEquals(
"range.start must be less than or equal to range.endInclusive",
exception.message
)
}

@Test
fun `FloatStrategy fixed constructor is equivalent to zero-width range`() {
val fixed = FloatStrategy(3.0f)
val range = FloatStrategy(3.0f..3.0f)
assertEquals(range.range.start, fixed.range.start)
assertEquals(range.range.endInclusive, fixed.range.endInclusive)
}

@Test
fun `FloatStrategy key is its own class`() {
assertSame(FloatStrategy::class, FloatStrategy().key)
}
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,102 @@
package dev.appoutlet.some.resolver

import dev.appoutlet.some.config.DefaultStrategyProvider
import dev.appoutlet.some.config.FloatStrategy
import dev.appoutlet.some.config.NullableStrategy
import dev.appoutlet.some.config.buildSomeConfig
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.test.defaultTestChain
import kotlin.random.Random
import kotlin.reflect.typeOf
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertTrue

class FloatResolverTest {
@Test
fun `FloatResolver generates float values`() {
val resolver = FloatResolver(Random.Default)
val resolver = FloatResolver(DefaultStrategyProvider(), Random.Default)

val result = resolver.resolve(typeOf<Float>(), defaultTestChain)
assertIs<Float>(result)
}

@Test
fun `FloatResolver generates values between 0 and 1`() {
val resolver = FloatResolver(Random.Default)
fun `FloatResolver generates values between 0 and 1 by default`() {
val resolver = FloatResolver(DefaultStrategyProvider(), Random.Default)

repeat(100) {
val result = resolver.resolve(typeOf<Float>(), defaultTestChain) as Float
assertTrue(result in 0.0f..<1.0f, "Expected value between 0.0 and 1.0, got $result")
}
}

@Test
fun `FloatResolver honors a custom range`() {
val resolver = FloatResolver(
DefaultStrategyProvider(mapOf(FloatStrategy::class to FloatStrategy(0.0f..10.0f))),
Random.Default
)

repeat(100) {
val result = resolver.resolve(typeOf<Float>(), defaultTestChain) as Float
assertTrue(result in 0.0f..<10.0f, "Expected value between 0.0 and 10.0, got $result")
}
}

@Test
fun `FloatResolver honors a negative range`() {
val resolver = FloatResolver(
DefaultStrategyProvider(mapOf(FloatStrategy::class to FloatStrategy(-5.0f..-1.0f))),
Random.Default
)

repeat(100) {
val result = resolver.resolve(typeOf<Float>(), defaultTestChain) as Float
assertTrue(result in -5.0f..<-1.0f, "Expected value between -5.0 and -1.0, got $result")
}
}

@Test
fun `FloatResolver always returns fixed value for pinned strategy`() {
val resolver = FloatResolver(
DefaultStrategyProvider(mapOf(FloatStrategy::class to FloatStrategy(5.0f))),
Random.Default
)

repeat(50) {
val result = resolver.resolve(typeOf<Float>(), defaultTestChain) as Float
assertEquals(5.0f, result)
}
}

@Test
fun `FloatResolver canResolve detects Float type`() {
val resolver = FloatResolver(Random.Default)
val resolver = FloatResolver(DefaultStrategyProvider(), Random.Default)
assertTrue(resolver.canResolve(typeOf<Float>()))
}

@Test
fun `FloatResolver rejects non-Float types`() {
val resolver = FloatResolver(Random.Default)
val resolver = FloatResolver(DefaultStrategyProvider(), Random.Default)
assertFalse(resolver.canResolve(typeOf<String>()))
assertFalse(resolver.canResolve(typeOf<Int>()))
assertFalse(resolver.canResolve(typeOf<Double>()))
}

@Test
fun `FloatStrategy integrates with SomeConfig`() {
val config = buildSomeConfig {
strategy(FloatStrategy(0.0f..10.0f))
}
val resolvers = config.buildResolvers()
val chain = ResolverChain(resolvers, config[NullableStrategy::class])

repeat(50) {
val result = chain.resolve(typeOf<Float>()) as Float
assertTrue(result in 0.0f..<10.0f, "Expected value between 0.0 and 10.0, got $result")
}
}
}
63 changes: 63 additions & 0 deletions docs/configuration/float-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Float Strategy

Controls the range of `Float` values generated during fixture generation.

## Configuration

The `FloatStrategy` takes a single `range` parameter — a `ClosedFloatingPointRange<Float>` specifying the inclusive minimum and inclusive maximum of generated `Float` values. The end of the range is treated as exclusive during generation, matching `random.nextDouble(from, until)` semantics.

```kotlin
// Default: 0.0 to 1.0 (exclusive end)
some { strategy(FloatStrategy()) }

// Positive range
some { strategy(FloatStrategy(0.0f..10.0f)) }

// Negative range
some { strategy(FloatStrategy(-5.0f..-1.0f)) }

// Range that spans zero
some { strategy(FloatStrategy(-1.0f..1.0f)) }
```

## Pinning a fixed value

A secondary constructor `FloatStrategy(fixed: Float)` is provided as a convenience for pinning generation to a single value. It is equivalent to `FloatStrategy(fixed..fixed)`.

```kotlin
// Pin to a single value
some { strategy(FloatStrategy(5.0f)) }

// Equivalent zero-width range
some { strategy(FloatStrategy(5.0f..5.0f)) }
```

Zero-width ranges (`start == endInclusive`) are always honored — the resolver returns the fixed value directly without invoking the random source.

## Default

| Property | Default value |
|----------|---------------|
| `range` | `0.0f..1.0f` |

The default range preserves the behavior of `random.nextFloat()` (`[0.0, 1.0)`), so existing users see no change.

## Affected types

The float strategy applies to the `Float` primitive type:

- `Float`

## Validation

The `FloatStrategy` constructor enforces one constraint on the `range` parameter via a `require()` check. It throws `IllegalArgumentException` on failure.

### Start must be less than or equal to end

`range.start` must be `<=` `range.endInclusive`. Inverted ranges are rejected; equal bounds (zero-width ranges) are allowed.

```kotlin
FloatStrategy(0.0f..1.0f) // OK
FloatStrategy(5.0f..2.0f) // IllegalArgumentException: range.start must be less than or equal to range.endInclusive
FloatStrategy(3.0f..3.0f) // OK — zero-width range, always returns 3.0f
```
1 change: 1 addition & 0 deletions docs/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ val stillNeverNull: Person = baseSome()
| `NullableStrategy` | `NullableStrategy.NullOnCircularReference` | Emits `null` for nullable circular references | [NullableStrategy](nullable-strategy.md) |
| `StringStrategy` | `StringStrategy.Random()` | Random lowercase alphabetic strings | [StringStrategy](string-strategy.md) |
| `CollectionStrategy` | `CollectionStrategy()` | Collections with 1 to 5 elements | [CollectionStrategy](collection-strategy.md) |
| `FloatStrategy` | `FloatStrategy()` | Floats in the range `0.0f..1.0f` | [FloatStrategy](float-strategy.md) |
| `DefaultValueStrategy` | `DefaultValueStrategy.UseDefault` | Uses Kotlin defaults for optional parameters | [DefaultValueStrategy](default-value-strategy.md) |
| `seed` | `null` | Uses non-deterministic `Random.Default` | — |

Expand Down
2 changes: 1 addition & 1 deletion docs/supported-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ For types not listed here, register a [custom factory](custom-factories.md) or s
| `Int` | `some<Int>()` | |
| `Long` | `some<Long>()` | |
| `Double` | `some<Double>()` | |
| `Float` | `some<Float>()` | |
| `Float` | `some<Float>()` | See [FloatStrategy](configuration/float-strategy.md) |
| `Boolean` | `some<Boolean>()` | |
| `Char` | `some<Char>()` | |
| `Byte` | `some<Byte>()` | |
Expand Down
3 changes: 2 additions & 1 deletion zensical.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ nav = [
"configuration/index.md" ,
"configuration/nullable-strategy.md",
"configuration/string-strategy.md",
"configuration/collection-strategy.md"
"configuration/collection-strategy.md",
"configuration/float-strategy.md"
] },
"supported-types.md",
"custom-factories.md",
Expand Down