Created
December 4, 2020 17:12
-
-
Save fsarradin/1db833861a7a80b0bbddf6605d2cf936 to your computer and use it in GitHub Desktop.
A configuration loader to that is able to get data from different sources with different key case schemas (zio-config 1.0.0-RC28)
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 java.lang.{Boolean => JBoolean} | |
import scala.util.{Failure, Success, Try} | |
import com.typesafe.config._ | |
import zio.config.PropertyTree.{Leaf, Record, Sequence} | |
import zio.config.{ConfigSource, PropertyTree, ReadError} | |
import zio.{IO, config} | |
/** | |
* Load a configuration from different sources based on the given | |
* configuration schema. | |
* | |
* Configuration is obtained in this order: | |
* 1. from command line arguments (the most visible one) | |
* 2. from environment variable | |
* 3. from service `application.conf` (with all the default values) | |
* | |
* Examples: | |
* - args:`--myConfig=111` & env:`MYCONFIG=222` => myConfig=111 | |
* - env:`MYCONFIG=222` & hocon:`myConfig:222` => myConfig=111 | |
* - args:`--myConfig=111` & hocon:`myConfig:222` => myConfig=111 | |
* | |
* It is possible for environment variables to come with a predefined | |
* prefix. This feature allows to get a specific namespace for your | |
* environment variables by using this prefix. See | |
* [[loadConfiguration]] for more details. | |
* | |
* Note: to ensure ensure source/data reconciliation (ie. matching) | |
* whatever case schema is used, every keys are converted | |
* lowercase. Eg. `--myConfig` is the same as `--myconfig` or | |
* `--MYCONFIG`. | |
* | |
* @param schema configuration schema | |
* @tparam A type of the configuration | |
*/ | |
case class ServiceConfigurationLoader[A <: ServiceParameters]( | |
schema: zio.config.ConfigDescriptor[A] | |
) { | |
import scala.jdk.CollectionConverters._ | |
import ServiceConfigurationLoader._ | |
private def noError[E]: ReadError[E] = ReadError.ListErrors(List.empty) | |
/* As parameters may appear in any kind of cases (upper case for sys | |
* env and camel case for the rest), we convert parameter names in | |
* lower case. | |
*/ | |
private val effectiveSchema = schema.mapKey(_.toLowerCase) | |
/** | |
* Retrieve configuration values. | |
* | |
* For environment variables, you may provide you own prefix. Eg. if | |
* your config key is named `myConfig` and you provide `MY_SERVICE_` | |
* as prefix, then the descriptor will match with the environment | |
* variable `MY_SERVICE_MYCONFIG`. | |
* | |
* @param prefix prefix used to namespaced you environment variables | |
* @param args command line arguments | |
* @return configuration values or an error message | |
*/ | |
def loadConfiguration( | |
prefix: String, | |
args: List[String] | |
): IO[ReadError[String], A] = | |
for { | |
configSource <- configurationSource(prefix, args) | |
serviceConfig <- configurationValuesFrom(configSource, effectiveSchema) | |
} yield serviceConfig | |
private def configurationValuesFrom( | |
configSource: ConfigSource, | |
configSchema: zio.config.ConfigDescriptor[A] | |
): IO[ReadError[String], A] = | |
IO.fromEither(config.read[A](configSchema.from(configSource))) | |
private def configurationSource( | |
prefix: String, | |
args: List[String] | |
): IO[ReadError[String], ConfigSource] = | |
for { | |
cmdConf <- configFromArgs(args) | |
sysConf <- configFromSystemEnv(prefix) | |
ressConf <- configFromResources | |
} yield cmdConf orElse sysConf orElse ressConf | |
private def configFromResources: IO[ReadError[String], ConfigSource] = | |
IO.fromEither(fromTypeSafeConfig(ConfigFactory.defaultApplication())) | |
private def fromTypeSafeConfig( | |
input: => com.typesafe.config.Config | |
): Either[ReadError[String], ConfigSource] = | |
Try { | |
input | |
} match { | |
case Failure(exception) => | |
Left(ReadError.SourceError(message = exception.getMessage)) | |
case Success(value) => | |
getPropertyTree(value) match { | |
case Left(value) => Left(ReadError.SourceError(message = value)) | |
case Right(value) => | |
Right( | |
ConfigSource.fromPropertyTree( | |
value, | |
"hocon", | |
zio.config.LeafForSequence.Invalid | |
) | |
) | |
} | |
} | |
private def getPropertyTree( | |
input: com.typesafe.config.Config | |
): Either[String, PropertyTree[String, String]] = { | |
def loopBoolean(value: Boolean) = Leaf(value.toString) | |
def loopNumber(value: Number) = Leaf(value.toString) | |
val loopNull = PropertyTree.empty | |
def loopString(value: String) = Leaf(value) | |
def loopList(values: List[ConfigValue]) = Sequence(values.map(loopAny)) | |
def loopConfig(config: ConfigObject) = | |
Record(config.asScala.toVector.map { case (key, value) => | |
// *** lower case conversion of the key | |
key.toLowerCase() -> loopAny(value) | |
}.toMap) | |
def loopAny(value: ConfigValue): PropertyTree[String, String] = | |
value.valueType() match { | |
case ConfigValueType.OBJECT => loopConfig(value.asInstanceOf[ConfigObject]) | |
case ConfigValueType.LIST => loopList(value.asInstanceOf[ConfigList].asScala.toList) | |
case ConfigValueType.BOOLEAN => loopBoolean(value.unwrapped().asInstanceOf[JBoolean]) | |
case ConfigValueType.NUMBER => loopNumber(value.unwrapped().asInstanceOf[Number]) | |
case ConfigValueType.NULL => loopNull | |
case ConfigValueType.STRING => loopString(value.unwrapped().asInstanceOf[String]) | |
} | |
Try(loopConfig(input.root())) match { | |
case Failure(t) => | |
Left( | |
"Unable to form the zio.config.PropertyTree from Hocon string." + | |
" This may be due to the presence of explicit usage of nulls in hocon string. " + | |
t.getMessage | |
) | |
case Success(value) => Right(value) | |
} | |
} | |
private def configFromSystemEnv( | |
prefix: String | |
): IO[ReadError[String], ConfigSource] = | |
IO.effectTotal(sys.env).map { env => | |
val configMap = | |
env.view | |
.filterKeys(_.startsWith(prefix)) | |
// *** lower case conversion of the key | |
.map { case (k, v) => k.substring(prefix.length).toLowerCase() -> v } | |
.toMap | |
ConfigSource.fromMap( | |
configMap, | |
source = "system environment", | |
keyDelimiter = Some(EnvVarKeyDelimiter), | |
valueDelimiter = Some(EnvVarValueDelimiter) | |
) | |
} | |
private def configFromArgs( | |
args: List[String] | |
): IO[ReadError[String], ConfigSource] = { | |
val convertedArgs = | |
args.map { arg => | |
// lift converts limited Array[String] to infinite Array[Option[String]] | |
val kv = arg.split("=", 2).lift | |
// *** lower case conversion of the key | |
val key = kv(0).map(_.toLowerCase()) | |
val result = | |
for { | |
k <- key | |
value <- kv(1) | |
} yield s"$k=$value" | |
result | |
.orElse(key) | |
// should always have a value | |
.get | |
} | |
val config = ConfigSource.fromCommandLineArgs( | |
convertedArgs, | |
keyDelimiter = Some(CommandLineKeyDelimiter) | |
) | |
IO(config).orElseFail(noError) | |
} | |
} | |
object ServiceConfigurationLoader { | |
val CommandLineKeyDelimiter = '.' | |
val EnvVarKeyDelimiter = '_' | |
val EnvVarValueDelimiter = ',' | |
} | |
/** | |
* Base parameter trait to extends by every services. | |
*/ | |
trait ServiceParameters { | |
val serviceName: String | |
val kafka: KafkaConfig | |
} | |
case class KafkaConfig( | |
bootstrapServers: String, | |
schemaRegistryUrl: String, | |
performCleanup: Boolean = false | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment