Skip to content

Commit

Permalink
SectionRenameDecoder: add new decoder wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
kitbellew committed Nov 30, 2024
1 parent b671a6c commit 9ed4cef
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 1 deletion.
3 changes: 3 additions & 0 deletions metaconfig-core/shared/src/main/scala/metaconfig/Conf.scala
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ object Conf {
}
}

def nestedWithin(keys: String*): Conf = keys
.foldRight(conf) { case (k, res) => Conf.Obj(k -> res) }

}

}
Expand Down
12 changes: 12 additions & 0 deletions metaconfig-core/shared/src/main/scala/metaconfig/ConfDecoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,16 @@ object ConfDecoder {
def orElse[A](a: ConfDecoder[A], b: ConfDecoder[A]): ConfDecoder[A] =
conf => a.read(conf).recoverWithOrCombine(b.read(conf))

implicit final class Implicits[A](private val self: ConfDecoder[A])
extends AnyVal {

def detectSectionRenames(implicit
settings: generic.Settings[A],
): ConfDecoder[A] = SectionRenameDecoder(self)

def withSectionRenames(renames: annotation.SectionRename*): ConfDecoder[A] =
SectionRenameDecoder(self, renames.toList)

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ object ConfDecoderExT {
def noTypos(implicit settings: generic.Settings[A]): ConfDecoderExT[S, A] =
NoTyposDecoder(self)

def detectSectionRenames(implicit
settings: generic.Settings[A],
): ConfDecoderExT[S, A] = SectionRenameDecoder(self)

def withSectionRenames(
renames: annotation.SectionRename*,
): ConfDecoderExT[S, A] = SectionRenameDecoder(self, renames.toList)

}

private[metaconfig] def buildFrom[V, S, A, B, Coll](
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package metaconfig.annotation

import scala.annotation.StaticAnnotation
import scala.collection.compat.immutable.ArraySeq
import scala.language.implicitConversions

import org.typelevel.paiges.Doc

Expand Down Expand Up @@ -32,3 +34,23 @@ final case class Section(name: String) extends StaticAnnotation
final case class TabCompleteAsPath() extends StaticAnnotation
final case class CatchInvalidFlags() extends StaticAnnotation
final case class TabCompleteAsOneOf(options: String*) extends StaticAnnotation

final case class SectionRename(oldName: String, newName: String)
extends StaticAnnotation {
require(oldName.nonEmpty && newName.nonEmpty)
val oldNameAsSeq: Seq[String] = ArraySeq.unsafeWrapArray(oldName.split('.'))
val newNameAsSeq: Seq[String] = ArraySeq.unsafeWrapArray(newName.split('.'))
locally {
val oldHead = oldNameAsSeq.head
require(
oldHead != newNameAsSeq.head,
s"SectionRename($oldName, $newName): rename under `$oldHead` instead",
)
}
override def toString: String =
s"Section '$oldName' is deprecated and renamed as '$newName'"
}
object SectionRename {
implicit def fromTuple(obj: (String, String)): SectionRename =
SectionRename(obj._1, obj._2)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package metaconfig.internal

import metaconfig._
import metaconfig.generic.Settings

import scala.annotation.tailrec

trait SectionRenameDecoder[A] extends Transformable[A] { self: A =>
protected val renames: List[annotation.SectionRename]
protected def renameSectionsAnd[B](
conf: Conf,
func: Conf => Configured[B],
): Configured[B] = SectionRenameDecoder.renameSections(renames)(conf)
.andThen(func)
}

object SectionRenameDecoder {

def apply[A](
dec: ConfDecoder[A],
renames: List[annotation.SectionRename],
): ConfDecoder[A] = dec match {
case x: Decoder[_] =>
if (x.renames eq renames) dec
else new Decoder[A](x.dec, (x.renames ++ renames).distinct)
case _ => new Decoder[A](dec, renames)
}

def apply[S, A](
dec: ConfDecoderExT[S, A],
renames: List[annotation.SectionRename],
): ConfDecoderExT[S, A] = dec match {
case x: DecoderEx[_, _] =>
if (x.renames eq renames) dec
else new DecoderEx[S, A](x.dec, (x.renames ++ renames).distinct)
case _ => new DecoderEx[S, A](dec, renames)
}

def apply[A](dec: ConfDecoder[A])(implicit ev: Settings[A]): ConfDecoder[A] =
fromSettings(ev, dec)(apply(_, _))

def apply[S, A](dec: ConfDecoderExT[S, A])(implicit
ev: Settings[A],
): ConfDecoderExT[S, A] = fromSettings(ev, dec)(apply(_, _))

private def fromSettings[D](ev: Settings[_], obj: D)(
f: (D, List[annotation.SectionRename]) => D,
): D = {
val list = ev.annotations.collect { case x: annotation.SectionRename => x }
if (list.isEmpty) obj else f(obj, list)
}

private class Decoder[A](
val dec: ConfDecoder[A],
val renames: List[annotation.SectionRename],
) extends ConfDecoder[A] with SectionRenameDecoder[ConfDecoder[A]] {
override def read(conf: Conf): Configured[A] =
renameSectionsAnd(conf, dec.read)
override def transform(f: SelfType => SelfType): SelfType =
apply(f(dec), renames)
}

private class DecoderEx[S, A](
val dec: ConfDecoderExT[S, A],
val renames: List[annotation.SectionRename],
) extends ConfDecoderExT[S, A] with SectionRenameDecoder[ConfDecoderExT[S, A]] {
override def read(state: Option[S], conf: Conf): Configured[A] =
renameSectionsAnd(conf, dec.read(state, _))
override def transform(f: SelfType => SelfType): SelfType =
apply(f(dec), renames)
}

@tailrec
private def renameSections(
values: List[annotation.SectionRename],
)(conf: Conf): Configured[Conf] = values match {
case head :: rest =>
val oldName = head.oldNameAsSeq
conf.getNestedConf(oldName: _*) match {
case Configured.Ok(oldVal: Conf) =>
val del = Conf.Obj.empty.nestedWithin(oldName: _*)
val add = oldVal.nestedWithin(head.newNameAsSeq: _*)
// remove on right (takes precedence), append on left (doesn't)
renameSections(rest)(ConfOps.merge(add, ConfOps.merge(conf, del)))
case x => x
}
case _ => Configured.Ok(conf)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ class DeriveConfDecoderExJVMSuite extends munit.FunSuite {
def checkOkStr[T, A](confStr: String, out: A, in: T = null)(implicit
loc: munit.Location,
decoder: ConfDecoderExT[T, A],
): Unit = {
): Unit = checkOkStrEx(decoder, confStr, out, in)

def checkOkStrEx[T, A](
decoder: ConfDecoderExT[T, A],
confStr: String,
out: A,
in: T = null,
)(implicit loc: munit.Location): Unit = {
val cfg = Input.String(confStr).parse(Hocon)
cfg.andThen(decoder.read(Option(in), _)) match {
case Configured.NotOk(err) => fail(err.toString)
Expand Down Expand Up @@ -84,4 +91,40 @@ class DeriveConfDecoderExJVMSuite extends munit.FunSuite {
)
}

test("nested param with rename") {
checkOkStrEx(
generic.deriveDecoderEx[Nested](Nested()).withSectionRenames(
"E.A" -> "e.a",
"E.B.B.Param" -> "e.b.b.param",
"E.B.C" -> "e.b.c",
).noTypos,
"""
|E {
| A = "xxx"
| B {
| B { Param = 3 }
| C {
| "+" = {
| k3 { param = 33 }
| }
| }
| }
|}
|""".stripMargin,
Nested(e =
Nested3(
a = "xxx",
b = Nested2(
a = "zzz",
b = OneParam(3),
c = Map("k2" -> OneParam(2), "k3" -> OneParam(33)),
),
),
),
Nested(e =
Nested3(a = "yyy", b = Nested2(a = "zzz", c = Map("k2" -> OneParam(2)))),
),
)
}

}

0 comments on commit 9ed4cef

Please sign in to comment.