ficus configuration load generic

1.8k Views Asked by At

Loading a ficus configuration like

loadConfiguration[T <: Product](): T = {
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
import net.ceedubs.ficus.Ficus._
val config: Config = ConfigFactory.load()
config.as[T]

fails with:

Cannot generate a config value reader for type T, because it has no apply method in a companion object that returns type T, and it doesn't have a primary constructor

when instead directly specifying a case class instead of T i.e. SomeClass it works just fine. What am I missing here?

1

There are 1 best solutions below

9
On BEST ANSWER

Ficus uses the type class pattern, which allows you to constrain generic types by specifying operations that must be available for them. Ficus also provides type class instance "derivation", which in this case is powered by a macro that can inspect the structure of a specific case class-like type and automatically create a type class instance.

The problem in this case is that T isn't a specific case class-like type—it's any old type that extends Product, which could be something nice like this:

case class EasyToDecode(a: String, b: String, c: String)

But it could also be:

trait X extends Product {
  val youWillNeverDecodeMe: String
}

The macro you've imported from ArbitraryTypeReader has no idea at this point, since T is generic here. So you'll need a different approach.

The relevant type class here is ValueReader, and you could minimally change your code to something like the following to make sure T has a ValueReader instance (note that the T: ValueReader syntax here is what's called a "context bound"):

import net.ceedubs.ficus.Ficus._
import net.ceedubs.ficus.readers.ValueReader
import com.typesafe.config.{ Config, ConfigFactory }

def loadConfiguration[T: ValueReader]: T = {
  val config: Config = ConfigFactory.load()
  config.as[T]

}

This specifies that T must have a ValueReader instance (which allows us to use .as[T]) but says nothing else about T, or about where its ValueReader instance needs to come from.

The person calling this method with a concrete type MyType then has several options. Ficus provides instances that are automatically available everywhere for many standard library types, so if MyType is e.g. Int, they're all set:

scala> ValueReader[Int]
res0: net.ceedubs.ficus.readers.ValueReader[Int] = net.ceedubs.ficus.readers.AnyValReaders$$anon$2@6fb00268

If MyType is a custom type, then either they can manually define their own ValueReader[MyType] instance, or they can import one that someone else has defined, or they can use generic derivation (which is what ArbitraryTypeReader does).

The key point here is that the type class pattern allows you as the author of a generic method to specify the operations you need, without saying anything about how those operations will be defined for a concrete type. You just write T: ValueReader, and your caller imports ArbitraryTypeReader as needed.