Repository: 7mind/izumi
Pull request #1177 (2020-08-11 21:21:28)

ConfigWriter fixes: fix regression when config minimization happened before GC, removed ConfigPathExtractor as it can seemingly no longer work with new `trisect` implementation, add band-aid to ConfigWriter to always include "activation" section before #779

codecov-commenter: # [Codecov](https://codecov.io/gh/7mind/izumi/pull/1177?src=pr&el=h1) Report
> Merging [#1177](https://codecov.io/gh/7mind/izumi/pull/1177?src=pr&el=desc) into [develop](https://codecov.io/gh/7mind/izumi/commit/016e14d3787fff0fd58b8e60d6d21b5f629eb1b6&el=desc) will **decrease** coverage by `0.07%`.
> The diff coverage is `86.36%`.


```diff
@@             Coverage Diff             @@
##           develop    #1177      +/-   ##
===========================================
- Coverage    54.06%   53.99%   -0.08%     
===========================================
  Files          408      406       -2     
  Lines         7698     7681      -17     
  Branches       371      382      +11     
===========================================
- Hits          4162     4147      -15     
+ Misses        3536     3534       -2     
```


Patch:
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/TriSplittedPlan.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/TriSplittedPlan.scala
index 394dcabcb..5a4da69d4 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/TriSplittedPlan.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/TriSplittedPlan.scala
@@ -3,8 +3,8 @@ package izumi.distage.model.plan
 import izumi.fundamentals.platform.strings.IzString._
 
 /**
-  * @param side    integrations, must be inhertied from the `shared` plan
   * @param primary plan producing roots, must be inherited from the `side` plan
+  * @param side    integrations, must be inhertied from the `shared` plan
   * @param shared  creates a part of the graph shared by both `side` and `primary` plans
   */
 final case class TriSplittedPlan(
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/operations/OperationOrigin.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/operations/OperationOrigin.scala
index 5c111ed80..6124d5ed5 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/operations/OperationOrigin.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/operations/OperationOrigin.scala
@@ -16,7 +16,7 @@ sealed trait OperationOrigin {
 
 object OperationOrigin {
 
-  case class EqualizedOperationOrigin(value: OperationOrigin) {
+  final case class EqualizedOperationOrigin(value: OperationOrigin) {
     override def hashCode(): Int = 0
 
     override def equals(obj: Any): Boolean = obj match {
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/planning/PlanSplittingOps.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/planning/PlanSplittingOps.scala
index 0402c166b..69f640560 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/planning/PlanSplittingOps.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/planning/PlanSplittingOps.scala
@@ -41,9 +41,9 @@ trait PlanSplittingOps { this: Planner =>
     assert(sharedPlan.declaredRoots == sharedKeys)
 
     TriSplittedPlan(
-      subplan,
-      primplan,
-      sharedPlan,
+      side = subplan,
+      primary = primplan,
+      shared = sharedPlan,
     )
   }
 
@@ -62,15 +62,12 @@ trait PlanSplittingOps { this: Planner =>
   }
 
   private[this] final def toSubplanNoRewrite(activation: Activation, appModule: ModuleBase, extractedRoots: Set[DIKey]): OrderedPlan = {
-    toSubplan(activation, appModule, extractedRoots, planNoRewrite)
-  }
-
-  private[this] final def toSubplan(activation: Activation, appModule: ModuleBase, extractedRoots: Set[DIKey], plan: PlannerInput => OrderedPlan): OrderedPlan = {
     if (extractedRoots.nonEmpty) {
       // exclude runtime
-      plan(PlannerInput(appModule, activation, extractedRoots))
+      planNoRewrite(PlannerInput(appModule, activation, extractedRoots))
     } else {
       OrderedPlan.empty
     }
   }
+
 }
diff --git a/distage/distage-extension-config/src/main/scala/distage/config/DistageConfig.scala b/distage/distage-extension-config/src/main/scala/distage/config/DistageConfig.scala
index c554597e8..ad0aa1b40 100644
--- a/distage/distage-extension-config/src/main/scala/distage/config/DistageConfig.scala
+++ b/distage/distage-extension-config/src/main/scala/distage/config/DistageConfig.scala
@@ -1,6 +1,6 @@
 package distage.config
 
-import izumi.distage.config.{codec, extractor, model}
+import izumi.distage.config.{codec, model}
 
 trait DistageConfig {
 
@@ -15,6 +15,4 @@ trait DistageConfig {
   type DIConfigReader[T] = codec.DIConfigReader[T]
   val DIConfigReader: codec.DIConfigReader.type = codec.DIConfigReader
 
-  type ConfigPathExtractorModule = extractor.ConfigPathExtractorModule
-
 }
diff --git a/distage/distage-extension-config/src/main/scala/distage/config/package.scala b/distage/distage-extension-config/src/main/scala/distage/config/package.scala
index 4f4a4b153..ea9b06fdc 100644
--- a/distage/distage-extension-config/src/main/scala/distage/config/package.scala
+++ b/distage/distage-extension-config/src/main/scala/distage/config/package.scala
@@ -1,6 +1,6 @@
 package distage
 
-import izumi.distage.config.{codec, extractor, model}
+import izumi.distage.config.{codec, model}
 
 package object config extends DistageConfig {
 
@@ -15,6 +15,4 @@ package object config extends DistageConfig {
   override type DIConfigReader[T] = codec.DIConfigReader[T]
   override val DIConfigReader: codec.DIConfigReader.type = codec.DIConfigReader
 
-  override type ConfigPathExtractorModule = extractor.ConfigPathExtractorModule
-
 }
diff --git a/distage/distage-extension-config/src/main/scala/izumi/distage/config/AppConfigModule.scala b/distage/distage-extension-config/src/main/scala/izumi/distage/config/AppConfigModule.scala
index df7ce3b85..17b68de0c 100644
--- a/distage/distage-extension-config/src/main/scala/izumi/distage/config/AppConfigModule.scala
+++ b/distage/distage-extension-config/src/main/scala/izumi/distage/config/AppConfigModule.scala
@@ -9,6 +9,7 @@ class AppConfigModule(appConfig: AppConfig) extends ModuleDef {
 
   def this(config: Config) = this(AppConfig(config))
 }
+
 object AppConfigModule {
   def apply(appConfig: AppConfig): AppConfigModule = new AppConfigModule(appConfig)
   def apply(config: Config): AppConfigModule = new AppConfigModule(config)
diff --git a/distage/distage-extension-config/src/main/scala/izumi/distage/config/extractor/ConfigPathExtractor.scala b/distage/distage-extension-config/src/main/scala/izumi/distage/config/extractor/ConfigPathExtractor.scala
deleted file mode 100644
index a509176cb..000000000
--- a/distage/distage-extension-config/src/main/scala/izumi/distage/config/extractor/ConfigPathExtractor.scala
+++ /dev/null
@@ -1,76 +0,0 @@
-package izumi.distage.config.extractor
-
-import com.typesafe.config.{Config, ConfigFactory}
-import izumi.distage.config.extractor.ConfigPathExtractor.{ConfigPath, ExtractConfigPath1, ResolvedConfig}
-import izumi.distage.model.definition.Binding.SingletonBinding
-import izumi.distage.model.definition.BindingTag.ConfTag
-import izumi.distage.model.definition.{Binding, ImplDef, Module, ModuleBase}
-import izumi.distage.model.planning.PlanningHook
-import izumi.distage.model.reflection.DIKey
-import izumi.fundamentals.platform.language.SourceFilePosition
-
-import scala.annotation.nowarn
-import scala.jdk.CollectionConverters._
-
-class ConfigPathExtractor extends PlanningHook {
-
-  override def hookDefinition(defn: ModuleBase): ModuleBase = {
-    val paths = defn.bindings.collect {
-      case ExtractConfigPath1(configPath) =>
-        configPath
-    }
-
-    val addResolvedConfigOp = resolvedConfigBindings(paths)
-
-    defn ++ Module.make(Set(addResolvedConfigOp))
-  }
-
-  private def resolvedConfigBindings(paths: Set[ConfigPath]): SingletonBinding[DIKey] = {
-    val resolvedConfig = ResolvedConfig(paths)
-    val target = DIKey.get[ResolvedConfig]
-    SingletonBinding(
-      target,
-      ImplDef.InstanceImpl(target.tpe, resolvedConfig),
-      Set.empty,
-      SourceFilePosition.unknown,
-    )
-  }
-
-}
-
-object ConfigPathExtractor {
-
-  object ExtractConfigPath1 {
-    def unapply(op: Binding): Option[ConfigPath] = {
-      op.tags.collectFirst {
-        case ConfTag(path) => ConfigPath(path)
-      }
-    }
-  }
-
-  final case class ResolvedConfig(requiredPaths: Set[ConfigPath]) {
-    @nowarn("msg=Unused import")
-    def minimized(source: Config): Config = {
-      import scala.collection.compat._
-      val paths = requiredPaths.map(_.toPath)
-
-      ConfigFactory.parseMap {
-        source
-          .root().unwrapped().asScala
-          .view
-          .filterKeys(key => paths.exists(_.startsWith(key)))
-          .toMap
-          .asJava
-      }
-    }
-  }
-
-  final case class ConfigPath(parts: Seq[String]) {
-    def toPath: String = parts.mkString(".")
-    override def toString: String = s"cfg:$toPath"
-  }
-  object ConfigPath {
-    def apply(path: String): ConfigPath = new ConfigPath(path.split('.').toIndexedSeq)
-  }
-
-}
diff --git a/distage/distage-extension-config/src/main/scala/izumi/distage/config/extractor/ConfigPathExtractorModule.scala b/distage/distage-extension-config/src/main/scala/izumi/distage/config/extractor/ConfigPathExtractorModule.scala
deleted file mode 100644
index d315b9bac..000000000
--- a/distage/distage-extension-config/src/main/scala/izumi/distage/config/extractor/ConfigPathExtractorModule.scala
+++ /dev/null
@@ -1,9 +0,0 @@
-package izumi.distage.config.extractor
-
-import izumi.distage.model.definition.BootstrapModuleDef
-import izumi.distage.model.planning.PlanningHook
-
-class ConfigPathExtractorModule extends BootstrapModuleDef {
-  many[PlanningHook]
-    .add[ConfigPathExtractor]
-}
diff --git a/distage/distage-extension-config/src/main/scala/izumi/distage/config/model/exceptions/DIConfigReadException.scala b/distage/distage-extension-config/src/main/scala/izumi/distage/config/model/exceptions/DIConfigReadException.scala
index 4f6296272..f3ba8dab4 100644
--- a/distage/distage-extension-config/src/main/scala/izumi/distage/config/model/exceptions/DIConfigReadException.scala
+++ b/distage/distage-extension-config/src/main/scala/izumi/distage/config/model/exceptions/DIConfigReadException.scala
@@ -2,4 +2,4 @@ package izumi.distage.config.model.exceptions
 
 import izumi.distage.model.exceptions.DIException
 
-class DIConfigReadException(message: String, cause: Throwable) extends DIException(message, cause)
+final class DIConfigReadException(message: String, cause: Throwable) extends DIException(message, cause)
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/ContainerResource.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/ContainerResource.scala
index 15428b2ee..37f52741b 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/ContainerResource.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/ContainerResource.scala
@@ -272,14 +272,14 @@ case class ContainerResource[F[_], T](
       out <- F.maybeSuspend {
         @nowarn("msg=method.*Bind.*deprecated")
         val cmd = Value(baseCmd)
-          .mut(config.name) { case (n, c) => c.withName(n) }
+          .mut(config.name)(_.withName(_))
           .mut(ports.nonEmpty)(_.withExposedPorts(ports.map(_.binding.getExposedPort).asJava))
           .mut(ports.nonEmpty)(_.withPortBindings(ports.map(_.binding).asJava))
           .mut(adjustedEnv.nonEmpty)(_.withEnv(adjustedEnv.map { case (k, v) => s"$k=$v" }.toList.asJava))
           .mut(config.cmd.nonEmpty)(_.withCmd(config.cmd.toList.asJava))
           .mut(config.entrypoint.nonEmpty)(_.withEntrypoint(config.entrypoint.toList.asJava))
-          .mut(config.cwd)((cwd, cmd) => cmd.withWorkingDir(cwd))
-          .mut(config.user)((user, cmd) => cmd.withUser(user))
+          .mut(config.cwd)(_.withWorkingDir(_))
+          .mut(config.user)(_.withUser(_))
           .mut(volumes.nonEmpty)(_.withVolumes(volumes.map(_.getVolume).asJava))
           .mut(volumes.nonEmpty)(_.withBinds(volumes.toList.asJava))
           .map(c => c.withHostConfig(c.getHostConfig.withAutoRemove(config.autoRemove)))
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerClientWrapper.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerClientWrapper.scala
index 58cc00540..489a4b5ac 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerClientWrapper.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerClientWrapper.scala
@@ -79,10 +79,10 @@ object DockerClientWrapper {
 
     private[this] lazy val rawClientConfig = Value(DefaultDockerClientConfig.createDefaultConfigBuilder())
       .mut(clientConfig.remote.filter(_ => clientConfig.useRemote))(
-        (c, b) => b.withDockerHost(c.host).withDockerTlsVerify(c.tlsVerify).withDockerCertPath(c.certPath).withDockerConfig(c.config)
+        (b, c) => b.withDockerHost(c.host).withDockerTlsVerify(c.tlsVerify).withDockerCertPath(c.certPath).withDockerConfig(c.config)
       )
       .mut(clientConfig.registry.filter(_ => clientConfig.useRegistry))(
-        (c, b) => b.withRegistryUrl(c.url).withRegistryUsername(c.username).withRegistryPassword(c.password).withRegistryEmail(c.email)
+        (b, c) => b.withRegistryUrl(c.url).withRegistryUsername(c.username).withRegistryPassword(c.password).withRegistryEmail(c.email)
       )
       .get.build()
 
diff --git a/distage/distage-framework/src/main/scala/izumi/distage/roles/bundled/ConfigWriter.scala b/distage/distage-framework/src/main/scala/izumi/distage/roles/bundled/ConfigWriter.scala
index 3834e83c9..9312fac5e 100644
--- a/distage/distage-framework/src/main/scala/izumi/distage/roles/bundled/ConfigWriter.scala
+++ b/distage/distage-framework/src/main/scala/izumi/distage/roles/bundled/ConfigWriter.scala
@@ -3,20 +3,17 @@ package izumi.distage.roles.bundled
 import java.nio.charset.StandardCharsets
 import java.nio.file.{Files, Paths}
 
-import scala.annotation.nowarn
 import com.typesafe.config.{Config, ConfigFactory, ConfigRenderOptions}
-import izumi.distage.config.extractor.ConfigPathExtractor.ResolvedConfig
-import izumi.distage.config.extractor.ConfigPathExtractorModule
 import izumi.distage.framework.services.RoleAppPlanner
+import izumi.distage.model.definition.BindingTag.ConfTag
 import izumi.distage.model.definition.Id
 import izumi.distage.model.effect.DIEffect
-import izumi.distage.model.plan.ExecutableOp.WiringOp
-import izumi.distage.model.plan.OrderedPlan
-import izumi.distage.model.plan.Wiring.SingletonWiring.Instance
-import izumi.distage.model.reflection.DIKey
-import izumi.distage.roles.bundled.ConfigWriter.{ConfigurableComponent, WriteReference}
+import izumi.distage.model.plan.operations.OperationOrigin
+import izumi.distage.model.plan.{ExecutableOp, OrderedPlan}
+import izumi.distage.roles.bundled.ConfigWriter.{ConfigPath, ConfigurableComponent, ExtractConfigPath, WriteReference}
 import izumi.distage.roles.model.meta.{RoleBinding, RolesInfo}
 import izumi.distage.roles.model.{RoleDescriptor, RoleTask}
+import izumi.functional.Value
 import izumi.fundamentals.platform.cli.model.raw.RawEntrypointParams
 import izumi.fundamentals.platform.cli.model.schema.{ParserDef, RoleParserSchema}
 import izumi.fundamentals.platform.language.unused
@@ -25,47 +22,53 @@ import izumi.logstage.api.IzLogger
 import izumi.logstage.api.logger.LogRouter
 import izumi.logstage.distage.LogstageModule
 
-import scala.util._
+import scala.annotation.nowarn
+import scala.collection.compat.immutable.ArraySeq
+import scala.util.Try
 
-class ConfigWriter[F[_]: DIEffect](
+final class ConfigWriter[F[_]](
   logger: IzLogger,
   launcherVersion: ArtifactVersion @Id("launcher-version"),
   roleInfo: RolesInfo,
-  context: RoleAppPlanner[F],
-  //appModule: ModuleBase@Id("application.module"),
+  roleAppPlanner: RoleAppPlanner[F],
+  F: DIEffect[F],
 ) extends RoleTask[F] {
 
-  override def start(roleParameters: RawEntrypointParams, @unused freeArgs: Vector[String]): F[Unit] = {
+  // fixme: always include `activation` section in configs (Used in RoleAppLauncherImpl#configActivationSection, but not seen in config bindings, since it's not read by DI)
+  //  After https://github.com/7mind/izumi/issues/779 this will no longer be necessary
+  private[this] val _HackyMandatorySection = ConfigPath("activation")
+
+  override def start(roleParameters: RawEntrypointParams, @unused freeArgs: Vector[String]): F[Unit] = F.maybeSuspend {
     val config = ConfigWriter.parse(roleParameters)
-    DIEffect[F].maybeSuspend(writeReferenceConfig(config))
+    writeReferenceConfig(config)
   }
 
   private[this] def writeReferenceConfig(options: WriteReference): Unit = {
     val configPath = Paths.get(options.targetDir).toFile
     logger.info(s"Config ${configPath.getAbsolutePath -> "directory to use"}...")
 
-    if (!configPath.exists())
+    if (!configPath.exists()) {
       configPath.mkdir()
+    }
 
-    //val maybeVersion = IzManifest.manifest[LAUNCHER]().map(IzManifest.read).map(_.version)
     logger.info(s"Going to process ${roleInfo.availableRoleBindings.size -> "roles"}")
 
-    val commonConfig = buildConfig(options, ConfigurableComponent("common", Some(launcherVersion)))
+    val commonComponent = ConfigurableComponent("common", Some(launcherVersion), None)
+    val commonConfig = buildConfig(options, commonComponent)
+
     if (!options.includeCommon) {
-      val commonComponent = ConfigurableComponent("common", Some(launcherVersion))
       writeConfig(options, commonComponent, None, commonConfig)
     }
 
     roleInfo.availableRoleBindings.foreach {
       role =>
-        val component = ConfigurableComponent(role.descriptor.id, role.source.map(_.version))
-        val refConfig = buildConfig(options, component.copy(parent = Some(commonConfig)))
-        val version = if (options.useLauncherVersion) {
-          Some(ArtifactVersion(launcherVersion.version))
+        val component = ConfigurableComponent(role.descriptor.id, role.source.map(_.version), Some(commonConfig))
+        val refConfig = buildConfig(options, component)
+        val versionedComponent = if (options.useLauncherVersion) {
+          component.copy(version = Some(ArtifactVersion(launcherVersion.version)))
         } else {
-          component.version
+          component
         }
-        val versionedComponent = component.copy(version = version)
 
         try {
           writeConfig(options, versionedComponent, None, refConfig)
@@ -83,18 +86,16 @@ class ConfigWriter[F[_]: DIEffect](
   }
 
   private[this] def buildConfig(config: WriteReference, cmp: ConfigurableComponent): Config = {
-    val referenceConfig = s"${cmp.componentId}-reference.conf"
-    logger.info(s"[${cmp.componentId}] Resolving $referenceConfig... with ${config.includeCommon -> "shared sections"}")
+    val referenceConfig = s"${cmp.roleId}-reference.conf"
+    logger.info(s"[${cmp.roleId}] Resolving $referenceConfig... with ${config.includeCommon -> "shared sections"}")
 
-    val reference = cmp
-      .parent
-      .filter(_ => config.includeCommon)
-      .fold(
-        ConfigFactory.parseResourcesAnySyntax(referenceConfig)
-      )(parent => ConfigFactory.parseResourcesAnySyntax(referenceConfig).withFallback(parent)).resolve()
+    val reference = Value(ConfigFactory.parseResourcesAnySyntax(referenceConfig))
+      .mut(cmp.parent.filter(_ => config.includeCommon))(_.withFallback(_))
+      .get
+      .resolve()
 
     if (reference.isEmpty) {
-      logger.warn(s"[${cmp.componentId}] Reference config is empty.")
+      logger.warn(s"[${cmp.roleId}] Reference config is empty.")
     }
 
     val resolved = ConfigFactory
@@ -110,39 +111,27 @@ class ConfigWriter[F[_]: DIEffect](
   private[this] def minimizedConfig(config: Config, role: RoleBinding): Option[Config] = {
     val roleDIKey = role.binding.key
 
-    val cfg = Seq(
-      new ConfigPathExtractorModule,
-      new LogstageModule(LogRouter.nullRouter, setupStaticLogRouter = false),
+    val bootstrapOverride = Seq(
+      new LogstageModule(LogRouter.nullRouter, setupStaticLogRouter = false)
     ).overrideLeft
 
-    val plans = context.reboot(cfg).makePlan(Set(roleDIKey, DIKey.get[ResolvedConfig]))
+    val plans = roleAppPlanner
+      .reboot(bootstrapOverride)
+      .makePlan(Set(roleDIKey))
 
-    def getConfig(plan: OrderedPlan): Option[Config] = {
-      plan
-        .filter[ResolvedConfig]
-        .collectFirst {
-          case WiringOp.UseInstance(_, Instance(_, r: ResolvedConfig), _) =>
-            r.minimized(config)
-        }
+    def getConfig(plan: OrderedPlan): Iterator[ConfigPath] = {
+      plan.steps.iterator.collect {
+        case ExtractConfigPath(path) => path
+      }
     }
 
-    def getConfigOrEmpty(plan: OrderedPlan) = {
-      getConfig(plan) getOrElse ConfigFactory.empty()
-    }
+    val resolvedConfig =
+      (getConfig(plans.app.primary) ++
+      getConfig(plans.app.side) ++
+      getConfig(plans.app.shared)).toSet + _HackyMandatorySection
 
     if (plans.app.primary.steps.exists(_.target == roleDIKey)) {
-      val cfg = getConfig(plans.app.primary)
-        .orElse(getConfig(plans.app.shared))
-        .orElse(getConfig(plans.app.side))
-
-      cfg
-        .map(_.withFallback(getConfigOrEmpty(plans.app.side)))
-        .map(_.withFallback(getConfigOrEmpty(plans.app.shared)))
-        .map(_.withFallback(getConfigOrEmpty(plans.runtime)))
-        .orElse {
-          logger.error(s"Couldn't produce minimized config for $roleDIKey")
-          None
-        }
+      Some(ConfigWriter.minimized(resolvedConfig, config))
     } else {
       logger.warn(s"$roleDIKey is not in the refined plan")
       None
@@ -150,43 +139,35 @@ class ConfigWriter[F[_]: DIEffect](
   }
 
   private[this] def writeConfig(config: WriteReference, cmp: ConfigurableComponent, suffix: Option[String], typesafeConfig: Config): Try[Unit] = {
-    val fileName = outputFileName(cmp.componentId, cmp.version, config.asJson, suffix)
+    val fileName = outputFileName(cmp.roleId, cmp.version, config.asJson, suffix)
     val target = Paths.get(config.targetDir, fileName)
 
     Try {
       val cfg = typesafeConfig.root().render(configRenderOptions.setJson(config.asJson))
       val bytes = cfg.getBytes(StandardCharsets.UTF_8)
       Files.write(target, bytes)
-      logger.info(s"[${cmp.componentId}] Reference config saved -> $target (${bytes.size} bytes)")
+      logger.info(s"[${cmp.roleId}] Reference config saved -> $target (${bytes.size} bytes)")
     }.recover {
       case e: Throwable =>
-        logger.error(s"[${cmp.componentId -> "component id" -> null}] Can't write reference config to $target, ${e.getMessage -> "message"}")
+        logger.error(s"[${cmp.roleId -> "component id" -> null}] Can't write reference config to $target, ${e.getMessage -> "message"}")
     }
   }
 
   // TODO: sdk?
   @nowarn("msg=Unused import")
   private[this] def cleanupEffectiveAppConfig(effectiveAppConfig: Config, reference: Config): Config = {
-    import scala.jdk.CollectionConverters._
     import scala.collection.compat._
+    import scala.jdk.CollectionConverters._
 
     ConfigFactory.parseMap(effectiveAppConfig.root().unwrapped().asScala.view.filterKeys(reference.hasPath).toMap.asJava)
   }
 
   private[this] def outputFileName(service: String, version: Option[ArtifactVersion], asJson: Boolean, suffix: Option[String]): String = {
-    val extension = if (asJson) {
-      "json"
-    } else {
-      "conf"
-    }
-
+    val extension = if (asJson) "json" else "conf"
     val vstr = version.map(_.version).getOrElse("0.0.0-UNKNOWN")
-    suffix match {
-      case Some(value) =>
-        s"$service-$value-$vstr.$extension"
-      case None =>
-        s"$service-$vstr.$extension"
-    }
+    val suffixStr = suffix.fold("")("-" + _)
+
+    s"$service$suffixStr-$vstr.$extension"
   }
 
   private[this] final val configRenderOptions = ConfigRenderOptions.defaults.setOriginComments(false).setComments(false)
@@ -212,9 +193,9 @@ object ConfigWriter extends RoleDescriptor {
   )
 
   final case class ConfigurableComponent(
-    componentId: String,
+    roleId: String,
     version: Option[ArtifactVersion],
-    parent: Option[Config] = None,
+    parent: Option[Config],
   )
 
   object Options extends ParserDef {
@@ -238,4 +219,41 @@ object ConfigWriter extends RoleDescriptor {
     )
   }
 
+  @nowarn("msg=Unused import")
+  def minimized(requiredPaths: Set[ConfigPath], source: Config): Config = {
+    import scala.collection.compat._
+    import scala.jdk.CollectionConverters._
+
+    val paths = requiredPaths.map(_.toPath)
+
+    ConfigFactory.parseMap {
+      source
+        .root().unwrapped().asScala
+        .view
+        .filterKeys(key => paths.exists(_.startsWith(key)))
+        .toMap
+        .asJava
+    }
+  }
+
+  object ExtractConfigPath {
+    def unapply(op: ExecutableOp): Option[ConfigPath] = {
+      op.origin.value match {
+        case defined: OperationOrigin.Defined =>
+          defined.binding.tags.collectFirst {
+            case ConfTag(path) => ConfigPath(path)
+          }
+        case _ =>
+          None
+      }
+    }
+  }
+
+  final case class ConfigPath(parts: Seq[String]) {
+    def toPath: String = parts.mkString(".")
+  }
+  object ConfigPath {
+    def apply(path: String): ConfigPath = new ConfigPath(ArraySeq.unsafeWrapArray(path.split('.')))
+  }
+
 }
diff --git a/fundamentals/fundamentals-functional/src/main/scala/izumi/functional/Value.scala b/fundamentals/fundamentals-functional/src/main/scala/izumi/functional/Value.scala
index 283206cb2..4433d6aa0 100644
--- a/fundamentals/fundamentals-functional/src/main/scala/izumi/functional/Value.scala
+++ b/fundamentals/fundamentals-functional/src/main/scala/izumi/functional/Value.scala
@@ -5,10 +5,10 @@ final class Value[A] private (private val value: A) extends AnyVal {
     new Value(f(this.value))
   }
 
-  @inline def mut[C, B](context: Option[C])(f: (C, A) => A): Value[A] = {
+  @inline def mut[C, B](context: Option[C])(f: (A, C) => A): Value[A] = {
     context match {
       case Some(ctx) =>
-        new Value(f(ctx, this.value))
+        new Value(f(this.value, ctx))
       case None =>
         this
     }


Test patch:
diff --git a/distage/distage-extension-config/src/test/scala/com/github/pshirshov/configapp/TestConfigApp.scala b/distage/distage-extension-config/src/test/scala/com/github/pshirshov/configapp/TestConfigApp.scala
index dd29f36b3..5aeb6cf30 100644
--- a/distage/distage-extension-config/src/test/scala/com/github/pshirshov/configapp/TestConfigApp.scala
+++ b/distage/distage-extension-config/src/test/scala/com/github/pshirshov/configapp/TestConfigApp.scala
@@ -2,14 +2,13 @@ package com.github.pshirshov.configapp
 
 import distage.Id
 import distage.config.{AppConfig, ConfigModuleDef}
-import izumi.distage.config.extractor.ConfigPathExtractor.ResolvedConfig
 import izumi.distage.model.PlannerInput
 import izumi.fundamentals.platform.language.Quirks
 
 // application
 trait TestAppService
 
-class TestConfigApp(val services: Set[TestAppService], val usedConfig: ResolvedConfig, val appConfig: AppConfig)
+class TestConfigApp(val services: Set[TestAppService], val appConfig: AppConfig)
 
 // config structures
 case class HostPort(port: Int, host: String)
@@ -80,9 +79,4 @@ object TestConfigApp {
     make[TestConfigApp]
   })
 
-  final val setDefinition = PlannerInput.noGC(new ConfigModuleDef {
-    many[TestAppService].add[DataPuller1]
-    makeConfigNamed[HostPort]("DataPuller1.target")
-    makeConfigNamed[HostPort]("DataPuller1.source")
-  })
 }
diff --git a/distage/distage-extension-config/src/test/scala/izumi/distage/config/ConfigTest.scala b/distage/distage-extension-config/src/test/scala/izumi/distage/config/ConfigTest.scala
index 4360f89f2..072b55f80 100644
--- a/distage/distage-extension-config/src/test/scala/izumi/distage/config/ConfigTest.scala
+++ b/distage/distage-extension-config/src/test/scala/izumi/distage/config/ConfigTest.scala
@@ -5,7 +5,6 @@ import com.github.pshirshov.configapp.SealedTrait2.{No, Yes}
 import com.github.pshirshov.configapp._
 import com.typesafe.config._
 import distage.Injector
-import izumi.distage.config.extractor.ConfigPathExtractorModule
 import izumi.distage.config.model.AppConfig
 import izumi.distage.model.PlannerInput
 import org.scalatest.wordspec.AnyWordSpec
@@ -26,24 +25,6 @@ final class ConfigTest extends AnyWordSpec {
   }
 
   "Config resolver" should {
-    "resolve config references" in {
-      val injector = Injector(new ConfigPathExtractorModule)
-      val plan = injector.plan(mkConfigModule("distage-config-test.conf")(TestConfigApp.definition))
-
-      val context = injector.produce(plan).unsafeGet()
-
-      assert(context.get[HttpServer1].listenOn.port == 8081)
-      assert(context.get[HttpServer2].listenOn.port == 8082)
-
-      assert(context.get[DataPuller1].target.port == 9001)
-      assert(context.get[DataPuller2].target.port == 9002)
-      assert(context.get[DataPuller3].target.port == 9003)
-
-      assert(context.get[Set[TestAppService]].size == 5)
-
-      val testConfigApp = context.get[TestConfigApp]
-      assert(!testConfigApp.usedConfig.minimized(testConfigApp.appConfig.config).entrySet().isEmpty)
-    }
 
 //    "be idempotent under Injector.finish" in {
 //      val injector = Injector(new ConfigPathExtractorModule)
@@ -62,15 +43,6 @@ final class ConfigTest extends AnyWordSpec {
 //      assert(context.get[Set[TestAppService]].size == 5)
 //    }
 
-    "resolve config references in set elements" in {
-      val injector = Injector(new ConfigPathExtractorModule)
-      val plan = injector.plan(mkConfigModule("distage-config-test.conf")(TestConfigApp.setDefinition))
-
-      val context = injector.produce(plan).unsafeGet()
-
-      assert(context.get[Set[TestAppService]].head.asInstanceOf[DataPuller1].target.port == 9001)
-    }
-
     "resolve config maps" in {
       val injector = Injector()
       val plan = injector.plan(mkConfigModule("map-test.conf")(TestConfigReaders.mapDefinition))
diff --git a/distage/distage-framework/src/test/resources/common-reference.conf b/distage/distage-framework/src/test/resources/common-reference.conf
index a86eadc6b..8974cc8eb 100644
--- a/distage/distage-framework/src/test/resources/common-reference.conf
+++ b/distage/distage-framework/src/test/resources/common-reference.conf
@@ -12,3 +12,11 @@ testservice {
 activation {
   axiscomponentaxis = incorrect
 }
+
+listconf {
+  ints = [1,2,1]
+}
+
+setElementConfig {
+  abc = bca
+}
diff --git a/distage/distage-framework/src/test/resources/testrole04-reference.conf b/distage/distage-framework/src/test/resources/testrole04-reference.conf
index 98afa04a2..e69de29bb 100644
--- a/distage/distage-framework/src/test/resources/testrole04-reference.conf
+++ b/distage/distage-framework/src/test/resources/testrole04-reference.conf
@@ -1,4 +0,0 @@
-
-listconf {
-  ints = [1,2,1]
-}
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
index e5ae828dd..94c301a13 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
@@ -23,10 +23,11 @@ import izumi.fundamentals.platform.resources.ArtifactVersion
 import izumi.logstage.api.IzLogger
 import org.scalatest.wordspec.AnyWordSpec
 
+import scala.jdk.CollectionConverters._
 import scala.util.Try
 
 class RoleAppTest extends AnyWordSpec with WithProperties {
-  private val prefix = "target/configwriter"
+  private final val targetPath = "target/configwriter"
 
   private val overrides = Map(
     "testservice.systemPropInt" -> "265",
@@ -263,47 +264,66 @@ class RoleAppTest extends AnyWordSpec with WithProperties {
     "produce config dumps and support minimization" in {
       val version = ArtifactVersion(s"0.0.0-${UUID.randomUUID().toString}")
       withProperties(overrides ++ Map(TestPlugin.versionProperty -> version.version)) {
-        TestEntrypoint.main(
-          Array(
-            "-ll",
-            logLevel,
-            ":configwriter",
-            "-t",
-            prefix,
-          )
-        )
+        TestEntrypoint.main(Array("-ll", logLevel, ":configwriter", "-t", targetPath))
       }
 
       val cwCfg = cfg("configwriter", version)
       val cwCfgMin = cfg("configwriter-minimized", version)
 
-      val roleCfg = cfg("testrole00", version)
-      val roleCfgMin = cfg("testrole00-minimized", version)
-
       assert(cwCfg.exists(), s"$cwCfg exists")
       assert(cwCfgMin.exists(), s"$cwCfgMin exists")
-      assert(roleCfg.exists(), s"$roleCfg exists")
-      assert(roleCfgMin.exists(), s"$roleCfgMin exists")
-
       assert(cwCfg.length() > cwCfgMin.length())
-      assert(roleCfg.length() > roleCfgMin.length())
 
-      val roleCfgMinStr = new String(Files.readAllBytes(roleCfgMin.toPath), UTF_8)
-      val roleCfgMinRestored = ConfigFactory.parseString(roleCfgMinStr)
+      val role0Cfg = cfg("testrole00", version)
+      val role0CfgMin = cfg("testrole00-minimized", version)
+
+      assert(role0Cfg.exists(), s"$role0Cfg exists")
+      assert(role0CfgMin.exists(), s"$role0CfgMin exists")
+      assert(role0Cfg.length() > role0CfgMin.length())
+
+      val role0CfgMinParsed = ConfigFactory.parseString(new String(Files.readAllBytes(role0CfgMin.toPath), UTF_8))
+
+      assert(!role0CfgMinParsed.hasPath("unrequiredEntry"))
+      assert(!role0CfgMinParsed.hasPath("logger"))
+      assert(!role0CfgMinParsed.hasPath("listconf"))
+
+      assert(role0CfgMinParsed.hasPath("integrationOnlyCfg"))
+      assert(role0CfgMinParsed.hasPath("integrationOnlyCfg2"))
+      assert(role0CfgMinParsed.hasPath("setElementConfig"))
+      assert(role0CfgMinParsed.hasPath("testservice2"))
+      assert(role0CfgMinParsed.hasPath("testservice"))
+
+      assert(role0CfgMinParsed.getString("testservice2.strval") == "xxx")
+      assert(role0CfgMinParsed.getString("testservice.overridenInt") == "111")
+      assert(role0CfgMinParsed.getInt("testservice.systemPropInt") == 265)
+      assert(role0CfgMinParsed.getList("testservice.systemPropList").unwrapped().asScala == List("111", "222"))
+
+      val role4Cfg = cfg("testrole04", version)
+      val role4CfgMin = cfg("testrole04-minimized", version)
+
+      assert(role4Cfg.exists(), s"$role4Cfg exists")
+      assert(role4CfgMin.exists(), s"$role4CfgMin exists")
+      assert(role4Cfg.length() > role4CfgMin.length())
+
+      val role4CfgMinParsed = ConfigFactory.parseString(new String(Files.readAllBytes(role4CfgMin.toPath), UTF_8))
+
+      assert(!role4CfgMinParsed.hasPath("unrequiredEntry"))
+      assert(!role4CfgMinParsed.hasPath("logger"))
+      assert(!role4CfgMinParsed.hasPath("integrationOnlyCfg"))
+      assert(!role4CfgMinParsed.hasPath("integrationOnlyCfg2"))
+      assert(!role4CfgMinParsed.hasPath("setElementConfig"))
+      assert(!role4CfgMinParsed.hasPath("testservice2"))
+      assert(!role4CfgMinParsed.hasPath("testservice"))
 
-      assert(!roleCfgMinRestored.hasPath("unrequiredEntry"))
-      assert(roleCfgMinRestored.hasPath("integrationOnlyCfg"))
-      assert(roleCfgMinRestored.hasPath("integrationOnlyCfg2"))
+      assert(role4CfgMinParsed.hasPath("listconf"))
 
-      assert(roleCfgMinRestored.hasPath("testservice"))
-      assert(roleCfgMinRestored.hasPath("testservice2"))
-      assert(roleCfgMinRestored.getString("testservice2.strval") == "xxx")
-      assert(roleCfgMinRestored.getString("testservice.overridenInt") == "111")
+//      assert(role0CfgMinParsed.hasPath("activation"))
+//      assert(role4CfgMinParsed.hasPath("activation"))
     }
 
   }
 
   private def cfg(role: String, version: ArtifactVersion): File = {
-    Paths.get(prefix, s"$role-${version.version}.json").toFile
+    Paths.get(targetPath, s"$role-${version.version}.json").toFile
   }
 }
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
index 784903725..30401b5dc 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
@@ -4,6 +4,7 @@ import distage.LocatorRef
 import izumi.distage.framework.model.IntegrationCheck
 import izumi.distage.model.definition.Axis
 import izumi.distage.model.effect.DIEffect
+import izumi.distage.roles.test.fixtures.roles.TestRole00.SetElementOnlyCfg
 import izumi.fundamentals.platform.integration.ResourceCheck
 import izumi.fundamentals.platform.language.Quirks._
 
@@ -12,6 +13,9 @@ import scala.collection.mutable
 object Fixture {
   trait Dummy
 
+  trait SetElement
+  final case class SetElement1(setElementOnlyCfg: SetElementOnlyCfg) extends SetElement
+
   case class TestServiceConf2(
     strval: String
   )
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
index a8f523cd2..c75c76ee9 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
@@ -10,7 +10,7 @@ import izumi.distage.roles.bundled.{ConfigWriter, Help}
 import izumi.distage.roles.test.fixtures.Fixture._
 import izumi.distage.roles.test.fixtures.TestPlugin.{InheritedCloseable, NotCloseable}
 import izumi.distage.roles.test.fixtures.roles.TestRole00
-import izumi.distage.roles.test.fixtures.roles.TestRole00.{IntegrationOnlyCfg, IntegrationOnlyCfg2, TestRole00Resource, TestRole00ResourceIntegrationCheck}
+import izumi.distage.roles.test.fixtures.roles.TestRole00.{IntegrationOnlyCfg, IntegrationOnlyCfg2, SetElementOnlyCfg, TestRole00Resource, TestRole00ResourceIntegrationCheck}
 import izumi.fundamentals.platform.resources.ArtifactVersion
 import izumi.reflect.TagK
 
@@ -27,6 +27,9 @@ class TestPluginBase[F[_]: TagK] extends CatsDIEffectModule with PluginDef with
   make[ArtifactVersion].named("launcher-version").from(ArtifactVersion(version))
   many[Dummy]
 
+  many[SetElement]
+    .add[SetElement1]
+
   make[TestTask00[F]]
   make[TestRole00[F]]
   make[TestRole01[F]]
@@ -46,6 +49,7 @@ class TestPluginBase[F[_]: TagK] extends CatsDIEffectModule with PluginDef with
 
   makeConfig[TestServiceConf]("testservice")
   makeConfig[IntegrationOnlyCfg]("integrationOnlyCfg")
+  makeConfig[SetElementOnlyCfg]("setElementConfig")
 
   makeConfigNamed[IntegrationOnlyCfg2]("integrationOnlyCfg2")
   make[IntegrationOnlyCfg2].from {
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala
index 8274997f8..a2a86f04a 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala
@@ -35,6 +35,7 @@ object roles {
     val conf: TestServiceConf,
     val conf2: TestServiceConf2,
     val dummies: Set[Dummy],
+    val setElems: Set[SetElement],
     val resource: TestRole00Resource[F],
     val resources: Set[TestResource],
     val conflict: Conflict,
@@ -62,6 +63,8 @@ object roles {
     final case class IntegrationOnlyCfg(flag: Boolean)
     final case class IntegrationOnlyCfg2(value: String)
 
+    final case class SetElementOnlyCfg(abc: String)
+
     final class TestRole00Resource[F[_]](private val it: TestRole00ResourceIntegrationCheck[F])
     final class TestRole00ResourceIntegrationCheck[F[_]: DIEffect](
       private val cfg: IntegrationOnlyCfg,