Created
October 11, 2019 14:44
-
-
Save kirked/827d7640da4eaae6c267a7083d191f2c to your computer and use it in GitHub Desktop.
Scala syntax augmentation for Typesafe config
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.typesafe.config.{Config, ConfigException, ConfigFactory} | |
import java.time.{Duration => JDuration} | |
import scala.collection.JavaConverters._ | |
import scala.reflect.ClassTag | |
import scala.util.Try | |
object config { | |
class ConfigOps(config: Config) { | |
def booleanOption(path: String): Option[Boolean] = | |
if (config.hasPath(path)) Some(config.getBoolean(path)) else None | |
def booleanOrElse(path: String, default: Boolean): Boolean = | |
booleanOption(path) getOrElse default | |
def intOption(path: String): Option[Int] = | |
if (config.hasPath(path)) Some(config.getInt(path)) else None | |
def intOrElse(path: String, default: Int): Int = | |
intOption(path) getOrElse default | |
def intList(path: String): List[Int] = | |
if (config.hasPath(path)) config.getIntList(path).asScala.toList.map(_.intValue) else Nil | |
def stringOption(path: String): Option[String] = | |
if (config.hasPath(path)) Some(config.getString(path)) else None | |
def stringOrElse(path: String, default: String): String = | |
stringOption(path) getOrElse default | |
def stringList(path: String): List[String] = | |
if (config.hasPath(path)) config.getStringList(path).asScala.toList else Nil | |
def configOption(path: String): Option[Config] = | |
if (config.hasPath(path)) Some(config.getConfig(path)) else None | |
def configOrElse(path: String, default: Config): Config = | |
configOption(path) getOrElse default | |
def configOrEmpty(path: String): Config = | |
configOrElse(path, ConfigFactory.empty) | |
def configList(path: String): List[Config] = | |
if (config.hasPath(path)) config.getConfigList(path).asScala.toList else Nil | |
def configAsStringMap(path: String): Map[String, String] = { | |
if (config.hasPath(path)) config.getConfig(path).entrySet.asScala.foldLeft(Map.empty[String, String]) { | |
case (map, entry) => map + (entry.getKey -> entry.getValue.unwrapped.toString) | |
} | |
else Map.empty | |
} | |
def durationOption(path: String): Option[JDuration] = | |
if (config.hasPath(path)) Some(config.getDuration(path)) else None | |
def getInstance[A: ClassTag](key: String): A = { | |
val expectedClass = implicitly[ClassTag[A]].runtimeClass | |
/* | |
* Load the value by calling a factory method inside a Scala object. | |
* | |
* Keep in mind that Scala `val`s are JVM methods. | |
*/ | |
def loadFromFactoryMethod(objectClass: Class[_], obj: AnyRef): Option[A] = { | |
val fullKey = s"$key.factory-method" | |
for { | |
methodName <- stringOption(fullKey) | |
method <- Try(objectClass.getMethod(methodName)).toOption | |
result <- Try(method.invoke(obj).asInstanceOf[A]).toOption | |
} yield result | |
} | |
def acceptableClass(klass: Class[_]): Boolean = | |
(expectedClass isAssignableFrom klass) | |
def is(obj: AnyRef): Option[A] = { | |
if (acceptableClass(obj.getClass)) Some(obj.asInstanceOf[A]) | |
else None | |
} | |
/* | |
* A Scala object is the singleton instance of the value, | |
* or provides a factory method for the value. | |
*/ | |
def loadFromObject(): Option[A] = { | |
val fullKey = s"$key.object" | |
val resultOpt = for { | |
objectName <- stringOption(fullKey) | |
objectClass <- loadClass(objectName) | |
instance <- Try(objectClass.getField("MODULE$").get(null)).toOption | |
} yield { | |
is(instance) orElse loadFromFactoryMethod(objectClass, instance) | |
} | |
resultOpt.flatten | |
} | |
def loadFromNewClassInstance(): Option[A] = { | |
val fullKey = s"$key.class" | |
for { | |
fqcn <- stringOption(fullKey) | |
klass <- loadClass(fqcn) if acceptableClass(klass) | |
instance <- Try(klass.getConstructor().newInstance().asInstanceOf[Object]).toOption | |
casted <- is(instance) | |
} yield casted | |
} | |
(loadFromObject orElse loadFromNewClassInstance) match { | |
case Some(a: A) => a | |
case None => | |
val where = config.origin.description | |
val msg = s"one of '$key.class'; '$key.object' (optionally with '$key.factory-method')" | |
throw new ConfigException.Missing(s"$where at $key, expected an instance specification, $msg") | |
} | |
} | |
private[this] def loadClass(fqcn: String): Option[Class[_]] = { | |
Try(Thread.currentThread.getContextClassLoader.loadClass(fqcn)).toOption | |
} | |
} | |
implicit def configSyntax(config: Config) = new ConfigOps(config) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment