From d2cdce5ac7516843d8691db7f81883a874b1cdf3 Mon Sep 17 00:00:00 2001 From: Thijs Broersen Date: Sat, 16 Nov 2024 00:50:31 +0100 Subject: [PATCH] feat: support all annotations in Scala 3 --- .../zio/config/KeyConversionFunctions.scala | 10 +- .../zio/config/derivation/annotations.scala | 2 + .../zio/config/magnolia/DeriveConfig.scala | 21 +-- .../zio/config/magnolia/package.scala | 5 + .../config/magnolia/AnnotationMacros.scala | 61 +++++++ .../config/magnolia/DefaultValueMacros.scala | 30 ++++ .../zio/config/magnolia/DeriveConfig.scala | 92 ++++++---- .../zio/config/magnolia/package.scala | 19 ++- .../zio/config/magnolia/KeyModifier.scala | 24 +++ .../magnolia/AnnotationMacrosSpec.scala | 102 +++++++++++ .../config/magnolia/DefaultValueSpec.scala | 14 +- .../config/magnolia/DeriveConfigSpec.scala | 159 ++++++++++++++++++ 12 files changed, 478 insertions(+), 61 deletions(-) create mode 100644 magnolia/shared/src/main/scala-dotty/zio/config/magnolia/AnnotationMacros.scala create mode 100644 magnolia/shared/src/main/scala-dotty/zio/config/magnolia/DefaultValueMacros.scala create mode 100644 magnolia/shared/src/main/scala/zio/config/magnolia/KeyModifier.scala create mode 100644 magnolia/shared/src/test/scala-dotty/zio/config/magnolia/AnnotationMacrosSpec.scala create mode 100644 magnolia/shared/src/test/scala/zio/config/magnolia/DeriveConfigSpec.scala diff --git a/core/shared/src/main/scala/zio/config/KeyConversionFunctions.scala b/core/shared/src/main/scala/zio/config/KeyConversionFunctions.scala index 5cc1f301f..1c8032c3e 100644 --- a/core/shared/src/main/scala/zio/config/KeyConversionFunctions.scala +++ b/core/shared/src/main/scala/zio/config/KeyConversionFunctions.scala @@ -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}" } diff --git a/derivation/shared/src/main/scala/zio/config/derivation/annotations.scala b/derivation/shared/src/main/scala/zio/config/derivation/annotations.scala index c039a7d8b..6a692dfa7 100644 --- a/derivation/shared/src/main/scala/zio/config/derivation/annotations.scala +++ b/derivation/shared/src/main/scala/zio/config/derivation/annotations.scala @@ -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 diff --git a/magnolia/shared/src/main/scala-2.12-2.13/zio/config/magnolia/DeriveConfig.scala b/magnolia/shared/src/main/scala-2.12-2.13/zio/config/magnolia/DeriveConfig.scala index 7947c163f..616e289ea 100644 --- a/magnolia/shared/src/main/scala-2.12-2.13/zio/config/magnolia/DeriveConfig.scala +++ b/magnolia/shared/src/main/scala-2.12-2.13/zio/config/magnolia/DeriveConfig.scala @@ -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] @@ -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 { diff --git a/magnolia/shared/src/main/scala-2.12-2.13/zio/config/magnolia/package.scala b/magnolia/shared/src/main/scala-2.12-2.13/zio/config/magnolia/package.scala index 96b6cde62..4dae2fcc8 100644 --- a/magnolia/shared/src/main/scala-2.12-2.13/zio/config/magnolia/package.scala +++ b/magnolia/shared/src/main/scala-2.12-2.13/zio/config/magnolia/package.scala @@ -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 } diff --git a/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/AnnotationMacros.scala b/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/AnnotationMacros.scala new file mode 100644 index 000000000..8cf2c7808 --- /dev/null +++ b/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/AnnotationMacros.scala @@ -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 diff --git a/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/DefaultValueMacros.scala b/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/DefaultValueMacros.scala new file mode 100644 index 000000000..2f53fdc6d --- /dev/null +++ b/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/DefaultValueMacros.scala @@ -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 diff --git a/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/DeriveConfig.scala b/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/DeriveConfig.scala index 9ce23bf4e..6558b689f 100644 --- a/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/DeriveConfig.scala +++ b/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/DeriveConfig.scala @@ -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] = @@ -36,7 +37,7 @@ 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]) = @@ -44,22 +45,24 @@ object DeriveConfig { 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]) @@ -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) => @@ -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 = @@ -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 = @@ -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) => @@ -198,6 +210,7 @@ object DeriveConfig { fieldConfigsWithDefaultValues, productName, fieldNames, + keyModifiers, lst => m.fromProduct(Tuple.fromArray(lst.toArray[Any])), castTo[Product](_).productIterator.toList ) @@ -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 _) @@ -235,7 +248,7 @@ object DeriveConfig { case None => Nil } - }: _* + }* ) } @@ -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) @@ -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] = @@ -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 = diff --git a/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/package.scala b/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/package.scala index 4fccb3835..d1948cc37 100644 --- a/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/package.scala +++ b/magnolia/shared/src/main/scala-dotty/zio/config/magnolia/package.scala @@ -18,8 +18,25 @@ package object magnolia { type discriminator = derivation.discriminator val discriminator: derivation.discriminator.type = derivation.discriminator + type kebabCase = derivation.kebabCase + val kebabCase: derivation.kebabCase.type = derivation.kebabCase + + type snakeCase = derivation.snakeCase + val snakeCase: derivation.snakeCase.type = derivation.snakeCase + + 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 + // If you happen to define a Config directly as an implicit, then automatically DeriveConfig will be available - implicit def deriveConfigFromConfig[A](implicit ev: Config[A]): DeriveConfig[A] = + given deriveConfigFromConfig[A](using ev: Config[A]): DeriveConfig[A] = DeriveConfig(ev, None) implicit class ConfigProviderOps[A](configProvider: ConfigProvider) { diff --git a/magnolia/shared/src/main/scala/zio/config/magnolia/KeyModifier.scala b/magnolia/shared/src/main/scala/zio/config/magnolia/KeyModifier.scala new file mode 100644 index 000000000..8913916b7 --- /dev/null +++ b/magnolia/shared/src/main/scala/zio/config/magnolia/KeyModifier.scala @@ -0,0 +1,24 @@ +package zio.config +package magnolia + +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 + } +} diff --git a/magnolia/shared/src/test/scala-dotty/zio/config/magnolia/AnnotationMacrosSpec.scala b/magnolia/shared/src/test/scala-dotty/zio/config/magnolia/AnnotationMacrosSpec.scala new file mode 100644 index 000000000..34bf404da --- /dev/null +++ b/magnolia/shared/src/test/scala-dotty/zio/config/magnolia/AnnotationMacrosSpec.scala @@ -0,0 +1,102 @@ +package zio.config +package magnolia + +import zio.config.magnolia.AnnotationMacros +import zio.ConfigProvider +import zio.test.Assertion.* +import zio.test.* + +object AnnotationMacrosSpec extends ZIOSpecDefault: + + def spec = + suiteAll("AnnotationMacros") { + suite(".nameOf")( + test("filter all name annotations") { + + @name("myname") + @name("myothername") + case class WithName() + + assert(AnnotationMacros.nameOf[WithName])(equalTo(List(name("myname"), name("myothername")))) + } + ) + suite(".discriminatorOf")( + test("filter all discriminator annotations") { + + @discriminator("mytype") + @discriminator("myothertype") + sealed trait WithDiscriminator + + assert(AnnotationMacros.discriminatorOf[WithDiscriminator])( + equalTo(List(discriminator("mytype"), discriminator("myothertype"))) + ) + } + ) + suite(".descriptionOf")( + test("filter all description annotations") { + + @describe("mydescription") + @describe("myotherdescription") + case class WithDescription() + + assert(AnnotationMacros.descriptionOf[WithDescription])( + equalTo(List(describe("mydescription"), describe("myotherdescription"))) + ) + } + ) + suite(".kebabCaseOf")( + test("filter all kebabCase annotations") { + + @kebabCase + case class WithKebabCase() + + assert(AnnotationMacros.kebabCaseOf[WithKebabCase])(equalTo(List(kebabCase()))) + } + ) + suite(".snakeCaseOf")( + test("filter all snakeCase annotations") { + + @snakeCase + case class WithSnakeCase() + + assert(AnnotationMacros.snakeCaseOf[WithSnakeCase])(equalTo(List(snakeCase()))) + } + ) + suite(".prefixOf")( + test("filter all prefix annotations") { + + @prefix("myprefix") + case class WithPrefix() + + assert(AnnotationMacros.prefixOf[WithPrefix])(equalTo(List(prefix("myprefix")))) + } + ) + suite(".postfixOf")( + test("filter all postfix annotations") { + + @postfix("mypostfix") + case class WithPostfix() + + assert(AnnotationMacros.postfixOf[WithPostfix])(equalTo(List(postfix("mypostfix")))) + } + ) + suite(".suffixOf")( + test("filter all suffix annotations") { + + @suffix("mysuffix") + case class WithSuffix() + + assert(AnnotationMacros.suffixOf[WithSuffix])(equalTo(List(suffix("mysuffix")))) + } + ) + suite(".fieldNamesOf")( + test("filter all field name annotations") { + + case class WithFieldName(@name("myname") name: String) + + assert(AnnotationMacros.fieldNamesOf[WithFieldName])(equalTo(List(("name", List(name("myname")))))) + } + ) + } + +end AnnotationMacrosSpec diff --git a/magnolia/shared/src/test/scala-dotty/zio/config/magnolia/DefaultValueSpec.scala b/magnolia/shared/src/test/scala-dotty/zio/config/magnolia/DefaultValueSpec.scala index 0baa5704b..3f6a7401f 100644 --- a/magnolia/shared/src/test/scala-dotty/zio/config/magnolia/DefaultValueSpec.scala +++ b/magnolia/shared/src/test/scala-dotty/zio/config/magnolia/DefaultValueSpec.scala @@ -5,25 +5,27 @@ import zio.Random import zio.test.Assertion.equalTo import zio.test._ import DefaultValueSpecUtils._ -import zio.config.magnolia.Macros +import zio.config.magnolia.DefaultValueMacros object DefaultValueSpec extends BaseSpec { val spec: Spec[TestConfig, Any] = suite("magnolia spec")( test("default value for primitives") { - assert(Macros.defaultValuesOf[A])(equalTo(List(("x", "defaultValue")))) + assert(DefaultValueMacros.defaultValuesOf[A])(equalTo(List(("x", "defaultValue")))) }, test("default value for nested types") { - assert(Macros.defaultValuesOf[B])(equalTo(List(("y", A("nonDefaultValue"))))) + assert(DefaultValueMacros.defaultValuesOf[B])(equalTo(List(("y", A("nonDefaultValue"))))) }, test("default value for sealed trait types") { - assert(Macros.defaultValuesOf[C])(equalTo(List(("z", X())))) + assert(DefaultValueMacros.defaultValuesOf[C])(equalTo(List(("z", X())))) }, test("default value for case object") { - assert(Macros.defaultValuesOf[D])(equalTo(List(("z", Z)))) + assert(DefaultValueMacros.defaultValuesOf[D])(equalTo(List(("z", Z)))) }, test("default value for multiple values") { - assert(Macros.defaultValuesOf[Mul])(equalTo(List(("a", A("x")), ("b", B(A("y"))), ("c", X()), ("d", Z)))) + assert(DefaultValueMacros.defaultValuesOf[Mul])( + equalTo(List(("a", A("x")), ("b", B(A("y"))), ("c", X()), ("d", Z))) + ) } ) } diff --git a/magnolia/shared/src/test/scala/zio/config/magnolia/DeriveConfigSpec.scala b/magnolia/shared/src/test/scala/zio/config/magnolia/DeriveConfigSpec.scala new file mode 100644 index 000000000..530d89d2d --- /dev/null +++ b/magnolia/shared/src/test/scala/zio/config/magnolia/DeriveConfigSpec.scala @@ -0,0 +1,159 @@ +package zio.config +package magnolia + +import zio.test.Assertion._ +import zio.test._ +import zio.ConfigProvider + +object DeriveConfigSpec extends ZIOSpecDefault { + + import DeriveConfigSpecUtils._ + + def spec = + suiteAll("DeriveConfig") { + suite(".derived")( + test("derive config for case class") { + + val desc = deriveConfig[Test] + val source = + ConfigProvider.fromMap(Map("aaBbCc" -> "1")) + + assertZIO(source.load(desc))(equalTo(Test(1))) + }, + test("derive config for case class with default value") { + + val desc = deriveConfig[TestDefaultValue] + val source = + ConfigProvider.fromMap(Map.empty) + + assertZIO(source.load(desc))(equalTo(TestDefaultValue(1))) + }, + test("derive config for case class with field name annotation") { + + val desc = deriveConfig[TestFieldName] + val source = + ConfigProvider.fromMap(Map("a" -> "1")) + + assertZIO(source.load(desc))(equalTo(TestFieldName(1))) + }, + test("derive config for sealed trait with discriminator and name annotation") { + + val desc = deriveConfig[TestDiscriminator] + val sourceA = + ConfigProvider.fromMap(Map("type" -> "a", "a" -> "1")) + val sourceB = + ConfigProvider.fromMap(Map("type" -> "TestDiscriminatorB", "b" -> "test")) + + assertZIO(sourceA.load(desc))(equalTo(TestDiscriminatorA(1))) && + assertZIO(sourceB.load(desc))(equalTo(TestDiscriminatorB("test"))) + }, + test("derive config for case class with snake case annotation") { + + val desc = deriveConfig[TestSnakeCase] + val source = + ConfigProvider.fromMap(Map("aa_bb_cc" -> "1")) + + assertZIO(source.load(desc))(equalTo(TestSnakeCase(1))) + }, + test("derive config for case class with kebab case annotation") { + + val desc = deriveConfig[TestKebabCase] + val source = + ConfigProvider.fromMap(Map("aa-bb-cc" -> "1")) + + assertZIO(source.load(desc))(equalTo(TestKebabCase(1))) + }, + test("derive config for case class with nested config") { + + val desc = deriveConfig[TestSnakeCaseWithNestedConfig] + val source = + ConfigProvider.fromMap(Map("aa_bb_cc" -> "1", "test_nested.aaBbCc" -> "2")) + + assertZIO(source.load(desc))(equalTo(TestSnakeCaseWithNestedConfig(1, Test(2)))) + }, + test("derive config for case class with nested kebab config") { + + val desc = deriveConfig[TestSnakeCaseWithNestedKebabConfig] + val source = + ConfigProvider.fromMap(Map("aa_bb_cc" -> "1", "test_nested.aa-bb-cc" -> "2")) + + assertZIO(source.load(desc))(equalTo(TestSnakeCaseWithNestedKebabConfig(1, TestKebabCase(2)))) + }, + test("derive config for case class with prefix annotation") { + + val desc = deriveConfig[TestPrefix] + val source = + ConfigProvider.fromMap(Map("testAaBbCc" -> "1")) + + assertZIO(source.load(desc))(equalTo(TestPrefix(1))) + }, + test("derive config for case class with prefix and snake case annotation") { + + val desc = deriveConfig[TestPrefixSnakeCase] + val source = + ConfigProvider.fromMap(Map("test_aa_bb_cc" -> "1")) + + assertZIO(source.load(desc))(equalTo(TestPrefixSnakeCase(1))) + }, + test("derive config for case class with postfix annotation") { + + val desc = deriveConfig[TestPostfix] + val source = + ConfigProvider.fromMap(Map("aaBbCcTest" -> "1")) + + assertZIO(source.load(desc))(equalTo(TestPostfix(1))) + }, + test("derive config for case class with suffix annotation") { + + val desc = deriveConfig[TestSuffix] + val source = + ConfigProvider.fromMap(Map("aaBbCcTest" -> "1")) + + assertZIO(source.load(desc))(equalTo(TestSuffix(1))) + } + ) + } + +} + +object DeriveConfigSpecUtils { + + case class Test(aaBbCc: Int) + + case class TestDefaultValue(aaBbCc: Int = 1) + + case class TestFieldName(@name("a") aaBbCc: Int) + + @discriminator("type") + sealed trait TestDiscriminator + @name("a") + case class TestDiscriminatorA(a: Int) extends TestDiscriminator + case class TestDiscriminatorB(b: String) extends TestDiscriminator + + @snakeCase + case class TestSnakeCase(aaBbCc: Int) + + @kebabCase + case class TestKebabCase(aaBbCc: Int) + + @snakeCase + case class TestSnakeCaseWithNestedConfig(aaBbCc: Int, testNested: Test) + + @snakeCase + case class TestSnakeCaseWithNestedKebabConfig(aaBbCc: Int, testNested: TestKebabCase) + + @prefix("test") + case class TestPrefix(aaBbCc: Int) + + @prefix("test") + @snakeCase + @kebabCase + case class TestPrefixSnakeCase(aaBbCc: Int) + + @postfix("test") + case class TestPostfix(aaBbCc: Int) + + @suffix("test") + case class TestSuffix(aaBbCc: Int) + +}