Posts Class-up Your Config
Post
Cancel

Class-up Your Config

In the previous example there was some repetition.
Parsing the config to a person was a bit complex, as well as creating a Path instance, UUID instance and so on.

A bigger problem is that it is not straightforward to write a system test that ensures that the config keys are read properly in the code.
For example, think what would happen if we change application-id to app-id in the config but not in the code (or vice versa), or change min-duration type in the code to Long.

Add PureConfig as a dependency

We will use PureConfig to class-up our config.
Go to its website and add the latest version to your build.sbt, e.g:

1
2
3
4
libraryDependencies ++= Seq(
  // more dependencies here
  "com.github.pureconfig" %% "pureconfig" % "0.12.0",
)

and then reload the sbt shell, or Import Changes in IntelliJ so it will download the jar for you and help you with code completion (it will also reload the sbt shell behind the scenes).

Model the config

Create a new Config case class under com.example.playground.configuration.
By convention, pure-config expects that each kebab-case-key in application.conf will be a camelCaseMember in the config class:

1
2
3
4
5
6
7
8
9
10
case class Config(
  name: String,
  host: URL,
  port: Int,
  hotels: List[String],
  minDuration: FiniteDuration,
  applicationId: UUID,
  sshDirectory: Path,
  developer: Person
)

Load the config

create a new injector module com.example.playground.configuration.Module that will provide the config using pure-config to load it:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.playground.configuration

import com.google.inject.{AbstractModule, Provides, Singleton}
import pureconfig.ConfigSource
import pureconfig.generic.auto._

class Module extends AbstractModule {
  val config: Config = ConfigSource.default.loadOrThrow[Config]

  @Provides()
  @Singleton()
  def configProvider: Config = config
}

Enable the module above by adding it to Play’s config at conf/application.conf under play.modules.enabled, e.g your play’s blob in the config may look like this:

1
2
3
4
play {
	http.secret.key="MePzCIzgeI8jhPfWg8RPGCFvLobM6K8bnCabNgSdDBc="
	modules.enabled += "com.example.playground.configuration.Module"
}

Use the config

Modify app/controllers/HomeController.scala to require the Config VO instead of the Configuration class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.example.playground.configuration.Config
//..
class HomeController @Inject()(cc: ControllerComponents, config: Config) extends AbstractController(cc) {
  //..
  def index() = Action { implicit request: Request[AnyContent] =>
    val message =
      s"""
         |name: ${config.name}
         |host: ${config.host}
         |port: ${config.port}
         |hotels: ${config.hotels}
         |minDuration: ${config.minDuration}
         |applicationId: ${config.applicationId}
         |sshDirectory: ${config.sshDirectory}
         |developer: ${config.developer}
         |""".stripMargin
    println(message)
    Ok(message)
  }
}

Notice that you also get code completion:

config with code completion

Run the app by running run inside the sbt shell and browse to http://localhost:9000/.
You should see the config.