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

SectionRenameDecoder: add new decoder wrapper #267

Merged
merged 1 commit into from
Dec 9, 2024
Merged
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
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
44 changes: 40 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,50 @@ 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)

private[metaconfig] def getPair(): (ConfEncoder[A], ConfDecoder[A]) =
(encoder, decoder)
}

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 _: Pair[_] => self.asInstanceOf[Pair[A]].getPair()
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) self 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
Loading