elderresearch / ssc

Simple Scala Config: Typesafe Config wrapped in a `Dynamic` blanket.

GitHub

Simple Scala Config

Typesafe Config wrapped in a Dynamic blanket.

Build Status Download Gitter

Overview

Typesafe Config is about as perfect as an application configuration system can be. HOCON is fantastic to work with, and the underlying Java implementation is both robust and consistent.

However, as a Scala developer, I'm not immune to wanting a little extra "sugar" mixed in with the robustly fantastic Java goodness. Simple Scala Config is an extremely thin wrapper (less than 100 SLOCs plus Readers) around Typesafe Config, allowing retrieval of configuration values using field-dereference syntax:

So instead of doing this:

// Load default config file
val conf = ConfigFactory.load()
// Get required config value
val timeout = conf.getDuration("akka.actor.typed.timeout")
// timeout: java.time.Duration = PT2S

// Get maybe existing config value
val abort = if (conf.hasPath("app.shouldAbort")) {
  conf.getBoolean("app.shouldAbort")
} else false
// abort: Boolean = false

You can do this:

// Load default config file
val conf = new SSConfig()
// conf: eri.commons.config.SSConfig = eri.commons.config.SSConfig@730f9b96

// Get required config value
val timeout = conf.akka.actor.typed.timeout.as[Duration]
// timeout: java.time.Duration = PT2S

// Get maybe existing config value
val abort = conf.app.shouldAbort.asOption[Boolean].getOrElse(false)
// abort: Boolean = false

Simple Scala Config is able to do this via the use of Scala's Dynamic facility. I find using this wrapper a bit more "idiomaticaly Scala" without being too far removed from the core library. The downside to using it is the configuration dereferencing looks like it is valid--due to the compiler accepting it--but in reality, dereferencing errors will be detected at runtime (just as they would with the core Typesafe Config), unless you always use the asOption[T] transform.

Using

The library is published via bintray, cross compiled against Scala 2.11.8 and 2.12.0-RC1. To use, add this to your sbt build definitions:

resolvers += Resolver.bintrayRepo("elderresearch", "OSS")
libraryDependencies += "com.elderresearch" %% "ssc" % "1.0.0"

It will transitively pull in the Typesafe Config and Scala Reflection libraries:

"com.typesafe" % "config" % "1.3.1"
"org.scala-lang" %  "scala-reflect"  % scalaVersion.value

Note: Requires Java 8, as does Typesafe Config >= 1.3.0

Examples

Basic

To use the default config loader in the root scope, instantiate or subclass SSConfig:

object MyConfig extends SSConfig()
// defined object MyConfig

// SSC adds support for Java `Path`
val tmp = MyConfig.myapp.tempdir.as[Path]
// tmp: java.nio.file.Path = /tmp/foo

// NB: Typesafe Config merges in system properties for you
val runtime = MyConfig.java.runtime.name.as[String]
// runtime: String = Java(TM) SE Runtime Environment

Nested Scope

To specify a nested scope, pass the path string into the constructor:

object AkkaConfig extends SSConfig("akka")
// defined object AkkaConfig

val akkaVersion = AkkaConfig.version.as[String]
// akkaVersion: String = 2.3.15

val timeout = AkkaConfig.actor.`creation-timeout`.as[Duration].getSeconds
// timeout: Long = 3

Custom Loading

To bypass the default config loading, pass in results from ConfigFactory (which also supports traditional Java properties files):

val props = new SSConfig(ConfigFactory.load("myprops.properties"))
// props: eri.commons.config.SSConfig = eri.commons.config.SSConfig@7b85d73

val version = props.version.as[String]
// version: String = "1.2.3"

Supported Types

Standard Typesafe Config Types

The standard Typesafe Config types are supported via a type parameter to the as[T] and asOption[T] methods:

