Search code examples
scalafunctional-programmingpureconfig

How to hide configuration management from the main function?


I have my entry point like this:

def main(args: Array[String]): Unit = {
  pureconfig.loadConfig[Conf] match {
    case Right(conf) => doStuff(conf)
    case Left(fail) => lectureUserAboutStuff(fail)
  }
}

So it looks like the main purpose of my program is to load a configuration, which is not true, the core of my program is to doStuff.

How to expressively represent this and hide undercover the configuration?

I would like something like this:

def main(args: Array[String]): Unit = {
  doStuff(conf)(failHandler)
}

In which is clear that the failure handling and the configuration loading is just accessory.


Solution

  • One of the most common way to use PureConfig from the main method of your program is to fail fast in case of error. The API provides loadConfigOrThrow which returns the configuration if the loading has been successful and throws a ConfigReaderException otherwise:

    import pureconfig.loadConfigOrThrow
    
    def main(args: Array[String]): Unit = {
      val conf = loadConfigOrThrow[Conf] // will throw if Conf cannot be loaded
      doStuff(conf)
    }
    

    The reason why pureconfig.loadConfig returns an Either is because loading configuration can fail. Either models both the failure case, with Left, and the success case, with Right. This is good for a library, because you never know where loadConfig will be called and consequently you don't want to throw an exception. While this is true in general, when loadConfig is used from a "surface" method, e.g. main, throwing exception or just exiting in case of config loading failure makes sense and that's why pureconfig provides loadConfigOrThrow.

    In case you don't like throwing exception or you prefer a custom failure handler, like lectureUserAboutStuff, you can create a helper:

    def loadConfig(): Option[Conf] = {
      val errorOrConf = pureconfig.loadConfig[Conf]
      errorOrConf.left.foreach(lectureUserAboutStuff)
      errorOrConf.toOption
    }
    
    def main(args: Array[String]): Unit =
      loadConfig().foreach(doStuff)
    

    One last thing to keep in mind is that a minor issue with this code is that the exit code of the application is not affected by configuration failures. If you want to set a exit code for when the configuration cannot be loaded, then change main to

    val configReaderFailureErrorCode = 42
    
    private def configFailureExit() =
      sys.exit(configReaderFailureErrorCode)
    
    def main(args: Array[String]): Unit =
      loadConfig().fold(configFailureExit())(doStuff)