diff --git a/.gitignore b/.gitignore index ded51d9b..e0e1404b 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ bin/ .DS_Store ### OpenCode ### -.opencode \ No newline at end of file +.opencode/* +!.opencode/agents \ No newline at end of file diff --git a/.opencode/agents/advisor.md b/.opencode/agents/advisor.md new file mode 100644 index 00000000..5519a498 --- /dev/null +++ b/.opencode/agents/advisor.md @@ -0,0 +1,82 @@ +--- +description: Opinionated senior Kotlin advisor for architecture, API, and best-practice decisions; reads code and current docs, challenges weak ideas, and avoids code changes. +mode: all +temperature: 0.2 +permission: + read: allow + glob: allow + grep: allow + list: allow + lsp: allow + question: allow + webfetch: allow + websearch: allow + skill: deny + task: deny + todowrite: deny + external_directory: deny + edit: + "*": deny + ".opencode/advice/**": allow + bash: + "*": deny + "pwd": allow + "ls": allow + "ls *": allow + "git status": allow + "git status *": allow + "git diff": allow + "git diff *": allow + "git log": allow + "git log *": allow + "git show": allow + "git show *": allow + "git branch": allow + "git branch *": allow + "git rev-parse *": allow + "which *": allow + "rg": allow + "rg *": allow +--- + +You are The Advisor, an experienced and opinionated Kotlin developer focused on helping the user make better engineering decisions before they commit to them. + +Your job is to think with the user, not to implement for them. + +Core behavior: +- Act like a senior Kotlin engineer with strong opinions grounded in real-world team practices. +- Help the user evaluate tradeoffs in API design, architecture, testing strategy, naming, maintainability, readability, performance, dependency choices, Gradle setup, and long-term project health. +- Challenge ideas that are fragile, over-engineered, non-idiomatic, or uncommon in healthy engineering teams. +- Push back when the user's direction looks weak, but first understand their context, constraints, and reasoning. +- Prefer practical advice over academic purity. +- Be direct and candid. Do not be vague just to be agreeable. + +How to reason: +- Start by clarifying the actual decision to be made. +- Look for hidden constraints, second-order effects, and maintenance cost. +- Explain why something is a good or bad idea in terms of tradeoffs, not taste alone. +- Distinguish between Kotlin-idiomatic guidance, JVM ecosystem norms, and company/team process concerns. +- If the topic depends on evolving standards, library guidance, or current ecosystem practice, research it on the web before giving a confident recommendation. +- When the codebase matters, inspect it before advising so your guidance matches the existing architecture and conventions. + +Interaction style: +- Most of the time, keep the advice in the normal chat. +- Use concise, structured recommendations. +- If the user proposes something risky, say so clearly and explain the risk. +- If there are multiple reasonable options, recommend one and explain why it wins here. +- If you need more context to give good advice, ask focused questions instead of guessing. + +Hard boundaries: +- Do not write or modify source code, tests, configuration, docs, or git state. +- Do not commit, stage, push, install, or run destructive or state-changing commands. +- Do not quietly drift into implementation work, even if the user starts asking for code. +- You may only write markdown notes inside `.opencode/advice/` when a durable artifact is genuinely useful, such as a decision memo, option comparison, or research summary. +- Do not create files in `.opencode/advice/` by default. Prefer chat unless a written artifact clearly adds value. + +When giving advice: +- Optimize for decisions that would hold up in a strong Kotlin code review. +- Prefer standard, boring, maintainable solutions unless there is a concrete reason to do something unusual. +- Call out anti-patterns, premature abstraction, misuse of language features, hidden coupling, and unnecessary complexity. +- When relevant, mention what established teams commonly do and what they usually avoid. + +If the user asks you to implement, refuse briefly and stay in advisory mode. Offer decision support, tradeoff analysis, or a written recommendation instead. diff --git a/.opencode/agents/doc-writer.md b/.opencode/agents/doc-writer.md new file mode 100644 index 00000000..2dc60865 --- /dev/null +++ b/.opencode/agents/doc-writer.md @@ -0,0 +1,88 @@ +--- +description: Writes and improves developer-facing documentation in `docs/`; understands the codebase, explains usage clearly, and organizes content for readers instead of source layout. +mode: all +temperature: 0.2 +permission: + read: allow + glob: allow + grep: allow + list: allow + lsp: allow + question: allow + webfetch: allow + websearch: allow + skill: deny + task: deny + todowrite: deny + external_directory: deny + edit: + "*": deny + "docs/**": allow + "zensical.toml": allow + bash: + "*": deny + "pwd": allow + "ls": allow + "ls *": allow + "git status": allow + "git status *": allow + "git diff": allow + "git diff *": allow + "git log": allow + "git log *": allow + "git show": allow + "git show *": allow + "git branch": allow + "git branch *": allow + "git rev-parse *": allow + "which *": allow + "rg": allow + "rg *": allow +--- + +You are Doc Writer, a documentation-focused agent for this codebase. + +Your job is to write and improve documentation inside `docs/`. + +Core behavior: +- Understand the codebase before writing. +- Write for developers using the library, not maintainers reading internal implementation details. +- Prioritize usage, common workflows, configuration, examples, caveats, and decision-making guidance. +- Organize documentation according to how a human learns and navigates the topic, not according to package names or source folders. +- Use simple, clear language. +- Follow Zensical authoring conventions used by this site, including its Markdown, frontmatter, admonitions, code blocks, content tabs, footnotes, and icons/emojis when they improve the page. + +Documentation standards: +- Start from the reader's likely question: what is this, when should I use it, how do I use it, and what should I watch out for? +- Prefer concrete examples over abstract explanations. +- Keep examples realistic, minimal, and easy to copy. +- Because the audience is developers, use short comments inside code snippets when they clarify important behavior, tradeoffs, or non-obvious details. +- Explain concepts in a logical order with smooth progression from basic usage to advanced customization. +- Avoid unnecessary discussion of internals unless it directly helps users apply the feature correctly. +- Be concise, but do not omit information the user needs to succeed. + +How to work: +- Inspect nearby docs before editing so your writing matches the project's existing style, structure, and frontmatter conventions. +- Inspect the codebase when needed to verify behavior, supported APIs, names, and limitations. +- If the current docs structure is confusing, improve the structure in a reader-first way. +- Add or update cross-links when they help navigation. +- If a topic depends on external standards or ecosystem conventions, verify them on the web before documenting them as fact. +- When creating or editing pages, use or match Zensical frontmatter fields already used in the docs, such as `title`, `description`, and `icon` where appropriate. +- Prefer Zensical-native presentation patterns over plain Markdown when they make documentation clearer, such as admonitions for caveats, content tabs for alternative examples, and footnotes for side details. + +Writing style: +- Clear, direct, and practical. +- Friendly but not chatty. +- Prefer short paragraphs and purposeful headings. +- Avoid marketing language, filler, and vague claims. +- Do not mirror implementation jargon unless the user-facing API uses it. +- Use code comments deliberately: they should explain why a snippet is written a certain way, not restate obvious syntax. + +Boundaries: +- Only modify files under `docs/` and `zensical.toml`. +- Do not edit source code, tests, build files, or opencode config. +- Do not commit, stage, push, install dependencies, or make unrelated changes. + +When unsure: +- Read more of the docs and code before writing. +- Ask focused questions if the requested audience, scope, or outcome is ambiguous. diff --git a/AGENTS.md b/AGENTS.md index 55928abd..5824e261 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,16 +37,19 @@ where each Resolver handles specific types. ## Project Structure -Documentation pages are under `docs/` and the site config is in `zensical.toml`. +The project is split into Gradle modules: -``` -src/main/kotlin/dev/appoutlet/some/ -├── config/ # Configuration, strategies, and builder -├── core/ # Core abstractions and resolver chain -├── exception/ # Custom exceptions -└── resolver/ # Type resolvers -``` +- `core` — Main library (`some-core`) +- `android` — Android integration (`some-android`) +- `kotest` — Kotest property-based testing integration (`some-kotest`) +Published artifacts are intended to be used as follows: + +- `some-core` — Base `some` / `someSetup` API for Java and Kotlin/JVM projects +- `some-android` — Android artifact that re-exports the core API, so consumers should not add `some-core` separately +- `some-kotest` — Kotest `Arb` integration that should be added alongside either `some-core` or `some-android` + +Documentation pages are under `docs/` and the site config is in `zensical.toml`. --- ## Code Style Guidelines @@ -134,4 +137,4 @@ Order matters - first match wins: 5. **Kotlin native types FIRST** (KotlinUuidResolver, KotlinInstantResolver, KotlinDurationResolver) 6. **Java types SECOND** (JavaUuidResolver, JavaInstantResolver, JavaDurationResolver, JavaZonedDateTimeResolver) 7. Collection resolvers (List, Set, Map, Array) -8. ClassResolver (fallback for classes with constructors) \ No newline at end of file +8. ClassResolver (fallback for classes with constructors) diff --git a/README.md b/README.md index 906b8346..02f5d8ba 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@ # Some ![Kotlin JVM](https://img.shields.io/badge/jvm-kotlin?style=for-the-badge&label=kotlin) -![Maven Central Version](https://img.shields.io/maven-central/v/dev.appoutlet/some?style=for-the-badge) -A Kotlin JVM library that generates populated instances of any Kotlin class for testing purposes, with zero configuration required. +A Kotlin test data generation library for JVM and Android tests, with optional Kotest `Arb` integration. ## The Problem @@ -38,16 +37,32 @@ val user = some() ## Installation -Add Some to your Gradle dependencies: +Some is published as three 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`. ```kotlin -testImplementation("dev.appoutlet:some:") +dependencies { + // Kotlin/JVM or Java tests + testImplementation("dev.appoutlet:some-core:{version}") + + // Android tests. Includes the shared Some API, so do not also add some-core. + testImplementation("dev.appoutlet:some-android:{version}") + + // Optional: Kotest property testing integration. + // Add this alongside either some-core or some-android. + testImplementation("dev.appoutlet:some-kotest:{version}") +} ``` ## Documentation 📖 Read the full documentation at **[some.appoutlet.dev](https://some.appoutlet.dev)** for installation, configuration, and advanced usage. +If you are upgrading from `0.2.1`, start with the [0.2.1 to 0.2.2 migration notes](docs/migration/0.2.1-to-0.2.2.md). + ## Contributing Interested in helping improve Some? Contributions are welcome. Please read the [contributing guide](CONTRIBUTING.md) to learn how to set up the project, run the checks, and prepare a good contribution. diff --git a/android/build.gradle.kts b/android/build.gradle.kts index f4bd4028..e3d0a528 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.detekt) alias(libs.plugins.dokka) + alias(libs.plugins.mavenPublish) } group = rootProject.group @@ -30,7 +31,7 @@ android { detekt { autoCorrect = true } dependencies { - implementation(projects.core) + api(projects.core) testImplementation(libs.androidx.compose.ui) testImplementation(libs.junit) @@ -38,3 +39,39 @@ dependencies { detektPlugins(libs.detekt.formatting) } + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + signAllPublications() + + coordinates(artifactId = "some-android") + + pom { + name.set("Some Android") + description.set("Android integration for Some, a Kotlin test data generation library.") + inceptionYear.set("2026") + url.set("https://github.com/MessiasLima/Some") + + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("repo") + } + } + + developers { + developer { + id.set("MessiasLima") + name.set("Messias Lima") + url.set("https://github.com/MessiasLima") + } + } + + scm { + url.set("https://github.com/MessiasLima/Some") + connection.set("scm:git:git://github.com/MessiasLima/Some.git") + developerConnection.set("scm:git:ssh://git@github.com/MessiasLima/Some.git") + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index f69090ef..23d66b31 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { dokkaHtmlPlugin(libs.dokka.versioning) dokka(projects.android) dokka(projects.core) + dokka(projects.kotest) } tasks.named("prepareKotlinBuildScriptModel") { diff --git a/docs/artifacts/some-android.md b/docs/artifacts/some-android.md new file mode 100644 index 00000000..eacaf013 --- /dev/null +++ b/docs/artifacts/some-android.md @@ -0,0 +1,48 @@ +--- +icon: lucide/smartphone +--- +# some-android + +Use `dev.appoutlet:some-android` in Android projects. + +`some-android` re-exports the shared Some API from `some-core`, so Android users should depend on `some-android` and should not add `some-core` separately. + +## Installation + +=== "Gradle (Kotlin DSL)" + + ```kotlin + testImplementation("dev.appoutlet:some-android:{version}") + ``` + +=== "Gradle (Groovy)" + + ```groovy + testImplementation 'dev.appoutlet:some-android:{version}' + ``` + +## When to use it + +- Android unit tests +- Android libraries and applications that want the shared Some API + +## Platform constraints + +- Android `minSdk 24` +- Shared API behavior follows the same Kotlin/JVM constraints as `some-core` + +## Artifact-specific behavior + +- Re-exports the shared API from `some-core` +- Consumers should not add both `some-android` and `some-core` +- Does not currently add Android-only strategies or supported types beyond the shared API + +## Shared behavior + +The main Some documentation applies here as well: + +- [Getting Started](../getting-started.md) +- [Strategies](../strategies/index.md) +- [Supported Types](../supported-types.md) +- [Type and Property Factories](../custom-factories.md) +- [Custom Resolvers](../custom-resolvers.md) diff --git a/docs/artifacts/some-core.md b/docs/artifacts/some-core.md new file mode 100644 index 00000000..39d8d7fe --- /dev/null +++ b/docs/artifacts/some-core.md @@ -0,0 +1,43 @@ +--- +icon: lucide/box +--- +# some-core + +Use `dev.appoutlet:some-core` in Java and Kotlin/JVM projects. + +This is the JVM artifact for Some's shared API, including `some()`, `someSetup {}`, strategies, providers, custom factories, custom resolvers, and the standard supported types. + +## Installation + +=== "Gradle (Kotlin DSL)" + + ```kotlin + testImplementation("dev.appoutlet:some-core:{version}") + ``` + +=== "Gradle (Groovy)" + + ```groovy + testImplementation 'dev.appoutlet:some-core:{version}' + ``` + +## When to use it + +- JVM test suites +- Kotlin/JVM libraries and applications +- Java projects that want the shared Some API + +## Platform constraints + +- JVM 17+ +- Kotlin 2.3.0+ + +## Shared behavior + +Most documented behavior in this site is shared across `some-core` and `some-android`. + +- [Getting Started](../getting-started.md) +- [Strategies](../strategies/index.md) +- [Supported Types](../supported-types.md) +- [Type and Property Factories](../custom-factories.md) +- [Custom Resolvers](../custom-resolvers.md) diff --git a/docs/artifacts/some-kotest.md b/docs/artifacts/some-kotest.md new file mode 100644 index 00000000..23d8e13a --- /dev/null +++ b/docs/artifacts/some-kotest.md @@ -0,0 +1,61 @@ +--- +icon: lucide/flask-conical +--- +# some-kotest + +Use `dev.appoutlet:some-kotest` when you want Kotest `Arb` integration for Some. + +`some-kotest` is an add-on integration. It should be used alongside either `some-core` or `some-android`. + +## Installation + +### JVM / Kotlin + +```kotlin +testImplementation("dev.appoutlet:some-core:{version}") +testImplementation("dev.appoutlet:some-kotest:{version}") +``` + +### Android + +```kotlin +testImplementation("dev.appoutlet:some-android:{version}") +testImplementation("dev.appoutlet:some-kotest:{version}") +``` + +## When to use it + +- Kotest property tests that should generate fixtures through Some +- Test suites that want `Arb.some()` instead of manually building Arbs + +## Artifact-specific behavior + +- Adds `Arb.some()` +- Accepts the same configuration DSL used by `some()` and `someSetup {}` +- Uses Kotest's `RandomSource` to keep generated values reproducible + +## Usage + +```kotlin +import dev.appoutlet.some.kotest.some +import io.kotest.property.Arb +import io.kotest.property.checkAll + +checkAll(Arb.some(), Arb.some()) { user, order -> + // assertions +} +``` + +```kotlin +checkAll( + Arb.some { + seed = 12345L + strategy(StringStrategy.Readable) + strategy(NullableStrategy.NeverNull) + } +) { user -> + // assertions +} +``` + +For the shared configuration model, see [Configuration](../configuration/index.md) and [Strategies](../strategies/index.md). diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 5673078a..a6c80c4d 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -3,7 +3,7 @@ icon: lucide/settings --- # Configuration -Some works with no configuration, but each generation call can be customized when a test needs deterministic output, different null behavior, readable strings, fixed collection sizes, or custom factories. +Some works with no configuration, but every generation call can be customized when a test needs deterministic output, different null behavior, constrained collections, or custom factories. Configuration is written inside `some {}` or `someSetup {}` blocks. Register strategies or factories in the block: @@ -12,7 +12,7 @@ val user = some { seed = 12345L strategy(StringStrategy.Readable) strategy(NullableStrategy.NeverNull) - strategy(CollectionStrategy(3..7)) + factory(Email::class) { Email("test@example.com") } } ``` @@ -46,6 +46,17 @@ val order = some() This is useful in test suites where several fixtures should follow the same rules. +## Building blocks + +The configuration DSL is intentionally small: + +| Setting | Purpose | +|---------|---------| +| `seed = ...` | Reproduce the same generated values across runs | +| `strategy(...)` | Override how a built-in type is generated | +| `factory(...)` | Override generation for an entire type | +| `property(...)` | Override generation for one constructor property | + ## Per-Call Overrides Reusable configurations can be overridden for a single call. The base instance is copied before the override is applied, so later calls still use the original setup. @@ -64,17 +75,6 @@ val result: Person = baseSome { val stillNeverNull: Person = baseSome() ``` -## Defaults - -| Strategy | Default | Description | More | -|----------|---------|-------------|------| -| `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` | — | - ## Reproducible Data Set `seed` when a test should produce the same values every run. @@ -88,49 +88,9 @@ some1() == some2() // true Without a seed, values can vary between runs. -## Common Recipes - -### No Null Values - -```kotlin -val user = some { - strategy(NullableStrategy.NeverNull) -} -``` - -### Readable Strings - -```kotlin -val user = some { - strategy(StringStrategy.Readable) -} -``` - -### Small Collection Sizes - -```kotlin -val order = some { - strategy(CollectionStrategy(2..3)) -} -``` - -### Generate All Optional Values - -```kotlin -val user = some { - strategy(DefaultValueStrategy.Generate) -} -``` - -### Custom Factories - -Use `factory` to customize a whole type, and `property` to customize one constructor property. - -```kotlin -val some = someSetup { - factory(Email::class) { Email("user${random.nextInt(1000)}@example.com") } - property(User::role) { "Admin" } -} -``` +## Related Docs -See [Type and Property Factories](../custom-factories.md) for detailed documentation. +- Use [Strategies](../strategies/index.md) for the built-in defaults and strategy behavior. +- Use [Supported Types](../supported-types.md) for the shared type coverage. +- Use [Type and Property Factories](../custom-factories.md) for custom generation rules. +- Use [Artifact guides](../artifacts/some-core.md) when you need platform-specific dependency guidance. diff --git a/docs/getting-started.md b/docs/getting-started.md index 5d3dc092..6a109710 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,36 +5,28 @@ icon: lucide/graduation-cap ## Installation -Add the `some` library to your test dependencies. It is available on Maven Central. +Some is published as three artifacts. Pick one base artifact for your platform. Add `some-kotest` only if you use Kotest property testing. -![Maven Central Version](https://img.shields.io/maven-central/v/dev.appoutlet/some?style=for-the-badge) +!!! info "Upgrading from an older version?" -Replace `{version}` with the latest release shown in the badge above. + If you are migrating from `0.2.1`, see the + [0.2.1 to 0.2.2 migration guide](migration/0.2.1-to-0.2.2.md). -=== "Gradle (Kotlin DSL)" - - ```kotlin - testImplementation("dev.appoutlet:some:{version}") - ``` - -=== "Gradle (Groovy)" - - ```groovy - testImplementation 'dev.appoutlet:some:{version}' - ``` +```kotlin +dependencies { + // Kotlin/JVM or Java tests + testImplementation("dev.appoutlet:some-core:{version}") -=== "Maven" + // Android tests. Includes the shared Some API, so do not also add some-core. + testImplementation("dev.appoutlet:some-android:{version}") - ```xml - - dev.appoutlet - some - {version} - test - - ``` + // Optional: Kotest property testing integration. + // Add this alongside either some-core or some-android. + testImplementation("dev.appoutlet:some-kotest:{version}") +} +``` -`kotlin-reflect` is included as a transitive dependency — you don't need to declare it separately. +Do not add both `some-core` and `some-android`. Android projects should use `some-android` only. ## Usage @@ -110,9 +102,10 @@ val users = some> { // [User(name=string-1234, age=17), User(name=string-5678, age=23)] ``` -For more configuration use cases and details, see the [Configuration overview](configuration/index.md). +For more configuration use cases and details, see the [Configuration overview](configuration/index.md), [Strategies](strategies/index.md), and [Supported Types](supported-types.md). ## Requirements - **Kotlin 2.3.0+** — required for `typeOf()` and other type introspection APIs -- **JVM 17+** — Some is JVM-only; Android and Kotlin/Native are not currently supported +- **JVM 17+** — `some-core` targets JVM 17+ +- **Android minSdk 24** — use `some-android` for Android projects diff --git a/docs/index.md b/docs/index.md index 9379d076..97b3a529 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,9 +6,7 @@ icon: lucide/package-open # Some -![Maven Central Version](https://img.shields.io/maven-central/v/dev.appoutlet/some?style=for-the-badge) - -A Kotlin JVM library that generates populated instances of any Kotlin class for testing. Zero configuration required. +A Kotlin test data generation library for JVM and Android tests, with optional Kotest `Arb` integration. ## The Problem @@ -66,6 +64,23 @@ Writing tests means creating test data — lots of it. Constructing data classes - **Extensible** — Ship custom `ResolverProvider` implementations discovered via `ServiceLoader` to add support for domain-specific, third-party, or internal application types — with custom strategies and no consumer configuration required. - **Deterministic by choice** — Set a seed for reproducible test data across runs, or default to random for variation. +## Modules + +Some is published as three 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`. + +Use [Getting Started](getting-started.md) to install Some and learn the shared API, and the [artifact guides](artifacts/some-core.md) when you need artifact-specific details. + +## Artifact rename + +!!! info "Artifact rename" + + We renamed the JVM artifact from `dev.appoutlet:some` to `dev.appoutlet:some-core`. + See the [0.2.1 to 0.2.2 migration guide](migration/0.2.1-to-0.2.2.md) for the upgrade steps. + ## Quick start ```kotlin diff --git a/docs/migration/0.2.1-to-0.2.2.md b/docs/migration/0.2.1-to-0.2.2.md new file mode 100644 index 00000000..103e3de2 --- /dev/null +++ b/docs/migration/0.2.1-to-0.2.2.md @@ -0,0 +1,75 @@ +--- +icon: lucide/arrow-right-left +--- +# Migrating from 0.2.1 to 0.2.2 + +Version `0.2.2` changes how you depend on Some. + +If you are upgrading from `0.2.1`, the main change is the artifact split: + +- `dev.appoutlet:some` becomes `dev.appoutlet:some-core` for JVM projects +- `dev.appoutlet:some-android` is the Android artifact + +## What changed in 0.2.2 + +In `0.2.1`, projects used a single artifact: + +```kotlin +testImplementation("dev.appoutlet:some:0.2.1") +``` + +In `0.2.2`, Some is published as separate artifacts: + +- JVM: `dev.appoutlet:some-core:0.2.2` +- Android: `dev.appoutlet:some-android:0.2.2` + +This is a dependency change, not a Kotlin API change. + +## What did not change + +- The package name is still `dev.appoutlet.some` +- You still call `some()` and `someSetup {}` +- Existing configuration and fixture generation APIs remain the same +- Your existing imports for the shared API do not need to change + +## JVM users + +Replace the old JVM dependency with `some-core`. + +Before: + +```kotlin +testImplementation("dev.appoutlet:some:0.2.1") +``` + +After: + +```kotlin +testImplementation("dev.appoutlet:some-core:0.2.2") +``` + +No source changes are required if you were already using `some()` or `someSetup {}`. + +## Android users + +If your Android tests were using the old `some` artifact, switch to `some-android`. + +Before: + +```kotlin +testImplementation("dev.appoutlet:some:0.2.1") +``` + +After: + +```kotlin +testImplementation("dev.appoutlet:some-android:0.2.2") +``` + +`some-android` re-exports the core API, so you should not add `some-core` separately. + +## Summary + +- JVM users: replace `some` with `some-core` +- Android users: replace `some` with `some-android` +- Keep the same package, imports, and shared Kotlin API diff --git a/docs/migration/index.md b/docs/migration/index.md new file mode 100644 index 00000000..c3b9afb5 --- /dev/null +++ b/docs/migration/index.md @@ -0,0 +1,10 @@ +--- +icon: lucide/arrow-right-left +--- +# Migration Guides + +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` diff --git a/docs/configuration/collection-strategy.md b/docs/strategies/collection-strategy.md similarity index 74% rename from docs/configuration/collection-strategy.md rename to docs/strategies/collection-strategy.md index 28a545c0..48f50439 100644 --- a/docs/configuration/collection-strategy.md +++ b/docs/strategies/collection-strategy.md @@ -2,9 +2,11 @@ Controls the size range of generated collections during fixture generation. +This strategy is part of the shared API available through both `some-core` and `some-android`. + ## Configuration -The `CollectionStrategy` takes a single `sizeRange` parameter — an `IntRange` specifying the minimum and maximum number of elements in generated collections. +The `CollectionStrategy` takes a single `sizeRange` parameter - an `IntRange` specifying the minimum and maximum number of elements in generated collections. ```kotlin // Default: 1 to 5 elements @@ -51,18 +53,18 @@ The `CollectionStrategy` constructor enforces two constraints on the `sizeRange` ```kotlin CollectionStrategy(-1..5) // IllegalArgumentException: sizeRange.start must be positive -CollectionStrategy(0..5) // OK — empty collections are allowed +CollectionStrategy(0..5) // OK - empty collections are allowed CollectionStrategy(1..5) // OK ``` ### End must be strictly greater than start -`sizeRange.last` (the end of the range) must be strictly greater than `sizeRange.first`. This means a range with equal bounds (e.g. `3..3`) is **not** valid — there must be at least one possible size. +`sizeRange.last` (the end of the range) must be strictly greater than `sizeRange.first`. This means a range with equal bounds (e.g. `3..3`) is not valid - there must be at least one possible size. ```kotlin CollectionStrategy(5..3) // IllegalArgumentException: sizeRange.end must be greater than or equal to sizeRange.start CollectionStrategy(3..3) // IllegalArgumentException: end is not greater than start -CollectionStrategy(3..4) // OK — produces collections of size 3 or 4 +CollectionStrategy(3..4) // OK - produces collections of size 3 or 4 ``` -Note that the error message says "greater than or equal to" but the actual check is strict (`last > first`). Equal bounds are rejected. +Note that the error message says greater than or equal to but the actual check is strict (`last > first`). Equal bounds are rejected. diff --git a/docs/configuration/default-value-strategy.md b/docs/strategies/default-value-strategy.md similarity index 93% rename from docs/configuration/default-value-strategy.md rename to docs/strategies/default-value-strategy.md index 3edd9a7c..db1bf03a 100644 --- a/docs/configuration/default-value-strategy.md +++ b/docs/strategies/default-value-strategy.md @@ -2,6 +2,8 @@ Controls whether data class constructor parameters with default values should use their Kotlin default or be replaced with a generated fixture value. +This strategy is part of the shared API available through both `some-core` and `some-android`. + The default is `DefaultValueStrategy.UseDefault`, which preserves Kotlin default values. ## Variants diff --git a/docs/configuration/float-strategy.md b/docs/strategies/float-strategy.md similarity index 72% rename from docs/configuration/float-strategy.md rename to docs/strategies/float-strategy.md index d17070cd..9d449e70 100644 --- a/docs/configuration/float-strategy.md +++ b/docs/strategies/float-strategy.md @@ -2,9 +2,11 @@ Controls the range of `Float` values generated during fixture generation. +This strategy is part of the shared API available through both `some-core` and `some-android`. + ## Configuration -The `FloatStrategy` takes a single `range` parameter — a `ClosedFloatingPointRange` 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. +The `FloatStrategy` takes a single `range` parameter - a `ClosedFloatingPointRange` 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) @@ -32,7 +34,7 @@ some { strategy(FloatStrategy(5.0f)) } 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. +Zero-width ranges (`start == endInclusive`) are always honored - the resolver returns the fixed value directly without invoking the random source. ## Default @@ -59,5 +61,5 @@ The `FloatStrategy` constructor enforces one constraint on the `range` parameter ```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 +FloatStrategy(3.0f..3.0f) // OK - zero-width range, always returns 3.0f ``` diff --git a/docs/strategies/index.md b/docs/strategies/index.md new file mode 100644 index 00000000..63ba8f41 --- /dev/null +++ b/docs/strategies/index.md @@ -0,0 +1,34 @@ +--- +icon: lucide/sliders-horizontal +--- +# Strategies + +Some ships with built-in strategies for the most common test data tuning scenarios. Register them with `strategy(...)` inside `some {}` or `someSetup {}` blocks. + +These strategies are part of the shared API consumed through both `some-core` and `some-android`. + +```kotlin +val user = some { + strategy(StringStrategy.Readable) + strategy(NullableStrategy.NeverNull) + strategy(CollectionStrategy(2..4)) +} +``` + +## Defaults + +| Strategy | Default | Description | +|----------|---------|-------------| +| `NullableStrategy` | `NullableStrategy.NullOnCircularReference` | Emits `null` only when a nullable circular reference would otherwise recurse forever | +| `StringStrategy` | `StringStrategy.Random()` | Random lowercase alphabetic strings | +| `CollectionStrategy` | `CollectionStrategy()` | Collections with 1 to 5 elements | +| `FloatStrategy` | `FloatStrategy()` | Floats in the range `0.0f..1.0f` | +| `DefaultValueStrategy` | `DefaultValueStrategy.UseDefault` | Uses Kotlin defaults for optional constructor parameters | + +## Available strategies + +- [Nullable Strategy](nullable-strategy.md) +- [String Strategy](string-strategy.md) +- [Collection Strategy](collection-strategy.md) +- [Float Strategy](float-strategy.md) +- [Default Value Strategy](default-value-strategy.md) diff --git a/docs/configuration/nullable-strategy.md b/docs/strategies/nullable-strategy.md similarity index 94% rename from docs/configuration/nullable-strategy.md rename to docs/strategies/nullable-strategy.md index e2730b05..1d9675d3 100644 --- a/docs/configuration/nullable-strategy.md +++ b/docs/strategies/nullable-strategy.md @@ -5,6 +5,8 @@ Controls whether nullable types produce `null` or a concrete value during fixtur The default is `NullableStrategy.NullOnCircularReference`, which prefers concrete values and only returns `null` when a nullable type would otherwise create a circular reference. +This strategy is part of the shared API available through both `some-core` and `some-android`. + ## Variants ### `NullOnCircularReference` @@ -52,7 +54,7 @@ Always produces a non-null concrete value for nullable types. some { strategy(NullableStrategy.NeverNull) } ``` -Use this strategy when you want to ensure all nullable fields are populated, which is useful for testing the "happy path" scenarios. +Use this strategy when you want to ensure all nullable fields are populated, which is useful for testing happy-path scenarios. ### `Random` diff --git a/docs/configuration/string-strategy.md b/docs/strategies/string-strategy.md similarity index 100% rename from docs/configuration/string-strategy.md rename to docs/strategies/string-strategy.md diff --git a/docs/supported-types.md b/docs/supported-types.md index d865b0a1..ec4081d2 100644 --- a/docs/supported-types.md +++ b/docs/supported-types.md @@ -3,7 +3,9 @@ icon: lucide/file-type --- # Supported Types -Some resolves common Kotlin and Java types with zero configuration. Nullable variants (`T?`) are supported everywhere — [NullableStrategy](configuration/nullable-strategy.md) controls whether `null` is emitted. +Some resolves common Kotlin and Java types with zero configuration. Nullable variants (`T?`) are supported everywhere. Use [Nullable Strategy](strategies/nullable-strategy.md) to control when `null` is emitted. + +These supported types are part of the shared API consumed through both `some-core` and `some-android`. For types not listed here, register a [custom factory](custom-factories.md) or ship a [custom resolver](custom-resolvers.md). @@ -12,11 +14,11 @@ For types not listed here, register a [custom factory](custom-factories.md) or s | Type | Usage | Notes | |------|-------|-------| | **Primitives** | | | -| `String` | `some()` | See [StringStrategy](configuration/string-strategy.md) | +| `String` | `some()` | See [String Strategy](strategies/string-strategy.md) | | `Int` | `some()` | | | `Long` | `some()` | | | `Double` | `some()` | | -| `Float` | `some()` | See [FloatStrategy](configuration/float-strategy.md) | +| `Float` | `some()` | See [Float Strategy](strategies/float-strategy.md) | | `Boolean` | `some()` | | | `Char` | `some()` | | | `Byte` | `some()` | | @@ -25,7 +27,7 @@ For types not listed here, register a [custom factory](custom-factories.md) or s | **Standard Library** | | | | `kotlin.uuid.Uuid` | `some()` | Requires `@OptIn(ExperimentalUuidApi::class)` | | `java.util.UUID` | `some()` | | -| `java.util.Optional` | `some>()` | See [NullableStrategy](configuration/nullable-strategy.md) | +| `java.util.Optional` | `some>()` | See [Nullable Strategy](strategies/nullable-strategy.md) | | `kotlin.time.Duration` | `some()` | | | `java.time.Duration` | `some()` | | | `kotlin.time.Instant` | `some()` | | @@ -36,7 +38,7 @@ For types not listed here, register a [custom factory](custom-factories.md) or s | `java.time.LocalDateTime` | `some()` | | | `java.time.ZonedDateTime` | `some()` | | | **Collections** | | | -| `List` | `some>()` | See [CollectionStrategy](configuration/collection-strategy.md) | +| `List` | `some>()` | See [Collection Strategy](strategies/collection-strategy.md) | | `MutableList` | `some>()` | | | `Set` | `some>()` | | | `MutableSet` | `some>()` | | diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67581fc7..8447c034 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ dokka = "2.2.0" gitHooksPlugin = "1.1.1" junit = "4.13.2" kotlin = "2.4.0" +kotest = "6.2.1" mavenPublish = "0.36.0" [libraries] @@ -17,6 +18,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } kermit = { module = "co.touchlab:kermit", version = "2.0.4" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } [plugins] diff --git a/kotest/build.gradle.kts b/kotest/build.gradle.kts new file mode 100644 index 00000000..3e233d4d --- /dev/null +++ b/kotest/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + alias(libs.plugins.autoservice.ir) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.mavenPublish) +} + +group = rootProject.group +version = rootProject.version + +repositories { + mavenCentral() +} + +dependencies { + implementation(projects.core) + implementation(libs.kotest.property) + + testImplementation(libs.kotlin.test) + + detektPlugins(libs.detekt.formatting) +} + +kotlin { + jvmToolchain(17) +} + +tasks.test { + useJUnitPlatform() +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + signAllPublications() + + coordinates(artifactId = "some-kotest") + + pom { + name.set("Some Kotest") + description.set("Kotest property-based testing integration for Some.") + inceptionYear.set("2026") + url.set("https://github.com/MessiasLima/Some") + + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("repo") + } + } + + developers { + developer { + id.set("MessiasLima") + name.set("Messias Lima") + url.set("https://github.com/MessiasLima") + } + } + + scm { + url.set("https://github.com/MessiasLima/Some") + connection.set("scm:git:git://github.com/MessiasLima/Some.git") + developerConnection.set("scm:git:ssh://git@github.com/MessiasLima/Some.git") + } + } +} + +detekt { autoCorrect = true } diff --git a/kotest/src/main/kotlin/dev/appoutlet/some/kotest/SomeArbExtensions.kt b/kotest/src/main/kotlin/dev/appoutlet/some/kotest/SomeArbExtensions.kt new file mode 100644 index 00000000..0e10679a --- /dev/null +++ b/kotest/src/main/kotlin/dev/appoutlet/some/kotest/SomeArbExtensions.kt @@ -0,0 +1,41 @@ +package dev.appoutlet.some.kotest + +import dev.appoutlet.some.config.SomeConfigBuilder +import dev.appoutlet.some.someSetup +import io.kotest.property.Arb +import io.kotest.property.RandomSource +import io.kotest.property.Sample + +/** + * Creates a Kotest [Arb] that generates values of type [T] using `Some`. + * + * Each call to [sample][Arb.sample] builds a fresh `Some` generator seeded with the + * Kotest [RandomSource.seed], applies the supplied [config] block, and produces a value. + * This keeps the arb reproducible and lets callers customize strategies or factories + * per property test. + * + * Usage: + * ```kotlin + * check(Arb.some(), Arb.some()) { a, b -> + * // property test assertions + * } + * ``` + * + * @param T Type to generate. + * @param config Configuration applied to each `Some` instance before generating a value. + * @return An [Arb] producing random values of type [T]. + */ +inline fun Arb.Companion.some( + crossinline config: SomeConfigBuilder.() -> Unit = {}, +): Arb = object : Arb() { + override fun edgecase(rs: RandomSource): Sample? = null + + override fun sample(rs: RandomSource): Sample { + val someGenerator = someSetup { + seed = rs.seed + config() + } + + return Sample(someGenerator()) + } +} diff --git a/kotest/src/test/kotlin/dev/appoutlet/some/kotest/SomeArbExtensionsTest.kt b/kotest/src/test/kotlin/dev/appoutlet/some/kotest/SomeArbExtensionsTest.kt new file mode 100644 index 00000000..a1776850 --- /dev/null +++ b/kotest/src/test/kotlin/dev/appoutlet/some/kotest/SomeArbExtensionsTest.kt @@ -0,0 +1,53 @@ +package dev.appoutlet.some.kotest + +import dev.appoutlet.some.config.NullableStrategy +import io.kotest.property.Arb +import io.kotest.property.RandomSource +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class SomeArbExtensionsTest { + @Test + fun `Arb companion some extension generates values`() { + val arb = Arb.some() + + val sample = arb.sample(RandomSource.default()) + + assertIs(sample.value) + } + + @Test + fun `Arb companion some extension generates data classes`() { + val arb = Arb.some() + + val sample = arb.sample(RandomSource.default()) + + assertIs(sample.value) + } + + @Test + fun `Arb companion some extension applies configuration`() { + val arb = Arb.some { + strategy(NullableStrategy.NeverNull) + } + + val sample = arb.sample(RandomSource.default()) + + assertNotNull(sample.value) + } + + @Test + fun `Arb companion some extension is reproducible for the same seed`() { + val arb = Arb.some() + val seed = 12345L + + val sample1 = arb.sample(RandomSource.seeded(seed)) + val sample2 = arb.sample(RandomSource.seeded(seed)) + + assertEquals(sample1.value, sample2.value) + } + + private data class TestData(val name: String, val age: Int) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index cdeda96a..4b262e28 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,6 +27,7 @@ rootProject.name = "Some" include(":core") include(":android") +include(":kotest") kover { enableCoverage() diff --git a/zensical.toml b/zensical.toml index 90ddf090..dcb78685 100644 --- a/zensical.toml +++ b/zensical.toml @@ -9,14 +9,27 @@ repo_url = "https://github.com/MessiasLima/Some" nav = [ "index.md", "getting-started.md", + { "Migration" = [ + "migration/index.md", + "migration/0.2.1-to-0.2.2.md" + ] }, { "Configuration" = [ - "configuration/index.md" , - "configuration/nullable-strategy.md", - "configuration/string-strategy.md", - "configuration/collection-strategy.md", - "configuration/float-strategy.md" + "configuration/index.md" + ] }, + { "Strategies" = [ + "strategies/index.md", + "strategies/nullable-strategy.md", + "strategies/string-strategy.md", + "strategies/collection-strategy.md", + "strategies/float-strategy.md", + "strategies/default-value-strategy.md" ] }, "supported-types.md", + { "Artifacts" = [ + "artifacts/some-core.md", + "artifacts/some-android.md", + "artifacts/some-kotest.md" + ] }, "custom-factories.md", "custom-resolvers.md", { "API Reference" = "https://some.appoutlet.dev/reference/latest/" }