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

Add annotations for key modifications #1399

Merged
merged 1 commit into from
Apr 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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}
Expand Down Expand Up @@ -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]
Expand All @@ -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 {
Expand All @@ -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(_))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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))
}
)

}
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