Main concepts

Readers all the way down

An application such as the one shown in QuickStart is merely a case class having its dependencies modeled as attributes.

Similarly the application configuration is a case class containing every piece of information needed to build the Application. How do we connect the 2?

This is the purpose of the @reader annotation on Application. The @reader annotation generates a reader method in the companion object of Application:

import cats.data.Reader

object Application {

  implicit def reader[A](implicit r1: Reader[A, HttpServer], r2: Reader[A, Database]): Reader[A, Application] =
    Reader(a => Application(r1(a), r2(a)))

}

The declaration above states that:

This means that we can instantiate the Application from an ApplicationConfig, provided that we get a way to instantiate an HttpServer and a Database from an ApplicationConfig. The @reader annotation on HttpServer gives us:

import cats.data.Reader

object HttpServer {

  implicit def reader[A](implicit r1: Reader[A, HttpConfig]): Reader[A, HttpServer] =
    Reader(a => HttpServer(r1(a)))

}

How can we build an HttpConfig from the ApplicationConfig? This is the purpose of the @readers annotation on ApplicationConfig. This will generate concrete readers for each member of ApplicationConfig:

object ApplicationConfig {

  implicit def httpConfigReader: Reader[ApplicationConfig, HttpConfig] =
    Reader(_.httpConfig)

  implicit def dbConfigReader: Reader[ApplicationConfig, DbConfig] =
    Reader(_.dbConfig)
}

Note that this also works with nested configuration objects. So if ApplicationConfig contains other configuration objects, themselves holding pieces of the configuration, everything will work fine as long as you use the same @readers annotation on the nested types:

case class PortConfig(port: Int)
case class HostConfig(host: String)

// don't forget this annotation!
@readers
case class HttpConfig(port: PortConfig, host: HostConfig)

@readers
case class ApplicationConfig(httpConfig: HttpConfig)

From there the magic of implicit resolution will give us a valid Application.reader[ApplicationConfig]. Well, almost. You might have noticed that Database is an interface, not a case class. How can we instantiate such an interface from the application configuration?

Deal with interfaces

For interfaces we need a special annotation which will specify a concrete implementation to instantiate, the @defaultReader annotation. It generates the following reader:

object Database {
  implicit def reader[A]: Reader[A, PostgresDatabase] =
    PostgresDatabase.reader[A]
}

This Reader instance is simply delegating to the Reader instance of PostgresDatabase and we now know that we have such an instance because there is a @reader annotation on PostgresDatabase.

Warning

warning ** All the components must be totally side-effects free when instantiated! ** They must not start a database connection or a http server or even do some logging!

Indeed, when using Readers to create components, the same Database component can be instantiated from different paths in the application graph and become “duplicated” at that stage (see create singletons to fix this). So it is particularly important that the “start” of an application is done in a very controlled way: start the application.

Summary

In summary, to wire an application with Grafter you need to annotate:

There are a few more things you might need to know: