Skip to content

Commit

Permalink
ConfDecoderEx: decode with initial value
Browse files Browse the repository at this point in the history
  • Loading branch information
kitbellew committed May 22, 2021
1 parent 3b02e73 commit 8204327
Show file tree
Hide file tree
Showing 12 changed files with 569 additions and 10 deletions.
27 changes: 26 additions & 1 deletion docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,17 @@ fileDecoder.read(Conf.fromString(".scalafmt.conf"))
fileDecoder.read(Conf.fromString(".foobar"))
```

## ConfDecoderEx and ConfDecoderExT

Similar to `ConfDecoder` but its `read` method takes an initial state as a parameter
rather than as part of the decoder instance definition. `ConfDecoderEx[A]` is an alias
for `ConfDecoderExT[A, A]`.

### Decoding collections

If a decoder for type `T` is defined, the package defines implicits to derive
decoders for `Option[T]`, `Seq[T]` and `Map[String, T]`.

## ConfEncoder

To convert a class instance into `Conf` use `ConfEncoder[T]`. It's possible to
Expand Down Expand Up @@ -241,6 +252,10 @@ bijectiveString.write(Bijective("write"))
bijectiveString.read(Conf.Str("write"))
```

## ConfCodecEx and ConfCodecExT

Similar to `ConfCodec` but derives from `ConfDecoderExT` instead of `ConfDecoder`.

## ConfError

`ConfError` is a helper to produce readable and potentially aggregated error
Expand Down Expand Up @@ -321,6 +336,8 @@ true when you have documentation to keep up-to-date as well.
```scala mdoc:silent:nest
implicit val decoder: ConfDecoder[User] =
generic.deriveDecoder[User](User("John", 42)).noTypos
implicit val decoderEx: ConfDecoderEx[User] =
generic.deriveDecoderEx[User](User("Jane", 24)).noTypos
```

```scala mdoc
Expand All @@ -336,6 +353,14 @@ ConfDecoder[User].read(Conf.parseString("""
name = John
age = Old
"""))
ConfDecoderEx[User].read(
Some(User(name = "Jack", age = 33)),
Conf.parseString("name = John")
)
ConfDecoderEx[User].read(
None,
Conf.parseString("name = John")
)
```

Sometimes automatic derivation fails, for example if your class contains fields
Expand All @@ -347,7 +372,7 @@ case class Funky(file: File)
implicit val surface = generic.deriveSurface[Funky]
```

This will fail wiith a fail cryptic compile error
This will fail with a fail cryptic compile error

```scala mdoc:fail
implicit val decoder = generic.deriveDecoder[Funky](Funky(new File("")))
Expand Down
14 changes: 14 additions & 0 deletions metaconfig-core/shared/src/main/scala/metaconfig/Conf.scala
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ object Conf {
val empty: Obj = Obj()
def apply(values: (String, Conf)*): Obj = Obj(values.toList)
}

def getEx[A](state: A, conf: Conf, path: Seq[String])(
implicit ev: ConfDecoderEx[A]
): Configured[A] =
ConfGet.getKey(conf, path) match {
case None => Configured.Ok(state)
case Some(subconf) => ev.read(Some(state), subconf)
}

def getSettingEx[A](state: A, conf: Conf, setting: Setting)(
implicit ev: ConfDecoderEx[A]
): Configured[A] =
getEx(state, conf, setting.name +: setting.alternativeNames)

}

