Denis Verhoturov
Denis Verhoturov

Reputation: 51

Value lookup resolves to a wrong scope in the sbt plugin

I'm trying to write a plugin for sbt for my project that will process resources. In a nutshell, it is maven profiles made in sbt. When I inspect prod:dictionary I get expected state of this Map, however, when I try prod:expandParameters I get an empty Map. How could I get the value of the dictionary from the scope of the exact configuration that command is run with?

project/ResourceFiltering.scala

import sbt._
import sbt.Keys._
import sbt.internal.util.ManagedLogger
import scala.util.matching.Regex

object ResourceFiltering extends AutoPlugin {

  override def trigger = AllRequirements

  sealed trait Keys {
    lazy val expandParameters = taskKey[Unit]("")

    lazy val extensions = settingKey[Seq[String]]("")
    lazy val pattern    = settingKey[Regex]("")
    lazy val dictionary = settingKey[Map[String, String]]("")
  }

  object autoImport extends Keys

  import autoImport._

  override val projectSettings: Seq[Def.Setting[_]] = Seq(
    Zero / extensions := Seq("conf", "properties", "xml"),
    Zero / pattern    := """(\$\{()\})""".r,
    Zero / dictionary := Map.empty,
    expandParameters := {
      val log: ManagedLogger = streams.value.log
      log.info(s"""|Parameter expansion
                   |Configuration: $configuration
                   |Extensions: ${extensions value}
                   |Pattern: ${pattern value}
                   |Dictionary: ${dictionary value}
                """.stripMargin)
    }
  )
}

build.sbt

enablePlugins(ResourceFiltering)

lazy val Prod  = config("prod")  extend Compile describedAs "Scope to build production packages."
lazy val Stage = config("stage") extend Compile describedAs "Scope to build stage packages."
lazy val Local = config("local") extend Compile describedAs "Scope to build local packages."

lazy val root = (project in file("."))
  .configs(Prod, Stage, Local)
  .settings(sharedSettings)

lazy val sharedSettings = 
  prodSettings ++ stageSettings ++ localSettings

lazy val defaults = Defaults.configSettings ++ Defaults.configTasks ++ Defaults.resourceConfigPaths

lazy val prodSettings = inConfig(Prod)(defaults ++ Seq(
  dictionary ++= Profiles.prod
))

lazy val stageSettings = inConfig(Stage)(defaults ++ Seq(
  dictionary ++= Profiles.stage
))

lazy val localSettings = inConfig(Local)(defaults ++ Seq(
  dictionary ++= Profiles.local
))

project/Profiles.scala

lazy val default: Map[String, String] = local

lazy val local: Map[String, String] = Map("example" -> "local")

lazy val stage: Map[String, String] = Map("example" -> "stage")

lazy val prod: Map[String, String] = Map("example" -> "prod")

Upvotes: 3

Views: 104

Answers (1)

Mario Galic
Mario Galic

Reputation: 48400

Analysing Plugins Best Practices docs I would make the following recommendations regarding configuration and scoping.

Provide default values in globalSettings instead of projectSettings like so

override lazy val globalSettings = Seq(
  dictionary := Map.empty
)

Next collect base configuration of expandParameters into its own sequence like so

lazy val baseResourceFilteringSettings: Seq[Def.Setting[_]] = Seq(
  extensions := Seq("conf", "properties", "xml"),
  pattern := """(\$\{()\})""".r,
  expandParameters := {
    val log: ManagedLogger = streams.value.log
    log.info(
      s"""|Parameter expansion
          |Configuration: $configuration
          |Extensions: ${extensions value}
          |Pattern: ${pattern value}
          |Dictionary: ${dictionary value}
       """.stripMargin
    )
  }
)

Note how dictionary is not initialised in baseResourceFilteringSettings, instead by default it will come from globalSettings.

Now we have taken care of our defaults and we have our base configuration, so we can proceed to "specialise" it by configuration scope using inConfig like so

lazy val localSettings = inConfig(Local)(defaults ++ Seq(
  dictionary ++= Profiles.local
) ++ baseResourceFilteringSettings)

Note how we have scoped baseResourceFilteringSettings to Local config, as well as dictionary ++= Profiles.local.

Now executing ;reload;local:expandParameters should output

[info] Parameter expansion
[info] Configuration: SettingKey(This / This / This / configuration)
[info] Extensions: List(conf, properties, xml)
[info] Pattern: (\$\{()\})
[info] Dictionary: Map(example -> local)

where we see Dictionary: Map(example -> local) as required.

Here is the complete code of ResourceFiltering

object ResourceFiltering extends AutoPlugin {

  override def trigger = AllRequirements

  sealed trait Keys {
    lazy val expandParameters = taskKey[Unit]("")

    lazy val extensions = settingKey[Seq[String]]("")
    lazy val pattern = settingKey[Regex]("")
    lazy val dictionary = settingKey[Map[String, String]]("")

    lazy val baseResourceFilteringSettings: Seq[Def.Setting[_]] = Seq(
      extensions := Seq("conf", "properties", "xml"),
      pattern := """(\$\{()\})""".r,
      expandParameters := {
        val log: ManagedLogger = streams.value.log
        log.info(
          s"""|Parameter expansion
              |Configuration: $configuration
              |Extensions: ${extensions value}
              |Pattern: ${pattern value}
              |Dictionary: ${dictionary value}
           """.stripMargin
        )
      }
    )
  }

  object autoImport extends Keys
  import autoImport._

  override lazy val globalSettings = Seq(
    dictionary := Map.empty
  )
}

Also consider moving configuration definitions into plugin like so

object ResourceFiltering extends AutoPlugin {

  override def trigger = AllRequirements

  sealed trait Keys {
    lazy val Prod = config("prod") extend Compile describedAs "Scope to build production packages."
    lazy val Stage = config("stage") extend Compile describedAs "Scope to build stage packages."
    lazy val Local = config("local") extend Compile describedAs "Scope to build local packages."

    lazy val expandParameters = taskKey[Unit]("")

    lazy val extensions = settingKey[Seq[String]]("")
    lazy val pattern = settingKey[Regex]("")
    lazy val dictionary = settingKey[Map[String, String]]("")

    lazy val baseResourceFilteringSettings: Seq[Def.Setting[_]] = Seq(
      extensions := Seq("conf", "properties", "xml"),
      pattern := """(\$\{()\})""".r,
      expandParameters := {
        val log: ManagedLogger = streams.value.log
        log.info(
          s"""|Parameter expansion
              |Configuration: $configuration
              |Extensions: ${extensions value}
              |Pattern: ${pattern value}
              |Dictionary: ${dictionary value}
           """.stripMargin
        )
      }
    )
  }

  object autoImport extends Keys
  import autoImport._

  override lazy val globalSettings = Seq(
    dictionary := Map.empty
  )

  override val projectSettings: Seq[Def.Setting[_]] =
    inConfig(Stage)(baseResourceFilteringSettings) ++
      inConfig(Prod)(baseResourceFilteringSettings) ++
      inConfig(Local)(baseResourceFilteringSettings)
}

This way we do not have to remember to add baseResourceFilteringSettings to config scope and can simply write

lazy val localSettings = inConfig(Local)(defaults ++ Seq(
  dictionary ++= Profiles.local
)

Upvotes: 2

Related Questions