Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: config reading with state v2 #92

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions metaconfig-core/shared/src/main/scala/metaconfig/Conf.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package metaconfig
import java.io.File
import scala.util.Try
import metaconfig.Extractors._
import metaconfig.ConfDecoder.ConfDecoderWithDefaultMaybe
import metaconfig.generic.Setting
import metaconfig.generic.Settings
import metaconfig.internal.CliParser
Expand All @@ -24,7 +25,7 @@ sealed abstract class Conf extends Product with Serializable {
def as[T](implicit ev: ConfDecoder[T]): Configured[T] =
ev.read(this)
def getSettingOrElse[T](setting: Setting, default: T)(
implicit ev: ConfDecoder[T]
implicit ev: ConfDecoderWithDefaultMaybe[T]
): Configured[T] =
ConfGet.getOrElse(this, default, setting.name, setting.alternativeNames: _*)
def get[T](path: String, extraNames: String*)(
Expand All @@ -33,7 +34,7 @@ sealed abstract class Conf extends Product with Serializable {
ConfGet.get(this, path, extraNames: _*)
def getOrElse[T](path: String, extraNames: String*)(
default: T
)(implicit ev: ConfDecoder[T]): Configured[T] =
)(implicit ev: ConfDecoderWithDefaultMaybe[T]): Configured[T] =
ConfGet.getOrElse(this, default, path, extraNames: _*)
poslegm marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import metaconfig.Extractors.Number
import metaconfig.generic.Settings
import metaconfig.internal.CanBuildFromDecoder
import metaconfig.internal.NoTyposDecoder
import metaconfig.internal.Priority
import java.nio.file.Path
import java.nio.file.Paths

Expand Down Expand Up @@ -42,6 +43,9 @@ trait ConfDecoder[A] { self =>

object ConfDecoder {

type ConfDecoderWithDefaultMaybe[A] =
Priority[ConfDecoderReader[WithDefault[A], A], ConfDecoder[A]]

@deprecated("Use ConfDecoder[T].read instead", "0.6.1")
def decode[T](conf: Conf)(implicit ev: ConfDecoder[T]): Configured[T] =
ev.read(conf)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package metaconfig

import scala.language.higherKinds
import metaconfig.ConfDecoderReader.ConfDecoderFactory
import metaconfig.generic.Settings
import metaconfig.internal.CanBuildFromDecoder

import scala.collection.compat.Factory
import scala.reflect.ClassTag

trait WithDefault[A] { self =>
def default: A

final def map[B](f: A => B): WithDefault[B] = new WithDefault[B] {
override def default: B = f(self.default)
}
}

object WithDefault {
def of[A](a: A): WithDefault[A] = new WithDefault[A] {
override def default: A = a
}
}

trait ConfDecoderReader[S, A] { self =>
def decoder: ConfDecoderFactory[S, A]

def local[S1](f: S1 => S): ConfDecoderReader[S1, A] =
new ConfDecoderReader[S1, A] {
override def decoder: ConfDecoderFactory[S1, A] = f.andThen(self.decoder)
}

def map[B](f: A => B): ConfDecoderReader[S, B] =
new ConfDecoderReader[S, B] {
def decoder = self.decoder.andThen(_.map(f))
}

def orElse(other: ConfDecoder[A]): ConfDecoderReader[S, A] =
new ConfDecoderReader[S, A] {
def decoder =
self.decoder.andThen(self => ConfDecoder.orElse(self, other))
}

def mapConfigured[TT](f: A => Configured[TT]): ConfDecoderReader[S, TT] =
new ConfDecoderReader[S, TT] {
def decoder = self.decoder.andThen(_.flatMap(f))
}

def noTypos(implicit ev: Settings[A]): ConfDecoderReader[S, A] =
new ConfDecoderReader[S, A] {
def decoder = self.decoder.andThen(_.noTypos)
}
}

object ConfDecoderReader {

type ConfDecoderFactory[S, A] = S => ConfDecoder[A]

implicit def canBuildFromConfDecoderReader[C[X] <: Iterable[X], A, S <: WithDefault[
C[A]
]](
implicit ev: ConfDecoder[A],
factory: Factory[A, C[A]],
classTag: ClassTag[A]
): ConfDecoderReader[S, C[A]] =
CanBuildFromDecoder.listReader

implicit def mapConfDecoderReader[A, S <: WithDefault[Map[String, A]]](
implicit ev: ConfDecoder[A],
classTag: ClassTag[A]
): ConfDecoderReader[S, Map[String, A]] = CanBuildFromDecoder.mapReader
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
package metaconfig.internal

import scala.language.higherKinds
import metaconfig.ConfDecoderReader.ConfDecoderFactory
import metaconfig.Configured.{NotOk, Ok}
import metaconfig._

import scala.collection.compat._
import scala.language.higherKinds
import scala.reflect.ClassTag
import metaconfig.Conf
import metaconfig.ConfDecoder
import metaconfig.ConfError
import metaconfig.Configured
import metaconfig.Configured.NotOk
import metaconfig.Configured.Ok

object CanBuildFromDecoder {
def mapReader[A, S <: WithDefault[Map[String, A]]](
implicit ev: ConfDecoder[A],
classTag: ClassTag[A]
): ConfDecoderReader[S, Map[String, A]] =
new ConfDecoderReader[S, Map[String, A]] {
override def decoder: ConfDecoderFactory[S, Map[String, A]] =
state =>
new ConfDecoder[Map[String, A]] {
private val underlying = CanBuildFromDecoder.map[A]
override def read(conf: Conf): Configured[Map[String, A]] =
conf match {
case Conf.Obj(("add", conf) :: Nil) =>
underlying.read(conf).map { res => state.default ++ res }
case els => underlying.read(els)
}
}
}

def map[A](
implicit ev: ConfDecoder[A],
classTag: ClassTag[A]
Expand All @@ -29,6 +44,32 @@ object CanBuildFromDecoder {
Ok(results.collect { case Ok(x) => x }.toMap)
}
}

def listReader[C[X] <: Iterable[X], A, S <: WithDefault[C[A]]](
implicit ev: ConfDecoder[A],
factory: Factory[A, C[A]],
classTag: ClassTag[A]
): ConfDecoderReader[S, C[A]] =
new ConfDecoderReader[S, C[A]] {
override def decoder: S => ConfDecoder[C[A]] =
state =>
new ConfDecoder[C[A]] {
private val underlying = list[C, A]

override def read(conf: Conf): Configured[C[A]] =
conf match {
case Conf.Obj(("add", conf) :: Nil) =>
val builder = factory.newBuilder
underlying.read(conf).map { res =>
state.default.foreach(builder += _)
res.foreach(builder += _)
builder.result()
}
case els => underlying.read(els)
}
}
}

def list[C[_], A](
implicit ev: ConfDecoder[A],
factory: Factory[A, C[A]],
Expand Down Expand Up @@ -58,5 +99,4 @@ object CanBuildFromDecoder {
NotOk(error)
}
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package metaconfig.internal

import scala.collection.mutable
import metaconfig.Conf
import metaconfig.ConfDecoder
import metaconfig.ConfError
import metaconfig.Configured
import metaconfig.{Conf, ConfDecoder, ConfError, Configured, WithDefault}
import metaconfig.ConfDecoder.ConfDecoderWithDefaultMaybe

object ConfGet {
def getKey(obj: Conf, keys: Seq[String]): Option[Conf] =
Expand All @@ -20,10 +18,11 @@ object ConfGet {
}

def getOrElse[T](conf: Conf, default: T, path: String, extraNames: String*)(
implicit ev: ConfDecoder[T]
implicit ev: ConfDecoderWithDefaultMaybe[T]
poslegm marked this conversation as resolved.
Show resolved Hide resolved
): Configured[T] = {
getKey(conf, path +: extraNames) match {
case Some(value) => ev.read(value)
case Some(value) =>
ev.fold(_.decoder(WithDefault.of(default)).read(value))(_.read(value))
case None => Configured.Ok(default)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package metaconfig.internal

/**
* Copied from https://github.com/monix/implicitbox/blob/master/shared/src/main/scala/implicitbox/Priority.scala
* */
private[metaconfig] sealed trait Priority[+P, +F] {
import Priority.{Fallback, Preferred}

def fold[B](f1: P => B)(f2: F => B): B =
this match {
case Preferred(x) => f1(x)
case Fallback(y) => f2(y)
}
}

private[metaconfig] object Priority extends FindPreferred {
final case class Preferred[P](get: P) extends Priority[P, Nothing]
final case class Fallback[F](get: F) extends Priority[Nothing, F]

def apply[P, F](implicit ev: Priority[P, F]): Priority[P, F] = ev
}

private[metaconfig] trait FindPreferred extends FindFallback {
implicit def preferred[P](implicit ev: P): Priority[P, Nothing] =
Priority.Preferred(ev)
}

private[metaconfig] trait FindFallback {
implicit def fallback[F](implicit ev: F): Priority[Nothing, F] =
Priority.Fallback(ev)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package metaconfig.typesafeconfig

import metaconfig.{Conf, ConfCodec, generic}

object ReadHoconToCaseClassSuite {
case class Bar(lst: List[String] = List("a"))

implicit val barSurface: generic.Surface[Bar] =
generic.deriveSurface

implicit val barCodec: ConfCodec[Bar] =
generic.deriveCodec[Bar](Bar())

case class Foo(
x: String = "x",
y: Int = 2,
bar: Bar = Bar(),
xs: Map[String, Int] = Map("a" -> 1),
ys: List[Int] = List(1, 2)
)

implicit val surface: generic.Surface[Foo] =
generic.deriveSurface

implicit val codec: ConfCodec[Foo] =
generic.deriveCodec[Foo](Foo())
}

class ReadHoconToCaseClassSuite extends munit.FunSuite {
import ReadHoconToCaseClassSuite.{Bar, Foo}

test("read case class simple") {
val hocon = """
x = "hello"
bar.lst = ["b"]
xs = {
b = 4
}
ys = [3]
"""

val parsed = Conf.parseString(hocon).get.as[Foo].get
val expected =
Foo("hello", 2, Bar(List("b")), Map("b" -> 4), List(3))
assertEquals(parsed, expected)
}

test("read case class from hocon with append") {
val hocon = """
x = "hello"
bar.lst.add = ["b"]
xs.add = {
b = 4
}
ys = [3]
"""

val parsed = Conf.parseString(hocon).get.as[Foo].get
val expected =
Foo("hello", 2, Bar(List("a", "b")), Map("a" -> 1, "b" -> 4), List(3))
assertEquals(parsed, expected)
}

}
Loading