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 Dec 9, 2024
1 parent b85f3f7 commit aa6b6e7
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 9 deletions.
43 changes: 43 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,47 @@ a = {
}
```

### Renaming sections

`ConfDecoderEx` and `ConfCodecEx` (as well their obsolete non-`Ex` variants)
also support renaming sections of configuration, in case they have been
restructured but still need to provide backwards compatibility.

This can be accomplished in one of two ways:
- via a call to `.withSectionRenames(...)` with explicit rename arguments
- via a call to `.detectSectionRenames` when the target type is provided
with one or more `@SectionRename(...)` annotations

```
@SectionRename("spouse" -> "family.spouse")
case class Human(
name: String = "",
family: Option[Family] = None
)
case class Family(
spouse: String,
children: List[String] = Nil
)
object Human {
/** will parse correctly:
* {{{
* name = "John Doe"
* spouse = "Jane Doe" # maps to `family.spouse = ...`
* }}}
*/
val decoderWithRenamesDetected =
generic.deriveDecoderEx(Human()).noTypos.detectSectionRenames
// this one will also understand `kids`
val decoderWithRenamesExplicit =
generic.deriveDecoderEx(Human()).noTypos.withSectionRenames(
"spouse" -> "family.spouse",
"kids" -> "family.children"
)
}
```

## ConfEncoder

To convert a class instance into `Conf` use `ConfEncoder[T]`. It's possible to
Expand Down Expand Up @@ -419,6 +460,8 @@ The following features are not supported by generic derivation

## @DeprecatedName

> See also [Renaming Sections](#renaming-sections)
As your configuration evolves, you may want to rename some settings but you have
existing users who are using the old name. Use the `@DeprecatedName` annotation
to continue supporting the old name even if you go ahead with the rename.
Expand Down
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
43 changes: 39 additions & 4 deletions metaconfig-core/shared/src/main/scala/metaconfig/ConfCodec.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package metaconfig

import metaconfig.generic.Settings

trait ConfCodec[A] extends ConfDecoder[A] with ConfEncoder[A] { self =>
def bimap[B](in: B => A, out: A => B): ConfCodec[B] = new ConfCodec[B] {
override def write(value: B): Conf = self.write(in(value))
Expand All @@ -9,16 +11,49 @@ trait ConfCodec[A] extends ConfDecoder[A] with ConfEncoder[A] { self =>

object ConfCodec {
def apply[A](implicit ev: ConfCodec[A]): ConfCodec[A] = ev

private[metaconfig] class Pair[A](
private[metaconfig] val encoder: ConfEncoder[A],
private[metaconfig] val decoder: ConfDecoder[A],
) extends ConfCodec[A] {
override def write(value: A): Conf = encoder.write(value)
override def read(conf: Conf): Configured[A] = decoder.read(conf)
}

implicit def EncoderDecoderToCodec[A](implicit
encode: ConfEncoder[A],
decode: ConfDecoder[A],
): ConfCodec[A] = new ConfCodec[A] {
override def write(value: A): Conf = encode.write(value)
override def read(conf: Conf): Configured[A] = decode.read(conf)
}
): ConfCodec[A] = new Pair(encode, decode)

val IntCodec: ConfCodec[Int] = ConfCodec[Int]
val StringCodec: ConfCodec[String] = ConfCodec[String]
val BooleanCodec: ConfCodec[Boolean] = ConfCodec[Boolean]

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

private[metaconfig] def getPair(): (ConfEncoder[A], ConfDecoder[A]) =
self match {
case t: Pair[_] =>
val pair = self.asInstanceOf[Pair[A]]
(pair.encoder, pair.decoder)
case _ => (self, self)
}

private[metaconfig] def withDecoder(
f: ConfDecoder[A] => ConfDecoder[A],
): ConfCodec[A] = {
val (encoder, decoder) = getPair()
val dec = f(decoder)
if (dec eq decoder) this else new ConfCodec.Pair(encoder, dec)
}

def detectSectionRenames(implicit settings: Settings[A]): ConfCodec[A] =
self.withDecoder(_.detectSectionRenames)

def withSectionRenames(renames: annotation.SectionRename*): ConfCodec[A] =
self.withDecoder(_.withSectionRenames(renames: _*))

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,34 @@ class ConfCodecExT[S, A](encoder: ConfEncoder[A], decoder: ConfDecoderExT[S, A])
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)
def withDecoder(
f: ConfDecoderExT[S, A] => ConfDecoderExT[S, A],
): ConfCodecExT[S, A] = {
val dec = f(decoder)
if (dec eq decoder) this else new ConfCodecExT(encoder, dec)
}

def noTypos(implicit settings: Settings[A]): ConfCodecExT[S, A] =
withDecoder(_.noTypos)

}

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

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

def detectSectionRenames(implicit
settings: Settings[A],
): ConfCodecExT[S, A] = self.withDecoder(_.detectSectionRenames)

def withSectionRenames(
renames: annotation.SectionRename*,
): ConfCodecExT[S, A] = self.withDecoder(_.withSectionRenames(renames: _*))

}

}

object ConfCodecEx {
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,16 @@ 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('.'))
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,91 @@
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 {

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

@tailrec
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 apply(x.asInstanceOf[DecoderEx[S, A]].dec, x.renames ++ renames)
case _ => new DecoderEx[S, A](dec, renames.distinct)
}

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

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

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 _ => renameSections(rest)(conf)
}
case _ => Configured.Ok(conf)
}

}
Loading

0 comments on commit aa6b6e7

Please sign in to comment.