Skip to content
Closed
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
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ Documentation pages are under `docs/` and the site config is in `zensical.toml`.

```
src/main/kotlin/dev/appoutlet/some/
├── config/ # Configuration, strategies, and builder
├── core/ # Core abstractions and resolver chain
├── config/ # Global configuration and builder
├── core/ # Core abstractions, strategies, and resolver chain
├── exception/ # Custom exceptions
└── resolver/ # Type resolvers
└── resolver/ # Type resolvers grouped by type
├── string/ # String resolver and strategy
├── primitive/ # Numeric and boolean resolvers
└── ...
```

---
Expand All @@ -54,7 +57,7 @@ src/main/kotlin/dev/appoutlet/some/
### File Organization
- **One class per file** - Each resolver, exception, and config class has its own file
- **File name matches class name** - `IntResolver.kt` contains `class IntResolver`
- **Package structure mirrors functionality** - resolvers in `resolver/`, config in `config/`
- **Package-by-type** - Group resolvers with their related strategies in a sub-package of `resolver/` (e.g., `resolver/string/` contains both `StringResolver` and `StringStrategy`).

### Naming Conventions
- **Resolvers**: `{Type}Resolver` (e.g., `IntResolver`, `ListResolver`)
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ You are welcome to use any coding tool you prefer, including AI-assisted tools s
What matters most is the quality of the result:

- **Code should be clear and idiomatic**: follow the project's conventions, keep changes focused, and make sure Detekt passes.
- **Package-by-type**: When adding new type support, group the resolver and its related strategies in a new sub-package under `dev.appoutlet.some.resolver`.
- **Tests should be meaningful**: test real behavior and important edge cases. Avoid tests that only exist to increase coverage without validating useful behavior.
- **Changes should be covered by tests**: new behavior and bug fixes should include appropriate tests, and the project coverage requirements must continue to pass.

Expand Down
21 changes: 21 additions & 0 deletions fix_backticks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os
import re

def fix_file(filepath):
if not os.path.isfile(filepath): return
with open(filepath, 'r') as f:
content = f.read()

# Replace the actual path in the file
new_content = content
# We used 'klass', 'obj', 'enm' in the directories. Let's stick with them.
# The error message said e.g. e: file:///app/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt:11:36 Unresolved reference 'class'.
# This is because I imported dev.appoutlet.some.resolver.klass.ClassResolver but maybe SomeConfig was using 'class' somewhere?

# Wait, I see:
# e: file:///app/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt:11:36 Unresolved reference 'class'.

# Let's check that line in SomeConfig.kt
return new_content

# (script incomplete, just checking)
Comment on lines +1 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Incomplete Script

This script is incomplete, unused, and contains commented-out thoughts. It should be removed from the repository to maintain a clean codebase.

114 changes: 30 additions & 84 deletions src/main/kotlin/dev/appoutlet/some/Some.kt
Original file line number Diff line number Diff line change
@@ -1,110 +1,56 @@
package dev.appoutlet.some

import dev.appoutlet.some.config.NullableStrategy
import dev.appoutlet.some.config.SomeConfig
import dev.appoutlet.some.config.SomeConfigBuilder
import dev.appoutlet.some.config.buildSomeConfig
import dev.appoutlet.some.core.ResolverChain
import dev.appoutlet.some.core.TypeResolver
import dev.appoutlet.some.core.get
import dev.appoutlet.some.resolver.nullable.NullableStrategy
import kotlin.random.Random
import kotlin.reflect.typeOf

