Search code examples
scalacase-class

Scala : How to use Global Config case class across application


I am new to scala, just started with my scala first application.

I have defined my config file under the resources folder, application.conf

  projectname{
     "application" {
     "host":127.0.0.1
     "port":8080
    }
 }

I have wrote one config parser file to parse from config file to case class

    case class AppConfig (config: Config) {
      val host = config.getString("projectname.application.host")
      val port = config.getInt("projectname.application.port")
    }

In my grpc server file, i have declared config as

    val config = AppConfig(ConfigFactory.load("application.conf"))

I want to use this config variable across application, rather than loading application.conf file everytime.

I want to have one bootstrap function which will parse this config one time, making it available across application


Solution

  • You can do this automatically with PureConfig.

    Add Pure Config to you build.sbt with:

    libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.11.0"
    

    and reload the sbt shell and update your dependencies.

    Now, let's say you have the following resource.conf file:

    host: example.com
    port: 80
    user: admin
    password: admin_password
    

    You can define a case class called AppConfig:

    case class AppConfig(
        host: String,
        port: Int,
        user: String,
        password: String
    )
    

    And create an instance of it, populated with the application config, using the loadConfig method:

    import pureconfig.generic.auto._
    
    val errorsOrConfig: Either[ConfigReaderFailures, AppConfig] = pureconfig.loadConfig[AppConfig]
    

    This returns Either an error or your AppConfig, depending on the values in the config itself.
    For example, if the value of port above will be eighty, instead of 80, you will get a detailed error, saying that the second config line (with the port: eighty) contained a string, but the only valid expected type is a number:

    ConfigReaderFailures(
        ConvertFailure(
            reason = WrongType(
            foundType = STRING,
            expectedTypes = Set(NUMBER)
        ),
        location = Some(
            ConfigValueLocation(
               new URL("file:~/repos/example-project/target/scala-2.12/classes/application.conf"),
               lineNumber = 2
            )
        ),
        path = "port"
        )
    )
    
    

    You can use loadConfigOrThrow if you want to get AppConfig instead of an Either.

    After you load this config once at the start of your application (as close as possible to your main function), you can use dependency injection to pass it along to all the other classes, simply by passing the AppConfig in the constructor.

    If you would like to wire up your Logic class (and other services) with the config case class using MacWire, as Krzysztof suggested in one of his options, you can see my answer here.

    The plain example (without MacWire), looks like this:

    package com.example
    
    import com.example.config.AppConfig
    
    object HelloWorld extends App {
     val config: AppConfig = pureconfig.loadConfigOrThrow[AppConfig]
     val logic = new Logic(config)
    }
    
    class Logic(config: AppConfig) {
       // do something with config
    }
    

    Where the AppConfig is defined in AppConfig.scala

    package com.example.config
    
    case class AppConfig(
        host: String,
        port: Int,
        user: String,
        password: String
    )
    

    As a bonus, when you use this config variable in your IDE, you will get code completion.

    Moreover, your config may be built from the supported types, such as String, Boolean, Int, etc, but also from other case classes that are build from the supported types (this is since a case class represents a value object, that contains data), as well as lists and options of supported types.
    This allows you to "class up" a complicated config file and get code completion. For instance, in application.conf:

    name: hotels_best_dishes
    host: "https://example.com"
    port: 80
    hotels: [
      "Club Hotel Lutraky Greece",
      "Four Seasons",
      "Ritz",
      "Waldorf Astoria"
    ]
    min-duration: 2 days
    currency-by-location {
      us = usd
      england = gbp
      il = nis
    }
    accepted-currency: [usd, gbp, nis]
    application-id: 00112233-4455-6677-8899-aabbccddeeff
    ssh-directory: /home/whoever/.ssh
    developer: {
      name: alice,
      age: 20
    }
    

    Then define a config case class in your code:

    import java.net.URL
    import java.util.UUID
    import scala.concurrent.duration.FiniteDuration
    import pureconfig.generic.EnumCoproductHint
    import pureconfig.generic.auto._
    
    case class Person(name: String, age: Int)
    
    sealed trait Currency
    case object Usd extends Currency
    case object Gbp extends Currency
    case object Nis extends Currency
    
    object Currency {
      implicit val currencyHint: EnumCoproductHint[Currency] = new EnumCoproductHint[Currency]
    }
    
    case class Config(
      name: String,
      host: URL,
      port: Int,
      hotels: List[String],
      minDuration: FiniteDuration,
      currencyByLocation: Map[String, Currency],
      acceptedCurrency: List[Currency],
      applicationId: UUID,
      sshDirectory: java.nio.file.Path,
      developer: Person
    )
    

    And load it with:

    val config: Config = pureconfig.loadConfigOrThrow[Config]