serval

Simple Environment Read VALues (yes, I tried my best to come up with a name that's also an animal) - Scala library for reading variables defined in ENV, inspired by Ciris which is awesome and I cannot recommend it enough. I wanted to replicate the experience of using Ciris to read ENV variables but in applications that (unfortunately) don't use cats-effect. Serval has zero dependencies and it's sole purpose is to read a Map[String, String] (like sys.env) into user-defined config case class(es).

Usage

This library is written in Scala 3 (mostly because I wanted to try it out) but can be used from 2.13.6+

To use the latest version in a Scala 3 project, include the following in your build.sbt:

libraryDependencies ++= Seq(
  "io.github.sakulk" %% "serval-core" % "0.4.0"
)

For Scala 2.13.6+ you need to add the -Ytasty-reader to your projects scalacOptions and include the following dependency:

libraryDependencies ++= Seq(
  "io.github.sakulk" %% "serval-legacy" % "0.4.0"
)

Minimal example

Scala 3:

case class MyConfig(foo: String, bar: Int, baz: String)

object MyConfig:
  import serval.read.{given, *}
  given EnvRead[MyConfig] =
    (
      env("CONFIG_FOO").or(env("CONFIG_FOO_2")),
      env("CONFIG_BAR").as[Int],
      env("CONFIG_BAZ").default("bazinga")
    ).mapN(MyConfig.apply)

serval.load[MyConfig](
  // use sys.env to load real ENV variables
  Map(
    "CONFIG_FOO" -> "foo",
    "CONFIG_BAR" -> "42"
  )
)
// res0: Either[EnvLoadException, MyConfig] = Right(
//   value = MyConfig(foo = "foo", bar = 42, baz = "bazinga")
// )

serval.load[MyConfig](
  Map("CONFIG_BAR" -> "bar")
)
// res1: Either[EnvLoadException, MyConfig] = Left(
//   value = serval.EnvLoadException: Failed to load config from environment:
//  - CONFIG_FOO|CONFIG_FOO_2: Variable missing
//  - CONFIG_BAR: Failed to parse Int from "bar"
// )

Scala 2.13:

case class MyConfig(foo: String, bar: Int, baz: String)

object MyConfig {
  import serval.read.legacy._
  implicit val envRead: EnvRead[MyConfig] =
    (
      env("CONFIG_FOO").or(env("CONFIG_FOO_2")),
      env("CONFIG_BAR").as[Int],
      env("CONFIG_BAZ").default("bazinga")
    ).mapN(MyConfig.apply)
}

serval.legacy.load[MyConfig](
  // use sys.env to load real ENV variables
  Map(
    "CONFIG_FOO" -> "foo",
    "CONFIG_BAR" -> "42"
  )
)
// res3: Either[EnvLoadException, MyConfig] = Right(
//   value = MyConfig(foo = "foo", bar = 42, baz = "bazinga")
// )