
zio.config.ConfigDescriptorModule.scala Maven / Gradle / Ivy
package zio.config
import com.github.ghik.silencer.silent
import scala.collection.mutable.{ListBuffer, Map => MutableMap}
import scala.reflect.ClassTag
@silent("Unused import")
trait ConfigDescriptorModule extends ConfigSourceModule { module =>
import ConfigDescriptorAdt._
import VersionSpecificSupport._
sealed trait ConfigDescriptor[A] { self =>
@deprecated("Use .to[B] if the transformation is to a case class. If not use use transform methods", since = "2.0")
def apply[B](app: A => B, unapp: B => Option[A]): ConfigDescriptor[B] =
ConfigDescriptorAdt.transformOrFailDesc(
this,
(a: A) => Right[String, B](app(a)),
unapp(_)
.fold[Either[String, A]](
Left("Unable to create case class instance")
)(Right(_))
)
/**
* Convert a `ConfigDescriptor[A]` to a config descriptor of a case class
*
* This works when `A` is a single value and `B` is a single parameter case class with
* the same type of parameter, or if `A` is an tuple and `B` is a case class with
* matching number of parameters and the same types.
*
* See the following example of reading a `USERNAME` which is a String and `PORT` which is an Int,
* and load it to a case class `Config`:
*
* {{{
* final case class Config(userName: String, port: Int)
*
* object Config {
* val dbConfig: ConfigDescriptor[Config] = (string("USERNAME") zip int("PORT")).to[Config]
* }
* }}}
*
* Note that the alternative of passing `(Config.apply, Config.unapply)` to transform the config descriptor
* is not compatible with Scala 3.
*/
def to[B <: Product](implicit conv: TupleConversion[B, A]): ConfigDescriptor[B] =
self.transform(
conv.from,
conv.to
)
/**
* `??` is an alias to `describe` which allows us to inject additional
* documentation to the configuration parameters.
*
* Example:
*
* {{{ val port = int("PORT") ?? "database port" }}}
*
* A more detailed example:
*
* Here is a program that describes (or a ConfigDescriptor that represents)
* reading a `USERNAME` which is a String and `PORT` which is an Int,
* and load it to a case class `Config`
*
* {{{
* final case class Config(userName: String, port: Int)
*
* object Config {
* val dbConfig: ConfigDescriptor[Config] =
* (string("USERNAME") zip int("PORT"))(Config.apply, Config.unapply)
* }
* }}}
*
* Later on you decided to annotate each one of them with extra documentation,
* which is later seen in error messages if config retrieval
* is a failure, and it's also used while documenting your configuration
* using `ConfigDocsModule`
*
* {{{
* val dbConfigWithDoc: ConfigDescriptor[Config] =
* (string("USERNAME") ?? "db username" zip
* int("PORT") ?? "db port"
* ).to[Config]
* }}}
*
* If you try and read this config from an empty source,
* it emits an error message with the details you provided.
*
* {{{
* import zio.config._, ConfigDescriptor._
* read(Config.databaseConfig from ConfigSource.fromMap(Map.empty))
*
* }}}
*
* returns:
*
* {{{
* ╥
* ╠══╦══╗
* ║ ║ ║
* ║ ║ ╠─MissingValue
* ║ ║ ║ path: PORT
* ║ ║ ║ Details: db port, value of type int
* ║ ║ ▼
* ║ ║
* ║ ╠─MissingValue
* ║ ║ path: USERNAME
* ║ ║ Details: db username, value of type string
* ║ ▼
* ▼
*
* }}}
*
* Or, you can also use a common documentation for an entire set of config parameters.
*
* {{{
* val detailedConfigDescriptor: ConfigDescriptor[Config] =
* configDescriptor ?? "Configuration related to database"
* }}}
*/
final def ??(description: String): ConfigDescriptor[A] =
describe(description)
/**
* zip is a ConfigDescriptor builder. We know `ConfigDescriptor`
* is a program that describes the retrieval of a set of configuration parameters.
*
* Below given is a `ConfigDescriptor` that describes the retrieval of a single config.
*
* {{{
* val port: ConfigDescriptor[String] = string("PORT")
* }}}
*
* However, in order to retrieve multiple configuration parameters,
* we can make use of `zip`.
*
* Example:
*
* {{{
* final case class Config(userName: String, port: Int)
*
* object Config {
* val dbConfig: ConfigDescriptor[Config] =
* (string("USERNAME") zip int("PORT")).to[Config]
* }
*
* }}}
*/
final def zip[B, C](
that: => ConfigDescriptor[B]
)(implicit Z: InvariantZip.WithOut[A, B, C]): ConfigDescriptor[C] =
ConfigDescriptorAdt
.zipDesc(self, that)
.transform[Z.Out](a => Z.combine(a._1, a._2), zOut => (Z.projectLeft(zOut), Z.projectRight(zOut)))
/**
* `<>` is an alias to function `orElse`.
* This is used to represent fall-back logic when we describe config retrievals.
*
* Example:
*
* {{{
* val config: ConfigDescriptor[String] = string("token") <> string("password")
* }}}
*
* This is a description that represents the following:
* Try to retrieve the value of a parameter called "token",
* or else try to retrieve the value of parameter called "password"
*
* We know `ConfigDescriptor` is a program that describes the retrieval
* of a set of configuration parameters.
*
* In the below example, we can either depend on a configuration called
* `password` or a `token` both being of the same type, in this case, a String.
*
* Example:
*
* {{{
*
* final case class Config(tokenOrPassword: String, port: Int)
*
* object Config {
* val databaseConfig: ConfigDescriptor[Config] =
* (string("token") <> string("password") zip int("PORT"))(Config.apply, Config.unapply)
* }
*
* }}}
*/
final def <>(that: => ConfigDescriptor[A]): ConfigDescriptor[A] =
self orElse that
/**
* `<*>` is an alias to function `zip`
*/
final def <*>[B](that: => ConfigDescriptor[B]): ConfigDescriptor[(A, B)] =
self zip that
/**
* `<+>` is an alias to function `orElseEither`.
* This is used to represent fall-back logic when we describe config retrievals. Unlike `orElse`, the
* the fall-back config parameter can have a different type in `orElseEither`.
*
* Example:
*
* {{{
*
* val config: ConfigDescriptor[Either[Int, String]] = int("MONTH") <+> string("MONTH")
*
* }}}
*
* This is a description that represents the following:
* Try to retrieve the value of a MONTH as an `Int`, and if there is a format error, try and retrieve it as a `String`.
*
* Detail:
*
* We know `ConfigDescriptor` is a program that describes the retrieval of a set of configuration parameters.
* In the below example, we can either depend on a configuration called `password`
* or a `token` both being of the same type, in this case, a String.
*
* Example:
*
* Given:
*
* {{{
*
* final case class BasicAuth(username: String, password: String)
* final case class OAuth(clientId: String, secret: String)
*
* val basicAuth: ConfigDescriptor[BasicAuth] =
* (string("USERNAME") zip string("PASSWORD")).to[BasicAuth]
*
* val oAuth: ConfigDescriptor[OAuth] =
* (string("CLIENT_ID") zip string("SECRET")).to[OAuth]
*
* val myConfig: ConfigDescriptor[Either[BasicAuth, OAuth]] =
* basicAuth <+> oAuth
*
* }}}
*
* then,
*
* {{{
*
* val source = ConfigSource.fromMap(Map("USERNAME" -> "abc", "PASSWORD" -> "cde")
*
* read(myConfig from source)
*
* }}}
*
* returns:
*
* {{{
*
* Left(BasicAuth("abc", "def")
*
* }}}
*
* Similarly,
*
* {{{
*
* val source = ConfigSource.fromMap(Map("CLIENT_ID" -> "xyz", "SECRET" -> "afg==")
*
* read(myConfig from source)
*
* }}}
*
* returns:
*
* {{{
*
* Right(OAuth("xyz", "afg==")
*
* }}}
*/
final def <+>[B](
that: => ConfigDescriptor[B]
): ConfigDescriptor[Either[A, B]] =
self orElseEither that
/**
* `default` function allows us to inject default values to existing config
*
* Example:
*
* {{{ val port = int("PORT").default(8080) }}}
*
* A more detailed example:
*
* Here is a program that describes (or a ConfigDescriptor that represents) reading a `USERNAME` which is a String and `PORT` which is an Int,
* and load it to a case class `Config`
*
* {{{
* final case class Config(userName: String, port: Int)
*
* object Config {
* val dbConfig: ConfigDescriptor[Config] =
* (string("USERNAME") zip int("PORT").default(8080)).to[Config]
* }
* }}}
*
* In the above case, if username is missing, then it prints out an error, however if PORT is missing, it falls back to 8080.
*
* In fact you can give a default to an entire config
*
* For example:
*
* {{{
* final case class Config(userName: String, port: Int)
*
* object Config {
* val dbConfig: ConfigDescriptor[Config] =
* (string("USERNAME") zip int("PORT")).to[Config].default(Config("jon", 8080))
* }
*
* }}}
*
* Sometimes this can be used along with automatic derivation supported through zio-config-magnolia.
*
* {{{
*
* import zio.config.magnolia._, zio.config._, ConfigDescriptor._
*
* final case class Config(userName: String, port: Int)
*
* object Config {
* val dbConfig: ConfigDescriptor[Config] =
* descriptor[Config].default(Config("jon", 8080))
* }
*
* // This is a typical example where we mix auto derivation with manual definitions.
*
* }}}
*/
final def default(value: A): ConfigDescriptor[A] =
ConfigDescriptorAdt.defaultDesc(self, value) ?? s"default value: $value"
/**
* `describe` function allows us to inject additional documentation to the configuration parameters.
*
* Example:
*
* {{{ val port = int("PORT") ?? "database port" }}}
*
* A more detailed example:
*
* Here is a program that describes (or a ConfigDescriptor that represents) reading a `USERNAME` which is a String and a `PORT` which is an Int,
* and load it to a case class `Config`
*
* {{{
* final case class Config(userName: String, port: Int)
*
* object Config {
* val dbConfig: ConfigDescriptor[Config] =
* (string("USERNAME") zip int("PORT")).to[Config]
* }
* }}}
*
* Later on you decided to annotate each one of them with extra documentation, which is later seen in error messages if config retrieval
* is a failure, and it's also used while documenting your configuration using `ConfigDocsModule`
*
* {{{
* val dbConfigWithDoc: ConfigDescriptor[Config] =
* (string("USERNAME") ?? "db username" zip int("PORT") ?? "db port").to[Config]
* }}}
*
* If you try and read this config from an empty source, it emits an error message with the details you provided.
*
* {{{
* import zio.config._, ConfigDescriptor._
*
* read(Config.databaseConfig from ConfigSource.fromMap(Map.empty))
*
* }}}
*
* returns:
*
* {{{
* ╥
* ╠══╦══╗
* ║ ║ ║
* ║ ║ ╠─MissingValue
* ║ ║ ║ path: PORT
* ║ ║ ║ Details: db port, value of type int
* ║ ║ ▼
* ║ ║
* ║ ╠─MissingValue
* ║ ║ path: USERNAME
* ║ ║ Details: db username, value of type string
* ║ ▼
* ▼
*
* }}}
*
* Or, you can also use a common documentation for an entire set of config parameters.
*
* {{{
* val detailedConfigDescriptor: ConfigDescriptor[Config] =
* configDescriptor ?? "Configuration related to database"
* }}}
*/
final def describe(description: String): ConfigDescriptor[A] =
ConfigDescriptorAdt.describeDesc(self, description)
/**
* Attach a source to the `ConfigDescriptor`.
*
* Example: {{{ val config = string("PORT") from ConfigSource.fromMap(Map.empty) }}}
*
* `config` is a description that says there is a key called `PORT` in constant map source.
* You can use the description to read the config
*
* {{{
*
* val either: ZIO[Any, ReadError[String], String] = read(config)
*
* }}}
*
* You can also tag a source per config field, or one global source to an entire config.
*
* {{{
* final case class Config(userName: String, port: Int)
* object Config {
* val dbConfig: ConfigDescriptor[Config] =
* (string("USERNAME") zip int("PORT")).to[Config]
* }
* }}}
*
* In the above example, `dbConfig` is not associated with any source. By default the source will be empty.
*
* To attach a config (especially during a read operation) is as easy as:
*
* {{{
* read(dbConfig from ConfigSource.fromMap(Map("USERNAME" -> "afs", "PORT" -> "8080"))
* // Right(Config("afs", 8080))
* }}}
*
* Obviously, source can be attached independently.
*
* {{{
*
* val configSource1: ConfigSource = ???
* val configSource2: ConfigSource = ???
*
* val dbConfig =
* (string("USERNAME") from configSource1 zip int("PORT")).to[Config] from configSource2
* }}}
*
* In the above case `read(dbConfig)` implies, zio-config tries to fetch `USERNAME` from configSource1, and if it
* fails (i.e, missing value) it goes and try with the global config which is `configSource2`.
* PORT will be fetched from configSource2.
*
* You can also try various sources for each field.
*
* {{{
*
* val configSource1: ConfigSource = ??? // Example: ConfigSource.fromMap(...)
* val configSource2: ConfigSource = ??? // Example: ConfigSource.fromTypesafeConfig(...)
*
* val dbConfig =
* (string("USERNAME") from configSource1.orElse(configSource2) zip
* int("PORT") from configSource2.orElse(configSource1)).to[Config] from configSource2
* }}}
*/
final def from(that: ConfigSource): ConfigDescriptor[A] =
self.updateSource(_.orElse(that))
/**
* mapKey allows user to convert the keys in a ConfigDescriptor.
*
* Example:
*
* Consider you have a config that looks like this
*
* {{{
*
* case class Config(url: String, port: Int)
*
* object Config {
*
* val config = (string("dbUrl") zip int("dbPort")).to[Config]
* }
*
* val source = Map(
* "DB_URL" -> "abc.com",
* "DB_PORT" -> "9090"
* )
*
* read(Config.config from ConfigSource.fromMap(source))
* // will fail since the source doesn't have the keys dbUrl and dbPort, but it has only DB_URL and DB_PORT
*
* }}}
*
* The above config retrieval fails since the keys dbUrl and dbPOrt exist, but it has only DB_URL and DB_PORT.
* In this situation, instead of rewriting the config we can do
*
* {{{
*
* import zio.config._, ConfigDescriptor._
*
* read(Config.config.mapKey(key => toSnakeCase(key).toUpperCase) from ConfigSource.fromMap(source))
* // Right(Config("abc.com", 9090))
*
* }}}
*/
def mapKey(f: K => K): ConfigDescriptor[A] = {
val descriptors: MutableMap[ConfigDescriptor[_], ConfigDescriptor[_]] =
MutableMap()
def loop[B](config: ConfigDescriptor[B]): ConfigDescriptor[B] =
config match {
case c @ Lazy(thunk) =>
val res = thunk()
descriptors.get(c) match {
case Some(value) => value.asInstanceOf[ConfigDescriptor[B]]
case None =>
val result = Lazy(() => loop(res))
descriptors.update(c, result)
result
}
case Source(source, propertyType) => Source(source, propertyType)
case DynamicMap(conf) => DynamicMap(loop(conf))
case Nested(path, conf) =>
Nested(f(path), loop(conf))
case Optional(conf) => Optional(loop(conf))
case Sequence(conf) => Sequence(loop(conf))
case Describe(conf, message) => Describe(loop(conf), message)
case Default(conf, value) => Default(loop(conf), value)
case TransformOrFail(conf, f, g) =>
TransformOrFail(loop(conf), f, g)
case Zip(conf1, conf2) => Zip(loop(conf1), loop(conf2))
case OrElseEither(conf1, conf2) =>
OrElseEither(loop(conf1), loop(conf2))
case OrElse(value1, value2) =>
OrElse(loop(value1), loop(value2))
}
loop(self)
}
/**
* `optional` function allows us to tag a configuration parameter as optional.
* It implies, even if it's missing configuration will be a success.
*
* Example:
*
* {{{ val port: ConfigDescriptor[Option[Int]] = int("PORT").optional }}}
*
* A more detailed example:
*
* Here is a program that describes (or a ConfigDescriptor that represents) reading a `USERNAME`
* which is a String and `PORT` which is an Int,
* and load it to a case class `Config`
*
* {{{
* final case class Config(userName: String, port: Option[Int])
*
* object Config {
* val dbConfig: ConfigDescriptor[Config] =
* (string("USERNAME") zip int("PORT").optional).to[Config]
* }
* }}}
*
* The fact that it is an optional in error messages if config retrieval
* is a failure, and it's also used while documenting your configuration using `ConfigDocsModule`
*
* {{{
* val dbConfigWithDoc: ConfigDescriptor[Config] =
* (string("USERNAME") ?? "db username" zip int("PORT") ?? "db port").to[Config]
* }}}
*
* {{{
* import zio.config._, ConfigDescriptor._
*
* val source = ConfigSource.fromMap(Map("USERNAME" -> "af"))
*
* read(Config.databaseConfig from source)
*
* }}}
*
* returns:
*
* {{{
* Config("af", None)
* }}}
*
* Similarly,
*
* {{{
* val source = ConfigSource.fromMap(Map("USERNAME" -> "af", "PORT" -> "8888"))
*
* read(Config.databaseConfig from source)
*
* }}}
*
* returns:
*
* {{{
* Config("af", Some(8888))
* }}}
*
* However, if you have given `PORT`, but it's not an integer,
* then it fails giving you the error details.
*
* Within the error message, it will also specify the fact
* that the parameter is an optional parameter,
* giving you an indication that you can either fix the parameter,
* or you can completely skip this parameter.
*
* Example:
*
* {{{
* import zio.config._, ConfigDescriptor._
*
* val source = ConfigSource.fromMap(Map("USERNAME" -> "af", "PORT" -> "abc"))
*
* read(Config.databaseConfig from source)
*
* }}}
*
* returns:
*
* {{{
*
* ╥
* ╠══╗
* ║ ║
* ║ ╠─FormatError
* ║ ║ cause: Provided value is abc, expecting the type int
* ║ ║ path: PORT
* ║ ▼
* ▼
*
* }}}
*
* Another interesting behaviour, but we often forget about optional parameters is when there is
* a presence of a part of the set of the config parameters
* representing a product, where the product itself is optional.
*
* Example:
*
* {{{
* final case class DbConfig(port: Int, host: String)
*
* object DbConfig {
* val dbConfig: ConfigDescriptor[Option[DbConfig]] =
* (int("PORT") zip string("HOST")).to[DbConfig].optional
* }
*
* }}}
*
* In this case if "PORT" is present in the source, but "HOST" is absent,
* then config retrieval will be a failure and not `None`.
* Similarly, if "HOST" is present but "PORT" is absent,
* the config retrieval will be a failure and not `None`.
*
* If both of the parameters are absent in the source, then the
* config retrieval will be a success and the output will be
* `None`. If both of them is present, then output will be `Some(DbConfig(..))`
*/
final def optional: ConfigDescriptor[Option[A]] =
ConfigDescriptorAdt.optionalDesc(self) ?? "optional value"
/**
* `orElse` is used to represent fall-back logic when we describe config retrievals.
*
* Example:
*
* {{{
* val config: ConfigDescriptor[String] = string("token") <> string("password")
* }}}
*
* This is a description that represents the following:
* Try to retrieve the value of a parameter called "token", or else try to retrieve the value of parameter called "password"
*
* We know `ConfigDescriptor` is a program that describes the retrieval of a set of configuration parameters.
* In the below example, we can either depend on a configuration called `password` or a `token` both being of the same type, in this case, a String.
*
* Example:
*
* {{{
*
* final case class Config(tokenOrPassword: String, port: Int)
*
* object Config {
* val databaseConfig: ConfigDescriptor[Config] =
* (string("token") <> string("password") zip int("PORT")).to[Config]
* }
*
* }}}
*
* Note: `orElse` is different from `orElseEither`.
*
* While `orElse` fall back to parameter which is of the same type of the original config parameter,
* `orElseEither` can fall back to a different type giving us `Either[A, B]`.
*
* `orElse` will be useful in retrieving configuration that are represented as coproducted (sealed trait). However,
* it may become fairly verbose, such that usage `zio-config-magnolia` to derive the config automatically, will become a reasonable
* alternative.
*/
final def orElse(that: => ConfigDescriptor[A]): ConfigDescriptor[A] =
ConfigDescriptorAdt.orElseDesc(self, that)
/**
* `orElseEither` is used to represent fall-back logic when we describe config retrievals. Unlike `orElse`,
* the fall-back config parameter can have a different type in `orElseEither`.
*
* Example:
*
* {{{
*
* val config: ConfigDescriptor[Either[Int, String]] = int("MONTH") <+> string("MONTH")
*
* }}}
*
* This is a description that represents the following:
* Try to retrieve the value of a MONTH as an `Int`, and if there is a format error, try and retrieve it as a `String`.
*
* Detail:
*
* We know `ConfigDescriptor` is a program that describes the retrieval of a set of configuration parameters.
* In the below example, we can either depend on a configuration called `password`
* or a `token` both being of the same type, in this case, a String.
*
* Example:
*
* Given:
*
* {{{
*
* final case class BasicAuth(username: String, password: String)
* final case class OAuth(clientId: String, secret: String)
*
* val basicAuth: ConfigDescriptor[BasicAuth] =
* (string("USERNAME") zip string("PASSWORD")).to[BasicAuth]
*
* val oAuth: ConfigDescriptor[OAuth] =
* (string("CLIENT_ID") zip string("SECRET")).to[OAuth]
*
* val myConfig: ConfigDescriptor[Either[BasicAuth, OAuth]] =
* basicAuth <+> oAuth
*
* }}}
*
* then,
*
* {{{
*
* val source = ConfigSource.fromMap(Map("USERNAME" -> "abc", "PASSWORD" -> "cde")
*
* read(myConfig from source)
*
* }}}
*
* returns:
*
* {{{
*
* Left(BasicAuth("abc", "def")
*
* }}}
*
* Similarly,
*
* {{{
*
* val source = ConfigSource.fromMap(Map("CLIENT_ID" -> "xyz", "SECRET" -> "afg==")
*
* read(myConfig from source)
*
* }}}
*
* returns:
*
* {{{
*
* Right(OAuth("xyz", "afg==")
*
* }}}
*/
final def orElseEither[B](
that: => ConfigDescriptor[B]
): ConfigDescriptor[Either[A, B]] =
ConfigDescriptorAdt.orElseEitherDesc(self, that)
/**
* Untag all sources associated with a `ConfigDescriptor`.
*
* As we know `ConfigDescriptor` represents a program that describes the retrieval of config parameters.
* In fact, the same program can be used to write back the config in various shapes.
*
* Either case, a `ConfigDescriptor` can exist without a `Source` attached.
*
* Example:
*
* {{{
* val stringConfig: ConfigDescriptor[String] = string("USERNAME")
* }}}
*
* Later on we can read the config by attaching a source.
*
* {{{
*
* val result = read(stringConfig from ConfigSource.fromMap(Map.empty))
*
* }}}
*
* However, you can attach a source to the configDescriptor at an earlier stage.
*
* For example:
*
* {{{
*
* val stringConfig: ConfigDescriptor[String] =
* string("USERNAME") from ConfigSource.fromMap(Map.empty)
*
* }}}
*
* Later on, you can simply read it using:
*
* {{{
* val result = read(stringConfig)
* }}}
*
* Using `unsourced`, you can now untag the source from `stringConfig`.
*
* {{{
*
* val stringConfigNoSource: ConfigDescriptor[String] =
* stringConfig.unsourced
* }}}
*
* This can be useful in test cases where you want to remove a source and attach a different source.
*
* Example:
*
* {{{
*
* val testSource: ConfigSource = ConfigSource.fromMap(Map(..))
*
* val result = stringConfig.unsourced from testSource
*
* }}}
*/
final def unsourced: ConfigDescriptor[A] =
self.updateSource(_ => ConfigSource.empty)
/**
* `updateSource` can update the source of an existing `ConfigDescriptor`
*
* Example:
*
* {{{
*
* val configSource1 = ConfigSource.fromMap(Map.empty)
* val configSource2 = ConfigSource.fromMap(Map("USERNAME" -> "abc"))
*
* val config = string("USERNAME") from configSource1
*
* val updatedConfig = config updateSource (_ orElse configSource2)
*
* }}}
*
* In the above example, we update the existing ConfigDescriptor to try another ConfigSource called configSource2,
* if it fails to retrieve the value of USERNAME from configSource1.
*/
final def updateSource(
f: ConfigSource => ConfigSource
): ConfigDescriptor[A] = {
val descriptors: MutableMap[ConfigDescriptor[_], ConfigDescriptor[_]] =
MutableMap()
def loop[B](
config: ConfigDescriptor[B]
): ConfigDescriptor[B] =
config match {
case c @ Lazy(thunk) =>
val res = thunk()
descriptors.get(c) match {
case Some(value) =>
value.asInstanceOf[ConfigDescriptor[B]]
case None =>
val result = Lazy(() => loop(res))
descriptors.update(c, result)
result
}
case Source(source, propertyType) =>
Source(f(source), propertyType)
case DynamicMap(conf) =>
DynamicMap(loop(conf))
case Nested(path, conf) =>
Nested(path, loop(conf))
case Optional(conf) =>
Optional(loop(conf))
case Sequence(conf) =>
Sequence(loop(conf))
case Describe(conf, message) =>
Describe(loop(conf), message)
case Default(conf, b) =>
Default(loop(conf), b)
case TransformOrFail(conf, f, g) =>
TransformOrFail(loop(conf), f, g)
case Zip(left, right) =>
Zip(loop(left), loop(right))
case OrElseEither(left, right) =>
OrElseEither(loop(left), loop(right))
case OrElse(value1, value2) =>
OrElse(loop(value1), loop(value2))
}
loop(self)
}
/**
* Fetch all the sources associated with a ConfigDescriptor.
*/
lazy val sources: Set[ConfigSource] = {
val sourceUpdatedConfigDescriptors: ListBuffer[ConfigDescriptor[_]] =
ListBuffer()
def loop(cfg: ConfigDescriptor[_], set: Set[ConfigSource]): Set[ConfigSource] = {
def runLoop(config: ConfigDescriptor[_], sourceOfConfig: Option[ConfigSource]): Set[ConfigSource] =
if (sourceUpdatedConfigDescriptors.contains(config)) {
set
} else {
sourceUpdatedConfigDescriptors += config
loop(config, sourceOfConfig.fold(set)(source => Set(source) ++ set))
}
def runLoopForBoth(left: ConfigDescriptor[_], right: ConfigDescriptor[_]): Set[ConfigSource] =
(sourceUpdatedConfigDescriptors.contains(left), sourceUpdatedConfigDescriptors.contains(right)) match {
case (true, true) =>
set
case (true, false) =>
sourceUpdatedConfigDescriptors += right
loop(right, set)
case (false, true) =>
sourceUpdatedConfigDescriptors += left
loop(left, set)
case (false, false) =>
sourceUpdatedConfigDescriptors ++= ListBuffer(left, right)
loop(left, set) ++ loop(right, set)
}
cfg match {
case Default(config, _) =>
runLoop(config, None)
case Describe(config, _) =>
runLoop(config, None)
case DynamicMap(config) =>
runLoop(config, None)
case Sequence(config) =>
runLoop(config, None)
case Lazy(get) =>
runLoop(get(), None)
case Nested(_, config) =>
runLoop(config, None)
case Optional(config) =>
runLoop(config, None)
case OrElse(left, right) =>
runLoopForBoth(left, right)
case OrElseEither(left, right) =>
runLoopForBoth(left, right)
case Source(source, _) =>
set ++ Set(source)
case TransformOrFail(config, _, _) =>
runLoop(config, None)
case Zip(left, right) =>
runLoopForBoth(left, right)
}
}
loop(self, Set.empty)
}
/**
* Given `A` and `B`, `f: A => B`, and `g: B => A`, then
* `transform` allows us to transform a `ConfigDescriptor[A]` to `ConfigDescriptor[B]`.
*
* Example :
* `transform` is useful especially when you define newtypes.
*
* {{{
* final case class Port(port: Int) extends AnyVal
*
* val config: ConfigDescriptor[Port] =
* int("PORT").transform[Port](Port.apply, _.int)
* }}}
*
* While `to: A => B` (in this case, `Int => Port`) is used to read to a `Port` case class,
* `from: B => A` (which is, `Port => Int`) is used when we want to write `Port` directly to a source representation.
*
* Example:
*
* {{{
*
* import zio.config.typesafe._ // as toJson is available only through zio-config-typesafe module
*
* val writtenBack: Either[String, PropertyTree[String, String]] = write(config, Port(8888))
*
* val jsonRepr: Either[String, String] = writtenBack.map(_.toJson) // { "port" : "8888" }
* val mapRepr: Either[String, Map[String, String]] = writtenBack.map(_.flattenString()) // Map("port" -> "8888")
* }}}
*/
final def transform[B](to: A => B, from: B => A): ConfigDescriptor[B] =
self.transformOrFail(a => Right(to(a)), b => Right(from(b)))
/**
* Given `A` and `B`, `transformOrFail` function is used to convert a `ConfigDescriptor[A]` to `ConfigDescriptor[B]`.
*
* It is important to note that both `to` and `fro` is fallible, allowing us to represent
* almost all possible relationships.
*
* Example:
*
* Let's define a simple `ConfigDescriptor`, that talks about retrieving a `S3Path` ( a bucket and prefix in AWS s3).
* Given you want to retrieve an S3Path from ConfigSource. Given a string, converting it to S3Path can fail, and even converting
* S3Path to a String can fail as well.
*
* {{{
* import java.time.DateTimeFormatter
* import java.time.LocalDate
*
* final case class S3Path(bucket: String , prefix: String, partition: LocalDate) {
* def convertToString(partitionPattern: String): Either[String, String] =
* Try { DateTimeFormatter.ofPattern(partitionPattern).format(partition) }.toEither
* .map(dateStr => s"\${bucket}/\${prefix}/\${dateStr}").swap.map(_.getMessage).swap
* }
*
* object S3Path {
* def fromStr(s3Path: String): Either[String, S3Path] = {
* val splitted = s3Path.split("/").toList
*
* if (splitted.size > 3)
* Left("Invalid s3 path")
* else
* for {
* bucket <- splitted.headOption.toRight("Empty s3 path")
* prefix <- splitted.lift(1).toRight("Invalid prefix, or empty prefix in s3 path")
* partition <- splitted.lift(2).toRight("Empty partition").flatMap(dateStr => LocalDate.parse(dateStr))
* } yield S3Path(bucket, prefix, partition)
* }
* }
*
* val s3PathConfig: ConfigDescriptor[S3Path] =
* string("S3_PATH").transformEither[S3Path](S3Path.fromStr, _.convertToString("yyyy-MM-dd"))
*
* }}}
*/
final def transformOrFail[B](
to: A => Either[String, B],
from: B => Either[String, A]
): ConfigDescriptor[B] =
ConfigDescriptorAdt.transformOrFailDesc(self, to, from)
final def transformOrFailLeft[B](f: A => Either[String, B])(g: B => A): ConfigDescriptor[B] =
self.transformOrFail(f, b => Right(g(b)))
final def transformOrFailRight[B](
f: A => B,
g: B => Either[String, A]
): ConfigDescriptor[B] =
self.transformOrFail(t => Right(f(t)), (g))
private[config] def zipWith[B, Out, C](that: => ConfigDescriptor[B])(to: Out => Either[String, C])(
from: C => Either[String, Out]
)(implicit Z: InvariantZip.WithOut[A, B, Out]): ConfigDescriptor[C] =
(self zip that)
.transformOrFail(to, from)
}
trait ConfigDescriptorFunctions {
/**
* `collectAll` is an alias to `sequence`. In Functional Programming terms,
* it is a Traverse implementation for ConfigDescriptor.
* In other words, it allows us to convert a `List` of `ConfigDescriptor[A]`
* to `ConfigDescriptor[List[A]]`.
*
* Example:
*
* {{{
* final case class Variables(variable1: Int, variable2: Option[Int])
*
* object CollectAllExample extends App with EitherImpureOps {
* val listOfConfig: List[ConfigDescriptor[Variables]] =
* List("GROUP1", "GROUP2", "GROUP3", "GROUP4")
* .map(
* group =>
* (int(s"\${group}_VARIABLE1") zip int(s"\${group}_VARIABLE2").optional).to[Variables]
* )
*
* val configOfList: ConfigDescriptor[List[Variables]] =
* collectAll(listOfConfig.head, listOfConfig.tail: _*)
*
* val map =
* Map(
* "GROUP1_VARIABLE1" -> "1",
* "GROUP1_VARIABLE2" -> "2",
* "GROUP2_VARIABLE1" -> "3",
* "GROUP2_VARIABLE2" -> "4",
* "GROUP3_VARIABLE1" -> "5",
* "GROUP3_VARIABLE2" -> "6",
* "GROUP4_VARIABLE1" -> "7"
* )
*
* // loadOrThrow here is only for the purpose of example
* val result: List[Variables] = read(configOfList from ConfigSource.fromMap(map, "constant")).loadOrThrow
*
* val written: PropertyTree[String, String] = write(configOfList, result).loadOrThrow
*
* assert(
* result == List(Variables(1, Some(2)), Variables(3, Some(4)), Variables(5, Some(6)), Variables(7, None))
* )
* }}}
*/
def collectAll[A](head: => ConfigDescriptor[A], tail: ConfigDescriptor[A]*): ConfigDescriptor[List[A]] =
tail.reverse
.map(lazyDesc(_))
.foldLeft[ConfigDescriptor[(A, List[A])]](
lazyDesc(head)
.transform((a: A) => (a, Nil), (b: (A, List[A])) => b._1)
)((b: ConfigDescriptor[(A, List[A])], a: ConfigDescriptor[A]) =>
(b.zipWith[A, (A, List[A], A), (A, List[A])](a)({ case (first, tail, a) =>
Right((first, a :: tail))
}) {
case (first, (head :: tail)) => Right((first, tail, head))
case _ => Left("Invalid list length")
})
)
.transformOrFailRight(
{ case (a, t) => a :: t },
l => l.headOption.toRight("Invalid list length").map(h => (h, l.tail))
)
/**
* enumeration allows user to up-cast all the subtypes to its super type defined by `D`.
* This is mainly useful in defining `coproducts` (`sealed trait`)
*
* Example:
* {{{
* sealed trait D
*
* case class A(a: String) extends D
* case class B(b: Int) extends D
* case class C(c: Double) extends D
*
* val config: ConfigDescriptor[D] =
* enumeration[D](
* string("a")(A.Apply, A.unapply),
* int("b")(B.apply, B.unapply),
* double("c")(C.apply, C.unapply)
* )
* }}}
*
* Currently enumeration supports to a maximum of 9 terms. If you have more terms, use `orElse`
* to combine the terms.
*
* {{{
* enumeration[D](a, b, c, d, e, f, g, h) orElse enumeration[D](i, j, k)
* }}}
*
* NOTE:
*
* Use zio-config-magnolia for better compile time safety when it comes to `sealed trait`,
* as it has strong compile time behaviour and makes sure all subtypes are being handled.
* On the other hand, `enumeration` doesn't complain at compile time if you forgot
* to pass the config descriptor of any of the subtype.
*
* Example:
*
* {{{
* import zio.config.magnolia._
*
* val config = descriptor[D]
* }}}
*/
def enumeration[D] = new PartiallyAppliedEnumeration[D]
class PartiallyAppliedEnumeration[D] {
def apply[X <: D](
desc1: ConfigDescriptor[X]
)(implicit tag: ClassTag[X]): ConfigDescriptor[D] =
desc1.transformOrFail(
(a: X) => Right(a: D),
(d: D) =>
d match {
case a: X => Right(a)
case _ =>
Left(
s"""
"Cannot write the config back because instance type doesn't match.
This can also happen if ConfigDescriptor is not aware of a particular subtype.
Make sure all subtypes (or the type being written back) has been passed to enumeration while creating ConfigDescriptor.
Or use auto derivation in zio-config-magnolia for better static/compile-time safety if its a sealed-trait"
"""
)
}
)
def apply[A <: D: ClassTag, B <: D: ClassTag](
desc1: ConfigDescriptor[A],
desc2: ConfigDescriptor[B]
): ConfigDescriptor[D] =
apply(desc1) orElse apply(desc2)
def apply[A <: D: ClassTag, B <: D: ClassTag, C <: D: ClassTag](
desc1: ConfigDescriptor[A],
desc2: ConfigDescriptor[B],
desc3: ConfigDescriptor[C]
): ConfigDescriptor[D] =
apply(desc1, desc2).orElse(apply[C](desc3))
def apply[A <: D: ClassTag, B <: D: ClassTag, C <: D: ClassTag, E <: D: ClassTag](
desc1: ConfigDescriptor[A],
desc2: ConfigDescriptor[B],
desc3: ConfigDescriptor[C],
desc4: ConfigDescriptor[E]
): ConfigDescriptor[D] =
apply(desc1, desc2, desc3) orElse apply[E](desc4)
def apply[A <: D: ClassTag, B <: D: ClassTag, C <: D: ClassTag, E <: D: ClassTag, F <: D: ClassTag](
desc1: ConfigDescriptor[A],
desc2: ConfigDescriptor[B],
desc3: ConfigDescriptor[C],
desc4: ConfigDescriptor[E],
desc5: ConfigDescriptor[F]
): ConfigDescriptor[D] =
apply(desc1, desc2, desc3, desc4) orElse apply(desc5)
def apply[
A <: D: ClassTag,
B <: D: ClassTag,
C <: D: ClassTag,
E <: D: ClassTag,
F <: D: ClassTag,
G <: D: ClassTag
](
desc1: ConfigDescriptor[A],
desc2: ConfigDescriptor[B],
desc3: ConfigDescriptor[C],
desc4: ConfigDescriptor[E],
desc5: ConfigDescriptor[F],
desc6: ConfigDescriptor[G]
): ConfigDescriptor[D] =
apply(desc1, desc2, desc3, desc4, desc5) orElse apply(desc6)
def apply[
A <: D: ClassTag,
B <: D: ClassTag,
C <: D: ClassTag,
E <: D: ClassTag,
F <: D: ClassTag,
G <: D: ClassTag,
H <: D: ClassTag
](
desc1: ConfigDescriptor[A],
desc2: ConfigDescriptor[B],
desc3: ConfigDescriptor[C],
desc4: ConfigDescriptor[E],
desc5: ConfigDescriptor[F],
desc6: ConfigDescriptor[G],
desc7: ConfigDescriptor[H]
): ConfigDescriptor[D] =
apply(desc1, desc2, desc3, desc4, desc5, desc6) orElse apply(desc7)
def apply[
A <: D: ClassTag,
B <: D: ClassTag,
C <: D: ClassTag,
E <: D: ClassTag,
F <: D: ClassTag,
G <: D: ClassTag,
H <: D: ClassTag,
I <: D: ClassTag
](
desc1: ConfigDescriptor[A],
desc2: ConfigDescriptor[B],
desc3: ConfigDescriptor[C],
desc4: ConfigDescriptor[E],
desc5: ConfigDescriptor[F],
desc6: ConfigDescriptor[G],
desc7: ConfigDescriptor[H],
desc8: ConfigDescriptor[I]
): ConfigDescriptor[D] =
apply(desc1, desc2, desc3, desc4, desc5, desc6, desc7) orElse apply(desc8)
def apply[
A <: D: ClassTag,
B <: D: ClassTag,
C <: D: ClassTag,
E <: D: ClassTag,
F <: D: ClassTag,
G <: D: ClassTag,
H <: D: ClassTag,
I <: D: ClassTag,
J <: D: ClassTag
](
desc1: ConfigDescriptor[A],
desc2: ConfigDescriptor[B],
desc3: ConfigDescriptor[C],
desc4: ConfigDescriptor[E],
desc5: ConfigDescriptor[F],
desc6: ConfigDescriptor[G],
desc7: ConfigDescriptor[H],
desc8: ConfigDescriptor[I],
desc9: ConfigDescriptor[J]
): ConfigDescriptor[D] =
apply(desc1, desc2, desc3, desc4, desc5, desc6, desc7, desc8) orElse apply(desc9)
}
/**
* `head` describes getting the head of a possible list value
*
* Example:
*
* {{{
* final case class Config(userName: String, port: Option[Int])
*
* object Config {
* val source =
* ConfigSource.fromMap(Map("USERNAME" -> "af,sa", "PORT" -> "1"), valueDelimiter = Some(','))
*
* val databaseConfig: ConfigDescriptor[Config] =
* (head(string("USERNAME")) zip int("PORT").optional).to[Config]
* }
*
* read(Config.databaseConfig from Config.source)
*
* // returns Config("af", 1)
* }}}
*/
def head[A](desc: ConfigDescriptor[A]): ConfigDescriptor[A] =
desc.orElse(
list(desc)
.transformOrFail[A](
_.headOption
.fold[Either[String, A]](Left("Element is missing"))(Right(_)),
v => Right(v :: Nil)
)
)
/**
* `head` describes getting the head of a possible list value
*
* Example:
*
* {{{
* final case class Config(userName: String, port: Option[Int])
*
* object Config {
* val source =
* ConfigSource.fromMap(Map("USERNAME" -> "af,sa", "PORT" -> "1"), valueDelimiter = Some(','))
*
* val databaseConfig: ConfigDescriptor[Config] =
* (head("USERNAME")(string) zip int("PORT").optional).to[Config]
* }
*
* read(Config.databaseConfig from Config.source)
*
* // returns Config("af", 1)
* }}}
*/
def head[A](path: K)(desc: => ConfigDescriptor[A]): ConfigDescriptor[A] =
nested(path)(head(desc))
/**
* `list(confgDescriptor)` represents just a list variant of configuration extraction.
*
* For example, we know `val config = string("USERNAME") from source`
* represents a program that says, there exists a key called
* "USERNAME" (in some ConfigSource called source)
* with a value that is of the type `String`.
*
* `list(config)` would then imply, there exists a list of `USERNAME -> value` pair.
*
* Given below is a complete example:
*
* {{{
* val json =
* s"""
* | xyz : [
* | {
* | "USERNAME" : "value1"
* | },
* |
* | {
* | "USERNAME" : "value2"
* | }
* | ]
* |""".stripMargin
*
* val config = string("USERNAME")
*
* // Within the key "xyz", we have a list of key-value pair, where key is always "USERNAME"
* // NOTE: In HOCON, there is always a need of key (in this case, xyz) at parent level.
*
* val listConfig = nested("xyz")(list(config))
*
* val userNames: ZIO[Any, ReadError[String], List[String]] =
* read(listConfig from TypesafeConfigSource.fromHoconString(json))
*
* }}}
*
* returns
*
* {{{
*
* List(value1, value2)
*
* }}}
*
* NOTE:
*
* `nested("xyz")(list(string("USERNAME"))` is same as `list("xyz")(string("USERNAME"))`
*/
def list[K, V, A](desc: => ConfigDescriptor[A]): ConfigDescriptor[List[A]] =
ConfigDescriptorAdt.sequenceDesc(desc)
/**
* `list("xyz")(confgDescriptor)` represents just a list variant of configDescriptor within the key `xyz`.
* Note that, `nested("xyz")(list(configDescriptor))` is same as `list("xyz")(configDescriptor)`.
*
* For example: `list("key")(string)` implies value of `key` is of the type `List[String]`
*
* Here is a more detailed example.
*
* We know `val config = string("USERNAME") from source`
* represents a program that says, there exists a key called
* "USERNAME" (in some ConfigSource called source)
* with a value that is of the type `String`.
*
* `list("xyz")(config)` would then imply, there exists a list of `USERNAME -> value` pair within the key "xyz".
*
* {{{
* val json =
* s"""
* | xyz : [
* | {
* | "USERNAME" : "value1"
* | },
* |
* | {
* | "USERNAME" : "value2"
* | }
* | ]
* |""".stripMargin
*
* val config = string("USERNAME")
*
* // Within the key "xyz", we have a list of key-value pair, where key is always "USERNAME"
* // NOTE: In HOCON, there is always a need of key (in this case, xyz) at parent level.
*
* val listConfig = list("xyz")(config)
*
* val userNames: ZIO[Any, ReadError[String], List[String]] =
* read(listConfig from ypesafeConfigSource.fromHoconString(json))
*
* }}}
*
* returns
*
* {{{
*
* List(value1, value2)
*
* }}}
*/
def list[A](
path: K
)(desc: => ConfigDescriptor[A]): ConfigDescriptor[List[A]] =
nested(path)(list(desc))
/**
* `listOrSingleton` is a flexible version of `list`. This means, even if the value is not of the type `List`
* it considers the value a singleton and returns `List(singleValue)`
*
* We `list("xyz")(confgDescriptor)` represents just a list variant of configDescriptor within the key `xyz`.
* That is `list("key")(string)` implies value of `key` is of the type `List[String]`
*
* However if the value of `key` was not a list, but instead a simple string, and if we are using `listOrSingleton`
* it will be considered as a `List`.
*
* Here is a more detailed example.
*
* {{{
* val json =
* s"""
* | USERNAME : {
* | "USERNAME" : "abc"
* | }
* |""".stripMargin
*
* val config = string("USERNAME")
*
* val usernames: ZIO[Any, ReadError[String], List[String]] =
* read(listOrSingleton("configs")(config) from TypesafeConfigSource.fromHoconString(json))
*
* }}}
*
* returns
*
* {{{
*
* List(value1)
*
* }}}
*/
def listOrSingleton[A](
path: K
)(desc: => ConfigDescriptor[A]): ConfigDescriptor[List[A]] =
list(path)(desc) orElse (
nested(path)(desc)
.transformOrFail[List[A]](
value => Right(List(value)),
_.headOption match {
case Some(value) => Right(value)
case None => Left("Cannot write an empty list back")
}
)
)
/**
* Retrieve a `Map`given an existing `ConfigDescriptor`.
*
* `map(configDescriptor)` is similar to `map(path)(configDescriptor)` except
* that there is no `path` associated with it. For the same reason, you would need
* the second version given below: `def map[A](path: K)(desc: => ConfigDescriptor[A])`
*
* Before we try to understand the semantics of `map(configDescriptor)`, let's understand the
* semantics of `map(path)(configDescriptor)`; a function with the same name given below,
* but it takes a path as well.
*
* `map("xyz")(confgDescriptor)` represents retrieving a map (of key value pairs) that exists within the key "xyz"
*
* Let's explain this in detail with an example: int("URL") implies there exists a value of the type string under the key "URL"
* On similar lines, map("URL")(int) implies there exists a value of the type `Map` under the key `URL` and the type of the
* value of each key in the map is of the type Int.
*
* Sidee note: Obviously, for complex types such as Map,
* you can also rely on zio-config-magnolia that allows you to retrieve
* any value of the type Map[String, A] for all type A,
* that has an instance of `Description` (refer zio-config-magnolia api docs)
*
* {{{
*
* val config = map("xyz")(int)
*
* val source: ConfigSource =
* TypesafeConfigSource.fromHoconString(
* "xyz" : {
* "key1" : "1"
* "key2" : "2"
* "key3" : "3"
* }
* )
*
* // Forming a TypesafeConfigSource from string returned an Either (being able to capture errors) because
* // the HOCON string can be an invalid string.
*
* val result = sourceOrFailed.flatMap(source => read(config from source))
* // Map("key1" -> 1, "key2" -> 2, "key3" -> 3)
* }}}
*
* We explained `map` using TypesafeConfigSource. However, for zio-config source doesn't really matter.
* For example, lets try to fetch a map from a flattened scala Map.
*
* {{{
* val source = ConfigSource.fromMap(
* Map(
* "xyz_key1" -> "1",
* "xyz_key2" -> "2",
* "xyz_key3" -> "3"
* ), keyDelimiter = Some('_')
* )
*
* val config = read(config from source)
* // Map("key1" -> 1, "key2" -> 2, "key3" -> 3)
*
* }}}
*
* Now what does it mean if we say ` val config = map(int("id")) ` instead of `val config = map("id")(int)`
*
* The difference is `map("id")(int)` implies there exists a map within the key `id`, whose values of are of the type `Int`
* On the other hand `map(int("id"))` implies there exists a map hose value is of the type {"id" : "Int"}
*
* Example:
* {{{
*
* val mapConfig = map(int("id"))
*
* // This means there exists a Map whose value is of the type {"String" : "Int"}.
*
* val sourceOrFailure: ConfigSource =
* TypesafeConfigSource.fromHoconString(
* s"""
* "abc" : {
* "key1" : { "id" : "2" },
* "key2" : { "id" : "3" }
* }
*
* """"
* )
*
* val result = read(nested("abc")(map(int("id"))) from source)
* // Map("key1" -> 1, "key2" -> 2)
*
* }}}
*
* This is really useful when the config source consist of a map but you need to fetch the value of the keys
* in the map from an nested key within itself. In this example it is "id".
*/
def map[A](desc: => ConfigDescriptor[A]): ConfigDescriptor[Map[K, A]] =
DynamicMap(lazyDesc(desc))
// ConfigDescriptorAdt.dynamicMapDesc(ConfigSourceFunctions.empty, desc)
/**
* `map("xyz")(confgDescriptor)` represents retrieving a map (of key value pairs) that exists within the key "xyz"
*
* Let's explain this in detail with an example: int("URL") implies there exists a value of the type string under the key "URL"
* On similar lines, map("URL")(int) implies there exists a value of the type `Map` under the key `URL` and the type of the
* value of each key in the map is of the type Int.
*
* Sidee note: Obviously, for complex types such as Map,
* you can also rely on zio-config-magnolia that allows you to retrieve
* any value of the type Map[String, A] for all type A,
* that has an instance of `Description` (refer zio-config-magnolia api docs)
*
* {{{
*
* val config = map("xyz")(int)
*
* val source: ConfigSource =
* TypesafeConfigSource.fromHoconString(
* "xyz" : {
* "key1" : "1"
* "key2" : "2"
* "key3" : "3"
* }
* )
*
* // Forming a TypesafeConfigSource from string returned an Either (being able to capture errors) because
* // the HOCON string can be an invalid string.
*
* val result = read(config from source)
* // Right(Map("key1" -> 1, "key2" -> 2, "key3" -> 3))
* }}}
*
* We explained `map` using TypesafeConfigSource. However, for zio-config source doesn't really matter.
* For example, lets try to fetch a map from a flattened scala Map.
*
* {{{
* val source = ConfigSource.fromMap(
* Map(
* "xyz_key1" -> "1",
* "xyz_key2" -> "2",
* "xyz_key3" -> "3"
* ), keyDelimiter = Some('_')
* )
*
* val config = read(config from source)
* // Right( Map("key1" -> 1, "key2" -> 2, "key3" -> 3))
*
* }}}
*/
def map[A](
path: K
)(desc: => ConfigDescriptor[A]): ConfigDescriptor[Map[K, A]] =
nested(path)(map(desc))
/**
* nested allows us to retrieve a config from a path `K`, where `K` is typically `String`.
*
* Example :
*
* {{{
* val config = nested("key")(string)
* val mapSource = ConfigSource.fromMap(
* "key" : "value"
* )
*
* val result = read(config from mapSource)
* // "value"
* }}}
*
* Note that `string("key")` is same as that of `nested("key")(string)`
*/
def nested[A](path: K)(desc: => ConfigDescriptor[A]): ConfigDescriptor[A] =
ConfigDescriptorAdt.nestedDesc(path, desc)
/**
* `set("xyz")(confgDescriptor)` represents just a set variant of configDescriptor within the key `xyz`.
* Note that, `nested("xyz")(set(configDescriptor))` is same as `set("xyz")(configDescriptor)`.
*
* For example: `set("key")(string)` implies value of `key` is of the type `Set[String]`
*
* Here is a more detailed example.
*
* `list("xyz")(string)` would then imply, there exists a set of type String under "xyz"
*
* {{{
* val json =
* s"""
* | xyz : ["a", "b"]
* |""".stripMargin
*
* val source: ConfigSource =
* TypesafeConfigSource.fromHoconString(json)
*
* read(set("xyz")(string) from source)
*
* }}}
*
* returns
*
* {{{
*
* Right(List(value1, value2))
*
* }}}
*/
def set[K, V, A](desc: => ConfigDescriptor[A]): ConfigDescriptor[Set[A]] =
list(desc).transformOrFail(distinctListToSet, s => Right(s.toList))
/**
* `set("xyz")(confgDescriptor)` represents just a set variant of configDescriptor within the key `xyz`.
* Note that, `nested("xyz")(set(configDescriptor))` is same as `set("xyz")(configDescriptor)`.
*
* For example: `set("key")(string)` implies value of `key` is of the type `Set[String]`
*
* Here is a more detailed example.
*
* `list("xyz")(string)` would then imply, there exists a set of type String under "xyz"
*
* {{{
* val json =
* s"""
* | xyz : ["a", "b"]
* |""".stripMargin
*
* val source: ConfigSource =
* TypesafeConfigSource.fromHoconString(json)
*
* read(set("xyz")(string) from source)
*
* }}}
*
* returns
*
* {{{
*
* List(value1, value2)
*
* }}}
*/
def set[A](
path: K
)(desc: => ConfigDescriptor[A]): ConfigDescriptor[Set[A]] =
nested(path)(set(desc))
private[config] def distinctListToSet[A](list: List[A]): Either[String, Set[A]] =
if (list.size == list.distinct.size) Right(list.toSet)
else Left("Duplicated values found")
}
private[config] object ConfigDescriptorAdt {
sealed case class Default[A](config: ConfigDescriptor[A], default: A) extends ConfigDescriptor[A]
sealed case class Describe[A](config: ConfigDescriptor[A], message: String) extends ConfigDescriptor[A]
sealed case class DynamicMap[A](config: ConfigDescriptor[A]) extends ConfigDescriptor[Map[K, A]]
sealed case class Lazy[A](get: () => ConfigDescriptor[A]) extends ConfigDescriptor[A]
sealed case class Nested[A](path: K, config: ConfigDescriptor[A]) extends ConfigDescriptor[A]
sealed case class Optional[A](config: ConfigDescriptor[A]) extends ConfigDescriptor[Option[A]]
sealed case class OrElse[A](left: ConfigDescriptor[A], right: ConfigDescriptor[A]) extends ConfigDescriptor[A]
sealed case class OrElseEither[A, B](left: ConfigDescriptor[A], right: ConfigDescriptor[B])
extends ConfigDescriptor[Either[A, B]]
sealed case class Sequence[A](config: ConfigDescriptor[A]) extends ConfigDescriptor[List[A]]
sealed case class Source[A](source: ConfigSource, propertyType: PropertyType[V, A]) extends ConfigDescriptor[A]
sealed case class Zip[A, B](left: ConfigDescriptor[A], right: ConfigDescriptor[B]) extends ConfigDescriptor[(A, B)]
sealed case class TransformOrFail[A, B](
config: ConfigDescriptor[A],
f: A => Either[String, B],
g: B => Either[String, A]
) extends ConfigDescriptor[B]
final def defaultDesc[A](config: => ConfigDescriptor[A], default: A): ConfigDescriptor[A] =
Default(lazyDesc(config), default)
final def describeDesc[A](config: => ConfigDescriptor[A], message: String): ConfigDescriptor[A] =
Describe(lazyDesc(config), message)
final def dynamicMapDesc[A](config: => ConfigDescriptor[A]): ConfigDescriptor[Map[K, A]] =
DynamicMap(lazyDesc(config))
final def lazyDesc[A](
config: => ConfigDescriptor[A]
): ConfigDescriptor[A] =
Lazy(() => config)
final def nestedDesc[A](path: K, config: => ConfigDescriptor[A]): ConfigDescriptor[A] =
Nested(path, lazyDesc(config))
final def optionalDesc[A](config: => ConfigDescriptor[A]): ConfigDescriptor[Option[A]] =
Optional(lazyDesc(config))
final def orElseDesc[A](left: => ConfigDescriptor[A], right: => ConfigDescriptor[A]): ConfigDescriptor[A] =
OrElse(lazyDesc(left), lazyDesc(right))
final def orElseEitherDesc[A, B](
left: => ConfigDescriptor[A],
right: => ConfigDescriptor[B]
): ConfigDescriptor[Either[A, B]] =
OrElseEither(lazyDesc(left), lazyDesc(right))
final def sequenceDesc[A](config: => ConfigDescriptor[A]): ConfigDescriptor[List[A]] =
Sequence(lazyDesc(config))
final def sourceDesc[A](source: ConfigSource, propertyType: PropertyType[V, A]): ConfigDescriptor[A] =
Source(source, propertyType)
final def zipDesc[A, B](left: => ConfigDescriptor[A], right: => ConfigDescriptor[B]): ConfigDescriptor[(A, B)] =
Zip(lazyDesc(left), lazyDesc(right))
final def transformOrFailDesc[A, B](
config: => ConfigDescriptor[A],
f: A => Either[String, B],
g: B => Either[String, A]
): ConfigDescriptor[B] =
TransformOrFail(lazyDesc(config), f, g)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy