Repository: MiSikora/laboratory
Pull request #313 (2024-05-21 13:09:20)

Add support for binary feature flags
## :bulb: Motivation

I want to simplify the most common use case when creating feature flags.

## :pencil: Checklist
<!-- Please make sure to go through the checklist and select check-boxes appropriate for your changes. -->
- [x] I updated the [changelog](https://github.com/MiSikora/laboratory/blob/trunk/docs/changelog.md).
- [x] I updated the [documentation](https://github.com/MiSikora/laboratory/tree/trunk/docs).
- [x] I updated the [samples](https://github.com/MiSikora/laboratory/tree/trunk/samples).



Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 91ce2a8..b7d4d36 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 - Gradle tasks are now cacheable.
 - Support for typesafe project accessors when declaring dependencies.
+- Shorthand for creating binary flags.
+    ```groovy
+    laboratory {
+      // Generates 'Feature' with two options - 'Enabled and 'Disabled'. 'Enabled' is the default one.
+      enabledFeature("Feature")
+      // Generates 'Feature' with two options - 'Enabled and 'Disabled'. 'Disabled' is the default one.
+      disabledFeature("Feature")
+    }
+    ```
 
 ### Changed
 - Gradle tasks are now always registered when plugin is applied in a project. If there is nothing to generate tasks will clear their respective output directories. 
diff --git a/docs/gradle-plugin.md b/docs/gradle-plugin.md
index fca6752..3b974a7 100644
--- a/docs/gradle-plugin.md
+++ b/docs/gradle-plugin.md
@@ -596,6 +596,30 @@ laboratory {
     }
   }
 
+  // Informs plugin to create 'enum class SomeFeature' during the generation period with two options.
+  // 'Enabled' and 'Disabled' and uses 'Enabled' as the default one.
+  enabledFeature("SomeFeature") {
+    // Uses the same options as feature() block except for `withOption()` and `withDefaultOption()`.
+    
+    withEnabled { option ->
+      option.feature("SupervisedFeature") {
+        // recursive feature generation
+      }
+    }
+  }
+
+  // Informs plugin to create 'enum class SomeFeature' during the generation period with two options.
+  // 'Enabled' and 'Disabled' and uses 'Disabled' as the default one.
+  disabled("SomeFeature") {
+    // Uses the same options as feature() block except for `withOption()` and `withDefaultOption()`.
+
+    withEnabled { option ->
+      option.feature("SupervisedFeature") {
+        // recursive feature generation
+      }
+    }
+  }
+
   // Configures feature flags storage. Useful when feature flags have multiple sources.
   sourcedStorage {
     // Overrides globally declared namespace. No value by default.
diff --git a/laboratory/gradle-plugin/api/gradle-plugin.api b/laboratory/gradle-plugin/api/gradle-plugin.api
index 688f2f5..a91f62f 100644
--- a/laboratory/gradle-plugin/api/gradle-plugin.api
+++ b/laboratory/gradle-plugin/api/gradle-plugin.api
@@ -1,4 +1,10 @@
 public final class io/mehow/laboratory/gradle/ChildFeatureFlagsInput : java/io/Serializable {
+	public final fun disabledFeature (Ljava/lang/String;)V
+	public final fun disabledFeature (Ljava/lang/String;Lorg/gradle/api/Action;)V
+	public static synthetic fun disabledFeature$default (Lio/mehow/laboratory/gradle/ChildFeatureFlagsInput;Ljava/lang/String;Lorg/gradle/api/Action;ILjava/lang/Object;)V
+	public final fun enabledFeature (Ljava/lang/String;)V
+	public final fun enabledFeature (Ljava/lang/String;Lorg/gradle/api/Action;)V
+	public static synthetic fun enabledFeature$default (Lio/mehow/laboratory/gradle/ChildFeatureFlagsInput;Ljava/lang/String;Lorg/gradle/api/Action;ILjava/lang/Object;)V
 	public final fun feature (Ljava/lang/String;Lorg/gradle/api/Action;)V
 }
 
@@ -18,30 +24,57 @@ public final class io/mehow/laboratory/gradle/FeatureFactoryInput : java/io/Seri
 	public final fun setPublic (Z)V
 }
 
-public final class io/mehow/laboratory/gradle/FeatureFlagInput : java/io/Serializable {
+public abstract class io/mehow/laboratory/gradle/FeatureFlagInput : java/io/Serializable {
+	public synthetic fun <init> (Ljava/lang/String;Lio/mehow/laboratory/gradle/PackageNameProvider;Lio/mehow/laboratory/gradle/SupervisorInput;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public final fun deprecated (Ljava/lang/String;)V
 	public final fun deprecated (Ljava/lang/String;Lio/mehow/laboratory/gradle/DeprecationLevel;)V
 	public static synthetic fun deprecated$default (Lio/mehow/laboratory/gradle/FeatureFlagInput;Ljava/lang/String;Lio/mehow/laboratory/gradle/DeprecationLevel;ILjava/lang/Object;)V
-	public final fun getDescription ()Ljava/lang/String;
-	public final fun getKey ()Ljava/lang/String;
+	public abstract fun getDescription ()Ljava/lang/String;
+	public abstract fun getKey ()Ljava/lang/String;
 	public final fun getPackageName ()Ljava/lang/String;
-	public final fun isPublic ()Z
-	public final fun setDescription (Ljava/lang/String;)V
-	public final fun setKey (Ljava/lang/String;)V
+	public abstract fun isPublic ()Z
+	public abstract fun setDescription (Ljava/lang/String;)V
+	public abstract fun setKey (Ljava/lang/String;)V
 	public final fun setPackageName (Ljava/lang/String;)V
-	public final fun setPublic (Z)V
+	public abstract fun setPublic (Z)V
+	public final fun withDefaultSource (Ljava/lang/String;)V
+	public final fun withSource (Ljava/lang/String;)V
+}
+
+public final class io/mehow/laboratory/gradle/FeatureFlagInput$BinaryOption : io/mehow/laboratory/gradle/FeatureFlagInput {
+	public fun getDescription ()Ljava/lang/String;
+	public fun getKey ()Ljava/lang/String;
+	public fun isPublic ()Z
+	public fun setDescription (Ljava/lang/String;)V
+	public fun setKey (Ljava/lang/String;)V
+	public fun setPublic (Z)V
+	public final fun withDisabled (Lorg/gradle/api/Action;)V
+	public final fun withEnabled (Lorg/gradle/api/Action;)V
+}
+
+public final class io/mehow/laboratory/gradle/FeatureFlagInput$MultiOption : io/mehow/laboratory/gradle/FeatureFlagInput {
+	public fun getDescription ()Ljava/lang/String;
+	public fun getKey ()Ljava/lang/String;
+	public fun isPublic ()Z
+	public fun setDescription (Ljava/lang/String;)V
+	public fun setKey (Ljava/lang/String;)V
+	public fun setPublic (Z)V
 	public final fun withDefaultOption (Ljava/lang/String;)V
 	public final fun withDefaultOption (Ljava/lang/String;Lorg/gradle/api/Action;)V
-	public final fun withDefaultSource (Ljava/lang/String;)V
 	public final fun withOption (Ljava/lang/String;)V
 	public final fun withOption (Ljava/lang/String;Lorg/gradle/api/Action;)V
-	public final fun withSource (Ljava/lang/String;)V
 }
 
 public abstract class io/mehow/laboratory/gradle/LaboratoryExtension {
 	public fun <init> ()V
 	public final fun dependency (Lorg/gradle/api/Project;)V
 	public final fun dependency (Lorg/gradle/api/internal/catalog/DelegatingProjectDependency;)V
+	public final fun disabledFeature (Ljava/lang/String;)V
+	public final fun disabledFeature (Ljava/lang/String;Lorg/gradle/api/Action;)V
+	public static synthetic fun disabledFeature$default (Lio/mehow/laboratory/gradle/LaboratoryExtension;Ljava/lang/String;Lorg/gradle/api/Action;ILjava/lang/Object;)V
+	public final fun enabledFeature (Ljava/lang/String;)V
+	public final fun enabledFeature (Ljava/lang/String;Lorg/gradle/api/Action;)V
+	public static synthetic fun enabledFeature$default (Lio/mehow/laboratory/gradle/LaboratoryExtension;Ljava/lang/String;Lorg/gradle/api/Action;ILjava/lang/Object;)V
 	public final fun feature (Ljava/lang/String;Lorg/gradle/api/Action;)V
 	public final fun featureFactory ()V
 	public final fun featureFactory (Lorg/gradle/api/Action;)V
diff --git a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt
index cfe62d9..eff25db 100644
--- a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt
+++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt
@@ -11,24 +11,50 @@ public class ChildFeatureFlagsInput internal constructor(
   private val packageNameProvider: PackageNameProvider,
   private val supervisor: SupervisorInput,
 ) : Serializable {
-  private val mutableFeatureInputs = mutableListOf<FeatureFlagInput>()
-
-  private val featureInputs: List<FeatureFlagInput> = mutableFeatureInputs
+  private val featureInputs = mutableListOf<FeatureFlagInput>()
 
   /**
-   * Generates a new supervised feature flag.
+   * Generates a new supervised [multi-option][FeatureFlagInput.MultiOption] feature flag.
    */
   public fun feature(
     name: String,
-    action: Action<FeatureFlagInput>,
+    action: Action<FeatureFlagInput.MultiOption>,
+  ) {
+    featureInputs += FeatureFlagInput.MultiOption(name, packageNameProvider, supervisor).apply(action::execute)
+  }
+
+  /**
+   * Generates a new supervised [binary][FeatureFlagInput.BinaryOption] feature flag that is enabled by default.
+   */
+  @JvmOverloads
+  public fun enabledFeature(
+    name: String,
+    action: Action<FeatureFlagInput.BinaryOption> = Action { },
+  ) {
+    binaryFeature(name, isEnabled = true, action)
+  }
+
+  /**
+   * Generates a new supervised [binary][FeatureFlagInput.BinaryOption] feature flag that is disabled by default.
+   */
+  @JvmOverloads
+  public fun disabledFeature(
+    name: String,
+    action: Action<FeatureFlagInput.BinaryOption> = Action { },
+  ) {
+    binaryFeature(name, isEnabled = false, action)
+  }
+
+  private fun binaryFeature(
+    name: String,
+    isEnabled: Boolean,
+    action: Action<FeatureFlagInput.BinaryOption>,
   ) {
-    mutableFeatureInputs += FeatureFlagInput(name, packageNameProvider, supervisor).let { input ->
-      action.execute(input)
-      return@let input
-    }
+    featureInputs += FeatureFlagInput.BinaryOption(name, isEnabled, packageNameProvider, supervisor)
+      .apply(action::execute)
   }
 
-  internal fun toModels(): List<FeatureFlagModel> = featureInputs.flatMap(FeatureFlagInput::toModels)
+  internal fun toModels(): List<FeatureFlagModel> = featureInputs.flatMap(FeatureFlagInput::toModelsWithChildren)
 
   internal companion object {
     private const val serialVersionUID = 0L
diff --git a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryTask.kt
index d80e4e7..184daa8 100644
--- a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryTask.kt
+++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryTask.kt
@@ -26,7 +26,7 @@ internal abstract class FeatureFactoryTask @Inject constructor(
   fun generateFeatureFactory() {
     outputDirectory.get().asFile.deleteRecursively()
     factory.orNull
-      ?.toModel(features.get().flatMap(FeatureFlagInput::toModels), "GeneratedFeatureFactory")
+      ?.toModel(features.get().flatMap(FeatureFlagInput::toModelsWithChildren), "GeneratedFeatureFactory")
       ?.prepare("featureGenerated")
       ?.writeTo(outputDirectory.get().asFile)
   }
diff --git a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagInput.kt
index 5999ab7..4f79ee2 100644
--- a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagInput.kt
+++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagInput.kt
@@ -7,9 +7,10 @@ import org.gradle.api.Action
 import java.io.Serializable
 
 /**
- * Representation of a generated feature flag. It must have at least one value and exactly one default value.
+ * Representation of a feature flag with multiple options.
+ * Can be either [MultiOption] or [BinaryOption].
  */
-public class FeatureFlagInput internal constructor(
+public sealed class FeatureFlagInput(
   private val name: String,
   packageNameProvider: PackageNameProvider,
   private val supervisor: SupervisorInput?,
@@ -17,7 +18,7 @@ public class FeatureFlagInput internal constructor(
   /**
    * Sets whether the generated feature flag should be public or internal.
    */
-  public var isPublic: Boolean = true
+  public abstract var isPublic: Boolean
 
   /**
    * Sets package name of the generated feature flag. Overwrites any previously set values.
@@ -31,54 +32,26 @@ public class FeatureFlagInput internal constructor(
   /**
    * Sets description of the generated feature flag.
    */
-  public var description: String? = null
+  public abstract var description: String?
 
   /**
    * Sets a custom key that will be used for generated option factory.
    */
-  public var key: String? = null
+  public abstract var key: String?
 
-  private val options = mutableListOf<FeatureFlagOptionInput>()
+  private val options = mutableMapOf<String, FeatureFlagOptionInput>()
 
-  private val childFeatureInputs = mutableListOf<ChildFeatureFlagsInput>()
+  private val childFeatureInputs = mutableMapOf<String, ChildFeatureFlagsInput>()
 
-  /**
-   * Adds a feature option.
-   */
-  public fun withOption(name: String): Unit = withOption(name, action = {})
-
-  /**
-   * Adds a feature option and configures features flags supervised by it.
-   */
-  public fun withOption(
-    name: String,
-    action: Action<ChildFeatureFlagsInput>,
-  ): Unit = withOption(name, isDefault = false, action = action)
-
-  /**
-   * Adds a feature value that will be used as a default value.
-   * Exactly one value must be set with this method.
-   */
-  public fun withDefaultOption(name: String): Unit = withDefaultOption(name, action = {})
-
-  /**
-   * Adds a feature value that will be used as a default value and configures features flags supervised by it.
-   * Exactly one value must be set with this method.
-   */
-  public fun withDefaultOption(
-    name: String,
-    action: Action<ChildFeatureFlagsInput>,
-  ): Unit = withOption(name, isDefault = true, action = action)
-
-  private fun withOption(
+  internal fun withOption(
     name: String,
     isDefault: Boolean,
     action: Action<ChildFeatureFlagsInput>,
   ) {
     val option = FeatureFlagOptionInput(name, isDefault)
     val supervisor = SupervisorInput(this, option)
-    childFeatureInputs += ChildFeatureFlagsInput(packageNameProvider, supervisor).apply(action::execute)
-    options += option
+    childFeatureInputs += name to ChildFeatureFlagsInput(packageNameProvider, supervisor).apply(action::execute)
+    options += name to option
   }
 
   private val sources = mutableListOf<FeatureFlagOptionInput>()
@@ -109,17 +82,18 @@ public class FeatureFlagInput internal constructor(
   /**
    * Annotates a feature flag as deprecated.
    */
-  @JvmOverloads public fun deprecated(
+  @JvmOverloads
+  public fun deprecated(
     message: String,
     level: DeprecationLevel = DeprecationLevel.Warning,
   ) {
     deprecation = DeprecationInput(message, level.kotlinLevel)
   }
 
-  internal fun toModel() = FeatureFlagModel(
+  internal fun toModel(): FeatureFlagModel = FeatureFlagModel(
     visibility = if (isPublic) Visibility.Public else Visibility.Internal,
-    className = ClassName(packageNameProvider.value.orEmpty(), name),
-    options = options.map(FeatureFlagOptionInput::toModel),
+    className = ClassName(packageName.orEmpty(), name),
+    options = options.values.map(FeatureFlagOptionInput::toModel),
     sourceOptions = sources.map(FeatureFlagOptionInput::toModel),
     key = key,
     description = description.orEmpty(),
@@ -127,7 +101,101 @@ public class FeatureFlagInput internal constructor(
     supervisor = supervisor?.toModel(),
   )
 
-  internal fun toModels() = listOf(toModel()) + childFeatureInputs.flatMap(ChildFeatureFlagsInput::toModels)
+  internal fun toModelsWithChildren() = buildList {
+    add(toModel())
+    addAll(childFeatureInputs.values.flatMap(ChildFeatureFlagsInput::toModels))
+  }
+
+  /**
+   * Representation of a feature flag with multiple options.
+   * It must have at least one option and exactly one default option.
+   */
+  public class MultiOption internal constructor(
+    name: String,
+    packageNameProvider: PackageNameProvider,
+    supervisor: SupervisorInput?,
+  ) : FeatureFlagInput(name, packageNameProvider, supervisor) {
+    override var isPublic: Boolean = true
+
+    override var description: String? = null
+
+    override var key: String? = null
+
+    /**
+     * Adds a feature option.
+     */
+    public fun withOption(name: String): Unit = withOption(name, action = {})
+
+    /**
+     * Adds a feature option and configures features flags supervised by it.
+     */
+    public fun withOption(
+      name: String,
+      action: Action<ChildFeatureFlagsInput>,
+    ): Unit = withOption(name, isDefault = false, action = action)
+
+    /**
+     * Adds a feature value that will be used as a default value.
+     * Exactly one value must be set with this method.
+     */
+    public fun withDefaultOption(name: String): Unit = withDefaultOption(name, action = {})
+
+    /**
+     * Adds a feature value that will be used as a default value and configures features flags supervised by it.
+     * Exactly one value must be set with this method.
+     */
+    public fun withDefaultOption(
+      name: String,
+      action: Action<ChildFeatureFlagsInput>,
+    ): Unit = withOption(name, isDefault = true, action = action)
+
+    internal companion object {
+      private const val serialVersionUID = 0L
+    }
+  }
+
+  /**
+   * Representation of a feature flag with only two options - "Enabled" and "Disabled".
+   */
+  public class BinaryOption internal constructor(
+    name: String,
+    private val isEnabled: Boolean,
+    packageNameProvider: PackageNameProvider,
+    supervisor: SupervisorInput?,
+  ) : FeatureFlagInput(name, packageNameProvider, supervisor) {
+    override var isPublic: Boolean = true
+
+    override var description: String? = null
+
+    override var key: String? = null
+
+    init {
+      withOption("Enabled", isDefault = isEnabled, action = {})
+      withOption("Disabled", isDefault = !isEnabled, action = {})
+    }
+
+    /**
+     * Configures "Enabled" option.
+     */
+    public fun withEnabled(
+      action: Action<ChildFeatureFlagsInput>,
+    ) {
+      withOption("Enabled", isDefault = isEnabled, action)
+    }
+
+    /**
+     * Configures "Disabled" option.
+     */
+    public fun withDisabled(
+      action: Action<ChildFeatureFlagsInput>,
+    ) {
+      withOption("Disabled", isDefault = !isEnabled, action)
+    }
+
+    internal companion object {
+      private const val serialVersionUID = 0L
+    }
+  }
 
   internal companion object {
     private const val serialVersionUID = 0L
diff --git a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagsTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagsTask.kt
index e135a2d..0b66a42 100644
--- a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagsTask.kt
+++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagsTask.kt
@@ -22,7 +22,7 @@ internal abstract class FeatureFlagsTask @Inject constructor(
     outputDirectory.get().asFile.deleteRecursively()
     val outputPath = outputDirectory.get().asFile
     inputFlags.get()
-      .flatMap(FeatureFlagInput::toModels)
+      .flatMap(FeatureFlagInput::toModelsWithChildren)
       .forEach { model -> model.prepare().writeTo(outputPath) }
   }
 }
diff --git a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureSourceFactoryTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureSourceFactoryTask.kt
index 8275331..0d5589e 100644
--- a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureSourceFactoryTask.kt
+++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureSourceFactoryTask.kt
@@ -29,7 +29,7 @@ internal abstract class FeatureSourceFactoryTask @Inject constructor(
     factory.orNull
       ?.toModel(
         features.get()
-          .flatMap(FeatureFlagInput::toModels)
+          .flatMap(FeatureFlagInput::toModelsWithChildren)
           .mapNotNull(FeatureFlagModel::source),
         "GeneratedFeatureSourceFactory",
       )
diff --git a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryExtension.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryExtension.kt
index 311b787..94d1b03 100644
--- a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryExtension.kt
+++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryExtension.kt
@@ -30,11 +30,46 @@ public abstract class LaboratoryExtension {
 
   internal val featureFlags: List<FeatureFlagInput> = mutableFeatureInputs
 
+  /**
+   * Generates a new [multi-option][FeatureFlagInput.MultiOption] feature flag.
+   */
   public fun feature(
     name: String,
-    action: Action<FeatureFlagInput>,
+    action: Action<FeatureFlagInput.MultiOption>,
+  ) {
+    mutableFeatureInputs += FeatureFlagInput.MultiOption(name, packageNameProvider, supervisor = null)
+      .apply(action::execute)
+  }
+
+  /**
+   * Generates a new supervised [binary][FeatureFlagInput.BinaryOption] feature flag that is enabled by default.
+   */
+  @JvmOverloads
+  public fun enabledFeature(
+    name: String,
+    action: Action<FeatureFlagInput.BinaryOption> = Action { },
+  ) {
+    binaryFeature(name, isEnabled = true, action)
+  }
+
+  /**
+   * Generates a new supervised [binary][FeatureFlagInput.BinaryOption] feature flag that is disabled by default.
+   */
+  @JvmOverloads
+  public fun disabledFeature(
+    name: String,
+    action: Action<FeatureFlagInput.BinaryOption> = Action { },
   ) {
-    mutableFeatureInputs += FeatureFlagInput(name, packageNameProvider, supervisor = null).apply(action::execute)
+    binaryFeature(name, isEnabled = false, action)
+  }
+
+  private fun binaryFeature(
+    name: String,
+    isEnabled: Boolean,
+    action: Action<FeatureFlagInput.BinaryOption>,
+  ) {
+    mutableFeatureInputs += FeatureFlagInput.BinaryOption(name, isEnabled, packageNameProvider, supervisor = null)
+      .apply(action::execute)
   }
 
   internal var factoryInput: FeatureFactoryInput? = null
@@ -52,10 +87,7 @@ public abstract class LaboratoryExtension {
    * that needs to have information about all feature flags for QA purposes.
    */
   public fun featureFactory(action: Action<FeatureFactoryInput>) {
-    factoryInput = FeatureFactoryInput(packageNameProvider).let { input ->
-      action.execute(input)
-      return@let input
-    }
+    factoryInput = FeatureFactoryInput(packageNameProvider).apply(action::execute)
   }
 
   internal var storageInput: SourcedFeatureStorageInput? = null
@@ -75,10 +107,7 @@ public abstract class LaboratoryExtension {
    * Feature storage generated by this method should be then used in the application.
    */
   public fun sourcedStorage(action: Action<SourcedFeatureStorageInput>) {
-    storageInput = SourcedFeatureStorageInput(packageNameProvider).let { input ->
-      action.execute(input)
-      return@let input
-    }
+    storageInput = SourcedFeatureStorageInput(packageNameProvider).apply(action::execute)
   }
 
   internal var featureSourcesFactory: FeatureFactoryInput? = null
@@ -96,10 +125,7 @@ public abstract class LaboratoryExtension {
    * that needs to have information about all feature flags for QA purposes.
    */
   public fun featureSourceFactory(action: Action<FeatureFactoryInput>) {
-    featureSourcesFactory = FeatureFactoryInput(packageNameProvider).let { input ->
-      action.execute(input)
-      return@let input
-    }
+    featureSourcesFactory = FeatureFactoryInput(packageNameProvider).apply(action::execute)
   }
 
   internal var optionFactoryInput: OptionFactoryInput? = null
@@ -116,10 +142,7 @@ public abstract class LaboratoryExtension {
    * be visible to it during compilation.
    */
   public fun optionFactory(action: Action<OptionFactoryInput>) {
-    optionFactoryInput = OptionFactoryInput(packageNameProvider).let { input ->
-      action.execute(input)
-      return@let input
-    }
+    optionFactoryInput = OptionFactoryInput(packageNameProvider).apply(action::execute)
   }
 
   /**
diff --git a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryTask.kt
index 3b51b53..12f2937 100644
--- a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryTask.kt
+++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryTask.kt
@@ -26,7 +26,7 @@ internal abstract class OptionFactoryTask @Inject constructor(
   fun generateSourcedFeatureStorage() {
     outputDirectory.get().asFile.deleteRecursively()
     factory.orNull
-      ?.toModel(features.get().flatMap(FeatureFlagInput::toModels))
+      ?.toModel(features.get().flatMap(FeatureFlagInput::toModelsWithChildren))
       ?.prepare()
       ?.writeTo(outputDirectory.get().asFile)
   }
diff --git a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt
index 6c92779..676aa0d 100644
--- a/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt
+++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt
@@ -28,7 +28,7 @@ internal abstract class SourcedFeatureStorageTask @Inject constructor(
   fun generateSourcedFeatureStorage() {
     outputDirectory.get().asFile.deleteRecursively()
     storage.orNull
-      ?.toModel(features.get().flatMap(FeatureFlagInput::toModels).sourceNames().distinct())
+      ?.toModel(features.get().flatMap(FeatureFlagInput::toModelsWithChildren).sourceNames().distinct())
       ?.prepare()
       ?.writeTo(outputDirectory.get().asFile)
   }
diff --git a/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-enabled-generate-child/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-enabled-generate-child/settings.gradle
new file mode 100644
index 0000000..e69de29
diff --git a/samples/basic/build.gradle.kts b/samples/basic/build.gradle.kts
index 5b00958..4a83b79 100644
--- a/samples/basic/build.gradle.kts
+++ b/samples/basic/build.gradle.kts
@@ -25,11 +25,8 @@ laboratory {
     withOption("Error")
   }
 
-  feature("ReportRootedDevice") {
+  disabledFeature("ReportRootedDevice") {
     description = "Reports during [cold start](https://developer.android.com/topic/performance/vitals/launch-time#cold) whether device is rooted"
-
-    withDefaultOption("Disabled")
-    withOption("Enabled")
   }
 
   feature("Authentication") {
diff --git a/samples/default-option/build.gradle.kts b/samples/default-option/build.gradle.kts
index 59dc949..bbb6737 100644
--- a/samples/default-option/build.gradle.kts
+++ b/samples/default-option/build.gradle.kts
@@ -15,20 +15,9 @@ laboratory {
 
   featureFactory()
 
-  feature("ShowAds") {
-    withDefaultOption("Enabled")
-    withOption("Disabled")
-  }
-
-  feature("ReportRootedDevice") {
-    withDefaultOption("Enabled")
-    withOption("Disabled")
-  }
-
-  feature("RequiredFingerprint") {
-    withDefaultOption("Enabled")
-    withOption("Disabled")
-  }
+  enabledFeature("ShowAds")
+  enabledFeature("ReportRootedDevice")
+  enabledFeature("RequiredFingerprint")
 }
 
 dependencies {
diff --git a/samples/multi-module/build.gradle.kts b/samples/multi-module/build.gradle.kts
index 2cfae7e..e5ac325 100644
--- a/samples/multi-module/build.gradle.kts
+++ b/samples/multi-module/build.gradle.kts
@@ -16,7 +16,7 @@ laboratory {
   featureFactory()
 
   dependency(project(":multi-module:multi-module-a"))
-  dependency(project(":multi-module:multi-module-b"))
+  dependency(projects.multiModule.multiModuleB)
 }
 
 dependencies {
diff --git a/samples/multi-module/multi-module-b/build.gradle.kts b/samples/multi-module/multi-module-b/build.gradle.kts
index 2f9079f..f8b63f4 100644
--- a/samples/multi-module/multi-module-b/build.gradle.kts
+++ b/samples/multi-module/multi-module-b/build.gradle.kts
@@ -16,13 +16,6 @@ laboratory {
     withOption("Error")
   }
 
-  feature("ShowAds") {
-    withDefaultOption("Enabled")
-    withOption("Disabled")
-  }
-
-  feature("ReportRootedDevice") {
-    withDefaultOption("Enabled")
-    withOption("Disabled")
-  }
+  enabledFeature("ShowAds")
+  enabledFeature("ReportRootedDevice")
 }


Test patch:
diff --git a/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt
index 219f5e0..6d22e00 100644
--- a/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt
+++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt
@@ -507,6 +507,46 @@ class GenerateFeatureFlagsTaskSpec : FunSpec({
     """.trimMargin()
   }
 
+  test("generates enabled feature flag") {
+    val fixture = "feature-flag-generate-enabled".toFixture()
+
+    gradleRunner.withProjectDir(fixture).build()
+
+    val feature = fixture.featureFile("Feature")
+    feature.shouldExist()
+
+    feature.readText() shouldContain """
+      |enum class Feature : io.mehow.laboratory.Feature<Feature> {
+      |  Enabled,
+      |  Disabled,
+      |  ;
+      |
+      |  override val defaultOption: Feature
+      |    get() = Enabled
+      |}
+    """.trimMargin()
+  }
+
+  test("generates disabled feature flag") {
+    val fixture = "feature-flag-generate-disabled".toFixture()
+
+    gradleRunner.withProjectDir(fixture).build()
+
+    val feature = fixture.featureFile("Feature")
+    feature.shouldExist()
+
+    feature.readText() shouldContain """
+      |enum class Feature : io.mehow.laboratory.Feature<Feature> {
+      |  Enabled,
+      |  Disabled,
+      |  ;
+      |
+      |  override val defaultOption: Feature
+      |    get() = Disabled
+      |}
+    """.trimMargin()
+  }
+
   test("generates supervised grandchild feature flag") {
     val fixture = "feature-flag-supervisor-generate-grandchild".toFixture()
 
@@ -612,4 +652,112 @@ class GenerateFeatureFlagsTaskSpec : FunSpec({
     val feature = fixture.featureFile("Feature")
     feature.shouldNotExist()
   }
+
+  test("generates supervised children for enabled feature flag") {
+    val fixture = "feature-flag-supervisor-enabled-generate-child".toFixture()
+
+    val result = gradleRunner.withProjectDir(fixture).build()
+
+    result.task(":generateFeatureFlags")!!.outcome shouldBe SUCCESS
+
+    val parent = fixture.featureFile("Parent")
+    parent.shouldExist()
+
+    parent.readText() shouldContain """
+      |enum class Parent : Feature<Parent> {
+      |  Enabled,
+      |  Disabled,
+      |  ;
+      |
+      |  override val defaultOption: Parent
+      |    get() = Enabled
+      |}
+    """.trimMargin()
+
+    val first = fixture.featureFile("DisabledChild")
+    first.shouldExist()
+
+    first.readText() shouldContain """
+      |enum class DisabledChild : Feature<DisabledChild> {
+      |  Enabled,
+      |  Disabled,
+      |  ;
+      |
+      |  override val defaultOption: DisabledChild
+      |    get() = Disabled
+      |
+      |  override val supervisorOption: Feature<*> = Parent.Enabled
+      |}
+    """.trimMargin()
+
+    val second = fixture.featureFile("EnabledChild")
+    second.shouldExist()
+
+    second.readText() shouldContain """
+      |enum class EnabledChild : Feature<EnabledChild> {
+      |  Enabled,
+      |  Disabled,
+      |  ;
+      |
+      |  override val defaultOption: EnabledChild
+      |    get() = Enabled
+      |
+      |  override val supervisorOption: Feature<*> = Parent.Disabled
+      |}
+    """.trimMargin()
+  }
+
+  test("generates supervised children for disabled feature flag") {
+    val fixture = "feature-flag-supervisor-disabled-generate-child".toFixture()
+
+    val result = gradleRunner.withProjectDir(fixture).build()
+
+    result.task(":generateFeatureFlags")!!.outcome shouldBe SUCCESS
+
+    val parent = fixture.featureFile("Parent")
+    parent.shouldExist()
+
+    parent.readText() shouldContain """
+      |enum class Parent : Feature<Parent> {
+      |  Enabled,
+      |  Disabled,
+      |  ;
+      |
+      |  override val defaultOption: Parent
+      |    get() = Disabled
+      |}
+    """.trimMargin()
+
+    val first = fixture.featureFile("DisabledChild")
+    first.shouldExist()
+
+    first.readText() shouldContain """
+      |enum class DisabledChild : Feature<DisabledChild> {
+      |  Enabled,
+      |  Disabled,
+      |  ;
+      |
+      |  override val defaultOption: DisabledChild
+      |    get() = Disabled
+      |
+      |  override val supervisorOption: Feature<*> = Parent.Enabled
+      |}
+    """.trimMargin()
+
+    val second = fixture.featureFile("EnabledChild")
+    second.shouldExist()
+
+    second.readText() shouldContain """
+      |enum class EnabledChild : Feature<EnabledChild> {
+      |  Enabled,
+      |  Disabled,
+      |  ;
+      |
+      |  override val defaultOption: EnabledChild
+      |    get() = Enabled
+      |
+      |  override val supervisorOption: Feature<*> = Parent.Disabled
+      |}
+    """.trimMargin()
+  }
 })
diff --git a/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-disabled/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-disabled/build.gradle
new file mode 100644
index 0000000..a490f17
--- /dev/null
+++ b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-disabled/build.gradle
@@ -0,0 +1,8 @@
+plugins {
+  id "org.jetbrains.kotlin.jvm"
+  id "io.mehow.laboratory"
+}
+
+laboratory {
+  disabledFeature("Feature")
+}
diff --git a/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-disabled/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-disabled/settings.gradle
new file mode 100644
index 0000000..e69de29
diff --git a/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-enabled/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-enabled/build.gradle
new file mode 100644
index 0000000..60ec2da
--- /dev/null
+++ b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-enabled/build.gradle
@@ -0,0 +1,8 @@
+plugins {
+  id "org.jetbrains.kotlin.jvm"
+  id "io.mehow.laboratory"
+}
+
+laboratory {
+  enabledFeature("Feature")
+}
diff --git a/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-enabled/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-enabled/settings.gradle
new file mode 100644
index 0000000..e69de29
diff --git a/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-disabled-generate-child/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-disabled-generate-child/build.gradle
new file mode 100644
index 0000000..a13075f
--- /dev/null
+++ b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-disabled-generate-child/build.gradle
@@ -0,0 +1,15 @@
+plugins {
+  id "org.jetbrains.kotlin.jvm"
+  id "io.mehow.laboratory"
+}
+
+laboratory {
+  disabledFeature("Parent") {
+    withEnabled { parent ->
+      parent.disabledFeature("DisabledChild")
+    }
+    withDisabled { parent ->
+      parent.enabledFeature("EnabledChild")
+    }
+  }
+}
diff --git a/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-disabled-generate-child/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-disabled-generate-child/settings.gradle
new file mode 100644
index 0000000..e69de29
diff --git a/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-enabled-generate-child/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-enabled-generate-child/build.gradle
new file mode 100644
index 0000000..fc77566
--- /dev/null
+++ b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-enabled-generate-child/build.gradle
@@ -0,0 +1,15 @@
+plugins {
+  id "org.jetbrains.kotlin.jvm"
+  id "io.mehow.laboratory"
+}
+
+laboratory {
+  enabledFeature("Parent") {
+    withEnabled { parent ->
+      parent.disabledFeature("DisabledChild")
+    }
+    withDisabled { parent ->
+      parent.enabledFeature("EnabledChild")
+    }
+  }
+}