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

Smol experimental macro to help with enum conversions #326

Open
wants to merge 1 commit into
base: chimney
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/test/scala/io/moia/protos/teleproto/EnumMacros.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.moia.protos.teleproto

import scalapb.GeneratedEnum
import scala.quoted.*
import scala.deriving.Mirror
import io.moia.food.food.Meal.Color.COLOR_BLUE.name

object EnumMacros:

inline def fromProtobufToEnum[I <: GeneratedEnum, O](using M: Mirror.SumOf[O]): I => O =
${ fromProtobufToEnumImpl[I, O] }

private def fromProtobufToEnumImpl[I <: GeneratedEnum: Type, O: Type](using Quotes): Expr[I => O] =
import quotes.reflect.*

def recGetLabels[T: Type]: List[String] =
Type.of[T] match
case '[EmptyTuple] => Nil
case '[label *: labelsTail] =>
val label = Type
.valueOfConstant[label]
.getOrElse {
report.errorAndAbort(s"Couldn't get the value of the label for type ${TypeRepr.of[label].show}.")
}
.asInstanceOf[String]
label :: recGetLabels[labelsTail]

val labelsOfEnumMembers =
Expr.summon[Mirror.SumOf[O]] match
case None =>
report.errorAndAbort("Couldn't summon the mirror of the enum.")
case Some(mirror) =>
mirror match
case '{ $m: Mirror.Sum { type MirroredElemLabels = elementLabels } } =>
recGetLabels[elementLabels]
case _ => report.errorAndAbort("Couln't get the labels of the enum members.")

val labelOfInputType = TypeRepr.of[I].show.split('.').last // last element of FQCN
val inputType = Expr(labelOfInputType)

val O = TypeRepr.of[O]
val sym = O.typeSymbol

// this will work *only* for Scala 3 enums
// look here: https://github.com/scalalandio/chimney/blob/master/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/datatypes/SealedHierarchiesPlatform.scala
// for a more general solution that works with any sealed hierarchy
def spawnInstance(label: Expr[String]): Expr[O] =
Apply(Select.unique(Ref(sym.companionModule), "valueOf"), List(label.asTerm)).asExprOf[O]

'{ (i: I) =>
{
val name: String = i.name
val prefix = $inputType + "_"
val withoutPrefix = name.toLowerCase.replace(prefix.toLowerCase, "")
val capitalized = withoutPrefix.split('_').map(_.capitalize).mkString

${ spawnInstance('capitalized) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,9 @@ import scala.reflect.ClassTag
class ProtocolBuffersRoundTripTest extends UnitTest with ScalaCheckPropertyChecks {
import ProtocolBuffersRoundTripTest.*

def fn[T <: GeneratedEnum](e: T)(implicit classTag: ClassTag[T]): Color = {
val name: String = e.name // "COLOR_YELLOW"

val className = classTag.runtimeClass.getSimpleName // "Color"

val x = className + "_" // "Color_"

val y = name.toLowerCase.replace(x.toLowerCase, "") // "yellow"

val z = y.split('_').map(_.capitalize).mkString // "Yellow"

Color.valueOf(z) // Yellow
}

given PartialTransformer[food.Meal.Color, Color] =
PartialTransformer.fromFunction(fn)
// types have to be provided explicitly!
PartialTransformer.fromFunction(EnumMacros.fromProtobufToEnum[food.Meal.Color, Color])

// given PartialTransformer[food.Meal.Color, Color] = PartialTransformer
// .define[food.Meal.Color, Color]
Expand Down Expand Up @@ -77,6 +64,18 @@ class ProtocolBuffersRoundTripTest extends UnitTest with ScalaCheckPropertyCheck

val mealGen: Gen[Meal] = Gen.oneOf(fruitBasketGen, lunchBoxGen).map(Meal.apply)

"Macro transform for enums" should {
"convert from Protocol Buffers to model" in {
val fun: food.Meal.Color => Color = EnumMacros.fromProtobufToEnum[food.Meal.Color, Color]

fun(food.Meal.Color.COLOR_YELLOW) shouldBe Color.Yellow
fun(food.Meal.Color.COLOR_RED) shouldBe Color.Red
fun(food.Meal.Color.COLOR_ORANGE) shouldBe Color.Orange
fun(food.Meal.Color.COLOR_PINK) shouldBe Color.Pink
fun(food.Meal.Color.COLOR_BLUE) shouldBe Color.Blue
}
}

"ProtocolBuffers" should {
"generate writer and reader that round trip successfully" in {
forAll(mealGen) { meal =>
Expand Down
Loading