object ConfOps {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package metaconfig

import metaconfig.generic.Settings

class ConfCodecExT[S, A](
encoder: ConfEncoder[A],
decoder: ConfDecoderExT[S, A]
) extends ConfDecoderExT[S, A]
with ConfEncoder[A] {
override def write(value: A): Conf = encoder.write(value)
override def read(state: Option[S], conf: Conf): Configured[A] =
decoder.read(state, conf)

def bimap[B](in: B => A, out: A => B): ConfCodecExT[S, B] =
new ConfCodecExT[S, B](encoder.contramap(in), decoder.map(out))

def noTypos(implicit settings: Settings[A]): ConfCodecExT[S, A] = {
val noTyposDecoder = decoder.noTypos
if (noTyposDecoder eq decoder) this
else new ConfCodecExT(encoder, noTyposDecoder)
}

}

object ConfCodecExT {
def apply[A, B](implicit ev: ConfCodecExT[A, B]): ConfCodecExT[A, B] = ev
}

object ConfCodecEx {
def apply[A](implicit obj: ConfCodecEx[A]): ConfCodecEx[A] = obj
}
198 changes: 198 additions & 0 deletions metaconfig-core/shared/src/main/scala/metaconfig/ConfDecoderExT.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package metaconfig

import scala.collection.compat._
import scala.reflect.ClassTag

import java.nio.file.Path
import java.nio.file.Paths

import metaconfig.internal.NoTyposDecoderEx

trait ConfDecoderExT[-S, A] {

def read(state: Option[S], conf: Conf): Configured[A]

}

object ConfDecoderExT {

def apply[S, A](implicit ev: ConfDecoderExT[S, A]): ConfDecoderExT[S, A] = ev

def from[S, A](f: (Option[S], Conf) => Configured[A]): ConfDecoderExT[S, A] =
(state, conf) => f(state, conf)

def fromPartial[S, A](expect: String)(
f: PartialFunction[(Option[S], Conf), Configured[A]]
): ConfDecoderExT[S, A] =
(state, conf) =>
f.applyOrElse(
(state, conf),
(_: (Option[S], Conf)) =>
Configured.NotOk(ConfError.typeMismatch(expect, conf))
)

def constant[S, A](value: A): ConfDecoderExT[S, A] =
(_, _) => Configured.ok(value)

implicit def confDecoder[S]: ConfDecoderExT[S, Conf] =
(_, conf) => Configured.Ok(conf)

implicit def subConfDecoder[S, A <: Conf: ClassTag]: ConfDecoderExT[S, A] =
fromPartial("Config") {
case (_, x: A) => Configured.Ok(x)
}

implicit def bigDecimalConfDecoder[S]: ConfDecoderExT[S, BigDecimal] =
fromPartial[S, BigDecimal]("Number") {
case (_, Conf.Num(x)) => Configured.Ok(x)
case (_, Conf.Str(Extractors.Number(n))) => Configured.Ok(n)
}

implicit def intConfDecoder[S]: ConfDecoderExT[S, Int] =
bigDecimalConfDecoder[S].map(_.toInt)

implicit def stringConfDecoder[S]: ConfDecoderExT[S, String] =
fromPartial[S, String]("String") {
case (_, Conf.Str(x)) => Configured.Ok(x)
}

implicit def unitConfDecoder[S]: ConfDecoderExT[S, Unit] =
from[S, Unit] { case _ => Configured.unit }

implicit def booleanConfDecoder[S]: ConfDecoderExT[S, Boolean] =
fromPartial[S, Boolean]("Bool") {
case (_, Conf.Bool(x)) => Configured.Ok(x)
case (_, Conf.Str("true" | "on" | "yes")) => Configured.Ok(true)
case (_, Conf.Str("false" | "off" | "no")) => Configured.Ok(false)
}

implicit def pathConfDecoder[S]: ConfDecoderExT[S, Path] =
stringConfDecoder[S].flatMap { path =>
Configured.fromExceptionThrowing(Paths.get(path))
}

implicit def canBuildOptionT[S, A](
implicit ev: ConfDecoderExT[S, A]
): ConfDecoderExT[S, Option[A]] =
(state, conf) =>
conf match {
case Conf.Null() => Configured.ok(None)
case _ => ev.read(state, conf).map(Some.apply)
}

implicit def canBuildOption[A](
implicit ev: ConfDecoderEx[A]
): ConfDecoderEx[Option[A]] =
(state, conf) =>
conf match {
case Conf.Null() => Configured.ok(None)
case _ => ev.read(state.flatten, conf).map(Some.apply)
}

implicit def canBuildStringMapT[S, A, CC[_, _]](
implicit ev: ConfDecoderExT[S, A],
factory: Factory[(String, A), CC[String, A]],
classTag: ClassTag[A]
): ConfDecoderExT[S, CC[String, A]] =
fromPartial(
s"Map[String, ${classTag.runtimeClass.getName}]"
) {
case (state, Conf.Obj(values)) =>
buildFrom(state, values, ev, factory)(_._2, (x, y) => (x._1, y))
}

implicit def canBuildStringMap[A, CC[x, y] <: collection.Iterable[(x, y)]](
implicit ev: ConfDecoderEx[A],
factory: Factory[(String, A), CC[String, A]],
classTag: ClassTag[A]
): ConfDecoderEx[CC[String, A]] = {
val none: Option[A] = None
fromPartial(
s"Map[String, ${classTag.runtimeClass.getName}]"
) {
case (_, Conf.Obj(values)) =>
buildFrom(none, values, ev, factory)(_._2, (x, y) => (x._1, y))
}
}

implicit def canBuildSeqT[S, A, C[_]](
implicit ev: ConfDecoderExT[S, A],
factory: Factory[A, C[A]],
classTag: ClassTag[A]
): ConfDecoderExT[S, C[A]] =
fromPartial(
s"List[${classTag.runtimeClass.getName}]"
) {
case (state, Conf.Lst(values)) =>
buildFrom(state, values, ev, factory)(identity, (_, x) => x)
}

implicit def canBuildSeq[A, C[x] <: collection.Iterable[x]](
implicit ev: ConfDecoderEx[A],
factory: Factory[A, C[A]],
classTag: ClassTag[A]
): ConfDecoderEx[C[A]] = {
val none: Option[A] = None
fromPartial(
s"List[${classTag.runtimeClass.getName}]"
) {
case (_, Conf.Lst(values)) =>
buildFrom(none, values, ev, factory)(identity, (_, x) => x)
}
}

implicit final class Implicits[S, A](self: ConfDecoderExT[S, A]) {

def read(state: Option[S], conf: Configured[Conf]): Configured[A] =
conf.andThen(self.read(state, _))

def map[B](f: A => B): ConfDecoderExT[S, B] =
(state, conf) => self.read(state, conf).map(f)

def flatMap[B](f: A => Configured[B]): ConfDecoderExT[S, B] =
(state, conf) => self.read(state, conf).andThen(f)

def orElse(other: ConfDecoderExT[S, A]): ConfDecoderExT[S, A] =
(state, conf) =>
self.read(state, conf).recoverWith { x =>
other.read(state, conf).recoverWith(x.combine)
}

def noTypos(implicit settings: generic.Settings[A]): ConfDecoderExT[S, A] =
if (self.isInstanceOf[NoTyposDecoderEx[_, _]]) self
else new NoTyposDecoderEx[S, A](self)

}

private[metaconfig] def buildFrom[V, S, A, B, Coll](
state: Option[S],
values: List[V],
ev: ConfDecoderExT[S, A],
factory: Factory[B, Coll]
)(a2conf: V => Conf, ab2c: (V, A) => B): Configured[Coll] = {
val successB = factory.newBuilder
val errorB = List.newBuilder[ConfError]
successB.sizeHint(values.length)
values.foreach { value =>
ev.read(state, a2conf(value)) match {
case Configured.NotOk(e) => errorB += e
case Configured.Ok(decoded) => successB += ab2c(value, decoded)
}
}
Configured(successB.result(), errorB.result(): _*)
}

}

object ConfDecoderEx {

def apply[A](implicit ev: ConfDecoderEx[A]): ConfDecoderEx[A] = ev

def from[A](f: (Option[A], Conf) => Configured[A]): ConfDecoderEx[A] =
ConfDecoderExT.from[A, A](f)

def fromPartial[A](expect: String)(
f: PartialFunction[(Option[A], Conf), Configured[A]]
): ConfDecoderEx[A] = ConfDecoderExT.fromPartial[A, A](expect)(f)

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ package object generic {
macro metaconfig.internal.Macros.deriveConfEncoderImpl[T]
def deriveCodec[T](default: T): ConfCodec[T] =
macro metaconfig.internal.Macros.deriveConfCodecImpl[T]

def deriveDecoderEx[T](default: T): ConfDecoderEx[T] =
macro metaconfig.internal.Macros.deriveConfDecoderExImpl[T]
def deriveCodecEx[T](default: T): ConfCodecEx[T] =
macro metaconfig.internal.Macros.deriveConfCodecExImpl[T]
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ class Macros(val c: blackbox.Context) {
"""
}

def deriveConfCodecExImpl[T: c.WeakTypeTag](default: Tree): Tree = {
val T = assumeClass[T]
q"""
new _root_.metaconfig.ConfCodecEx[$T](
_root_.metaconfig.generic.deriveEncoder[$T],
_root_.metaconfig.generic.deriveDecoderEx[$T]($default)
)
"""
}

def deriveConfEncoderImpl[T: c.WeakTypeTag]: Tree = {
val T = assumeClass[T]
val params = this.params(T)
Expand Down Expand Up @@ -124,6 +134,10 @@ class Macros(val c: blackbox.Context) {
def deriveConfDecoderExImpl[T: c.WeakTypeTag](default: Tree): Tree = {
val T = assumeClass[T]
val Tclass = T.typeSymbol.asClass
val optionT = weakTypeOf[Option[T]]
val resT = weakTypeOf[ConfDecoderEx[T]]
val retvalT = weakTypeOf[Configured[T]]

val settings = c.inferImplicitValue(weakTypeOf[Settings[T]])
if (settings == null || settings.isEmpty) {
c.abort(
Expand All @@ -135,15 +149,24 @@ class Macros(val c: blackbox.Context) {
}
val paramss = Tclass.primaryConstructor.asMethod.paramLists
if (paramss.isEmpty || paramss.head.isEmpty)
return q"_root_.metaconfig.ConfDecoder.constant($default)"
return q"""
new $resT {
def read(
state: $optionT,
conf: _root_.metaconfig.Conf
): $retvalT = {
Configured.Ok(state.getOrElse($default))
}
}
"""

val (head :: params) :: Nil = paramss
def next(param: Symbol): Tree = {
val P = param.info.resultType
val name = param.name.decodedName.toString
val getter = T.member(param.name)
val fallback = q"tmp.$getter"
q"conf.getSettingOrElse[$P](settings.unsafeGet($name), $fallback)"
q"Conf.getSettingEx[$P]($fallback, conf, settings.unsafeGet($name))"
}
val product = params.foldLeft(next(head)) {
case (accum, param) => q"$accum.product(${next(param)})"
Expand All @@ -168,12 +191,13 @@ class Macros(val c: blackbox.Context) {
val ctor = q"new $T(..$args)"

q"""
new ${weakTypeOf[ConfDecoder[T]]} {
new $resT {
def read(
state: $optionT,
conf: _root_.metaconfig.Conf
): ${weakTypeOf[Configured[T]]} = {
): $retvalT = {
val settings = $settings
val tmp = $default
val tmp = state.getOrElse($default)
$product.map { t => $ctor }
}
}
Expand Down
Loading

0 comments on commit 8204327

Please sign in to comment.