From 6660fefe5c5fa7d87f005c81393038478541cf9e Mon Sep 17 00:00:00 2001 From: Claw Date: Sat, 11 Jul 2026 19:26:07 -0700 Subject: [PATCH] F-012: polish external deps catalog UX (library(), pure generators, docs) Add library() for stable catalog names, validate GAV coordinates, remove configuration-time println spam, unit-test name generators, and document projectDependencies + typed build-dependencies patterns in DEPS-CATALOG.md. --- README.md | 18 +++ TICKETS.md | 2 +- application/settings.gradle.kts | 9 +- docs/ARCHITECTURE.md | 5 +- docs/DEPS-CATALOG.md | 148 ++++++++++++++++++ docs/PROGRESS.md | 22 +++ plugins/deps/build.gradle.kts | 6 + .../tools/forma/deps/catalog/Generators.kt | 91 ++++++++--- .../deps/catalog/VersionCatalogBuilder.kt | 101 +++++++++--- .../forma/deps/catalog/GeneratorsTest.kt | 59 +++++++ 10 files changed, 418 insertions(+), 43 deletions(-) create mode 100644 docs/DEPS-CATALOG.md create mode 100644 plugins/deps/src/test/kotlin/tools/forma/deps/catalog/GeneratorsTest.kt diff --git a/README.md b/README.md index 907c3020..69ecd706 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,24 @@ androidLibrary( Worker / contributor host setup (JDK 17+, Android SDK platform 33): see [`docs/ENV.md`](docs/ENV.md) and `source scripts/env-mac.sh`. +## External dependency catalogs + +Declare third-party libraries and plugins once in `settings.gradle.kts` with +`projectDependencies` (version catalog), or use typed Kotlin catalogs +(`build-dependencies/` pattern). Full guide: [`docs/DEPS-CATALOG.md`](docs/DEPS-CATALOG.md). + +```gradle +// settings.gradle.kts +projectDependencies( + "libs", + "com.jakewharton.timber:timber:5.0.1", + library("io.coil-kt:coil:2.1.0", name = "coil"), + bundle(name = "room", "androidx.room:room-runtime:2.5.1", /* … */), + plugin("androidx.navigation:navigation-safe-args-gradle-plugin", "2.7.4"), +) +// modules: deps(libs.jakewhartonTimber, libs.coil, libs.bundles.room) +``` + ## Progress DSL names match code entrypoints (`widget`, `util`, …). **Validation** = diff --git a/TICKETS.md b/TICKETS.md index 05fc515b..59d5c849 100644 --- a/TICKETS.md +++ b/TICKETS.md @@ -19,7 +19,7 @@ Update this file when picking or finishing work. Cron workers must pick the **hi |----|--------|-------|-------| | F-010 | done | Document strict dependency matrix from live code (not only README) | `docs/DEPENDENCY-MATRIX.md` + README summary aligned to validators | | F-011 | done | Tighten validation for `api` / `impl` (Dagger2-friendly boundaries) | app/binary/androidLibrary no longer EmptyValidator; impl still no→impl; docs matrix updated | -| F-012 | todo | External deps catalog UX + tooling polish | `plugins/deps` catalog generators | +| F-012 | done | External deps catalog UX + tooling polish | `library()` + pure generators + GAV validation + unit tests + `docs/DEPS-CATALOG.md` | | F-013 | todo | Compose support for Android library/ui targets | GH #96 | | F-014 | todo | Sample app: gold-standard multi-feature structure | Home/characters already present; modernize | | F-015 | todo | Android project tutorial (getting started) | GH #53 | diff --git a/application/settings.gradle.kts b/application/settings.gradle.kts index 720897d9..13fe3766 100644 --- a/application/settings.gradle.kts +++ b/application/settings.gradle.kts @@ -1,4 +1,5 @@ import tools.forma.deps.catalog.bundle +import tools.forma.deps.catalog.library import tools.forma.deps.catalog.plugin import tools.forma.deps.catalog.projectDependencies import tools.forma.deps.core.CustomConfiguration @@ -66,10 +67,16 @@ val roomVersion = "2.5.1" val ksp = CustomConfiguration("ksp") +// Version catalog UX: bare GAV, library(name=…), bundle, plugin — see docs/DEPS-CATALOG.md projectDependencies( "libs", "com.jakewharton.timber:timber:5.0.1", - bundle(name = "coil", "io.coil-kt:coil:$coilVersion", "io.coil-kt:coil-base:$coilVersion"), + // Explicit short names (library()) keep accessors stable: libs.coil, libs.coilBase + bundle( + name = "coil", + library("io.coil-kt:coil:$coilVersion", name = "coil"), + library("io.coil-kt:coil-base:$coilVersion", name = "coilBase"), + ), bundle( name = "room", "androidx.sqlite:sqlite:$sqliteVersion", diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 126f487e..89935013 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -177,8 +177,9 @@ compose flag, owners mandatory flag, Java compatibility). `FileDependency`, `PlatformDependency`, `MixedDependency`, `EmptyDependency`) - Application: `applyDependencies` — validates each project dep, applies plugin side-effects from catalog registrations, sets transitive flags -- Catalog: `projectDependencies` / `bundle` / `plugin` in settings - (`tools.forma.deps.catalog`) + name generators +- Catalog: `projectDependencies` / `library` / `bundle` / `plugin` in settings + (`tools.forma.deps.catalog`) + pure name generators + GAV validation + (user guide: [`DEPS-CATALOG.md`](DEPS-CATALOG.md)) - Sample also uses hand-written catalogs in `build-dependencies/dependencies` (`Androidx`, `Google`, `Test`, …) diff --git a/docs/DEPS-CATALOG.md b/docs/DEPS-CATALOG.md new file mode 100644 index 00000000..d137d799 --- /dev/null +++ b/docs/DEPS-CATALOG.md @@ -0,0 +1,148 @@ +# Forma external dependency catalogs + +Forma has **two** complementary ways to declare third-party libraries. This doc is the +user-facing guide for **F-012** catalog UX (settings-level version catalogs) and how it +relates to the typed demo catalogs under `build-dependencies/`. + +## 1. Settings version catalog — `projectDependencies` + +Declare libraries, bundles, and plugins once in `settings.gradle.kts`. Forma fills a +Gradle [version catalog](https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog) +so modules can depend on type-safe accessors (`libs.*`). + +```kotlin +import tools.forma.deps.catalog.bundle +import tools.forma.deps.catalog.library +import tools.forma.deps.catalog.plugin +import tools.forma.deps.catalog.projectDependencies +import tools.forma.deps.core.CustomConfiguration + +val ksp = CustomConfiguration("ksp") + +projectDependencies( + "libs", // catalog name → accessors under `libs` + // Bare GAV → auto name (see generators below) + "com.jakewharton.timber:timber:5.0.1", + // Explicit short name when auto name is awkward + library("io.coil-kt:coil:2.1.0", name = "coil"), + library("io.coil-kt:coil-base:2.1.0", name = "coilBase"), + bundle( + name = "room", + "androidx.sqlite:sqlite:2.2.0", + "androidx.room:room-runtime:2.5.1", + // … + ), + plugin( + id = "com.google.devtools.ksp:symbol-processing-gradle-plugin", + version = "1.9.0-1.0.13", + configuration = ksp, + "androidx.room:room-compiler:2.5.1" + ), + plugin("androidx.navigation:navigation-safe-args-gradle-plugin", "2.7.4"), +) +``` + +### Entry kinds + +| Entry | Factory | Catalog result | Notes | +|-------|---------|----------------|-------| +| GAV string | `"group:artifact:version"` | `libs.` | Must be exactly three `:` segments | +| Named library | `library(gav, name = "…")` | `libs.` | Prefer when auto name is long/unclear | +| Bundle | `bundle(name, …gavs)` | `libs.bundles.` | Members also registered as libraries | +| Plugin | `plugin(id, version, …)` | `libs.plugins.` | Optional companion libs + `CustomConfiguration` | + +### Auto name generators + +`defaultNameGenerator` (libraries) and `pluginNameGenerator` (plugins) camelCase the +coordinate after dropping common tokens (`com`, `org`, `androidx`, `google`, `plugin`, …) +and the version segment (libraries only). + +| Coordinate | Generated accessor | +|------------|--------------------| +| `com.jakewharton.timber:timber:5.0.1` | `jakewhartonTimber` | +| `io.coil-kt:coil-base:2.1.0` | `coilKtBase` | +| `androidx.room:room-runtime:2.5.1` | `roomRuntime` | +| `androidx.navigation:navigation-safe-args-gradle-plugin` | `navigationSafeArgs` | + +If every token is filtered, generators throw with a message asking for an explicit +`library(…, name = …)` instead of producing an empty name. + +Generators are **pure** (no configuration-time `println`). Use Gradle debug logging if +you need to inspect catalog contents. + +### Using catalog deps in targets + +```kotlin +androidLibrary( + packageName = "…", + dependencies = deps( + libs.jakewhartonTimber, + libs.bundles.room, + ) + deps(target(":some:api")), +) +``` + +`deps(Provider…)` / `Provider.dep` bridge catalog entries into Forma's +`NamedDependency` model. When a library was registered with a plugin's +`CustomConfiguration` (e.g. KSP), `applyDependencies` applies that plugin and uses the +custom configuration automatically. + +### Plugin + processor wiring + +```kotlin +plugin( + id = "com.google.devtools.ksp:symbol-processing-gradle-plugin", + version = "…", + configuration = CustomConfiguration("ksp"), + "androidx.room:room-compiler:2.5.1" // registered on the `ksp` configuration +) +``` + +At dependency application time, consuming the room-compiler coordinate applies the KSP +plugin once and adds the processor with `isTransitive = true`. + +## 2. Typed catalogs — `build-dependencies/` (sample pattern) + +The sample app also ships **hand-written** Kotlin objects +(`androidx`, `google`, `test`, …) under `build-dependencies/dependencies`. Those use +`String.dep` / `deps(...)` and nest transitive graphs explicitly (e.g. `androidx.appcompat` +pulls fragment, core, …). + +| Approach | Best for | Trade-offs | +|----------|----------|------------| +| `projectDependencies` version catalog | App-level third-party pins, plugins, short `libs.*` accessors | Names generated or explicit; less control over nested transitive graphs | +| Typed `build-dependencies` objects | Large shared graphs, team-owned versions, non-transitive-by-default trees | More boilerplate; included via `includeBuild` + demo plugin | + +Both can coexist: the sample uses catalogs for timber/coil/room/plugins and typed objects +for the androidx/google surface used across features. + +## 3. API surface (plugins `:deps`) + +| Symbol | Package | Role | +|--------|---------|------| +| `projectDependencies` | `tools.forma.deps.catalog` | Settings DSL entry | +| `library` / `LibraryDep` | same | Explicit library registration | +| `bundle` / `BundleDep` | same | Named library groups | +| `plugin` / `PluginDep` | same | Plugin + optional config/libs | +| `defaultNameGenerator` / `pluginNameGenerator` | same | Pure name helpers (overridable) | +| `parseGroupArtifactVersion` | same | GAV validation | +| `deps` / `String.dep` / `Provider.dep` | root (`dependencies.kt`) | Bridge into `FormaDependency` | +| `applyDependencies` | `tools.forma.deps.core` | Wire deps + plugin side effects | + +## 4. Practical tips + +1. **Pin versions in one place** — either catalog constants in `settings.gradle.kts` or + `versions` object in a typed catalog module; avoid scattering GAVs across targets. +2. **Prefer `library(…, name = …)`** for public-facing short names you will type often. +3. **Use `bundle`** when several artifacts always travel together (Room, Coil). +4. **Keep transitive control intentional** — bare catalog `deps(libs.foo)` follows Forma's + default non-transitive named-deps path unless a plugin registration forces transitive. +5. **Invalid GAV fails fast** — `group:artifact:version` only; two or four segments throw + a clear `IllegalArgumentException` at configuration time. + +## Related + +- Live project-dep matrix: [`DEPENDENCY-MATRIX.md`](DEPENDENCY-MATRIX.md) +- Architecture map: [`ARCHITECTURE.md`](ARCHITECTURE.md) §2.4 +- Sample settings: `application/settings.gradle.kts` +- Sample typed catalogs: `build-dependencies/dependencies/src/main/kotlin/` diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index d25358d4..76726f26 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2,6 +2,28 @@ Newest entries first. +## 2026-07-11 — F-012 External deps catalog UX + +- **Ticket:** F-012 → `done` +- **Branch:** `forma/F-012-deps-catalog-ux` (from `origin/v2`) +- **Code (`plugins/deps` catalog):** + - Pure name generators (removed configuration-time `println`) + - Clear GAV validation via `parseGroupArtifactVersion` (exactly `group:artifact:version`) + - Fail-fast when auto-name would be empty (all tokens filtered) + - New `library(gav, name = …)` factory for stable short accessors + - `projectDependencies` accepts `library()` / `bundle()` / `plugin()` / bare GAV; better error for unknown types + - Unit tests: `GeneratorsTest` (`:deps:test`) +- **Sample:** `application/settings.gradle.kts` uses `library(..., name = "coil"|"coilBase")` inside the coil bundle +- **Docs:** `docs/DEPS-CATALOG.md` (user guide); README section; ARCHITECTURE §2.4 pointer +- **Verify (real tool output, OpenJDK 17 + SDK 33):** + - `plugins/`: `./gradlew build` → **BUILD SUCCESSFUL** + - `plugins/`: `./gradlew :deps:test` → **BUILD SUCCESSFUL** (GeneratorsTest) + - `application/`: `./gradlew build` → **BUILD SUCCESSFUL** (2080 tasks) + - Configuration log no longer spam-prints `Generated name …` +- **Commits/PRs:** this run — push + PR base `v2` +- **Blockers:** none +- **Next step:** F-013 Compose support (GH #96) + ## 2026-07-11 — F-011 api/impl + composition-root validation - **Ticket:** F-011 → `done` diff --git a/plugins/deps/build.gradle.kts b/plugins/deps/build.gradle.kts index b87fe365..d75a2c2b 100644 --- a/plugins/deps/build.gradle.kts +++ b/plugins/deps/build.gradle.kts @@ -26,4 +26,10 @@ dependencies { implementation(project(":target")) implementation(project(":config")) implementation(gradleKotlinDsl()) + + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() } diff --git a/plugins/deps/src/main/java/tools/forma/deps/catalog/Generators.kt b/plugins/deps/src/main/java/tools/forma/deps/catalog/Generators.kt index a2f67857..8afc4212 100644 --- a/plugins/deps/src/main/java/tools/forma/deps/catalog/Generators.kt +++ b/plugins/deps/src/main/java/tools/forma/deps/catalog/Generators.kt @@ -3,6 +3,12 @@ package tools.forma.deps.catalog import java.util.Locale import org.gradle.configurationcache.extensions.capitalized +/** + * Tokens dropped when turning Maven coordinates / plugin ids into Gradle version-catalog + * accessor names (e.g. `com.jakewharton.timber:timber:5.0.1` → `jakewhartonTimber`). + * + * Keep this list conservative: filtering too aggressively yields empty or colliding names. + */ val filteredTokens = listOf( "com", @@ -18,23 +24,70 @@ val filteredTokens = "plugin" ) -fun pluginNameGenerator(groupArtifactVersion: String) = - groupArtifactVersion - .split(":") - .fold(emptyList()) { acc, s -> acc + s.split(".", "-") } - .filter { it !in filteredTokens } - .distinct() - .joinToString("") { it.capitalized() } - .let { name -> name.replaceFirstChar { it.lowercase(Locale.getDefault()) } } - .also { println("Generated name $it for $groupArtifactVersion") } +/** + * Generates a camelCase catalog name for a plugin id (version optional). + * + * Examples: + * - `androidx.navigation:navigation-safe-args-gradle-plugin` → `navigationSafeArgs` + * - `com.google.devtools.ksp:symbol-processing-gradle-plugin` → `devtoolsKspSymbolProcessing` + */ +fun pluginNameGenerator(groupArtifactVersion: String): String = + generateName( + tokens = + groupArtifactVersion + .split(":") + .fold(emptyList()) { acc, s -> acc + s.split(".", "-") }, + source = groupArtifactVersion, + kind = "plugin" + ) + +/** + * Generates a camelCase catalog name for a library coordinate (`group:artifact:version`). + * The version segment is ignored for naming. + * + * Examples: + * - `com.jakewharton.timber:timber:5.0.1` → `jakewhartonTimber` + * - `io.coil-kt:coil-base:2.1.0` → `coilKtBase` + */ +fun defaultNameGenerator(groupArtifactVersion: String): String = + generateName( + tokens = + groupArtifactVersion + .split(":") + .dropLast(1) + .fold(emptyList()) { acc, s -> acc + s.split(".", "-") }, + source = groupArtifactVersion, + kind = "library" + ) + +internal fun generateName( + tokens: List, + source: String, + kind: String +): String { + val name = + tokens + .filter { it.isNotBlank() && it !in filteredTokens } + .distinct() + .joinToString("") { it.capitalized() } + .let { raw -> raw.replaceFirstChar { it.lowercase(Locale.getDefault()) } } + + require(name.isNotBlank()) { + "Could not generate version-catalog $kind name for '$source' " + + "(all path tokens were filtered). Pass an explicit name= instead." + } + return name +} -fun defaultNameGenerator(groupArtifactVersion: String) = - groupArtifactVersion - .split(":") - .dropLast(1) - .fold(emptyList()) { acc, s -> acc + s.split(".", "-") } - .filter { it !in filteredTokens } - .distinct() - .joinToString("") { it.capitalized() } - .let { name -> name.replaceFirstChar { it.lowercase(Locale.getDefault()) } } - .also { println("Generated name $it for $groupArtifactVersion") } +/** + * Parse `group:artifact:version` into parts. Used by catalog builders so callers get a clear + * error instead of a cryptic `IndexOutOfBoundsException`. + */ +fun parseGroupArtifactVersion(notation: String): Triple { + val parts = notation.split(":") + require(parts.size == 3 && parts.all { it.isNotBlank() }) { + "Invalid Maven coordinate '$notation'. Expected exactly group:artifact:version " + + "(three non-blank segments separated by ':')." + } + return Triple(parts[0], parts[1], parts[2]) +} diff --git a/plugins/deps/src/main/java/tools/forma/deps/catalog/VersionCatalogBuilder.kt b/plugins/deps/src/main/java/tools/forma/deps/catalog/VersionCatalogBuilder.kt index b884e430..b391f5b7 100644 --- a/plugins/deps/src/main/java/tools/forma/deps/catalog/VersionCatalogBuilder.kt +++ b/plugins/deps/src/main/java/tools/forma/deps/catalog/VersionCatalogBuilder.kt @@ -9,15 +9,50 @@ import tools.forma.config.FormaSettingsStore import tools.forma.deps.core.CustomConfiguration import tools.forma.deps.core.NamedDependency +/** + * Declares a Gradle version catalog from Forma settings. + * + * Supported entries: + * - bare `String` coordinates → library with [defaultNameGenerator] + * - [LibraryDep] via [library] → library with optional explicit name + * - [BundleDep] via [bundle] → named bundle of libraries + * - [PluginDep] via [plugin] → plugin + optional companion libraries / custom configuration + * + * Example: + * ``` + * projectDependencies( + * "libs", + * "com.jakewharton.timber:timber:5.0.1", + * library("io.coil-kt:coil:2.1.0", name = "coil"), + * bundle(name = "room", "androidx.room:room-runtime:2.5.1", ...), + * plugin( + * id = "com.google.devtools.ksp:symbol-processing-gradle-plugin", + * version = "…", + * configuration = CustomConfiguration("ksp"), + * "androidx.room:room-compiler:2.5.1" + * ) + * ) + * // → libs.jakewhartonTimber, libs.coil, libs.bundles.room, libs.plugins.… + * ``` + * + * @see docs/DEPS-CATALOG.md + */ fun Settings.projectDependencies(name: String = "libs", vararg deps: Any) { dependencyResolutionManagement { it.versionCatalogs { container -> container.create(name) { builder -> deps.forEach { dep -> when (dep) { - is String -> builder.addLibrary(settings, dep) - is BundleDep -> builder.addBundle(settings, dep) - is PluginDep -> builder.addPlugin(settings, dep) + is String -> builder.addLibrary(dep) + is LibraryDep -> builder.addLibrary(dep) + is BundleDep -> builder.addBundle(dep) + is PluginDep -> builder.addPlugin(this@projectDependencies, dep) + else -> + throw IllegalArgumentException( + "Unsupported projectDependencies entry " + + "${dep::class.qualifiedName}. Use a GAV String, library(), " + + "bundle(), or plugin()." + ) } } } @@ -25,6 +60,22 @@ fun Settings.projectDependencies(name: String = "libs", vararg deps: Any) { } } +/** + * Explicit library entry. Prefer this over a bare String when the auto-generated accessor name + * would be unclear or when you want a stable short name (e.g. `libs.coil` instead of + * `libs.coilKt`). + */ +fun library( + groupArtifactVersion: String, + name: String? = null, + nameGenerator: (String) -> String = ::defaultNameGenerator +) = LibraryDep(groupArtifactVersion, name, nameGenerator) + +class LibraryDep( + val groupArtifactVersion: String, + val name: String? = null, + val nameGenerator: (String) -> String = ::defaultNameGenerator +) fun bundle( name: String, @@ -75,7 +126,7 @@ fun VersionCatalogBuilder.addPlugin( plugin(name, id).version { it.strictly(version) } dependencies.forEach { dep -> if (configuration != null) { - addLibrary(settings, dep) { depNameGenerator(it) } + addLibrary(dep) { depNameGenerator(it) } FormaSettingsStore.registerConfiguration( configuration.name, settings.providers.provider { @@ -84,7 +135,7 @@ fun VersionCatalogBuilder.addPlugin( dep ) } else { - addLibrary(settings, dep, depNameGenerator) + addLibrary(dep, depNameGenerator) } } } @@ -100,43 +151,53 @@ fun VersionCatalogBuilder.addPlugin(settings: Settings, pluginDep: PluginDep) = depNameGenerator = pluginDep.depNameGenerator ) -fun VersionCatalogBuilder.addBundle(settings: Settings, bundle: BundleDep) { - addBundle(settings, bundle.name, *bundle.groupArtifactVersions, nameGenerator = bundle.nameGenerator) +fun VersionCatalogBuilder.addBundle(bundle: BundleDep) { + addBundle(bundle.name, *bundle.groupArtifactVersions, nameGenerator = bundle.nameGenerator) } // todo split lib name and version fun VersionCatalogBuilder.addBundle( - settings: Settings, name: String, vararg groupArtifactVersion: Any, nameGenerator: (String) -> String = ::defaultNameGenerator ) { - bundle(name, groupArtifactVersion.map { addLibrary(settings, it, nameGenerator) }) + bundle(name, groupArtifactVersion.map { addLibrary(it, nameGenerator) }) +} + +fun VersionCatalogBuilder.addLibrary(library: LibraryDep): String { + val catalogName = library.name ?: library.nameGenerator(library.groupArtifactVersion) + return registerLibrary(catalogName, library.groupArtifactVersion) } // todo split lib name and version fun VersionCatalogBuilder.addLibrary( - settings: Settings, dependency: Any, nameGenerator: (String) -> String = ::defaultNameGenerator ): String { return when (dependency) { is String -> { - val (group, artifact, version) = dependency.split(":") - nameGenerator(dependency).apply { - library(this, group, artifact).version { it.strictly(version) } - } + val catalogName = nameGenerator(dependency) + registerLibrary(catalogName, dependency) } + is LibraryDep -> addLibrary(dependency) is NamedDependency -> { - val name = dependency.names.first().name - val (group, artifact, version) = name.split(":") - nameGenerator(name).apply { - library(this, group, artifact).version { it.strictly(version) } - } + val notation = dependency.names.first().name + val catalogName = nameGenerator(notation) + registerLibrary(catalogName, notation) } else -> throw IllegalArgumentException( - "Dependency type ${dependency::class.qualifiedName} is not supported" + "Dependency type ${dependency::class.qualifiedName} is not supported. " + + "Use a GAV String, library(), or NamedDependency." ) } } + +private fun VersionCatalogBuilder.registerLibrary( + catalogName: String, + notation: String +): String { + val (group, artifact, version) = parseGroupArtifactVersion(notation) + library(catalogName, group, artifact).version { it.strictly(version) } + return catalogName +} diff --git a/plugins/deps/src/test/kotlin/tools/forma/deps/catalog/GeneratorsTest.kt b/plugins/deps/src/test/kotlin/tools/forma/deps/catalog/GeneratorsTest.kt new file mode 100644 index 00000000..b7e1b989 --- /dev/null +++ b/plugins/deps/src/test/kotlin/tools/forma/deps/catalog/GeneratorsTest.kt @@ -0,0 +1,59 @@ +package tools.forma.deps.catalog + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class GeneratorsTest { + + @Test + fun `defaultNameGenerator drops common tokens and version`() { + assertEquals( + "jakewhartonTimber", + defaultNameGenerator("com.jakewharton.timber:timber:5.0.1") + ) + assertEquals("coilKt", defaultNameGenerator("io.coil-kt:coil:2.1.0")) + assertEquals("coilKtBase", defaultNameGenerator("io.coil-kt:coil-base:2.1.0")) + assertEquals("roomRuntime", defaultNameGenerator("androidx.room:room-runtime:2.5.1")) + assertEquals("sqlite", defaultNameGenerator("androidx.sqlite:sqlite:2.2.0")) + } + + @Test + fun `pluginNameGenerator drops gradle plugin noise`() { + assertEquals( + "navigationSafeArgs", + pluginNameGenerator("androidx.navigation:navigation-safe-args-gradle-plugin") + ) + assertEquals( + "firebaseCrashlytics", + pluginNameGenerator("com.google.firebase:firebase-crashlytics-gradle") + ) + assertEquals( + "devtoolsKspSymbolProcessing", + pluginNameGenerator("com.google.devtools.ksp:symbol-processing-gradle-plugin") + ) + assertEquals( + "toolsFormaDemoDependencies", + pluginNameGenerator("tools.forma.demo:dependencies") + ) + } + + @Test + fun `parseGroupArtifactVersion requires three segments`() { + assertEquals( + Triple("g", "a", "1.0"), + parseGroupArtifactVersion("g:a:1.0") + ) + assertFailsWith { parseGroupArtifactVersion("g:a") } + assertFailsWith { parseGroupArtifactVersion("g:a:1:extra") } + assertFailsWith { parseGroupArtifactVersion("g::1.0") } + } + + @Test + fun `name generators fail clearly when everything is filtered`() { + assertFailsWith { + // group/artifact tokens are all in filteredTokens + defaultNameGenerator("com.android:android:1.0") + } + } +}