-
Notifications
You must be signed in to change notification settings - Fork 47
mapN and parMapN don't appear to compose with or #850
Description
Tested against 3.8.0
We have a configuration that shares a few parameters across environments and needs different parameters for local development and production. We are representing this as a sealed trait, which can be minimized to this:
sealed trait Config extends Product with Serializable
final case class DevConfig(a: Int, b: String) extends Config
final case class ProdConfig(a: Int, c: Int) extends ConfigOriginally, the ConfigValue was modeled by creating a ConfigValue[Effect, DevConfig] and a ConfigValue[Effect, ProdConfig], and composing them with or, which can be simplified to this:
val a = ciris.env("TEST_A").as[Int]
val b = ciris.env("TEST_B")
val c = ciris.env("TEST_C").as[Int]
val parMapNInsideOr: ConfigValue[Effect, Config] = {
val dev = (a, b).parMapN(DevConfig).widen[Config]
val prod = (a, c).parMapN(ProdConfig).widen[Config]
dev.or(prod)
}This doesn't seem to work, which appears to be related to the interplay between ciris.ConfigError#isMissing and ciris.ConfigError#or.
I had to kind of crack the package visibility to get visibility into why this was happening, by testing in a sandbox using a _root_.ciris package 😅 , and adding this hack:
implicit final class Helpers[F[_], A](private val cv: ConfigValue[F, A]) extends AnyVal {
def debug(tag: String): ConfigValue[F, A] = cv.transform { ce =>
println(s"[$tag] $ce")
ce
}
}Instrumenting the minimized version like this (switching to mapN for stable order, but the results are the same with parMapN):
val a = ciris.env("TEST_A").as[Int].debug("a")
val b = ciris.env("TEST_B").debug("b")
val c = ciris.env("TEST_C").as[Int].debug("c")
val mapNInsideOr: ConfigValue[Effect, Config] = {
val dev = (a, b).mapN(DevConfig).widen[Config].debug("DevConfig")
val prod = (a, c).mapN(ProdConfig).widen[Config].debug("ProdConfig")
dev.or(prod).debug("devOrProd")
}And setting these environment variables:
export TEST_A=1
export TEST_C=2Produces this output:
[a] Loaded(Loaded, Some(ConfigKey(environment variable TEST_A)), 1)
[b] Failed(Missing(ConfigKey(environment variable TEST_B)))
[DevConfig] Failed(And(Loaded, Missing(ConfigKey(environment variable TEST_B))))
[devOrProd] Failed(And(Loaded, Missing(ConfigKey(environment variable TEST_B))))
This seems like it should have attempted to load the prod config, which did not happen, and I think the reason for this is that the failure includes the successfully loaded a, so the check errors.forall(_.isMissing) in ciris.ConfigError#isMissing fails.
Interestingly, inverting this does work as expected (though this rapidly fails to scale with the complexity of the ADT):
val orInsideMapN: ConfigValue[Effect, Config] = {
(
a,
b.map(_.asLeft[Int]).or(c.map(_.asRight)).debug("bOrC")
).mapN { (a, bOrC) =>
bOrC.fold(
DevConfig(a, _),
ProdConfig(a, _)
)
}.debug("config")
}With the same environment as above, this produces:
[a] Loaded(Loaded, Some(ConfigKey(environment variable TEST_A)), 1)
[b] Failed(Missing(ConfigKey(environment variable TEST_B)))
[c] Loaded(Loaded, Some(ConfigKey(environment variable TEST_C)), 2)
[bOrC] Loaded(Or(Loaded, Missing(ConfigKey(environment variable TEST_B))), Some(ConfigKey(environment variable TEST_C)), Right(5))
[69] [config] Loaded(Loaded, None, ProdConfig(5,5))
I've uploaded the full code I used to test in this gist
Is this behavior intended?
If so, is there a better way to compose ADTs built from environment variables that I missed?