Skip to content

Commit

Permalink
Add annotations for key modifications (#1399)
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanFinochenko committed Apr 28, 2024
1 parent bfaa748 commit 655ae01
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 8 deletions.
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))
}
)

}

0 comments on commit 655ae01

Please sign in to comment.