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

Feat/support all annotations in scala 3 #1471

Open
wants to merge 3 commits into
base: series/4.x
Choose a base branch
from
Open
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
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 @@ -89,13 +89,15 @@ object DeriveConfig {
case object NoneModifier extends CaseModifier
case class Prefix(prefix: String) extends KeyModifier
case class Postfix(postfix: String) extends KeyModifier
case class Suffix(suffix: 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 Suffix(suffix) => addSuffixToKey(suffix)
case NoneModifier => identity
}
}
Expand Down Expand Up @@ -141,6 +143,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,54 @@ final case class DeriveConfig[A](desc: Config[A], metadata: Option[DeriveConfig.

object DeriveConfig {

def apply[A](implicit ev: DeriveConfig[A]): DeriveConfig[A] =
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
case class Suffix(suffix: 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 Suffix(suffix) => addSuffixToKey(suffix)
case NoneModifier => identity
}
}

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 +150,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 +162,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 +193,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 +210,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 +232,7 @@ object DeriveConfig {
fieldConfigsWithDefaultValues,
productName,
fieldNames,
keyModifiers,
lst => m.fromProduct(Tuple.fromArray(lst.toArray[Any])),
castTo[Product](_).productIterator.toList
)
Expand All @@ -213,10 +248,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 +270,7 @@ object DeriveConfig {

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

Expand All @@ -245,7 +280,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 +289,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 +309,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 _)
Comment on lines +340 to +342
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please review this carefully, because I think L341 should be enabled and L342 disabled. This changes the existing behaviour only to match the Scala 2 behaviour.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L340 I already adjusted.

}

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