diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fc14e3..78fb03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,16 +11,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `featured-registry` module — the runtime `FlagRegistry` global singleton and its `FlagRegistryDelegate` expect/actual are removed. Use `GeneratedFeaturedRegistry.all` (produced by the `dev.androidbroadcast.featured.application` plugin) or build an explicit `List>` instead. - `featured-gradle-plugin` — `generateFlagRegistrar` task, `FlagRegistrarGenerator`, and `GenerateFlagRegistrarTask` are removed. Per-module `GeneratedFlagRegistrar.kt` files are no longer generated. -- Sample API — `registerSampleFlags()` is removed (was specific to the sample app's legacy wiring). The sample now uses `SampleFeatureFlags.all` directly. +- Sample API — `registerSampleFlags()` is removed (was specific to the sample app's legacy wiring). The sample now uses `GeneratedFeaturedRegistry.all` (produced by the aggregator plugin) instead. ### Changed - `FeatureFlagsDebugScreen` signature is now `(configValues: ConfigValues, registry: List>, modifier: Modifier = Modifier)` — accepts an explicit registry list instead of reading the (removed) `FlagRegistry` singleton. Pass `GeneratedFeaturedRegistry.all` for the recommended aggregator-plugin flow, or build the list inline for small projects. +- `:sample:shared` is now a pure aggregator: it applies `dev.androidbroadcast.featured.application`, declares `featuredAggregation(project(":sample:feature-*"))`, and consumes `GeneratedFeaturedRegistry.all`. The hand-written `SampleFeatureFlags.kt` is removed. +- Generator file names include a module-derived suffix (`GeneratedLocalFlagsSampleFeatureCheckout.kt`, etc.) — eliminates JVM class-name collisions when multiple modules share the same classpath. `@file:JvmName` is no longer emitted. +- `ExtensionFunctionGenerator` now emits `suspend` extension functions — `ConfigValues.getValue` has always been suspend; the generated callers now match. `GeneratedLocalFlags*` / `GeneratedRemoteFlags*` objects are widened to `public` so observer bridges can reference them across module boundaries. ### Added - Featured library plugin now publishes a per-module feature-flag manifest as a consumable Gradle artifact (`featuredManifest` configuration, schema v1). Existing flag-generation pipeline is unchanged. Consumer-side aggregation arrives in a follow-up release. - New `dev.androidbroadcast.featured.application` Gradle plugin: aggregates `featured-manifest.json` artifacts from project dependencies declared via `featuredAggregation(project(...))` and generates `object GeneratedFeaturedRegistry { val all: List> }` in `build/generated/featured/commonMain/`. Apply alongside `dev.androidbroadcast.featured` in the application module; wire the output directory into your source set manually (e.g., `kotlin.sourceSets.commonMain.kotlin.srcDir(...)`). Modules declaring `enum` flags also require a regular `implementation(project(...))` dependency in the consumer so the enum class is on the compile classpath; primitive-only modules need only `featuredAggregation(...)`. +- Three KMP sample feature modules — `:sample:feature-checkout`, `:sample:feature-promotions`, `:sample:feature-ui` — each declaring its own flags via the `featured { ... }` DSL. Serves as the canonical multi-module reference. +- `EnumDropdown` component in `featured-debug-ui` for overriding `enum`-typed flags in `FeatureFlagsDebugScreen`; `ConfigParam` now carries `enumConstants: List?` populated by codegen so the debug UI can render the dropdown without reflection. +- `featured-gradle-plugin` extracted to a `build-logic/` included build; `pluginManagement { includeBuild("build-logic") }` in the root `settings.gradle.kts` exposes it to all main-build subprojects without a version coordinate. + +### Fixed + +- iOS framework can now `export(project(":sample:feature-*"))` without the K/N `ObjCExportCodeGenerator` crashing — requires `api(project(...))` linkage in the aggregator module so K/N has access to type adapters for generic `ConfigParam` specializations. ## [1.0.0-Beta1] - 2026-05-17 diff --git a/featured-gradle-plugin/CLAUDE.md b/build-logic/featured-gradle-plugin/CLAUDE.md similarity index 100% rename from featured-gradle-plugin/CLAUDE.md rename to build-logic/featured-gradle-plugin/CLAUDE.md diff --git a/featured-gradle-plugin/README.md b/build-logic/featured-gradle-plugin/README.md similarity index 100% rename from featured-gradle-plugin/README.md rename to build-logic/featured-gradle-plugin/README.md diff --git a/featured-gradle-plugin/build.gradle.kts b/build-logic/featured-gradle-plugin/build.gradle.kts similarity index 100% rename from featured-gradle-plugin/build.gradle.kts rename to build-logic/featured-gradle-plugin/build.gradle.kts diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/AndroidProguardWiring.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/AndroidProguardWiring.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/AndroidProguardWiring.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/AndroidProguardWiring.kt diff --git a/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGenerator.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGenerator.kt new file mode 100644 index 0000000..9853328 --- /dev/null +++ b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGenerator.kt @@ -0,0 +1,120 @@ +package dev.androidbroadcast.featured.gradle + +/** + * Generates `GeneratedLocalFlags.kt` and `GeneratedRemoteFlags.kt` — public + * objects containing one typed `ConfigParam` property per declared flag. + * + * Generated example for a local Boolean flag `dark_mode` in module `:sample:feature-checkout`: + * ```kotlin + * public object GeneratedLocalFlagsSampleFeatureCheckout { + * public val darkMode = ConfigParam("dark_mode", false, category = "UI") + * } + * ``` + * + * The object name and file name include a module-derived suffix (e.g. `SampleFeatureCheckout`) + * so that each module's generated class has a unique JVM name, avoiding duplicate-class errors + * when multiple modules are assembled into the same DEX or JAR. + * + * These objects are `public` so that consumers in other modules (e.g. observe-bridge + * composites, feature-module bridges) can reference the `ConfigParam` instances directly + * via `observe(GeneratedLocalFlagsSampleFeatureCheckout.x)` without going through the + * generated extension functions in [ExtensionFunctionGenerator]. + */ +public object ConfigParamGenerator { + private const val PACKAGE = "dev.androidbroadcast.featured.generated" + private const val CONFIG_PARAM_IMPORT = "dev.androidbroadcast.featured.ConfigParam" + private const val LOCAL_OBJECT_PREFIX = "GeneratedLocalFlags" + private const val REMOTE_OBJECT_PREFIX = "GeneratedRemoteFlags" + + /** + * Returns the generated object name for local flags in the given module. + * + * Examples: + * - `":app"` → `"GeneratedLocalFlagsApp"` + * - `":sample:feature-checkout"` → `"GeneratedLocalFlagsSampleFeatureCheckout"` + */ + public fun localObjectName(modulePath: String): String = "$LOCAL_OBJECT_PREFIX${modulePath.modulePathToFileSuffix()}" + + /** + * Returns the generated object name for remote flags in the given module. + * + * Examples: + * - `":app"` → `"GeneratedRemoteFlagsApp"` + * - `":sample:feature-promotions"` → `"GeneratedRemoteFlagsSampleFeaturePromotions"` + */ + public fun remoteObjectName(modulePath: String): String = "$REMOTE_OBJECT_PREFIX${modulePath.modulePathToFileSuffix()}" + + /** + * Returns the emitted `.kt` file name for the local-flags object of the given module. + * + * The object name is derived from the module path, making the JVM class name unique per module. + */ + public fun localFileName(modulePath: String): String = "${localObjectName(modulePath)}.kt" + + /** + * Returns the emitted `.kt` file name for the remote-flags object of the given module. + * + * The object name is derived from the module path, making the JVM class name unique per module. + */ + public fun remoteFileName(modulePath: String): String = "${remoteObjectName(modulePath)}.kt" + + /** + * Generates the Kotlin source for the module-specific local-flags and remote-flags objects + * as a pair. + * + * The object names and file names include a module-derived suffix (see [localObjectName] / + * [remoteObjectName]) so that each module's classes are unique at the JVM level. + * + * Returns a pair of `(localSource, remoteSource)`. Either may be an empty string + * if there are no flags of that type. + */ + public fun generate( + entries: List, + modulePath: String, + ): Pair { + val (local, remote) = entries.partition { it.isLocal } + return generateObject(local, localObjectName(modulePath)) to + generateObject(remote, remoteObjectName(modulePath)) + } + + private fun generateObject( + entries: List, + objectName: String, + ): String { + if (entries.isEmpty()) return "" + return buildString { + appendLine("// Auto-generated by Featured Gradle Plugin — do not edit manually.") + appendLine("package $PACKAGE") + appendLine() + appendLine("import $CONFIG_PARAM_IMPORT") + appendLine() + appendLine("public object $objectName {") + entries.forEach { entry -> + appendLine(" public val ${entry.propertyName} = ${entry.toConfigParamExpression()}") + } + append("}") + } + } + + private fun LocalFlagEntry.toConfigParamExpression(): String { + val typeArg = type + val namedArgs = + buildList { + add("key = \"$key\"") + add("defaultValue = ${formatDefault()}") + if (description != null) add("description = \"$description\"") + if (category != null) add("category = \"$category\"") + if (isEnum) add("enumConstants = kotlin.enums.enumEntries<$type>()") + } + return "ConfigParam<$typeArg>(${namedArgs.joinToString(", ")})" + } + + private fun LocalFlagEntry.formatDefault(): String = + when { + isEnum -> "$type.$defaultValue" + type == "String" -> defaultValue + type == "Long" -> "${defaultValue}L" + type == "Float" -> "${defaultValue}f" + else -> defaultValue + } +} diff --git a/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGenerator.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGenerator.kt new file mode 100644 index 0000000..c9e89f1 --- /dev/null +++ b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGenerator.kt @@ -0,0 +1,122 @@ +package dev.androidbroadcast.featured.gradle + +/** + * Generates the `GeneratedFlagExtensions.kt` source file — internal extension + * functions on `ConfigValues` for each declared flag. + * + * **Local Boolean flags** get an `is…Enabled()` extension returning the raw `Boolean`: + * ```kotlin + * internal suspend fun ConfigValues.isDarkModeEnabled(): Boolean = getValue(GeneratedLocalFlags.darkMode).value + * ``` + * + * **Local non-Boolean flags** get a `get…()` extension returning the raw value type: + * ```kotlin + * internal suspend fun ConfigValues.getMaxRetries(): Int = getValue(GeneratedLocalFlags.maxRetries).value + * ``` + * + * **Remote flags** get a `get…()` extension returning `ConfigValue` so callers can + * inspect the value source (DEFAULT / REMOTE / etc.): + * ```kotlin + * internal suspend fun ConfigValues.getPromoBannerEnabled(): ConfigValue = + * getValue(GeneratedRemoteFlags.promoBannerEnabled) + * ``` + * + * Extensions are `internal` because no external production consumer depends on them — modules + * that need `ConfigParam` values directly use `observe(GeneratedLocalFlags.x)` against the + * now-`public` generated objects. The `suspend` modifier is required because + * `ConfigValues.getValue` is a `suspend` function. + * + * Note: the ProGuard `-assumevalues` rules emitted by [ProguardRulesGenerator] target the + * non-suspend JVM signature and are therefore **no-ops** for the current generated shape. + * This is a known follow-up item — see tracked issue for the per-function DCE rework. + * + * **JVM class-name uniqueness:** `@file:JvmName` is intentionally absent — it is not + * supported on Kotlin/Native targets. Instead, the emitted file is named + * `GeneratedFlagExtensions.kt` where `` is derived from the Gradle module + * path (e.g. `SampleFeatureCheckout`). The Kotlin compiler derives the JVM class name from + * the file name, so `GeneratedFlagExtensionsSampleFeatureCheckoutKt` is unique per module + * without any JVM-specific annotation. + */ +public object ExtensionFunctionGenerator { + private const val PACKAGE = "dev.androidbroadcast.featured.generated" + private const val CONFIG_VALUES_IMPORT = "dev.androidbroadcast.featured.ConfigValues" + private const val CONFIG_VALUE_IMPORT = "dev.androidbroadcast.featured.ConfigValue" + + /** + * Returns the emitted `.kt` file name for the given Gradle module path. + * + * The file-name suffix is derived by splitting the module path on all non-alphanumeric + * characters and PascalCasing each segment, ensuring a valid Kotlin identifier without + * any JVM-specific annotation. The Kotlin compiler derives the JVM class name from this + * file name (e.g. `GeneratedFlagExtensionsSampleFeatureCheckoutKt`). + * + * Examples: + * - `":app"` → `"GeneratedFlagExtensionsApp.kt"` + * - `":feature:checkout"` → `"GeneratedFlagExtensionsFeatureCheckout.kt"` + * - `":sample:feature-checkout"` → `"GeneratedFlagExtensionsSampleFeatureCheckout.kt"` + */ + public fun fileName(modulePath: String): String = "GeneratedFlagExtensions${modulePath.modulePathToFileSuffix()}.kt" + + /** + * Returns the legacy `@file:JvmName` value that was previously emitted into the source file. + * + * This function is retained for use by [ProguardRulesGenerator], which needs to reference + * the JVM class name in `-assumevalues` rules. Note that those rules are currently no-ops + * because the generated extensions are `suspend` (ProGuard rework is a follow-up item). + * + * Examples: `":app"` → `"FeaturedApp_FlagExtensionsKt"`, + * `":feature:checkout"` → `"FeaturedFeatureCheckout_FlagExtensionsKt"`. + */ + public fun jvmFileName(modulePath: String): String = "Featured${modulePath.modulePathToIdentifier()}_FlagExtensionsKt" + + /** + * Generates the full source text for the module-specific `GeneratedFlagExtensions.kt`. + * + * Returns an empty string if [entries] is empty. + */ + public fun generate( + entries: List, + modulePath: String, + ): String { + if (entries.isEmpty()) return "" + val needsConfigValue = entries.any { !it.isLocal } + val localObjectName = ConfigParamGenerator.localObjectName(modulePath) + val remoteObjectName = ConfigParamGenerator.remoteObjectName(modulePath) + + return buildString { + appendLine("// Auto-generated by Featured Gradle Plugin — do not edit manually.") + appendLine() + appendLine("package $PACKAGE") + appendLine() + appendLine("import $CONFIG_VALUES_IMPORT") + if (needsConfigValue) appendLine("import $CONFIG_VALUE_IMPORT") + val (localEntries, remoteEntries) = entries.partition { it.isLocal } + if (localEntries.isNotEmpty()) { + appendLine("import $PACKAGE.$localObjectName") + } + if (remoteEntries.isNotEmpty()) { + appendLine("import $PACKAGE.$remoteObjectName") + } + appendLine() + entries.forEach { entry -> + appendLine(entry.toExtensionFunction(localObjectName, remoteObjectName)) + } + }.trimEnd() + "\n" + } + + private fun LocalFlagEntry.toExtensionFunction( + localObjectName: String, + remoteObjectName: String, + ): String { + val objectRef = if (isLocal) localObjectName else remoteObjectName + return if (isLocal) { + val funcName = extensionFunctionName() + "internal suspend fun ConfigValues.$funcName(): $type = getValue($objectRef.$propertyName).value\n" + } else { + // Remote flags always use get… regardless of type — the return is ConfigValue, + // so callers can inspect the value source. + val funcName = "get${propertyName.capitalized()}" + "internal suspend fun ConfigValues.$funcName(): ConfigValue<$type> = getValue($objectRef.$propertyName)\n" + } + } +} diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedApplicationPlugin.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedApplicationPlugin.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedApplicationPlugin.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedApplicationPlugin.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedExtension.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedExtension.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedExtension.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedExtension.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtils.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtils.kt similarity index 56% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtils.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtils.kt index b1f93fe..029ae22 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtils.kt +++ b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtils.kt @@ -15,8 +15,11 @@ internal fun String.toCamelCase(): String = }.joinToString("") /** - * Derives a unique JVM class name suffix from a Gradle module path, used in - * `@file:JvmName(...)` to prevent class name conflicts across modules. + * Derives a PascalCase identifier from a Gradle module path. + * + * Splits on `:` only; segments containing hyphens or other special characters are + * preserved as single PascalCase words (e.g. `"feature-checkout"` → `"Feature-checkout"`). + * Used internally for identifier derivation. * * Examples: * - `":app"` → `"App"` @@ -30,6 +33,26 @@ internal fun String.modulePathToIdentifier(): String = .joinToString("") { segment -> segment.replaceFirstChar { it.uppercase() } } .ifEmpty { "Root" } +/** + * Derives a PascalCase file-name suffix from a Gradle module path, safe for use as a + * Kotlin source-file name component. + * + * Unlike [modulePathToIdentifier], this function splits on ALL non-alphanumeric characters + * (`:`, `-`, `.`, `_`, etc.) so that path segments like `"feature-checkout"` produce + * `"FeatureCheckout"` rather than `"Feature-checkout"`. + * + * Examples: + * - `":app"` → `"App"` + * - `":feature:checkout"` → `"FeatureCheckout"` + * - `":sample:feature-checkout"` → `"SampleFeatureCheckout"` + * - `":"` → `"Root"` + */ +internal fun String.modulePathToFileSuffix(): String = + split(Regex("[^A-Za-z0-9]+")) + .filter { it.isNotBlank() } + .joinToString("") { segment -> segment.replaceFirstChar { it.uppercase() } } + .ifEmpty { "Root" } + internal fun String.capitalized(): String = replaceFirstChar { it.uppercase() } /** diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagSpec.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagSpec.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagSpec.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagSpec.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateConfigParamTask.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateConfigParamTask.kt similarity index 63% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateConfigParamTask.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateConfigParamTask.kt index 624e787..3704eb7 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateConfigParamTask.kt +++ b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateConfigParamTask.kt @@ -15,9 +15,11 @@ import org.gradle.api.tasks.TaskAction /** * Gradle task that reads the [ResolveFlagsTask] output and generates three Kotlin source files: * - * - `GeneratedLocalFlags.kt` — internal object with one `ConfigParam` per local flag. - * - `GeneratedRemoteFlags.kt` — internal object with one `ConfigParam` per remote flag. - * - `GeneratedFlagExtensions.kt` — public extension functions on `ConfigValues`, one per flag. + * - `GeneratedLocalFlags.kt` — public object with one `ConfigParam` per local flag. + * - `GeneratedRemoteFlags.kt` — public object with one `ConfigParam` per remote flag. + * - `GeneratedFlagExtensions.kt` — internal extension functions on `ConfigValues`, + * one per flag. The suffix is derived from [modulePath] (e.g. `SampleFeatureCheckout`) + * so that each module's file produces a unique JVM class name. * * All files are written to [outputDir] (`build/generated/featured/commonMain/`). * Add [outputDir] to the Kotlin compilation source set: @@ -36,7 +38,7 @@ public abstract class GenerateConfigParamTask : DefaultTask() { @get:PathSensitive(PathSensitivity.NONE) public abstract val flagsFile: RegularFileProperty - /** The Gradle module path used to derive the `@file:JvmName` suffix. */ + /** The Gradle module path (e.g. `":sample:feature-checkout"`) used to derive the file-name suffix. */ @get:Input public abstract val modulePath: Property @@ -48,19 +50,24 @@ public abstract class GenerateConfigParamTask : DefaultTask() { public fun generate() { val entries = flagsFile.parseLocalFlagEntries() val dir = outputDir.get().asFile + // Clean before writing — the extension file name changed from the fixed + // "GeneratedFlagExtensions.kt" to a module-specific name, so stale files + // from previous runs must be removed to avoid duplicate-class compile errors. + dir.deleteRecursively() dir.mkdirs() - val (localSource, remoteSource) = ConfigParamGenerator.generate(entries) - val extensionsSource = ExtensionFunctionGenerator.generate(entries, modulePath.get()) + val path = modulePath.get() + val (localSource, remoteSource) = ConfigParamGenerator.generate(entries, path) + val extensionsSource = ExtensionFunctionGenerator.generate(entries, path) if (localSource.isNotEmpty()) { - dir.resolve("GeneratedLocalFlags.kt").writeText(localSource) + dir.resolve(ConfigParamGenerator.localFileName(path)).writeText(localSource) } if (remoteSource.isNotEmpty()) { - dir.resolve("GeneratedRemoteFlags.kt").writeText(remoteSource) + dir.resolve(ConfigParamGenerator.remoteFileName(path)).writeText(remoteSource) } if (extensionsSource.isNotEmpty()) { - dir.resolve("GeneratedFlagExtensions.kt").writeText(extensionsSource) + dir.resolve(ExtensionFunctionGenerator.fileName(modulePath.get())).writeText(extensionsSource) } val local = entries.count { it.isLocal } diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateIosConstValTask.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateIosConstValTask.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateIosConstValTask.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateIosConstValTask.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTask.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTask.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTask.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTask.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateXcconfigTask.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateXcconfigTask.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateXcconfigTask.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateXcconfigTask.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGenerator.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGenerator.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGenerator.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGenerator.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt similarity index 93% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt index a4b072c..7ea04ca 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt +++ b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt @@ -41,7 +41,5 @@ public data class LocalFlagEntry( public companion object { public const val FLAG_TYPE_LOCAL: String = "local" public const val FLAG_TYPE_REMOTE: String = "remote" - internal const val GENERATED_LOCAL_OBJECT = "GeneratedLocalFlags" - internal const val GENERATED_REMOTE_OBJECT = "GeneratedRemoteFlags" } } diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ResolveFlagsTask.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ResolveFlagsTask.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ResolveFlagsTask.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ResolveFlagsTask.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ScanResultParser.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ScanResultParser.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ScanResultParser.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ScanResultParser.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/XcconfigGenerator.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/XcconfigGenerator.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/XcconfigGenerator.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/XcconfigGenerator.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/AggregationContract.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/AggregationContract.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/AggregationContract.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/AggregationContract.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GenerateFeaturedRegistryTask.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GenerateFeaturedRegistryTask.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GenerateFeaturedRegistryTask.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GenerateFeaturedRegistryTask.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGenerator.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGenerator.kt similarity index 96% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGenerator.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGenerator.kt index b23888f..554ebe9 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGenerator.kt +++ b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGenerator.kt @@ -59,6 +59,9 @@ internal object GeneratedFeaturedRegistryGenerator { add("defaultValue = $defaultLiteral") if (descriptor.description != null) add("description = \"${escapeKotlinString(descriptor.description)}\"") if (descriptor.category != null) add("category = \"${escapeKotlinString(descriptor.category)}\"") + if (descriptor.valueType == ValueType.ENUM) { + add("enumConstants = kotlin.enums.enumEntries<${descriptor.enumTypeFqn}>()") + } } // Kotlin accepts trailing commas in listOf() — always emit one for uniform diffs. appendLine(" ConfigParam<$typeArg>(${args.joinToString(", ")}),") diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifest.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifest.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifest.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifest.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestContract.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestContract.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestContract.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestContract.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/GenerateFeaturedManifestTask.kt b/build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/GenerateFeaturedManifestTask.kt similarity index 100% rename from featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/GenerateFeaturedManifestTask.kt rename to build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/manifest/GenerateFeaturedManifestTask.kt diff --git a/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/app/build.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/app/build.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/app/build.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/app/build.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/app/src/main/AndroidManifest.xml b/build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/app/src/main/AndroidManifest.xml similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/app/src/main/AndroidManifest.xml rename to build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/app/src/main/AndroidManifest.xml diff --git a/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/build.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/build.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/build.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/build.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-checkout/build.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-checkout/build.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-checkout/build.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-checkout/build.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-checkout/src/main/AndroidManifest.xml b/build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-checkout/src/main/AndroidManifest.xml similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-checkout/src/main/AndroidManifest.xml rename to build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-checkout/src/main/AndroidManifest.xml diff --git a/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-profile/build.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-profile/build.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-profile/build.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-profile/build.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-profile/src/main/AndroidManifest.xml b/build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-profile/src/main/AndroidManifest.xml similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-profile/src/main/AndroidManifest.xml rename to build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/feature-profile/src/main/AndroidManifest.xml diff --git a/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/gradle.properties b/build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/gradle.properties similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/gradle.properties rename to build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/gradle.properties diff --git a/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/settings.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/settings.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/settings.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/aggregator-multi-module-project/settings.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/android-project/build.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/android-project/build.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/android-project/build.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/android-project/build.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/android-project/settings.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/android-project/settings.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/android-project/settings.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/android-project/settings.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/android-project/src/main/AndroidManifest.xml b/build-logic/featured-gradle-plugin/src/test/fixtures/android-project/src/main/AndroidManifest.xml similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/android-project/src/main/AndroidManifest.xml rename to build-logic/featured-gradle-plugin/src/test/fixtures/android-project/src/main/AndroidManifest.xml diff --git a/featured-gradle-plugin/src/test/fixtures/android-project/src/main/kotlin/dev/androidbroadcast/featured/testapp/CheckoutVariant.kt b/build-logic/featured-gradle-plugin/src/test/fixtures/android-project/src/main/kotlin/dev/androidbroadcast/featured/testapp/CheckoutVariant.kt similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/android-project/src/main/kotlin/dev/androidbroadcast/featured/testapp/CheckoutVariant.kt rename to build-logic/featured-gradle-plugin/src/test/fixtures/android-project/src/main/kotlin/dev/androidbroadcast/featured/testapp/CheckoutVariant.kt diff --git a/featured-gradle-plugin/src/test/fixtures/jvm-empty-featured-project/build.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/jvm-empty-featured-project/build.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/jvm-empty-featured-project/build.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/jvm-empty-featured-project/build.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/jvm-empty-featured-project/settings.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/jvm-empty-featured-project/settings.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/jvm-empty-featured-project/settings.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/jvm-empty-featured-project/settings.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/build.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/build.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/kmp-publish-project/build.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/build.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/gradle.properties b/build-logic/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/gradle.properties similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/kmp-publish-project/gradle.properties rename to build-logic/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/gradle.properties diff --git a/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/module/build.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/module/build.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/kmp-publish-project/module/build.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/module/build.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/module/src/commonMain/kotlin/.gitkeep b/build-logic/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/module/src/commonMain/kotlin/.gitkeep similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/kmp-publish-project/module/src/commonMain/kotlin/.gitkeep rename to build-logic/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/module/src/commonMain/kotlin/.gitkeep diff --git a/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/settings.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/settings.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/kmp-publish-project/settings.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/kmp-publish-project/settings.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/app/build.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/app/build.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/manifest-publish-project/app/build.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/app/build.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/app/src/main/AndroidManifest.xml b/build-logic/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/app/src/main/AndroidManifest.xml similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/manifest-publish-project/app/src/main/AndroidManifest.xml rename to build-logic/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/app/src/main/AndroidManifest.xml diff --git a/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/build.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/build.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/manifest-publish-project/build.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/build.gradle.kts diff --git a/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/gradle.properties b/build-logic/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/gradle.properties similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/manifest-publish-project/gradle.properties rename to build-logic/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/gradle.properties diff --git a/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/settings.gradle.kts b/build-logic/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/settings.gradle.kts similarity index 100% rename from featured-gradle-plugin/src/test/fixtures/manifest-publish-project/settings.gradle.kts rename to build-logic/featured-gradle-plugin/src/test/fixtures/manifest-publish-project/settings.gradle.kts diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGeneratorTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGeneratorTest.kt similarity index 60% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGeneratorTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGeneratorTest.kt index 1ea6cb5..7ca7cb7 100644 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGeneratorTest.kt +++ b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGeneratorTest.kt @@ -6,20 +6,22 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class ConfigParamGeneratorTest { + private val modulePath = ":app" + // ── local flags ─────────────────────────────────────────────────────────── @Test - fun `generates GeneratedLocalFlags object for local boolean flag`() { + fun `generates module-suffixed local flags object for local boolean flag`() { val entries = listOf(localEntry("dark_mode", "false", "Boolean")) - val (local, _) = ConfigParamGenerator.generate(entries) - assertContains(local, "object GeneratedLocalFlags") + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) + assertContains(local, "object GeneratedLocalFlagsApp") assertContains(local, "val darkMode = ConfigParam") } @Test fun `generated local ConfigParam uses named key argument`() { val entries = listOf(localEntry("dark_mode", "false", "Boolean")) - val (local, _) = ConfigParamGenerator.generate(entries) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) assertContains(local, "key = \"dark_mode\"") assertContains(local, "defaultValue = false") } @@ -27,67 +29,67 @@ class ConfigParamGeneratorTest { @Test fun `generated local ConfigParam includes description when present`() { val entry = localEntry("dark_mode", "false", "Boolean").copy(description = "Enable dark mode") - val (local, _) = ConfigParamGenerator.generate(listOf(entry)) + val (local, _) = ConfigParamGenerator.generate(listOf(entry), modulePath) assertContains(local, "description = \"Enable dark mode\"") } @Test fun `generated local ConfigParam includes category when present`() { val entry = localEntry("dark_mode", "false", "Boolean").copy(category = "UI") - val (local, _) = ConfigParamGenerator.generate(listOf(entry)) + val (local, _) = ConfigParamGenerator.generate(listOf(entry), modulePath) assertContains(local, "category = \"UI\"") } @Test fun `generated local ConfigParam omits null description`() { val entries = listOf(localEntry("dark_mode", "false", "Boolean")) - val (local, _) = ConfigParamGenerator.generate(entries) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) assertTrue(!local.contains("description ="), "Null description must not appear in output") } @Test - fun `local object is internal`() { + fun `local object is public`() { val entries = listOf(localEntry("dark_mode", "false", "Boolean")) - val (local, _) = ConfigParamGenerator.generate(entries) - assertContains(local, "internal object GeneratedLocalFlags") + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) + assertContains(local, "public object GeneratedLocalFlagsApp") } @Test fun `formats Long default with L suffix`() { val entries = listOf(localEntry("timeout", "5000L", "Long")) - val (local, _) = ConfigParamGenerator.generate(entries) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) assertContains(local, "defaultValue = 5000L") } @Test fun `formats Float default with f suffix`() { val entries = listOf(localEntry("ratio", "0.5f", "Float")) - val (local, _) = ConfigParamGenerator.generate(entries) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) assertContains(local, "defaultValue = 0.5f") } @Test fun `passes String default as-is (already quoted)`() { val entries = listOf(localEntry("url", "\"https://x.com\"", "String")) - val (local, _) = ConfigParamGenerator.generate(entries) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) assertContains(local, "defaultValue = \"https://x.com\"") } // ── remote flags ────────────────────────────────────────────────────────── @Test - fun `generates GeneratedRemoteFlags object for remote flag`() { + fun `generates module-suffixed remote flags object for remote flag`() { val entries = listOf(remoteEntry("promo_banner", "false", "Boolean")) - val (_, remote) = ConfigParamGenerator.generate(entries) - assertContains(remote, "object GeneratedRemoteFlags") + val (_, remote) = ConfigParamGenerator.generate(entries, modulePath) + assertContains(remote, "object GeneratedRemoteFlagsApp") assertContains(remote, "val promoBanner = ConfigParam") } @Test - fun `remote object is internal`() { + fun `remote object is public`() { val entries = listOf(remoteEntry("promo", "false", "Boolean")) - val (_, remote) = ConfigParamGenerator.generate(entries) - assertContains(remote, "internal object GeneratedRemoteFlags") + val (_, remote) = ConfigParamGenerator.generate(entries, modulePath) + assertContains(remote, "public object GeneratedRemoteFlagsApp") } // ── empty cases ─────────────────────────────────────────────────────────── @@ -95,20 +97,20 @@ class ConfigParamGeneratorTest { @Test fun `returns empty string for local when no local flags`() { val entries = listOf(remoteEntry("promo", "false", "Boolean")) - val (local, _) = ConfigParamGenerator.generate(entries) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) assertTrue(local.isEmpty(), "Expected empty local source when no local flags") } @Test fun `returns empty string for remote when no remote flags`() { val entries = listOf(localEntry("dark_mode", "false", "Boolean")) - val (_, remote) = ConfigParamGenerator.generate(entries) + val (_, remote) = ConfigParamGenerator.generate(entries, modulePath) assertTrue(remote.isEmpty(), "Expected empty remote source when no remote flags") } @Test fun `both empty for empty entries list`() { - val (local, remote) = ConfigParamGenerator.generate(emptyList()) + val (local, remote) = ConfigParamGenerator.generate(emptyList(), modulePath) assertEquals("", local) assertEquals("", remote) } @@ -118,40 +120,82 @@ class ConfigParamGeneratorTest { @Test fun `generated local file imports ConfigParam`() { val entries = listOf(localEntry("flag", "false", "Boolean")) - val (local, _) = ConfigParamGenerator.generate(entries) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) assertContains(local, "import dev.androidbroadcast.featured.ConfigParam") } @Test fun `generated file has auto-generated comment`() { val entries = listOf(localEntry("flag", "false", "Boolean")) - val (local, _) = ConfigParamGenerator.generate(entries) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) assertContains(local, "Auto-generated by Featured Gradle Plugin") } + // ── module-derived naming ───────────────────────────────────────────────── + + @Test + fun `different modules produce different object names`() { + val entries = listOf(localEntry("dark_mode", "false", "Boolean")) + val (localA, _) = ConfigParamGenerator.generate(entries, ":feature:checkout") + val (localB, _) = ConfigParamGenerator.generate(entries, ":feature:ui") + assertContains(localA, "object GeneratedLocalFlagsFeatureCheckout") + assertContains(localB, "object GeneratedLocalFlagsFeatureUi") + } + + @Test + fun `hyphenated module segment produces valid object name`() { + val entries = listOf(localEntry("dark_mode", "false", "Boolean")) + val (local, _) = ConfigParamGenerator.generate(entries, ":sample:feature-checkout") + assertContains(local, "object GeneratedLocalFlagsSampleFeatureCheckout") + } + + @Test + fun `localFileName uses module suffix`() { + assertEquals("GeneratedLocalFlagsSampleFeatureCheckout.kt", ConfigParamGenerator.localFileName(":sample:feature-checkout")) + } + + @Test + fun `remoteFileName uses module suffix`() { + assertEquals("GeneratedRemoteFlagsSampleFeaturePromotions.kt", ConfigParamGenerator.remoteFileName(":sample:feature-promotions")) + } + // ── enum flags ──────────────────────────────────────────────────────────── @Test fun `generates enum ConfigParam with fqn type argument`() { val entries = listOf(localEntry("checkout_variant", "LEGACY", "com.example.CheckoutVariant")) - val (local, _) = ConfigParamGenerator.generate(entries) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) assertContains(local, "ConfigParam") } @Test fun `enum default value uses fqn dot constant syntax`() { val entries = listOf(localEntry("checkout_variant", "LEGACY", "com.example.CheckoutVariant")) - val (local, _) = ConfigParamGenerator.generate(entries) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) assertContains(local, "defaultValue = com.example.CheckoutVariant.LEGACY") } @Test fun `enum flag is included in local object`() { val entries = listOf(localEntry("checkout_variant", "LEGACY", "com.example.CheckoutVariant")) - val (local, _) = ConfigParamGenerator.generate(entries) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) assertContains(local, "val checkoutVariant = ConfigParam") } + @Test + fun `enum flag emits enumConstants with kotlin enumEntries call`() { + val entries = listOf(localEntry("checkout_variant", "LEGACY", "com.example.CheckoutVariant")) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) + assertContains(local, "enumConstants = kotlin.enums.enumEntries()") + } + + @Test + fun `non-enum flag does not emit enumConstants`() { + val entries = listOf(localEntry("dark_mode", "false", "Boolean")) + val (local, _) = ConfigParamGenerator.generate(entries, modulePath) + assertTrue(!local.contains("enumConstants ="), "Non-enum flag must not emit enumConstants") + } + // ── helpers ─────────────────────────────────────────────────────────────── private fun localEntry( @@ -162,7 +206,7 @@ class ConfigParamGeneratorTest { key = key, defaultValue = default, type = type, - moduleName = ":app", + moduleName = modulePath, propertyName = key.toCamelCase(), flagType = LocalFlagEntry.FLAG_TYPE_LOCAL, ) @@ -175,7 +219,7 @@ class ConfigParamGeneratorTest { key = key, defaultValue = default, type = type, - moduleName = ":app", + moduleName = modulePath, propertyName = key.toCamelCase(), flagType = LocalFlagEntry.FLAG_TYPE_REMOTE, ) diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGeneratorTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGeneratorTest.kt similarity index 74% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGeneratorTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGeneratorTest.kt index ad2e938..82f2d2e 100644 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGeneratorTest.kt +++ b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGeneratorTest.kt @@ -8,26 +8,33 @@ import kotlin.test.assertTrue class ExtensionFunctionGeneratorTest { private val modulePath = ":feature:checkout" - // ── JVM file name ───────────────────────────────────────────────────────── + // ── generated file name ─────────────────────────────────────────────────── @Test - fun `jvmFileName for app module`() { - val name = ExtensionFunctionGenerator.jvmFileName(":app") + fun `fileName for app module`() { + val name = ExtensionFunctionGenerator.fileName(":app") assertContains(name, "App") - assertContains(name, "FlagExtensionsKt") + assertTrue(name.endsWith(".kt"), "File name must end with .kt") } @Test - fun `jvmFileName for nested module`() { - val name = ExtensionFunctionGenerator.jvmFileName(":feature:checkout") + fun `fileName for nested module`() { + val name = ExtensionFunctionGenerator.fileName(":feature:checkout") assertContains(name, "FeatureCheckout") } @Test - fun `jvmFileName for different modules are distinct`() { - val a = ExtensionFunctionGenerator.jvmFileName(":feature:checkout") - val b = ExtensionFunctionGenerator.jvmFileName(":feature:ui") - assertFalse(a == b, "Different modules must produce distinct JVM names") + fun `fileName for hyphenated module segment`() { + val name = ExtensionFunctionGenerator.fileName(":sample:feature-checkout") + assertContains(name, "SampleFeatureCheckout") + assertFalse(name.contains("-"), "File name must not contain hyphens") + } + + @Test + fun `fileName for different modules are distinct`() { + val a = ExtensionFunctionGenerator.fileName(":feature:checkout") + val b = ExtensionFunctionGenerator.fileName(":feature:ui") + assertFalse(a == b, "Different modules must produce distinct file names") } // ── empty input ─────────────────────────────────────────────────────────── @@ -44,21 +51,21 @@ class ExtensionFunctionGeneratorTest { fun `generates is…Enabled extension for local boolean flag`() { val entries = listOf(localEntry("dark_mode", "Boolean")) val source = ExtensionFunctionGenerator.generate(entries, modulePath) - assertContains(source, "fun ConfigValues.isDarkModeEnabled(): Boolean") + assertContains(source, "suspend fun ConfigValues.isDarkModeEnabled(): Boolean") } @Test fun `local boolean extension returns raw value`() { val entries = listOf(localEntry("dark_mode", "Boolean")) val source = ExtensionFunctionGenerator.generate(entries, modulePath) - assertContains(source, "getValue(GeneratedLocalFlags.darkMode).value") + assertContains(source, "getValue(GeneratedLocalFlagsFeatureCheckout.darkMode).value") } @Test - fun `local boolean extension is public`() { + fun `local boolean extension is internal suspend`() { val entries = listOf(localEntry("dark_mode", "Boolean")) val source = ExtensionFunctionGenerator.generate(entries, modulePath) - assertContains(source, "public fun ConfigValues.isDarkModeEnabled()") + assertContains(source, "internal suspend fun ConfigValues.isDarkModeEnabled()") } // ── local non-boolean flag ──────────────────────────────────────────────── @@ -67,15 +74,15 @@ class ExtensionFunctionGeneratorTest { fun `generates get… extension for local int flag`() { val entries = listOf(localEntry("max_retries", "Int")) val source = ExtensionFunctionGenerator.generate(entries, modulePath) - assertContains(source, "fun ConfigValues.getMaxRetries(): Int") - assertContains(source, "getValue(GeneratedLocalFlags.maxRetries).value") + assertContains(source, "suspend fun ConfigValues.getMaxRetries(): Int") + assertContains(source, "getValue(GeneratedLocalFlagsFeatureCheckout.maxRetries).value") } @Test fun `generates get… extension for local string flag`() { val entries = listOf(localEntry("api_url", "String")) val source = ExtensionFunctionGenerator.generate(entries, modulePath) - assertContains(source, "fun ConfigValues.getApiUrl(): String") + assertContains(source, "suspend fun ConfigValues.getApiUrl(): String") } // ── local enum flag ─────────────────────────────────────────────────────── @@ -84,8 +91,8 @@ class ExtensionFunctionGeneratorTest { fun `generates get… extension for local enum flag`() { val entries = listOf(localEntry("checkout_variant", "com.example.CheckoutVariant")) val source = ExtensionFunctionGenerator.generate(entries, modulePath) - assertContains(source, "fun ConfigValues.getCheckoutVariant(): com.example.CheckoutVariant") - assertContains(source, "getValue(GeneratedLocalFlags.checkoutVariant).value") + assertContains(source, "suspend fun ConfigValues.getCheckoutVariant(): com.example.CheckoutVariant") + assertContains(source, "getValue(GeneratedLocalFlagsFeatureCheckout.checkoutVariant).value") } @Test @@ -101,8 +108,8 @@ class ExtensionFunctionGeneratorTest { fun `generates get… extension returning ConfigValue for remote flag`() { val entries = listOf(remoteEntry("promo_banner", "Boolean")) val source = ExtensionFunctionGenerator.generate(entries, modulePath) - assertContains(source, "fun ConfigValues.getPromoBanner(): ConfigValue") - assertContains(source, "getValue(GeneratedRemoteFlags.promoBanner)") + assertContains(source, "suspend fun ConfigValues.getPromoBanner(): ConfigValue") + assertContains(source, "getValue(GeneratedRemoteFlagsFeatureCheckout.promoBanner)") } @Test @@ -110,7 +117,7 @@ class ExtensionFunctionGeneratorTest { val entries = listOf(remoteEntry("promo_banner", "Boolean")) val source = ExtensionFunctionGenerator.generate(entries, modulePath) assertFalse( - source.contains("GeneratedRemoteFlags.promoBanner).value"), + source.contains("GeneratedRemoteFlagsFeatureCheckout.promoBanner).value"), "Remote extensions must return full ConfigValue, not unwrapped value", ) } @@ -118,10 +125,12 @@ class ExtensionFunctionGeneratorTest { // ── file structure ──────────────────────────────────────────────────────── @Test - fun `generated file has JvmName annotation`() { + fun `generated file does not contain JvmName annotation`() { + // @file:JvmName is not supported on Kotlin/Native; class-name uniqueness is + // achieved via the module-derived file name instead. val entries = listOf(localEntry("flag", "Boolean")) val source = ExtensionFunctionGenerator.generate(entries, modulePath) - assertContains(source, "@file:JvmName(\"${ExtensionFunctionGenerator.jvmFileName(modulePath)}\")") + assertFalse(source.contains("@file:JvmName"), "Generated file must not contain @file:JvmName") } @Test diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginIntegrationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginIntegrationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginIntegrationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginIntegrationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagContainerTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagContainerTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagContainerTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagContainerTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtilsTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtilsTest.kt similarity index 65% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtilsTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtilsTest.kt index 91f41eb..ff8625f 100644 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtilsTest.kt +++ b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtilsTest.kt @@ -62,4 +62,36 @@ class FlagEntryUtilsTest { fun `modulePathToIdentifier bare colon returns Root`() { assertEquals("Root", ":".modulePathToIdentifier()) } + + // ── modulePathToFileSuffix ──────────────────────────────────────────────── + + @Test + fun `modulePathToFileSuffix for root app module`() { + assertEquals("App", ":app".modulePathToFileSuffix()) + } + + @Test + fun `modulePathToFileSuffix for nested module`() { + assertEquals("FeatureCheckout", ":feature:checkout".modulePathToFileSuffix()) + } + + @Test + fun `modulePathToFileSuffix for hyphenated segment`() { + assertEquals("SampleFeatureCheckout", ":sample:feature-checkout".modulePathToFileSuffix()) + } + + @Test + fun `modulePathToFileSuffix for deeply nested module`() { + assertEquals("FeaturePaymentUi", ":feature:payment:ui".modulePathToFileSuffix()) + } + + @Test + fun `modulePathToFileSuffix empty string returns Root`() { + assertEquals("Root", "".modulePathToFileSuffix()) + } + + @Test + fun `modulePathToFileSuffix bare colon returns Root`() { + assertEquals("Root", ":".modulePathToFileSuffix()) + } } diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateIosConstValTaskRegistrationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateIosConstValTaskRegistrationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateIosConstValTaskRegistrationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateIosConstValTaskRegistrationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTaskRegistrationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTaskRegistrationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTaskRegistrationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTaskRegistrationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateXcconfigTaskRegistrationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateXcconfigTaskRegistrationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateXcconfigTaskRegistrationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateXcconfigTaskRegistrationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGeneratorTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGeneratorTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGeneratorTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGeneratorTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntryTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntryTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntryTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntryTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/XcconfigGeneratorTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/XcconfigGeneratorTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/XcconfigGeneratorTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/XcconfigGeneratorTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationConfigurationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationConfigurationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationConfigurationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationConfigurationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationDescriptorIntegrityTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationDescriptorIntegrityTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationDescriptorIntegrityTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationDescriptorIntegrityTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationDuplicateKeyTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationDuplicateKeyTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationDuplicateKeyTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationDuplicateKeyTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationIntegrationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationIntegrationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationIntegrationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationIntegrationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationParseErrorTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationParseErrorTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationParseErrorTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/FeaturedAggregationParseErrorTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GenerateFeaturedRegistryTaskRegistrationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GenerateFeaturedRegistryTaskRegistrationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GenerateFeaturedRegistryTaskRegistrationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GenerateFeaturedRegistryTaskRegistrationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGeneratorTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGeneratorTest.kt similarity index 96% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGeneratorTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGeneratorTest.kt index 5ecaf60..bd73c34 100644 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGeneratorTest.kt +++ b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GeneratedFeaturedRegistryGeneratorTest.kt @@ -146,6 +146,17 @@ class GeneratedFeaturedRegistryGeneratorTest { ) assertContains(source, "ConfigParam") assertContains(source, "defaultValue = com.example.CheckoutVariant.LEGACY") + assertContains(source, "enumConstants = kotlin.enums.enumEntries()") + } + + @Test + fun `BOOLEAN flag does not emit enumConstants`() { + val source = + GeneratedFeaturedRegistryGenerator.generate( + manifests = listOf(manifest(":app", flag(key = "dark_mode", valueType = ValueType.BOOLEAN, defaultValue = "false"))), + packageName = FEATURED_REGISTRY_PACKAGE, + ) + assertFalse(source.contains("enumConstants"), "enumConstants must not appear for non-enum types") } @Test diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedKmpPublicationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedKmpPublicationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedKmpPublicationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedKmpPublicationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestConfigurationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestConfigurationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestConfigurationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestConfigurationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestEmptyDslTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestEmptyDslTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestEmptyDslTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestEmptyDslTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestIntegrationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestIntegrationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestIntegrationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestIntegrationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestMappingTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestMappingTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestMappingTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestMappingTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestSerializationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestSerializationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestSerializationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/FeaturedManifestSerializationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/GenerateFeaturedManifestTaskRegistrationTest.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/GenerateFeaturedManifestTaskRegistrationTest.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/GenerateFeaturedManifestTaskRegistrationTest.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/GenerateFeaturedManifestTaskRegistrationTest.kt diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/TestFixtureSupport.kt b/build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/TestFixtureSupport.kt similarity index 100% rename from featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/TestFixtureSupport.kt rename to build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/manifest/TestFixtureSupport.kt diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..201ab5a --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,61 @@ +@file:Suppress("UnstableApiUsage") + +// pluginManagement must be the first block per Gradle's settings-script rules. + +pluginManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + gradlePluginPortal() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +// Propagate VERSION_NAME (and any other properties) from the root gradle.properties into +// this included build so that Vanniktech maven-publish resolves the correct version. +// The root gradle.properties is the single source of truth — do not duplicate VERSION_NAME here. +// +// providers.fileContents() registers the file with Gradle's configuration-cache fingerprint, +// unlike a raw FileInputStream read which is invisible to the cache. When root gradle.properties +// changes (e.g. VERSION_NAME bump), the cache entry is invalidated and the new value is picked up. +val parentPropertiesText: Provider = + providers.fileContents(layout.rootDirectory.file("../gradle.properties")).asText + +gradle.beforeProject { + val parentProps = + java.util.Properties().apply { + parentPropertiesText.orNull?.reader()?.use { load(it) } + } + parentProps.forEach { key, value -> + extensions.extraProperties[key.toString()] = value.toString() + } + // Set project.version directly so Vanniktech's providers.gradleProperty fallback also works. + (parentProps.getProperty("VERSION_NAME") ?: "unspecified").let { version = it } +} + +rootProject.name = "build-logic" +include(":featured-gradle-plugin") diff --git a/build.gradle.kts b/build.gradle.kts index 0ace319..f26360a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,14 @@ plugins { alias(libs.plugins.dokka) } +tasks.register("publishToMavenCentral") { + dependsOn(gradle.includedBuild("build-logic").task(":featured-gradle-plugin:publishToMavenCentral")) +} + +tasks.register("publishToMavenLocal") { + dependsOn(gradle.includedBuild("build-logic").task(":featured-gradle-plugin:publishToMavenLocal")) +} + spotless { val ktlintVersion = libs.versions.ktlint.get() kotlin { diff --git a/core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigParam.kt b/core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigParam.kt index 6a465af..0b65f54 100644 --- a/core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigParam.kt +++ b/core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigParam.kt @@ -58,6 +58,11 @@ public class ConfigParam * Useful for tracking parameter lifecycle. */ public val since: String? = null, + /** + * All declared constants of the enum type, or `null` for non-enum params. + * Populated by the code generator via `enumValues().toList()`; not set for hand-written params. + */ + public val enumConstants: List? = null, ) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -70,7 +75,8 @@ public class ConfigParam valueType == other.valueType && description == other.description && category == other.category && - since == other.since + since == other.since && + enumConstants == other.enumConstants } override fun hashCode(): Int = key.hashCode() @@ -85,6 +91,7 @@ public class ConfigParam appendIfPresent(key = "description", description) appendIfPresent(key = "category", category) appendIfPresent(key = "since", since) + appendIfPresent(key = "enumConstants", enumConstants) append(')') } @@ -131,6 +138,7 @@ public inline fun ConfigParam( description: String? = null, category: String? = null, since: String? = null, + enumConstants: List? = null, ): ConfigParam = ConfigParam( key = key, @@ -139,4 +147,5 @@ public inline fun ConfigParam( description = description, category = category, since = since, + enumConstants = enumConstants, ) diff --git a/core/src/commonTest/kotlin/dev/androidbroadcast/featured/ConfigParamTest.kt b/core/src/commonTest/kotlin/dev/androidbroadcast/featured/ConfigParamTest.kt index ac5db35..0c9484a 100644 --- a/core/src/commonTest/kotlin/dev/androidbroadcast/featured/ConfigParamTest.kt +++ b/core/src/commonTest/kotlin/dev/androidbroadcast/featured/ConfigParamTest.kt @@ -59,6 +59,7 @@ class ConfigParamTest { assertNull(param.description) assertNull(param.category) assertNull(param.since) + assertNull(param.enumConstants) } @Test @@ -99,4 +100,58 @@ class ConfigParamTest { assertTrue(result.contains("since='1.0'")) assertTrue(result.contains("description='desc'")) } + + @Test + fun testEnumConstantsDefaultsToNull() { + val param = ConfigParam(key = "flag", defaultValue = true) + + assertNull(param.enumConstants) + } + + @Test + fun testEqualsReturnsFalseWhenEnumConstantsDiffer() { + val paramWithConstants = + ConfigParam( + key = "flag", + defaultValue = false, + enumConstants = listOf(true, false), + ) + val paramWithoutConstants = + ConfigParam( + key = "flag", + defaultValue = false, + ) + + assertNotEquals(paramWithConstants, paramWithoutConstants) + } + + @Test + fun testEqualsReturnsTrueWhenEnumConstantsAreIdentical() { + val param1 = + ConfigParam( + key = "flag", + defaultValue = false, + enumConstants = listOf(true, false), + ) + val param2 = + ConfigParam( + key = "flag", + defaultValue = false, + enumConstants = listOf(true, false), + ) + + assertEquals(param1, param2) + } + + @Test + fun testToStringIncludesEnumConstantsWhenPresent() { + val param = + ConfigParam( + key = "flag", + defaultValue = false, + enumConstants = listOf(true, false), + ) + + assertTrue(param.toString().contains("enumConstants=")) + } } diff --git a/featured-bom/build.gradle.kts b/featured-bom/build.gradle.kts index d00b7cf..d94ac3d 100644 --- a/featured-bom/build.gradle.kts +++ b/featured-bom/build.gradle.kts @@ -20,7 +20,6 @@ dependencies { api(project(":featured-compose")) api(project(":featured-debug-ui")) api(project(":featured-testing")) - api(project(":featured-gradle-plugin")) api(project(":featured-lint-rules")) api(project(":featured-platform")) diff --git a/featured-debug-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/debugui/FeatureFlagsDebugScreen.kt b/featured-debug-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/debugui/FeatureFlagsDebugScreen.kt index 0e4828b..199dd88 100644 --- a/featured-debug-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/debugui/FeatureFlagsDebugScreen.kt +++ b/featured-debug-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/debugui/FeatureFlagsDebugScreen.kt @@ -14,7 +14,11 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Badge import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -155,6 +159,15 @@ public fun FeatureFlagsDebugScreen( ) } }, + onEnumSelect = { newValue -> + scope.launch { + @Suppress("UNCHECKED_CAST") + configValues.override( + item.param as ConfigParam, + newValue, + ) + } + }, onResetToDefault = { scope.launch { configValues.resetOverride(item.param) @@ -174,6 +187,7 @@ private fun FlagItemCard( item: DebugFlagItem<*>, onToggleBoolean: (Boolean) -> Unit, onScalarInput: (Any) -> Unit, + onEnumSelect: (Any) -> Unit, onResetToDefault: () -> Unit, modifier: Modifier = Modifier, ) { @@ -225,6 +239,21 @@ private fun FlagItemCard( ) } + @Suppress("UNCHECKED_CAST") + val enumConstants = item.param.enumConstants as List>? + if (enumConstants != null) { + EnumDropdown( + label = item.key, + currentValue = item.currentValue as Enum<*>, + options = enumConstants, + onSelect = onEnumSelect, + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + ) + } + if (item.isOverridden) { HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) Row( @@ -339,3 +368,47 @@ private fun SourceBadge(source: ConfigValue.Source) { Text(text = style.label, style = MaterialTheme.typography.labelSmall) } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@Suppress("ktlint:standard:function-naming") +private fun EnumDropdown( + label: String, + currentValue: Enum<*>, + options: List>, + onSelect: (Any) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + modifier = modifier, + ) { + OutlinedTextField( + value = currentValue.name, + onValueChange = {}, + readOnly = true, + label = { Text(label) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = + Modifier + .menuAnchor(type = ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled = true) + .fillMaxWidth(), + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + options.forEach { option -> + DropdownMenuItem( + text = { Text(option.name) }, + onClick = { + onSelect(option) + expanded = false + }, + ) + } + } + } +} diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGenerator.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGenerator.kt deleted file mode 100644 index f698f90..0000000 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGenerator.kt +++ /dev/null @@ -1,73 +0,0 @@ -package dev.androidbroadcast.featured.gradle - -/** - * Generates `GeneratedLocalFlags.kt` and `GeneratedRemoteFlags.kt` — internal objects - * containing one typed `ConfigParam` property per declared flag. - * - * Generated example for a local Boolean flag `dark_mode`: - * ```kotlin - * internal object GeneratedLocalFlags { - * val darkMode = ConfigParam("dark_mode", false, category = "UI") - * } - * ``` - * - * These objects are `internal` — consumers access flags exclusively through the - * generated extension functions in [ExtensionFunctionGenerator]. - */ -public object ConfigParamGenerator { - private const val PACKAGE = "dev.androidbroadcast.featured.generated" - private const val CONFIG_PARAM_IMPORT = "dev.androidbroadcast.featured.ConfigParam" - - /** - * Generates the Kotlin source for `GeneratedLocalFlags.kt` and - * `GeneratedRemoteFlags.kt` as a pair. - * - * Returns a pair of `(localSource, remoteSource)`. Either may be an empty string - * if there are no flags of that type. - */ - public fun generate(entries: List): Pair { - val (local, remote) = entries.partition { it.isLocal } - return generateObject(local, LocalFlagEntry.GENERATED_LOCAL_OBJECT) to - generateObject(remote, LocalFlagEntry.GENERATED_REMOTE_OBJECT) - } - - private fun generateObject( - entries: List, - objectName: String, - ): String { - if (entries.isEmpty()) return "" - return buildString { - appendLine("// Auto-generated by Featured Gradle Plugin — do not edit manually.") - appendLine("package $PACKAGE") - appendLine() - appendLine("import $CONFIG_PARAM_IMPORT") - appendLine() - appendLine("internal object $objectName {") - entries.forEach { entry -> - appendLine(" val ${entry.propertyName} = ${entry.toConfigParamExpression()}") - } - append("}") - } - } - - private fun LocalFlagEntry.toConfigParamExpression(): String { - val typeArg = type - val namedArgs = - buildList { - add("key = \"$key\"") - add("defaultValue = ${formatDefault()}") - if (description != null) add("description = \"$description\"") - if (category != null) add("category = \"$category\"") - } - return "ConfigParam<$typeArg>(${namedArgs.joinToString(", ")})" - } - - private fun LocalFlagEntry.formatDefault(): String = - when { - isEnum -> "$type.$defaultValue" - type == "String" -> defaultValue - type == "Long" -> "${defaultValue}L" - type == "Float" -> "${defaultValue}f" - else -> defaultValue - } -} diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGenerator.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGenerator.kt deleted file mode 100644 index cadad7a..0000000 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGenerator.kt +++ /dev/null @@ -1,88 +0,0 @@ -package dev.androidbroadcast.featured.gradle - -/** - * Generates `GeneratedFlagExtensions.kt` — public extension functions on `ConfigValues` - * for each declared flag. - * - * **Local Boolean flags** get an `is…Enabled()` extension returning the raw `Boolean`: - * ```kotlin - * fun ConfigValues.isDarkModeEnabled(): Boolean = getValue(GeneratedLocalFlags.darkMode).value - * ``` - * - * **Local non-Boolean flags** get a `get…()` extension returning the raw value type: - * ```kotlin - * fun ConfigValues.getMaxRetries(): Int = getValue(GeneratedLocalFlags.maxRetries).value - * ``` - * - * **Remote flags** get a `get…()` extension returning `ConfigValue` so callers can - * inspect the value source (DEFAULT / REMOTE / etc.): - * ```kotlin - * fun ConfigValues.getPromoBannerEnabled(): ConfigValue = - * getValue(GeneratedRemoteFlags.promoBannerEnabled) - * ``` - * - * The file uses `@file:JvmName(...)` with a module-derived suffix to guarantee unique - * JVM class names when multiple modules apply the plugin — required for accurate - * per-function ProGuard `-assumevalues` rules. - */ -public object ExtensionFunctionGenerator { - private const val PACKAGE = "dev.androidbroadcast.featured.generated" - private const val CONFIG_VALUES_IMPORT = "dev.androidbroadcast.featured.ConfigValues" - private const val CONFIG_VALUE_IMPORT = "dev.androidbroadcast.featured.ConfigValue" - - /** - * Returns the `@file:JvmName` value for the given Gradle module path. - * - * Examples: `":app"` → `"FeaturedApp_FlagExtensionsKt"`, - * `":feature:checkout"` → `"FeaturedFeatureCheckout_FlagExtensionsKt"`. - */ - public fun jvmFileName(modulePath: String): String = "Featured${modulePath.modulePathToIdentifier()}_FlagExtensionsKt" - - /** - * Generates the full source text for `GeneratedFlagExtensions.kt`. - * - * Returns an empty string if [entries] is empty. - */ - public fun generate( - entries: List, - modulePath: String, - ): String { - if (entries.isEmpty()) return "" - val jvmName = jvmFileName(modulePath) - val needsConfigValue = entries.any { !it.isLocal } - - return buildString { - appendLine("// Auto-generated by Featured Gradle Plugin — do not edit manually.") - appendLine("@file:JvmName(\"$jvmName\")") - appendLine() - appendLine("package $PACKAGE") - appendLine() - appendLine("import $CONFIG_VALUES_IMPORT") - if (needsConfigValue) appendLine("import $CONFIG_VALUE_IMPORT") - val (localEntries, remoteEntries) = entries.partition { it.isLocal } - if (localEntries.isNotEmpty()) { - appendLine("import $PACKAGE.${LocalFlagEntry.GENERATED_LOCAL_OBJECT}") - } - if (remoteEntries.isNotEmpty()) { - appendLine("import $PACKAGE.${LocalFlagEntry.GENERATED_REMOTE_OBJECT}") - } - appendLine() - entries.forEach { entry -> - appendLine(entry.toExtensionFunction()) - } - }.trimEnd() + "\n" - } - - private fun LocalFlagEntry.toExtensionFunction(): String { - val objectRef = if (isLocal) LocalFlagEntry.GENERATED_LOCAL_OBJECT else LocalFlagEntry.GENERATED_REMOTE_OBJECT - return if (isLocal) { - val funcName = extensionFunctionName() - "public fun ConfigValues.$funcName(): $type = getValue($objectRef.$propertyName).value\n" - } else { - // Remote flags always use get… regardless of type — the return is ConfigValue, - // so callers can inspect the value source. - val funcName = "get${propertyName.capitalized()}" - "public fun ConfigValues.$funcName(): ConfigValue<$type> = getValue($objectRef.$propertyName)\n" - } - } -} diff --git a/sample/CLAUDE.md b/sample/CLAUDE.md new file mode 100644 index 0000000..cc554da --- /dev/null +++ b/sample/CLAUDE.md @@ -0,0 +1,29 @@ +# Sample CLAUDE.md + +The sample is intentionally a multi-module demonstration of the Featured plugin family. + +## Module map + +- `:sample:feature-checkout` — owns `CheckoutVariant` enum + 2 local flags (`new_checkout` Boolean, `checkout_variant` enum). +- `:sample:feature-promotions` — 1 remote flag (`promo_banner_enabled` Boolean). +- `:sample:feature-ui` — 2 local UI flags (`main_button_red` Boolean, `new_feature_section_enabled` Boolean). +- `:sample:shared` — pure aggregator (`dev.androidbroadcast.featured.application`). Contains Compose UI (`FeaturedSample`, `SampleApp`) and `SampleViewModel`. No flag declarations of its own. +- `:sample:android-app` — Activity shell; wires `DataStoreConfigValueProvider` + `FeatureFlagsDebugScreen`. +- `:sample:desktop` — JVM shell; uses `InMemoryConfigValueProvider`. +- `iosApp/` — Xcode project consuming `FeaturedSampleApp.framework` (static, produced by `:sample:shared`). + +## Observe-bridge convention + +Each `:sample:feature-*` module ships `*FlagObservers.kt` with public `ConfigValues` extensions +(e.g. `mainButtonRedFlow()`, `setMainButtonRed()`). UI consumers should call these instead of +referencing `GeneratedLocalFlags*` / `GeneratedRemoteFlags*` directly. + +## Adding a flag + +1. Edit the feature module's `build.gradle.kts` — add a declaration inside `featured { localFlags { ... } }` or `featured { remoteFlags { ... } }`. +2. Add a public observer / setter in `*FlagObservers.kt`. +3. If the UI needs it, expose a `StateFlow` + setter in `SampleViewModel`. + +## Aggregation + +`:sample:shared` declares `featuredAggregation(project(":sample:feature-*"))` for all three modules and wires the `generateFeaturedRegistry` task output into `commonMain`. The resulting `GeneratedFeaturedRegistry.all` is passed to `FeatureFlagsDebugScreen`. diff --git a/sample/android-app/src/main/kotlin/dev/androidbroadcast/featured/sample/MainActivity.kt b/sample/android-app/src/main/kotlin/dev/androidbroadcast/featured/sample/MainActivity.kt index 817588d..66e6728 100644 --- a/sample/android-app/src/main/kotlin/dev/androidbroadcast/featured/sample/MainActivity.kt +++ b/sample/android-app/src/main/kotlin/dev/androidbroadcast/featured/sample/MainActivity.kt @@ -9,15 +9,15 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import dev.androidbroadcast.featured.CheckoutVariant import dev.androidbroadcast.featured.ConfigValues import dev.androidbroadcast.featured.SampleApp -import dev.androidbroadcast.featured.SampleFeatureFlags import dev.androidbroadcast.featured.datastore.DataStoreConfigValueProvider import dev.androidbroadcast.featured.datastore.registerConverter import dev.androidbroadcast.featured.debugui.FeatureFlagsDebugScreen import dev.androidbroadcast.featured.enumConverter +import dev.androidbroadcast.featured.generated.GeneratedFeaturedRegistry import dev.androidbroadcast.featured.platform.defaultLocalProvider +import dev.androidbroadcast.featured.sample.checkout.CheckoutVariant class MainActivity : ComponentActivity() { // ConfigValues is held at Activity scope for this sample. @@ -40,7 +40,7 @@ class MainActivity : ComponentActivity() { if (showDebug) { BackHandler { showDebug = false } - FeatureFlagsDebugScreen(configValues = configValues, registry = SampleFeatureFlags.all) + FeatureFlagsDebugScreen(configValues = configValues, registry = GeneratedFeaturedRegistry.all) } else { SampleApp( configValues = configValues, diff --git a/sample/feature-checkout/build.gradle.kts b/sample/feature-checkout/build.gradle.kts new file mode 100644 index 0000000..95a9bcc --- /dev/null +++ b/sample/feature-checkout/build.gradle.kts @@ -0,0 +1,60 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidKmpLibrary) + id("dev.androidbroadcast.featured") +} + +kotlin { + jvmToolchain(21) + + android { + namespace = "dev.androidbroadcast.featured.sample.checkout" + compileSdk = + libs.versions.android.compileSdk + .get() + .toInt() + minSdk = + libs.versions.android.minSdk + .get() + .toInt() + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + jvm() + + sourceSets { + commonMain.dependencies { + api(project(":core")) + api(libs.kotlinx.coroutines.core) + } + } + + sourceSets.commonMain.get().kotlin.srcDir( + tasks.named("generateConfigParam").map { it.outputs.files.singleFile }, + ) +} + +featured { + localFlags { + boolean("new_checkout", default = false) { + description = "Enable the redesigned checkout flow" + category = "checkout" + } + enum( + key = "checkout_variant", + typeFqn = "dev.androidbroadcast.featured.sample.checkout.CheckoutVariant", + default = "LEGACY", + ) { + description = "Controls which checkout flow variant is shown to the user" + category = "checkout" + } + } +} diff --git a/sample/feature-checkout/src/androidMain/AndroidManifest.xml b/sample/feature-checkout/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..b2d3ea1 --- /dev/null +++ b/sample/feature-checkout/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sample/feature-checkout/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/checkout/CheckoutFlagObservers.kt b/sample/feature-checkout/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/checkout/CheckoutFlagObservers.kt new file mode 100644 index 0000000..5c92ebe --- /dev/null +++ b/sample/feature-checkout/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/checkout/CheckoutFlagObservers.kt @@ -0,0 +1,17 @@ +package dev.androidbroadcast.featured.sample.checkout + +import dev.androidbroadcast.featured.ConfigValues +import dev.androidbroadcast.featured.generated.GeneratedLocalFlagsSampleFeatureCheckout +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +public fun ConfigValues.newCheckoutFlow(): Flow = observe(GeneratedLocalFlagsSampleFeatureCheckout.newCheckout).map { it.value } + +public fun ConfigValues.checkoutVariantFlow(): Flow = + observe(GeneratedLocalFlagsSampleFeatureCheckout.checkoutVariant).map { + it.value + } + +public suspend fun ConfigValues.setNewCheckout(value: Boolean) { + override(GeneratedLocalFlagsSampleFeatureCheckout.newCheckout, value) +} diff --git a/sample/feature-checkout/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/checkout/CheckoutVariant.kt b/sample/feature-checkout/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/checkout/CheckoutVariant.kt new file mode 100644 index 0000000..82fb01e --- /dev/null +++ b/sample/feature-checkout/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/checkout/CheckoutVariant.kt @@ -0,0 +1,7 @@ +package dev.androidbroadcast.featured.sample.checkout + +public enum class CheckoutVariant { + LEGACY, + NEW_SINGLE_PAGE, + NEW_MULTI_STEP, +} diff --git a/sample/feature-promotions/build.gradle.kts b/sample/feature-promotions/build.gradle.kts new file mode 100644 index 0000000..fc23e29 --- /dev/null +++ b/sample/feature-promotions/build.gradle.kts @@ -0,0 +1,52 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidKmpLibrary) + id("dev.androidbroadcast.featured") +} + +kotlin { + jvmToolchain(21) + + android { + namespace = "dev.androidbroadcast.featured.sample.promotions" + compileSdk = + libs.versions.android.compileSdk + .get() + .toInt() + minSdk = + libs.versions.android.minSdk + .get() + .toInt() + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + jvm() + + sourceSets { + commonMain.dependencies { + api(project(":core")) + api(libs.kotlinx.coroutines.core) + } + } + + sourceSets.commonMain.get().kotlin.srcDir( + tasks.named("generateConfigParam").map { it.outputs.files.singleFile }, + ) +} + +featured { + remoteFlags { + boolean("promo_banner_enabled", default = false) { + description = "Show the promotional banner on the main screen" + category = "promotions" + } + } +} diff --git a/sample/feature-promotions/src/androidMain/AndroidManifest.xml b/sample/feature-promotions/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..b2d3ea1 --- /dev/null +++ b/sample/feature-promotions/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sample/feature-promotions/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/promotions/PromotionsFlagObservers.kt b/sample/feature-promotions/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/promotions/PromotionsFlagObservers.kt new file mode 100644 index 0000000..093c677 --- /dev/null +++ b/sample/feature-promotions/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/promotions/PromotionsFlagObservers.kt @@ -0,0 +1,15 @@ +package dev.androidbroadcast.featured.sample.promotions + +import dev.androidbroadcast.featured.ConfigValues +import dev.androidbroadcast.featured.generated.GeneratedRemoteFlagsSampleFeaturePromotions +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +public fun ConfigValues.promoBannerEnabledFlow(): Flow = + observe(GeneratedRemoteFlagsSampleFeaturePromotions.promoBannerEnabled).map { + it.value + } + +public suspend fun ConfigValues.setPromoBannerEnabled(value: Boolean) { + override(GeneratedRemoteFlagsSampleFeaturePromotions.promoBannerEnabled, value) +} diff --git a/sample/feature-ui/build.gradle.kts b/sample/feature-ui/build.gradle.kts new file mode 100644 index 0000000..1bbbe9f --- /dev/null +++ b/sample/feature-ui/build.gradle.kts @@ -0,0 +1,56 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidKmpLibrary) + id("dev.androidbroadcast.featured") +} + +kotlin { + jvmToolchain(21) + + android { + namespace = "dev.androidbroadcast.featured.sample.ui" + compileSdk = + libs.versions.android.compileSdk + .get() + .toInt() + minSdk = + libs.versions.android.minSdk + .get() + .toInt() + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + jvm() + + sourceSets { + commonMain.dependencies { + api(project(":core")) + api(libs.kotlinx.coroutines.core) + } + } + + sourceSets.commonMain.get().kotlin.srcDir( + tasks.named("generateConfigParam").map { it.outputs.files.singleFile }, + ) +} + +featured { + localFlags { + boolean("main_button_red", default = true) { + description = "Tint the main button red (otherwise blue)" + category = "ui" + } + boolean("new_feature_section_enabled", default = true) { + description = "Show the new-feature section on the main screen" + category = "ui" + } + } +} diff --git a/sample/feature-ui/src/androidMain/AndroidManifest.xml b/sample/feature-ui/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..b2d3ea1 --- /dev/null +++ b/sample/feature-ui/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sample/feature-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/ui/UiFlagObservers.kt b/sample/feature-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/ui/UiFlagObservers.kt new file mode 100644 index 0000000..e2a389d --- /dev/null +++ b/sample/feature-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/ui/UiFlagObservers.kt @@ -0,0 +1,19 @@ +package dev.androidbroadcast.featured.sample.ui + +import dev.androidbroadcast.featured.ConfigValues +import dev.androidbroadcast.featured.generated.GeneratedLocalFlagsSampleFeatureUi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +public fun ConfigValues.mainButtonRedFlow(): Flow = observe(GeneratedLocalFlagsSampleFeatureUi.mainButtonRed).map { it.value } + +public suspend fun ConfigValues.setMainButtonRed(value: Boolean) { + override(GeneratedLocalFlagsSampleFeatureUi.mainButtonRed, value) +} + +public fun ConfigValues.newFeatureSectionEnabledFlow(): Flow = + observe(GeneratedLocalFlagsSampleFeatureUi.newFeatureSectionEnabled).map { it.value } + +public suspend fun ConfigValues.setNewFeatureSectionEnabled(value: Boolean) { + override(GeneratedLocalFlagsSampleFeatureUi.newFeatureSectionEnabled, value) +} diff --git a/sample/shared/build.gradle.kts b/sample/shared/build.gradle.kts index d2867b2..4e8f030 100644 --- a/sample/shared/build.gradle.kts +++ b/sample/shared/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) alias(libs.plugins.skie) + id("dev.androidbroadcast.featured.application") } kotlin { @@ -34,6 +35,9 @@ kotlin { iosTarget.binaries.framework { baseName = "FeaturedSampleApp" isStatic = true + export(project(":sample:feature-checkout")) + export(project(":sample:feature-promotions")) + export(project(":sample:feature-ui")) } } @@ -54,6 +58,22 @@ kotlin { // the public signatures of SampleApp / SampleViewModel — must be api to compile // downstream consumers like :sample:desktop. Pre-existing leak from #182. api(project(":core")) + + // CheckoutVariant appears in StateFlow in SampleViewModel public API; + // observe-bridge extensions are called from :sample:android-app / :sample:desktop. + api(project(":sample:feature-checkout")) + api(project(":sample:feature-promotions")) + api(project(":sample:feature-ui")) } } + + sourceSets.commonMain.get().kotlin.srcDir( + tasks.named("generateFeaturedRegistry").map { it.outputs.files.singleFile.parentFile }, + ) +} + +dependencies { + featuredAggregation(project(":sample:feature-checkout")) + featuredAggregation(project(":sample:feature-promotions")) + featuredAggregation(project(":sample:feature-ui")) } diff --git a/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/FeaturedSample.kt b/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/FeaturedSample.kt index d54e52d..e9cb530 100644 --- a/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/FeaturedSample.kt +++ b/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/FeaturedSample.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import dev.androidbroadcast.featured.sample.checkout.CheckoutVariant /** * Main sample screen demonstrating `@LocalFlag` and `@RemoteFlag` usage end-to-end. diff --git a/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleFeatureFlags.kt b/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleFeatureFlags.kt deleted file mode 100644 index 757a3bd..0000000 --- a/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleFeatureFlags.kt +++ /dev/null @@ -1,94 +0,0 @@ -package dev.androidbroadcast.featured - -/** - * Checkout flow variant used to demonstrate multivariate (enum) feature flags. - */ -public enum class CheckoutVariant { - /** The original multi-screen checkout flow. */ - LEGACY, - - /** New single-page checkout (A/B experiment arm A). */ - NEW_SINGLE_PAGE, - - /** New multi-step checkout with progress indicator (A/B experiment arm B). */ - NEW_MULTI_STEP, -} - -/** - * Feature flags for the sample app. - * - * In a real consumer project, Boolean/Int/String flags would be declared in - * `build.gradle.kts` using the Featured Gradle DSL, and the plugin would generate - * typed `ConfigParam` objects and `ConfigValues` extension functions automatically: - * - * ```kotlin - * // build.gradle.kts - * featured { - * localFlags { - * boolean("main_button_red", default = true) { category = "ui" } - * boolean("new_feature_section_enabled", default = true) { category = "ui" } - * } - * remoteFlags { - * boolean("promo_banner_enabled", default = false) { - * description = "Show promotional banner" - * } - * } - * } - * ``` - * - * Enum-typed flags (like [checkoutVariant]) are declared manually as `ConfigParam` - * until enum support is added to the DSL. - * - * The sample module is part of the library's own build and cannot apply the plugin - * to itself, so all flags are declared manually here for demonstration purposes. - * [SampleFeatureFlags.all] is the single source of truth for the Debug UI registry. - */ -public object SampleFeatureFlags { - public val mainButtonRed: ConfigParam = - ConfigParam( - key = "main_button_red", - defaultValue = true, - description = "Enable red color for the main button", - category = "ui", - ) - - public val newFeatureSectionEnabled: ConfigParam = - ConfigParam( - key = "new_feature_section_enabled", - defaultValue = true, - description = "Show the new feature section in the main screen", - category = "ui", - ) - - public val newCheckout: ConfigParam = - ConfigParam( - key = "new_checkout", - defaultValue = false, - description = "Enable the redesigned checkout flow", - ) - - public val promoBannerEnabled: ConfigParam = - ConfigParam( - key = "promo_banner_enabled", - defaultValue = false, - description = "Show a promotional banner on the main screen (remote-controlled)", - category = "promotions", - ) - - public val checkoutVariant: ConfigParam = - ConfigParam( - key = "checkout_variant", - defaultValue = CheckoutVariant.LEGACY, - description = "Controls which checkout flow variant is shown to the user", - category = "checkout", - ) - - public val all: List> = - listOf( - mainButtonRed, - newFeatureSectionEnabled, - newCheckout, - promoBannerEnabled, - checkoutVariant, - ) -} diff --git a/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleViewModel.kt b/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleViewModel.kt index 420da49..5b41d1d 100644 --- a/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleViewModel.kt +++ b/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleViewModel.kt @@ -2,6 +2,16 @@ package dev.androidbroadcast.featured import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dev.androidbroadcast.featured.sample.checkout.CheckoutVariant +import dev.androidbroadcast.featured.sample.checkout.checkoutVariantFlow +import dev.androidbroadcast.featured.sample.checkout.newCheckoutFlow +import dev.androidbroadcast.featured.sample.checkout.setNewCheckout +import dev.androidbroadcast.featured.sample.promotions.promoBannerEnabledFlow +import dev.androidbroadcast.featured.sample.promotions.setPromoBannerEnabled +import dev.androidbroadcast.featured.sample.ui.mainButtonRedFlow +import dev.androidbroadcast.featured.sample.ui.newFeatureSectionEnabledFlow +import dev.androidbroadcast.featured.sample.ui.setMainButtonRed +import dev.androidbroadcast.featured.sample.ui.setNewFeatureSectionEnabled import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map @@ -12,7 +22,10 @@ public class SampleViewModel( private val configValues: ConfigValues, ) : ViewModel() { public val flagActive: StateFlow = - configValues.asStateFlow(SampleFeatureFlags.mainButtonRed, viewModelScope) + configValues + .mainButtonRedFlow() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), true) + // matches default declared in :sample:feature-ui build.gradle.kts public val mainButtonColor: StateFlow = flagActive @@ -25,31 +38,44 @@ public class SampleViewModel( ) public fun setMainButtonColorFlag(value: Boolean) { - viewModelScope.launch { - configValues.override(SampleFeatureFlags.mainButtonRed, value) - } + viewModelScope.launch { configValues.setMainButtonRed(value) } } - /** - * Controls visibility of the "New Feature" section. - * Demonstrates the [ConfigParam.isEnabled] guard pattern for entry-point gating. - */ public val newFeatureSectionEnabled: StateFlow = - configValues.asStateFlow(SampleFeatureFlags.newFeatureSectionEnabled, viewModelScope) + configValues + .newFeatureSectionEnabledFlow() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), true) + // matches default declared in :sample:feature-ui build.gradle.kts - /** - * Whether the promotional banner should be shown. - * In production this would be driven by Firebase Remote Config. - */ public val promoBannerEnabled: StateFlow = - configValues.asStateFlow(SampleFeatureFlags.promoBannerEnabled, viewModelScope) + configValues + .promoBannerEnabledFlow() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), false) + // matches default declared in :sample:feature-promotions build.gradle.kts + + public val newCheckout: StateFlow = + configValues + .newCheckoutFlow() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), false) + // matches default declared in :sample:feature-checkout build.gradle.kts - /** - * The active checkout variant, driven remotely. - * Demonstrates multivariate enum flags resolved from a remote provider. - */ public val checkoutVariant: StateFlow = - configValues.asStateFlow(SampleFeatureFlags.checkoutVariant, viewModelScope) + configValues + .checkoutVariantFlow() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), CheckoutVariant.LEGACY) + // matches default declared in :sample:feature-checkout build.gradle.kts + + public fun setNewFeatureSectionEnabled(value: Boolean) { + viewModelScope.launch { configValues.setNewFeatureSectionEnabled(value) } + } + + public fun setPromoBannerEnabled(value: Boolean) { + viewModelScope.launch { configValues.setPromoBannerEnabled(value) } + } + + public fun setNewCheckout(value: Boolean) { + viewModelScope.launch { configValues.setNewCheckout(value) } + } public sealed interface MainButtonColor { public data object Red : MainButtonColor diff --git a/settings.gradle.kts b/settings.gradle.kts index 1b45cb1..0ab3b91 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,7 @@ rootProject.name = "Featured" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { + includeBuild("build-logic") @Suppress("UnstableApiUsage") repositories { @@ -36,8 +37,10 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } -include(":featured-gradle-plugin") include(":sample:shared") +include(":sample:feature-checkout") +include(":sample:feature-promotions") +include(":sample:feature-ui") include(":sample:android-app") include(":sample:desktop") include(":core")