Skip to content

Commit

Permalink
feat: support all annotations in Scala 3
Browse files Browse the repository at this point in the history
  • Loading branch information
ThijsBroersen committed Nov 15, 2024
1 parent b832b8b commit d2cdce5
Show file tree
Hide file tree
Showing 12 changed files with 478 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ private[config] trait KeyConversionFunctions {
/**
* Add a post fix to an existing key
*/
def addPostFixToKey(string: String): String => String =
s => s"${s}${string.capitalize}"
def addPostFixToKey(postfix: String): String => String =
s => s"${s}${postfix.capitalize}"

/**
* Add a suffix to an existing key
*/
def addSuffixToKey(suffix: String): String => String =
s => s"${s}${suffix.capitalize}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@ final case class discriminator(keyName: String = "type") extends StaticAnnotatio
final case class kebabCase() extends StaticAnnotation
final case class snakeCase() extends StaticAnnotation
final case class prefix(prefix: String) extends StaticAnnotation
// @deprecated("Use `suffix` instead", "4.0.3")
final case class postfix(postfix: String) extends StaticAnnotation
final case class suffix(suffix: String) extends StaticAnnotation
Original file line number Diff line number Diff line change
Expand Up @@ -80,26 +80,6 @@ object DeriveConfig {

type Typeclass[T] = DeriveConfig[T]

sealed trait KeyModifier
sealed trait CaseModifier extends KeyModifier

object KeyModifier {
case object KebabCase extends CaseModifier
case object SnakeCase extends CaseModifier
case object NoneModifier extends CaseModifier
case class Prefix(prefix: String) extends KeyModifier
case class Postfix(postfix: String) extends KeyModifier

def getModifierFunction(keyModifier: KeyModifier): String => String =
keyModifier match {
case KebabCase => toKebabCase
case SnakeCase => toSnakeCase
case Prefix(prefix) => addPrefixToKey(prefix)
case Postfix(postfix) => addPostFixToKey(postfix)
case NoneModifier => identity
}
}

final def wrapSealedTrait[T](
labels: Seq[String],
desc: Config[T]
Expand Down Expand Up @@ -141,6 +121,7 @@ object DeriveConfig {
val modifiers = annotations.collect {
case p: prefix => KeyModifier.Prefix(p.prefix)
case p: postfix => KeyModifier.Postfix(p.postfix)
case p: suffix => KeyModifier.Suffix(p.suffix)
}.toList

val caseModifier = annotations.collectFirst {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ package object magnolia {
type prefix = derivation.prefix
val prefix: derivation.prefix.type = derivation.prefix

// @deprecated("Use `suffix` instead", "4.0.3")
type postfix = derivation.postfix
// @deprecated("Use `suffix` instead", "4.0.3")
val postfix: derivation.postfix.type = derivation.postfix

type suffix = derivation.suffix
val suffix: derivation.suffix.type = derivation.suffix
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package zio.config.magnolia

import scala.quoted.*
import zio.config.derivation._

private[magnolia] object AnnotationMacros:
inline def nameOf[T]: List[name] = ${ filterAnnotations[T, name] }
inline def discriminatorOf[T]: List[discriminator] = ${ filterAnnotations[T, discriminator] }
inline def descriptionOf[T]: List[describe] = ${ filterAnnotations[T, describe] }
inline def caseModifier[T]: List[kebabCase | snakeCase] = ${ filterAnnotations[T, kebabCase | snakeCase] }
inline def kebabCaseOf[T]: List[kebabCase] = ${ filterAnnotations[T, kebabCase] }
inline def snakeCaseOf[T]: List[snakeCase] = ${ filterAnnotations[T, snakeCase] }
inline def keyModifiers[T]: List[prefix | postfix | suffix] = ${ filterAnnotations[T, prefix | postfix | suffix] }
inline def prefixOf[T]: List[prefix] = ${ filterAnnotations[T, prefix] }
inline def postfixOf[T]: List[postfix] = ${ filterAnnotations[T, postfix] }
inline def suffixOf[T]: List[suffix] = ${ filterAnnotations[T, suffix] }
inline def fieldNamesOf[T]: List[(String, List[name])] = ${ filterFieldAnnotations[T, name] }
inline def fieldDescriptionsOf[T]: List[(String, List[describe])] = ${
filterFieldAnnotations[T, describe]
}

private def filterAnnotations[T: Type, A: Type](using Quotes): Expr[List[A]] = {
import quotes.reflect.*

val annotationTpe = TypeRepr.of[A]

val annotations = TypeRepr
.of[T]
.typeSymbol
.annotations
.collect:
case term if term.tpe <:< annotationTpe => term

Expr.ofList(annotations.reverse.map(_.asExprOf[A]))
}

private def filterFieldAnnotations[T: Type, A: Type](using Quotes): Expr[List[(String, List[A])]] =
import quotes.reflect.*

val annotationTpe = TypeRepr.of[A]

val namedAnnotations = TypeRepr
.of[T]
.typeSymbol
.primaryConstructor
.paramSymss
.flatten
.map(field => field.name -> field.annotations)

Expr
.ofList(
namedAnnotations
.map:
case (name, terms) =>
name -> terms.collect:
case term if term.tpe <:< annotationTpe => term
.map:
case (name, terms) => Expr(name) -> terms.reverse.map(_.asExprOf[A])
.map((name, annotations) => Expr.ofTuple((name, Expr.ofList(annotations))))
)
end AnnotationMacros
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package zio.config.magnolia

import scala.quoted.*
import zio.config.derivation._

private[magnolia] object DefaultValueMacros:

inline def defaultValuesOf[T]: List[(String, Any)] = ${ defaultValues[T] }
def defaultValues[T: Type](using Quotes): Expr[List[(String, Any)]] =
import quotes.reflect.*
val tpe = TypeRepr.of[T]

val sym = tpe.typeSymbol

val namesOfFieldsWithDefaultValues =
sym.caseFields.filter(s => s.flags.is(Flags.HasDefault)).map(_.name)

val companionClas =
sym.companionClass

val defaultRefs =
companionClas.declarations
.filter(_.name.startsWith("$lessinit$greater$default"))
.map(Ref(_))

Expr.ofList(namesOfFieldsWithDefaultValues.zip(defaultRefs).map { case (n, ref) =>
Expr.ofTuple(Expr(n), ref.asExpr)
})

end DefaultValueMacros
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import DeriveConfig._
import zio.{Chunk, Config, ConfigProvider, LogLevel}, Config._
import zio.config.syntax._
import zio.config.derivation._
import scala.annotation.nowarn

final case class DeriveConfig[A](desc: Config[A], metadata: Option[DeriveConfig.Metadata] = None) {
def ??(description: String): DeriveConfig[A] =
Expand All @@ -36,30 +37,32 @@ final case class DeriveConfig[A](desc: Config[A], metadata: Option[DeriveConfig.

object DeriveConfig {

def apply[A](implicit ev: DeriveConfig[A]): DeriveConfig[A] =
def apply[A](using ev: DeriveConfig[A]): DeriveConfig[A] =
ev

def from[A](desc: Config[A]) =
DeriveConfig(desc, None)

sealed trait Metadata {
def originalName: String = this match {
case Metadata.Object(name, _) => name.originalName
case Metadata.Product(name, _) => name.originalName
case Metadata.Coproduct(name, _) => name.originalName
case Metadata.Object(name, _) => name.originalName
case Metadata.Product(name, _, _) => name.originalName
case Metadata.Coproduct(name, _, _) => name.originalName
}

def alternativeNames: List[String] = this match {
case Metadata.Object(_, _) => Nil
case Metadata.Product(name, _) => name.alternativeNames
case Metadata.Coproduct(name, _) => name.alternativeNames
case Metadata.Object(_, _) => Nil
case Metadata.Product(name, _, _) => name.alternativeNames
case Metadata.Coproduct(name, _, _) => name.alternativeNames
}
}

object Metadata {
final case class Object[T](name: ProductName, constValue: T) extends Metadata
final case class Product(name: ProductName, fields: List[FieldName]) extends Metadata
final case class Coproduct(name: CoproductName, metadata: List[Metadata]) extends Metadata
final case class Object[T](name: ProductName, constValue: T) extends Metadata
final case class Product(name: ProductName, fields: List[FieldName], keyModifiers: List[KeyModifier])
extends Metadata
final case class Coproduct(name: CoproductName, metadata: List[Metadata], keyModifiers: List[KeyModifier])
extends Metadata
}

final case class FieldName(originalName: String, alternativeNames: List[String], descriptions: List[String])
Expand Down Expand Up @@ -125,7 +128,7 @@ object DeriveConfig {
desc.metadata
) :: summonDeriveConfigForCoProduct[ts]

inline def summonDeriveConfigAll[T <: Tuple]: List[DeriveConfig[_]] =
inline def summonDeriveConfigAll[T <: Tuple]: List[DeriveConfig[?]] =
inline erasedValue[T] match
case _: EmptyTuple => Nil
case _: (t *: ts) =>
Expand All @@ -137,20 +140,29 @@ object DeriveConfig {
case _: (t *: ts) => constValue[t].toString :: labelsOf[ts]

inline def customNamesOf[T]: List[String] =
Macros.nameOf[T].map(_.name)
AnnotationMacros.nameOf[T].map(_.name)

inline def customFieldNamesOf[T]: Map[String, name] =
Macros.fieldNameOf[T].flatMap { case (str, nmes) => nmes.map(name => (str, name)) }.toMap
AnnotationMacros.fieldNamesOf[T].flatMap { case (str, nmes) => nmes.map(name => (str, name)) }.toMap

inline given derived[T](using m: Mirror.Of[T]): DeriveConfig[T] =
lazy val keyModifiers =
(AnnotationMacros.keyModifiers[T] ++ AnnotationMacros.caseModifier[T])
.map:
case p: prefix => KeyModifier.Prefix(p.prefix)
case p: postfix @nowarn => KeyModifier.Postfix(p.postfix)
case p: suffix => KeyModifier.Suffix(p.suffix)
case _: kebabCase => KeyModifier.KebabCase
case _: snakeCase => KeyModifier.SnakeCase

inline m match
case s: Mirror.SumOf[T] =>
val coproductName: CoproductName =
CoproductName(
originalName = constValue[m.MirroredLabel],
alternativeNames = customNamesOf[T],
descriptions = Macros.documentationOf[T].map(_.describe),
typeDiscriminator = Macros.discriminator[T].headOption.map(_.keyName)
descriptions = AnnotationMacros.descriptionOf[T].map(_.describe),
typeDiscriminator = AnnotationMacros.discriminatorOf[T].headOption.map(_.keyName)
)

lazy val subClassDescriptions =
Expand All @@ -159,14 +171,14 @@ object DeriveConfig {
lazy val desc =
mergeAllProducts(subClassDescriptions.map(castTo[DeriveConfig[T]]), coproductName.typeDiscriminator)

DeriveConfig.from(tryAllKeys(desc.desc, None, coproductName.alternativeNames))
DeriveConfig.from(tryAllKeys(desc.desc, None, coproductName.alternativeNames, keyModifiers))

case m: Mirror.ProductOf[T] =>
val productName =
ProductName(
originalName = constValue[m.MirroredLabel],
alternativeNames = customNamesOf[T],
descriptions = Macros.documentationOf[T].map(_.describe)
descriptions = AnnotationMacros.descriptionOf[T].map(_.describe)
)

lazy val originalFieldNamesList =
Expand All @@ -176,10 +188,10 @@ object DeriveConfig {
customFieldNamesOf[T]

lazy val documentations =
Macros.fieldDocumentationOf[T].toMap
AnnotationMacros.fieldDescriptionsOf[T].toMap

lazy val fieldAndDefaultValues: Map[String, Any] =
Macros.defaultValuesOf[T].toMap
DefaultValueMacros.defaultValuesOf[T].toMap

lazy val fieldNames =
originalFieldNamesList.foldRight(Nil: List[FieldName]) { (str, list) =>
Expand All @@ -198,6 +210,7 @@ object DeriveConfig {
fieldConfigsWithDefaultValues,
productName,
fieldNames,
keyModifiers,
lst => m.fromProduct(Tuple.fromArray(lst.toArray[Any])),
castTo[Product](_).productIterator.toList
)
Expand All @@ -213,10 +226,10 @@ object DeriveConfig {
allDescs
.map(desc =>
desc.metadata match {
case Some(Metadata.Product(productName, fields)) if (fields.nonEmpty) =>
tryAllKeys(desc.desc, Some(productName.originalName), productName.alternativeNames)
case Some(_) => desc.desc
case None => desc.desc
case Some(Metadata.Product(productName, fields, keyModifiers)) if (fields.nonEmpty) =>
tryAllKeys(desc.desc, Some(productName.originalName), productName.alternativeNames, keyModifiers)
case Some(_) => desc.desc
case None => desc.desc
}
)
.reduce(_ orElse _)
Expand All @@ -235,7 +248,7 @@ object DeriveConfig {

case None => Nil
}
}: _*
}*
)
}

Expand All @@ -245,7 +258,7 @@ object DeriveConfig {
defaultValues: Map[String, Any],
fieldNames: List[String],
descriptors: List[DeriveConfig[Any]]
): List[DeriveConfig[_]] =
): List[DeriveConfig[?]] =
descriptors.zip(fieldNames).map { case (desc, fieldName) =>
defaultValues.get(fieldName) match {
case Some(any) => DeriveConfig(desc.desc.withDefault(any), desc.metadata)
Expand All @@ -254,9 +267,10 @@ object DeriveConfig {
}

def mergeAllFields[T](
allDescs: => List[DeriveConfig[_]],
allDescs: => List[DeriveConfig[?]],
productName: ProductName,
fieldNames: => List[FieldName],
keyModifiers: List[KeyModifier],
f: List[Any] => T,
g: T => List[Any]
): DeriveConfig[T] =
Expand All @@ -273,23 +287,37 @@ object DeriveConfig {
else
val listOfDesc =
fieldNames.zip(allDescs).map { case (fieldName, desc) =>
val fieldDesc = tryAllKeys(desc.desc, Some(fieldName.originalName), fieldName.alternativeNames)
val fieldDesc = tryAllKeys(desc.desc, Some(fieldName.originalName), fieldName.alternativeNames, keyModifiers)
fieldName.descriptions.foldRight(fieldDesc)((doc, desc) => desc ?? doc)
}

val descOfList =
Config.collectAll(listOfDesc.head, listOfDesc.tail: _*)
Config.collectAll(listOfDesc.head, listOfDesc.tail*)

DeriveConfig(descOfList.map(f), Some(Metadata.Product(productName, fieldNames)))
DeriveConfig(descOfList.map(f), Some(Metadata.Product(productName, fieldNames, keyModifiers)))

def tryAllKeys[A](
desc: Config[A],
originalKey: Option[String],
alternativeKeys: List[String]
alternativeKeys: List[String],
keyModifiers: List[KeyModifier]
): Config[A] =

val sortedKeyModifiers = keyModifiers.sortWith {
case (a: CaseModifier, b: CaseModifier) => false
case (a: CaseModifier, _) => false
case (_, b: CaseModifier) => true
case _ => false
}

val modifyKey: String => String =
sortedKeyModifiers.map(KeyModifier.getModifierFunction).foldLeft(_)((key, modifier) => modifier(key))

alternativeKeys match {
case Nil => originalKey.fold(desc)(desc.nested(_))
case keys => keys.view.map(desc.nested(_)).reduce(_ orElse _)
case Nil =>
originalKey.fold(desc)(k => desc.nested(modifyKey(k)))
// case keys => keys.view.map(k => desc.nested(modifyKey(k))).reduce(_ orElse _) // Looks like the Scala 3 implementation modifies alternative names while the Scala 2 implementations treats them as is.
case keys => keys.view.map(k => desc.nested(k)).reduce(_ orElse _)
}

def castTo[T](a: Any): T =
Expand Down
Loading

0 comments on commit d2cdce5

Please sign in to comment.