Skip to content

Commit

Permalink
Hoccon Integration, Fully Automatic Config Derviation, Coproduct supp…
Browse files Browse the repository at this point in the history
…ort and List support (#173)

* Convert to cons of values

* adding list with 2 failed tests

* Implement list and add typesafe integration

* Update comments

* Add typesafe dependency

* Fix automatic config description

* Add examples

* Typesafe config

* add support for hocon substitution

* add support for hocon substitution

* Simplify list operation

* Add list impl

* Fix coproduct bug in magnnolia

* Add property bsaed tests for magnolia module

* Implement list

* Implementing list

* Implementing list

* Implementing list

* Implementing list

* Implement magnolia version for typesafe config

* Buggy orelseeither

* Fix or else either

* Fix the optional type

* Removed empty values

* Working version with list typeconfig and magnolia, but no writes

* Remove printlns

* Add tests for magnolia and more examples

* Working test case in magnolia

* More examples

* Add discord badge instead of gitter

* Update documentations

* Fix refined test support

* Fix all tests based on list

* Fix all test cases

* Fix all test cases

* Make cross compilation work

* Make documentations compile

* Include more help

* Make typesafe config into zio

* Add more examples

* Fix cross compilation issues
  • Loading branch information
afsalthaj authored Jan 4, 2020
1 parent 34f3f57 commit ffa1d2d
Show file tree
Hide file tree
Showing 32 changed files with 1,172 additions and 394 deletions.
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
# zio-config

[![CircleCI](https://circleci.com/gh/zio/zio-config/tree/master.svg?style=svg)](https://circleci.com/gh/zio/zio-config/tree/master)
[![Gitter](https://badges.gitter.im/ZIO/zio-config.svg)](https://gitter.im/ZIO/zio-config?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![badge-discord]][link-discord]

A [zio](https://github.com/scalaz/scalaz-zio) based configuration parsing library.

The library aims to have a powerful & purely functional, yet a thin interface to access configuration information inside an application.
Please find more details in the [website](https://zio.github.io/zio-config/).
Feel free to head on to [examples](https://github.com/zio/zio-config/tree/master/examples/src/main/scala/zio/config/examples) straight away as well.

Notable features:
1. Read config
2. Write config
3. Document config
4. Report on config
Feel free to head on to [examples](examples/src/main/scala/zio/config/examples) straight away as well.

[badge-discord]: https://img.shields.io/discord/629491597070827530?logo=discord "chat on discord"
[link-discord]: https://discord.gg/2ccFBr4 "Discord"
22 changes: 17 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ lazy val root =
project
.in(file("."))
.settings(skip in publish := true)
.aggregate(zioConfig, zioConfigMagnolia, examples, zioConfigRefined)
.aggregate(zioConfig, zioConfigMagnolia, examples, zioConfigRefined, zioConfigTypesafe)

lazy val zioConfig =
module("zio-config", "core")
Expand Down Expand Up @@ -93,15 +93,27 @@ lazy val examples = module("zio-config-examples", "examples")
"com.propensive" %% "magnolia" % magnoliaVersion
)
)
.dependsOn(zioConfig, zioConfigMagnolia, zioConfigRefined)
.dependsOn(zioConfig, zioConfigMagnolia, zioConfigRefined, zioConfigTypesafe)

lazy val zioConfigMagnolia = module("zio-config-magnolia", "magnolia")
.settings(
libraryDependencies ++= Seq(
"com.propensive" %% "magnolia" % magnoliaVersion
)
"com.propensive" %% "magnolia" % magnoliaVersion,
"dev.zio" %% "zio-test" % zioVersion % Test,
"dev.zio" %% "zio-test-sbt" % zioVersion % Test
),
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
)
.dependsOn(zioConfig)
.dependsOn(zioConfig % "compile->compile;test->test")

lazy val zioConfigTypesafe =
module("zio-config-typesafe", "typesafe")
.settings(
libraryDependencies ++= Seq(
"com.typesafe" % "config" % "1.4.0"
)
)
.dependsOn(zioConfig)

def module(moduleName: String, fileName: String): Project =
Project(moduleName, file(fileName))
Expand Down
24 changes: 17 additions & 7 deletions core/src/main/scala/zio/config/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import zio.system.System
import zio.{ IO, UIO, ZIO }
import zio.system.System.Live.system.lineSeparator

trait Config[A] {
trait Config[A] { self =>
def config: Config.Service[A]
}
object Config {
Expand All @@ -30,15 +30,24 @@ object Config {
)

def fromEnv[K, V, A](
configDescriptor: ConfigDescriptor[String, String, A]
configDescriptor: ConfigDescriptor[String, String, A],
valueDelimiter: Option[String] = None
): IO[ReadErrors[Vector[String], String], Config[A]] =
make(ConfigSource.fromEnv, configDescriptor)
make(ConfigSource.fromEnv(valueDelimiter), configDescriptor)

def fromMap[A](
map: Map[String, String],
configDescriptor: ConfigDescriptor[String, String, A]
configDescriptor: ConfigDescriptor[String, String, A],
pathDelimiter: String = "."
): IO[ReadErrors[Vector[String], String], Config[A]] =
make[String, String, A](ConfigSource.fromMap(map), configDescriptor)
make[String, String, A](ConfigSource.fromMap(map, pathDelimiter), configDescriptor)

def fromMultiMap[A](
map: Map[String, ::[String]],
configDescriptor: ConfigDescriptor[String, String, A],
pathDelimiter: String = "."
): IO[ReadErrors[Vector[String], String], Config[A]] =
make[String, String, A](ConfigSource.fromMultiMap(map, pathDelimiter), configDescriptor)

// If reading a file, this can have read errors as well as throwable when trying to read the file
def fromPropertyFile[A](
Expand All @@ -63,7 +72,8 @@ object Config {
)

def fromPropertyFile[K, V, A](
configDescriptor: ConfigDescriptor[String, String, A]
configDescriptor: ConfigDescriptor[String, String, A],
valueDelimiter: Option[String] = None
): ZIO[System, ReadErrors[Vector[String], String], Config[A]] =
make(ConfigSource.fromProperty, configDescriptor)
make(ConfigSource.fromProperty(valueDelimiter), configDescriptor)
}
60 changes: 27 additions & 33 deletions core/src/main/scala/zio/config/ConfigDescriptor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,7 @@ package zio.config
import scala.concurrent.duration.Duration

import java.net.URI
import zio.config.ConfigDescriptor.Default
import zio.config.ConfigDescriptor.OrElseEither
import zio.config.ConfigDescriptor.Source
import zio.config.ConfigDescriptor.Describe
import zio.config.ConfigDescriptor.Optional
import zio.config.ConfigDescriptor.Zip
import zio.config.ConfigDescriptor.Empty
import zio.config.ConfigDescriptor.Nested
import zio.config.ConfigDescriptor.XmapEither
import zio.config.ConfigDescriptor._

sealed trait ConfigDescriptor[K, V, A] { self =>
final def zip[B](that: => ConfigDescriptor[K, V, B]): ConfigDescriptor[K, V, (A, B)] =
Expand Down Expand Up @@ -42,10 +34,7 @@ sealed trait ConfigDescriptor[K, V, A] { self =>
self orElseEither that

final def orElse(that: => ConfigDescriptor[K, V, A]): ConfigDescriptor[K, V, A] =
(self orElseEither that).xmap {
case Right(value) => value
case Left(value) => value
}(b => Right(b))
ConfigDescriptor.OrElse(self, that)

final def <>(that: => ConfigDescriptor[K, V, A]): ConfigDescriptor[K, V, A] =
self orElse that
Expand All @@ -54,7 +43,7 @@ sealed trait ConfigDescriptor[K, V, A] { self =>
ConfigDescriptor.Optional(self) ?? "optional value"

final def default(value: A): ConfigDescriptor[K, V, A] =
ConfigDescriptor.Default(self, value) ?? s"default value: $value"
ConfigDescriptor.Default(self, value) ?? s"default value: $value ("

final def describe(description: String): ConfigDescriptor[K, V, A] =
ConfigDescriptor.Describe(self, description)
Expand All @@ -76,28 +65,28 @@ sealed trait ConfigDescriptor[K, V, A] { self =>

final def updateSource(f: ConfigSource[K, V] => ConfigSource[K, V]): ConfigDescriptor[K, V, A] = {
def loop[B](config: ConfigDescriptor[K, V, B]): ConfigDescriptor[K, V, B] = config match {
case a @ Empty() => a
case Source(path, source, propertyType) => Source(path, f(source), propertyType)
case Nested(conf, path) => Nested(loop(conf), path)
case Nested(path, conf) => Nested(path, loop(conf))
case Sequence(conf) => Sequence(loop(conf))
case Describe(config, message) => Describe(loop(config), message)
case Default(value, value2) => Default(loop(value), value2)
case Optional(config) => Optional(loop(config))
case XmapEither(config, f, g) => XmapEither(loop(config), f, g)
case Zip(conf1, conf2) => Zip(loop(conf1), loop(conf2))
case OrElseEither(value1, value2) => OrElseEither(loop(value1), loop(value2))
case OrElse(value1, value2) => OrElse(loop(value1), loop(value2))
}
loop(self)
}
}

object ConfigDescriptor {

final case class Empty[K, V, A]() extends ConfigDescriptor[K, V, Option[A]]

final case class Source[K, V, A](path: K, source: ConfigSource[K, V], propertyType: PropertyType[V, A])
extends ConfigDescriptor[K, V, A]

final case class Nested[K, V, A](config: ConfigDescriptor[K, V, A], path: K) extends ConfigDescriptor[K, V, A]
final case class Sequence[K, V, A](config: ConfigDescriptor[K, V, A]) extends ConfigDescriptor[K, V, List[A]]

final case class Nested[K, V, A](path: K, config: ConfigDescriptor[K, V, A]) extends ConfigDescriptor[K, V, A]

final case class Describe[K, V, A](config: ConfigDescriptor[K, V, A], message: String)
extends ConfigDescriptor[K, V, A]
Expand All @@ -118,25 +107,32 @@ object ConfigDescriptor {
final case class OrElseEither[K, V, A, B](left: ConfigDescriptor[K, V, A], right: ConfigDescriptor[K, V, B])
extends ConfigDescriptor[K, V, Either[A, B]]

def empty[K, V, A]: ConfigDescriptor[K, V, Option[A]] = ConfigDescriptor.Empty()
final case class OrElse[K, V, A](left: ConfigDescriptor[K, V, A], right: ConfigDescriptor[K, V, A])
extends ConfigDescriptor[K, V, A]

def sequence[K, V, A](configList: List[ConfigDescriptor[K, V, A]]): ConfigDescriptor[K, V, List[A]] =
configList.foldRight(
Empty[K, V, A]().xmap(_.toList)(_.headOption)
def sequence[K, V, A](configList: ::[ConfigDescriptor[K, V, A]]): ConfigDescriptor[K, V, ::[A]] = {
val reversed = configList.reverse
reversed.tail.foldLeft(
reversed.head.xmap(a => ::(a, Nil))(b => b.head)
)(
(b, a) =>
b.xmapEither2(a)((aa, bb) => Right(aa :: bb))(
t => {
t.headOption.fold[Either[String, (A, List[A])]](
Left("Input does not match config description. It may have fewer entries than config requires")
)(ll => Right((ll, t.tail)))
}
b.xmapEither2(a)((as, a) => {
Right(::(a, as))
})(
t => Right((::(t.tail.head, t.tail.tail), t.head))
)
)
}

def collectAll[K, V, A](configList: List[ConfigDescriptor[K, V, A]]): ConfigDescriptor[K, V, List[A]] =
def collectAll[K, V, A](configList: ::[ConfigDescriptor[K, V, A]]): ConfigDescriptor[K, V, ::[A]] =
sequence(configList)

def list[K, V, A](desc: ConfigDescriptor[K, V, A]): ConfigDescriptor[K, V, List[A]] =
ConfigDescriptor.Sequence(desc)

def nested[K, V, A](path: K)(desc: ConfigDescriptor[K, V, A]): ConfigDescriptor[K, V, A] =
ConfigDescriptor.Nested(path, desc)

def string(path: String): ConfigDescriptor[String, String, String] =
ConfigDescriptor.Source(path, ConfigSource.empty, PropertyType.StringType) ?? "value of type string"
def boolean(path: String): ConfigDescriptor[String, String, Boolean] =
Expand All @@ -161,6 +157,4 @@ object ConfigDescriptor {
ConfigDescriptor.Source(path, ConfigSource.empty, PropertyType.UriType) ?? "value of type uri"
def duration(path: String): ConfigDescriptor[String, String, Duration] =
ConfigDescriptor.Source(path, ConfigSource.empty, PropertyType.DurationType) ?? "value of type duration"
def nested[K, V, A](path: K)(desc: ConfigDescriptor[K, V, A]): ConfigDescriptor[K, V, A] =
ConfigDescriptor.Nested(desc, path)
}
15 changes: 12 additions & 3 deletions core/src/main/scala/zio/config/ConfigDocsFunctions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,25 @@ private[config] trait ConfigDocsFunctions {
docs: ConfigDocs[K, V]
): ConfigDocs[K, V] =
config match {
case ConfigDescriptor.Empty() => docs
case ConfigDescriptor.Source(path, source, _) =>
Path(
path,
Descriptions(source.sourceDescription ++ descAcc.descriptions)
)

case ConfigDescriptor.Default(c, _) =>
loop(descAcc, c, docs)

case ConfigDescriptor.Sequence(c) =>
loop(Descriptions("value of type list" :: descAcc.descriptions), c, docs)

case ConfigDescriptor.Describe(c, description) =>
loop(Descriptions(description :: descAcc.descriptions), c, docs)

case ConfigDescriptor.Optional(c) =>
loop(descAcc, c, docs)

case ConfigDescriptor.Nested(c, path) =>
case ConfigDescriptor.Nested(path, c) =>
ConfigDocs.NestedPath(path, loop(descAcc, c, docs))

case ConfigDescriptor.XmapEither(c, _, _) =>
Expand All @@ -43,6 +46,12 @@ private[config] trait ConfigDocsFunctions {
loop(descAcc, left, docs),
loop(descAcc, right, docs)
)

case ConfigDescriptor.OrElse(left, right) =>
ConfigDocs.OneOf(
loop(descAcc, left, docs),
loop(descAcc, right, docs)
)
}

loop(Descriptions(Nil), config, Empty)
Expand All @@ -64,7 +73,7 @@ private[config] trait ConfigDocsFunctions {
val updated: Details[V] =
docs match {
case Descriptions(descriptions) =>
DescriptionsWithValue(flattened.get(initialValue :+ path), descriptions)
DescriptionsWithValue(flattened.get(initialValue :+ path).map(_.head), descriptions)
case a => a
}
Path(path, updated)
Expand Down
Loading

0 comments on commit ffa1d2d

Please # to comment.