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 325fd63 commit dc642a7
Show file tree
Hide file tree
Showing 14 changed files with 473 additions and 125 deletions.
5 changes: 1 addition & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,7 @@ lazy val zioConfigTypesafeMagnoliaTests = projectMatrix
)
)
.dependsOn(zioConfig % "compile->compile;test->test", zioConfigTypesafe, zioConfigMagnolia, zioConfigDerivation)
.jvmPlatform(
scalaVersions = Seq(Scala212, Scala213 /*, Scala3*/ ),
settings = jvmSettings
) // FIXME: annotations for Scala 3 are not implemented, tests in zioConfigTypesafeMagnoliaTests fail
.jvmPlatform(scalaVersions = Seq(Scala212, Scala213, Scala3), settings = jvmSettings)

lazy val docs = projectMatrix
.in(file("zio-config-docs"))
Expand Down
10 changes: 8 additions & 2 deletions core/src/main/scala/zio/config/KeyConversionFunctions.scala
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
21 changes: 1 addition & 20 deletions magnolia/src/main/scala-2/zio/config/magnolia/DeriveConfig.scala
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
5 changes: 5 additions & 0 deletions magnolia/src/main/scala-2/zio/config/magnolia/package.scala
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
92 changes: 60 additions & 32 deletions magnolia/src/main/scala-3/zio/config/magnolia/DeriveConfig.scala
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 dc642a7

Please sign in to comment.