// Define an example in-line config
val src =
  """
    | booleanVal = true
    | intVal = 3
    | doubleVal = 1e-200
    | longVal = 4878955355435272204
    | floatVal = 3.14
    | stringVal = "Ceci n'est pas une pipe."
    | durationVal = 400ns
    | sizeVal = 0.5GB
    | sizeVals = [ 0.5K, 1M, 2G, 3T, 4P ]
    | pathVal = /dev/null
    | fileVal = /dev/zero
    | addrVal = 192.168.34.42
    | uuidVal = fed6cc29-1cc4-46ed-9c04-56261730f44c
    | timeVals = [ 1m, 5m, 15m, 30m, 45m, 1h ]
    | phoneVal = "1-881-555-1212"
    | configVal = { a = 1, b = 2, c = 3 }
  """.stripMargin
val conf = new SSConfig(ConfigFactory.parseString(src))
val bv: Boolean = conf.booleanVal.as[Boolean]
// bv: Boolean = true

val iv: Int = conf.intVal.as[Int]
// iv: Int = 3

val dv: Double = conf.doubleVal.as[Double]
// dv: Double = 1.0E-200

val lv: Long = conf.longVal.as[Long]
// lv: Long = 4878955355435272204

val fv: Float = conf.floatVal.as[Float]
// fv: Float = 3.14

val sv: String = conf.stringVal.as[String]
// sv: String = Ceci n'est pas une pipe.

val tv: Duration = conf.durationVal.as[Duration]
// tv: java.time.Duration = PT0.0000004S

val mv: ConfigMemorySize = conf.sizeVal.as[ConfigMemorySize]
// mv: com.typesafe.config.ConfigMemorySize = ConfigMemorySize(500000000)

Access to the underlying Config object is also supported:

val cv: Config = conf.configVal.as[Config]
// cv: com.typesafe.config.Config = Config(SimpleConfigObject({"a":1,"b":2,"c":3}))

Extended Type Support

In addition to the types supported by Typesafe Config, converters for some additional Java types are provided (see Defining New Readers below for instructions on adding your own.):

val pv: Path = conf.pathVal.as[Path]
// pv: java.nio.file.Path = /dev/null

val zv: File = conf.fileVal.as[File]
// zv: java.io.File = /dev/zero

val av: InetAddress = conf.addrVal.as[InetAddress]
// av: java.net.InetAddress = /192.168.34.42

val uv: UUID = conf.uuidVal.as[UUID]
// uv: java.util.UUID = fed6cc29-1cc4-46ed-9c04-56261730f44c

Array/Sequence Values

HOCON supports array values. To retrieve this values, use as[Seq[T]] or asOption[Seq[T]]. For any type T you should be able to also retrieve Seq[T] if defined in an HOCON array.

val times = conf.timeVals.as[Seq[Duration]]
// times: Seq[java.time.Duration] = Buffer(PT1M, PT5M, PT15M, PT30M, PT45M, PT1H)

val sizes = conf.sizeVals.as[Seq[ConfigMemorySize]]
// sizes: Seq[com.typesafe.config.ConfigMemorySize] = Buffer(ConfigMemorySize(512), ConfigMemorySize(1048576), ConfigMemorySize(2147483648), ConfigMemorySize(3298534883328), ConfigMemorySize(4503599627370496))

Defining New Readers

Both standard and core types are extracted through the as[T] and asOption[T] methods via ConfigReader[T] and StringReader[T] type classes. To define a converter from a String to your desired type Foo, place in scope an implicit instance of StringReader[Foo]. For example, supposed we wanted to support reading in a phone number type:

case class PhoneNumber(countryCode: Int, areaCode: Int, exchange: Int, extension: Int)
// defined class PhoneNumber

implicit object PhoneReader extends StringReader[PhoneNumber] {
  val pat = "(\\d)-(\\d+)-(\\d+)-(\\d+)".r
  def apply(valueStr: String): PhoneNumber = {
    val pat(cc, ac, ex, et) = valueStr
    PhoneNumber(cc.toInt, ac.toInt, ex.toInt, et.toInt)
  }
}
// defined object PhoneReader

val phone = conf.phoneVal.as[PhoneNumber]
// phone: PhoneNumber = PhoneNumber(1,881,555,1212)

License

The license is Apache 2.0. See LICENSE.