Configuration

The Config effect algebra is part of the frees-config module, and it allows obtaining values from configuration files at any point in the monadic computation flow of a Freestyle program. The current implementation exposes the most important combinators found in Typesafe Config.

To enable this integration, you can depend on frees-config:

libraryDependencies += "io.frees" %% "frees-config" % "0.8.2"

Operations

The set of abstract operations of the ConfigM algebra and Config object are specified as follows:

sealed trait Config {
  def hasPath(path: String): Boolean
  def config(path: String): Option[Config]
  def string(path: String): Option[String]
  def boolean(path: String): Option[Boolean]
  def int(path: String): Option[Int]
  def double(path: String): Option[Double]
  def stringList(path: String): List[String]
  def duration(path: String, unit: TimeUnit): Option[Long]
}

@free sealed trait ConfigM {
  def load: FS[Config]
  def empty: FS[Config]
  def parseString(s: String): FS[Config]
  def loadAs[T: ConfigDecoder]: FS[T]
  def parseStringAs[T: ConfigDecoder](s: String): FS[T]
}

The frees-config module contains a built-in handler which you can use out of the box with target types such as Try, Future, monix.eval.Task, fs2.Task, and any other type in general that can satisfy a MonadError[M, Throwable] constraint.

Example by using Case Classy

Freestyle integrates with the Case Classy library to make it easy to decode Typesafe config - or other untyped structured data - into case class hierarchies.

In the following example, we will review the steps we have to take to use the Case Classy library along with config algebra in a pure program.

Provided we have a configuration file in our classpath following the Typesafe config conventions called application.conf with the following value:

disallowedStates = ["reverted", "closed"]

Before we do anything else, we’ll need to add the usual set of imports from Freestyle and cats to create our algebras:

import freestyle.free._
import freestyle.free.implicits._
import cats._
import cats.implicits._

import scala.util.Try

We’ll create a case class containing all the config values required by our program:

case class AppConfig(disallowedStates: List[String])
// defined class AppConfig

Additionally, we have to write the config decoder for that case class:

import classy.config._
// import classy.config._

implicit val configDecoder: ConfigDecoder[AppConfig] =
  readConfig[List[String]]("disallowedStates").map(AppConfig.apply)
// configDecoder: classy.config.ConfigDecoder[AppConfig] = Instance(classy.Decoder$$Lambda$7992/874024934@21490e5b)

We will define a simple algebra with a stub handler that returns a list of issue states for illustration purposes:

@free trait IssuesService {
  def states: FS[List[String]]
}
// defined trait IssuesService
// defined object IssuesService

implicit val issuesServiceHandler: IssuesService.Handler[Try] = new IssuesService.Handler[Try] {
  def states: Try[List[String]] = Try(List("open", "reverted", "in progress", "closed"))
}
// issuesServiceHandler: IssuesService.Handler[scala.util.Try] = $anon$1@2e6162df

At this point, we may aggregate our issues algebra with any other algebras in a @module which will automatically compose monadic actions derived from using different algebras:

import freestyle.free.config._
// import freestyle.free.config._

import freestyle.free.config.implicits._
// import freestyle.free.config.implicits._

@module trait App {
  val issuesService: IssuesService
  val config: ConfigM
}
// defined trait App
// defined object App

And finally, we can create a program and compose our config algebra in the same monadic comprehension along with our issues service. As you may observe, we’re calling loadAs[AppConfig] method to populate config values into our case class.

def filteredStates[F[_]](implicit app : App[F]): FreeS[F, List[String]] =
  for {
    currentStates <- app.issuesService.states
    config <- app.config.loadAs[AppConfig]
  } yield currentStates.filterNot(config.disallowedStates.contains)
// filteredStates: [F[_]](implicit app: App[F])freestyle.free.FreeS[F,List[String]]

Once we have a program, we can interpret it to our desired runtime, in this case scala.util.Try:

filteredStates[App.Op].interpret[Try]
// res0: scala.util.Try[List[String]] = Success(List(open, in progress))

Example by using Typesafe Config

If we decide to use the Typesafe Config object instead of the Case Classy library, we won’t need to create a case class or a config decoder.

So, given the example described above, the required code will be pretty similar except we’ll load the Config object and use the methods that it provides:

def filteredStates[F[_]](implicit app : App[F]): FreeS[F, List[String]] =
  for {
    currentStates <- app.issuesService.states
	  config <- app.config.load
	  disallowedStates = config.stringList("disallowedStates")
  } yield currentStates.filterNot(disallowedStates.getOrElse(Nil).contains)
// filteredStates: [F[_]](implicit app: App[F])freestyle.free.FreeS[F,List[String]]