From 655ae01623dcafdf1fa08bf16304a593974f0e9a Mon Sep 17 00:00:00 2001 From: IvanFinochenko Date: Sun, 28 Apr 2024 20:10:04 +0300 Subject: [PATCH] Add annotations for key modifications (#1399) --- .../zio/config/KeyConversionFunctions.scala | 4 +- .../zio/config/derivation/annotations.scala | 4 + .../zio/config/magnolia/DeriveConfig.scala | 58 +++++++- .../zio/config/magnolia/package.scala | 11 ++ .../zio/config/magnolia/AnnotationsTest.scala | 127 ++++++++++++++++++ 5 files changed, 196 insertions(+), 8 deletions(-) create mode 100644 typesafe-magnolia-tests/shared/src/test/scala/zio/config/magnolia/AnnotationsTest.scala diff --git a/core/shared/src/main/scala/zio/config/KeyConversionFunctions.scala b/core/shared/src/main/scala/zio/config/KeyConversionFunctions.scala index f9e9b7ad9..5cc1f301f 100644 --- a/core/shared/src/main/scala/zio/config/KeyConversionFunctions.scala +++ b/core/shared/src/main/scala/zio/config/KeyConversionFunctions.scala @@ -50,11 +50,11 @@ private[config] trait KeyConversionFunctions { * Add a prefix to an existing key */ def addPrefixToKey(prefix: String): String => String = - s => s"${prefix}${s}" + s => s"${prefix}${s.capitalize}" /** * Add a post fix to an existing key */ def addPostFixToKey(string: String): String => String = - s => s"${s}${string}" + s => s"${s}${string.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 a880b9dbf..c039a7d8b 100644 --- a/derivation/shared/src/main/scala/zio/config/derivation/annotations.scala +++ b/derivation/shared/src/main/scala/zio/config/derivation/annotations.scala @@ -57,3 +57,7 @@ final case class name(name: String) extends StaticAnnotation * }}} */ final case class discriminator(keyName: String = "type") extends StaticAnnotation +final case class kebabCase() extends StaticAnnotation +final case class snakeCase() extends StaticAnnotation +final case class prefix(prefix: String) extends StaticAnnotation +final case class postfix(postfix: 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 c8cf4b824..6e7c63915 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 @@ -1,8 +1,8 @@ package zio.config.magnolia import magnolia._ -import zio.{Config, LogLevel, Chunk} import zio.config._ +import zio.{Chunk, Config, LogLevel} import java.net.URI import java.time.{LocalDate, LocalDateTime, LocalTime, OffsetDateTime} @@ -80,6 +80,26 @@ 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] @@ -102,12 +122,38 @@ object DeriveConfig { final def prepareSealedTraitName(annotations: Seq[Any]): Option[String] = annotations.collectFirst { case d: name => d.name } - final def prepareFieldName(annotations: Seq[Any], name: String): String = - annotations.collectFirst { case d: name => d.name }.getOrElse(name) + final def prepareFieldName( + annotations: Seq[Any], + name: String, + keyModifiers: List[KeyModifier], + caseModifier: CaseModifier + ): String = + annotations.collectFirst { case d: name => d.name }.getOrElse { + val modifyKey = keyModifiers + .foldLeft(identity[String] _) { case (allModifications, keyModifier) => + allModifications.andThen(KeyModifier.getModifierFunction(keyModifier)) + } + .andThen(KeyModifier.getModifierFunction(caseModifier)) + modifyKey(name) + } + + final def checkKeyModifier(annotations: Seq[Any]): (List[KeyModifier], CaseModifier) = { + val modifiers = annotations.collect { + case p: prefix => KeyModifier.Prefix(p.prefix) + case p: postfix => KeyModifier.Postfix(p.postfix) + }.toList + + val caseModifier = annotations.collectFirst { + case _: kebabCase => KeyModifier.KebabCase + case _: snakeCase => KeyModifier.SnakeCase + }.getOrElse(KeyModifier.NoneModifier) + modifiers -> caseModifier + } final def combine[T](caseClass: CaseClass[DeriveConfig, T]): DeriveConfig[T] = { - val descriptions = caseClass.annotations.collect { case d: describe => d.describe } - val ccName = prepareClassName(caseClass.annotations, caseClass.typeName.short) + val descriptions = caseClass.annotations.collect { case d: describe => d.describe } + val ccName = prepareClassName(caseClass.annotations, caseClass.typeName.short) + val (keyModifiers, caseModifier) = checkKeyModifier(caseClass.annotations) val res = caseClass.parameters.toList match { @@ -128,7 +174,7 @@ object DeriveConfig { .map(_.asInstanceOf[describe].describe) val raw = param.typeclass.desc - val withNesting = nest(prepareFieldName(param.annotations, param.label))(raw) + val withNesting = nest(prepareFieldName(param.annotations, param.label, keyModifiers, caseModifier))(raw) val described = descriptions.foldLeft(withNesting)(_ ?? _) param.default.fold(described)(described.withDefault(_)) 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 0de212195..96b6cde62 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 @@ -15,4 +15,15 @@ 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 + + type postfix = derivation.postfix + val postfix: derivation.postfix.type = derivation.postfix } diff --git a/typesafe-magnolia-tests/shared/src/test/scala/zio/config/magnolia/AnnotationsTest.scala b/typesafe-magnolia-tests/shared/src/test/scala/zio/config/magnolia/AnnotationsTest.scala new file mode 100644 index 000000000..d7509a051 --- /dev/null +++ b/typesafe-magnolia-tests/shared/src/test/scala/zio/config/magnolia/AnnotationsTest.scala @@ -0,0 +1,127 @@ +package zio.config.magnolia + +import zio.config.read +import zio.config.typesafe.TypesafeConfigProvider +import zio.test.Assertion.equalTo +import zio.test.{Spec, ZIOSpecDefault, assertZIO} +import zio.{Config, IO} + +object AnnotationsTest extends ZIOSpecDefault { + + object KebabTest { + @kebabCase + case class Foo(fooFoo: String) + @kebabCase + case class AnotherFoo(nestedAnotherFoo: String) + @kebabCase + case class Bar(@name("bArBaR-Bar") barBarBar: String) + @kebabCase + case class MyConfig(foo: Foo, anotherFoo: AnotherFoo, bar: Bar) + + val myConfigAutomatic: Config[MyConfig] = deriveConfig[MyConfig] + } + + object SnakeTest { + @snakeCase + case class Foo(fooFoo: String) + @snakeCase + case class AnotherFoo(nestedAnotherFoo: String) + @snakeCase + case class Bar(@name("bArBaR-Bar") barBarBar: String) + @snakeCase + case class MyConfig(foo: Foo, anotherFoo: AnotherFoo, bar: Bar) + + val myConfigAutomatic: Config[MyConfig] = deriveConfig[MyConfig] + } + + object PrefixAndPostfix { + @postfix("test") + case class Foo(fooFoo: String) + @prefix("dev") + case class AnotherFoo(nestedAnotherFoo: String) + @snakeCase + case class Bar(@name("bArBaR-Bar") barBarBar: String) + @snakeCase + @prefix("prod") + case class AnotherBar(bar: String) + @kebabCase + @prefix("test") + @postfix("deprecated") + case class NextBar(barValue: String) + @snakeCase + case class MyConfig(foo: Foo, anotherFoo: AnotherFoo, bar: Bar, anotherBar: AnotherBar, nextBar: NextBar) + + val myConfigAutomatic: Config[MyConfig] = deriveConfig[MyConfig] + } + + override def spec: Spec[Any, Config.Error] = + suite("AnnotationsTest")( + test("kebab case") { + import KebabTest._ + val hocconConfig = + s""" + |foo { + | foo-foo = "value1" + |} + |another-foo { + | nested-another-foo = "value2" + |} + |bar { + | bArBaR-Bar = "value3" + |} + |""".stripMargin + val result: IO[Config.Error, MyConfig] = + read(myConfigAutomatic from TypesafeConfigProvider.fromHoconString(hocconConfig)) + val expected = MyConfig(Foo("value1"), AnotherFoo("value2"), Bar("value3")) + assertZIO(result)(equalTo(expected)) + }, + test("snake case") { + import SnakeTest._ + + val hocconConfig = + s""" + |foo { + | foo_foo = "value1" + |} + |another_foo { + | nested_another_foo = "value2" + |} + |bar { + | bArBaR-Bar = "value3" + |} + |""".stripMargin + val result: IO[Config.Error, MyConfig] = + read(myConfigAutomatic from TypesafeConfigProvider.fromHoconString(hocconConfig)) + val expected = MyConfig(Foo("value1"), AnotherFoo("value2"), Bar("value3")) + assertZIO(result)(equalTo(expected)) + }, + test("prefix and postfix") { + import PrefixAndPostfix._ + + val hocconConfig = + s""" + |foo { + | fooFooTest = "value1" + |} + |another_foo { + | devNestedAnotherFoo = "value2" + |} + |bar { + | bArBaR-Bar = "value3" + |} + |another_bar { + | prod_bar = "value4" + |} + |next_bar { + | test-bar-value-deprecated = "value5" + |} + |""".stripMargin + val result: IO[Config.Error, MyConfig] = + read(myConfigAutomatic from TypesafeConfigProvider.fromHoconString(hocconConfig)) + val expected = + MyConfig(Foo("value1"), AnotherFoo("value2"), Bar("value3"), AnotherBar("value4"), NextBar("value5")) + assertZIO(result)(equalTo(expected)) + } + ) + +}