/**
* Fixture generator configured with a resolver chain and shared random source.
*
* Instances are created by [someSetup] and can be reused to generate multiple values with the same configuration.
*
* @param resolvers Ordered resolver list used to generate values.
* @param random Random source shared by resolvers created for this instance.
* @param config Configuration used by this generator.
*/
class Some(
val resolvers: List<TypeResolver>,
val random: Random,
val config: SomeConfig
) {
/**
* Generates a fixture value of type [T] using this instance's configuration.
*
* @param T Type to generate.
* @return Generated value of type [T].
*/
@Suppress("MemberNameEqualsClassName")
inline fun <reified T> some(): T {
val nullableStrategy = config[NullableStrategy::class]
val session = ResolverChain(resolvers, nullableStrategy)
return session.resolve(typeOf<T>()) as T
inline fun <reified T : Any> some(): T {
val strategy = config.get<NullableStrategy>() ?: NullableStrategy.default
val chain = ResolverChain(resolvers, strategy)
return chain.resolve(typeOf<T>()) as T
}
}
Comment on lines 12 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Breaking Changes & Nullability Regression

  1. Missing invoke Operators: The removal of the invoke operators (operator fun <reified T> invoke(): T and operator fun <reified T> invoke(noinline config: SomeConfigBuilder.() -> Unit): T) breaks existing tests (e.g., SetupIntegrationTest.kt lines 103 and 110) and is a major breaking change for users of the library.
  2. Nullability Restriction: Restricting T to Any (via <reified T : Any>) prevents generating nullable types directly (e.g., some<String?>()). Since the library explicitly supports nullable types via NullableResolver and NullableStrategy, this constraint should be removed.
class Some(
    val resolvers: List<TypeResolver>,
    val random: Random,
    val config: SomeConfig
) {
    inline fun <reified T> some(): T {
        val strategy = config.get<NullableStrategy>() ?: NullableStrategy.default
        val chain = ResolverChain(resolvers, strategy)
        return chain.resolve(typeOf<T>()) as T
    }

    inline operator fun <reified T> invoke(): T = some()

    inline operator fun <reified T> invoke(noinline configuration: SomeConfigBuilder.() -> Unit = {}): T {
        val aggregatedConfig = this.config.toBuilder().apply(configuration).build()
        return Some(aggregatedConfig.buildResolvers(random), random, aggregatedConfig).some()
    }
}


/**
* Generates a fixture value of type [T].
*
* Enables concise usage such as `some<User>()` when `some` is a [Some] instance.
*
* @param T Type to generate.
* @return Generated value of type [T].
*/
inline operator fun <reified T> invoke(): T = some()

/**
* Generates a fixture value of type [T] with per-call configuration overrides.
*
* Overrides are applied to a copy of this instance's configuration and do not mutate the original [Some].
*
* @param T Type to generate.
* @param config Configuration overrides for this call.
* @return Generated value of type [T].
*/
inline operator fun <reified T> invoke(noinline config: SomeConfigBuilder.() -> Unit = {}): T {
val aggregatedConfig = this.config.toBuilder().apply(config).build()
return Some(aggregatedConfig.buildResolvers(random), random, aggregatedConfig).some()
inline fun <reified T : Any> some(noinline configuration: (SomeConfigBuilder.() -> Unit)? = null): T {
val config = if (configuration != null) {
buildSomeConfig(configuration)
} else {
defaultSomeConfig
}
val some = Some(config.buildResolvers(), config.buildRandom(), config)
return some.some<T>()
}
Comment on lines +24 to 32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Performance Regression & Nullability Constraint

  1. Performance Overhead: When configuration is null, the current implementation instantiates a new Some instance and rebuilds the entire resolver chain (which triggers reflection and ServiceLoader discovery) on every single call to some(). It should delegate to someDefault<T>() to reuse the cached defaultResolvers.
  2. Nullability Constraint: Remove the : Any upper bound constraint on T to allow direct generation of nullable types.
inline fun <reified T> some(noinline configuration: (SomeConfigBuilder.() -> Unit)? = null): T {
    if (configuration == null) {
        return someDefault<T>()
    }
    val config = buildSomeConfig(configuration)
    val some = Some(config.buildResolvers(), config.buildRandom(), config)
    return some.some<T>()
}


/**
* Creates a reusable [Some] generator.
*
* Use this when multiple fixtures should share the same configuration and random source.
*
* @param config Configuration applied to the created generator.
* @return A configured [Some] instance.
*/
fun someSetup(config: SomeConfigBuilder.() -> Unit = {}): Some {
val someConfig = buildSomeConfig(config)
val random = someConfig.buildRandom()
return Some(someConfig.buildResolvers(random), random, someConfig)
fun someSetup(configuration: SomeConfigBuilder.() -> Unit = {}): Some {
val config = buildSomeConfig(configuration)
return Some(config.buildResolvers(), config.buildRandom(), config)
}

/**
* Lazily-created default configuration used by top-level [some].
*/
val defaultConfig: SomeConfig by lazy { SomeConfig() }
@PublishedApi
internal val defaultSomeConfig: SomeConfig by lazy { buildSomeConfig() }

/**
* Lazily-created default resolver chain used by top-level [some].
*/
val defaultResolvers: List<TypeResolver> by lazy { defaultConfig.buildResolvers() }
@PublishedApi
internal val defaultResolvers: List<TypeResolver> by lazy {
defaultSomeConfig.buildResolvers()
}

/**
* Generates a fixture value using the default configuration.
*
* @param T Type to generate.
* @return Generated value of type [T].
*/
inline fun <reified T> some(): T {
val nullableStrategy = defaultConfig[NullableStrategy::class]
val session = ResolverChain(defaultResolvers, nullableStrategy)
return session.resolve(typeOf<T>()) as T
@PublishedApi
internal inline fun <reified T : Any> someDefault(): T {
val strategy = defaultSomeConfig.get<NullableStrategy>() ?: NullableStrategy.default
val chain = ResolverChain(defaultResolvers, strategy)
return chain.resolve(typeOf<T>()) as T
}
Comment on lines +47 to 52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Nullability Constraint Regression

Remove the : Any upper bound constraint on T to allow direct generation of nullable types.

Suggested change
@PublishedApi
internal inline fun <reified T : Any> someDefault(): T {
val strategy = defaultSomeConfig.get<NullableStrategy>() ?: NullableStrategy.default
val chain = ResolverChain(defaultResolvers, strategy)
return chain.resolve(typeOf<T>()) as T
}
@PublishedApi
internal inline fun <reified T> someDefault(): T {
val strategy = defaultSomeConfig.get<NullableStrategy>() ?: NullableStrategy.default
val chain = ResolverChain(defaultResolvers, strategy)
return chain.resolve(typeOf<T>()) as T
}


/**
* Generates a fixture value using one-off configuration overrides.
*
* @param T Type to generate.
* @param config Configuration applied only to this generation call.
* @return Generated value of type [T].
*/
inline fun <reified T> some(noinline config: SomeConfigBuilder.() -> Unit = {}): T {
val someSetup = someSetup(config)
return someSetup.some<T>()
fun buildSomeConfig(configuration: SomeConfigBuilder.() -> Unit = {}): SomeConfig {
return SomeConfigBuilder().apply(configuration).build()
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.appoutlet.some.config

import dev.appoutlet.some.core.Strategy
import dev.appoutlet.some.core.StrategyProvider
import kotlin.reflect.KClass

Expand Down
47 changes: 0 additions & 47 deletions src/main/kotlin/dev/appoutlet/some/config/DefaultValueStrategy.kt

This file was deleted.

Loading