I'm trying to create inline def to generate Json codec for any enums in Scala 3. For this I need to have access to valueOf method of the parent of the enum. Something like this:
inline def gen[T](using JsonCodec[String], T <:< reflect.Enum): JsonCodec[T] = ???
How this can be achieved?
After reading comments now I changed my code to:
val decoder: JsonDecoder[T] = JsonDecoder[String].mapOrFail(v => Try(${getEnum[T]}(v)).fold(e => Left(e.getMessage), v => Right(v)))
val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
JsonCodec.apply(encoder, decoder)
def getEnum[T: Type](using Quotes): Expr[String => T] =
import quotes.reflect.*
val companion = Ref(TypeTree.of[T].symbol.companionModule)
Select.unique(companion, "valueOf").asExprOf[String => T]
And compiler complains with:
Malformed macro.
Expected the splice ${...} to be at the top of the RHS:
inline def foo(inline x: X, ..., y: Y): Int = ${ impl('x, ... 'y) }
* The contents of the splice must call a static method
* All arguments must be quoted
As a json library you seem to use https://zio.github.io/zio-json
Malformed macro.
Expected the splice
${...}
to be at the top of the RHS
I guess the error is understandable. You're not supposed to use macro implementations directly like that.
Either you have a macro (inline method) getEnum
and its implementation getEnumImpl
(returning an Expr
) and you use getEnum
(not getEnumImpl
)
import zio.json.{JsonDecoder, JsonEncoder, JsonCodec}
import scala.quoted.*
inline def gen[T]: JsonCodec[T] =
val decoder: JsonDecoder[T] = JsonDecoder[String].mapOrFail(v => util.Try(getEnum[T](v)).fold(e => Left(e.getMessage), v => Right(v)))
val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
JsonCodec.apply(encoder, decoder)
inline def getEnum[T]: String => T = ${ getEnumImpl[T] }
def getEnumImpl[T: Type](using Quotes): Expr[String => T] =
import quotes.reflect.*
val companion = Ref(TypeTree.of[T].symbol.companionModule)
Select.unique(companion, "valueOf").asExprOf[String => T]
or getEnum
is a macro implementation itself and you use it in another macro implementation
import zio.json.{JsonDecoder, JsonEncoder, JsonCodec}
import scala.quoted.*
inline def gen[T]: JsonCodec[T] = ${genImpl[T]}
def genImpl[T: Type](using Quotes): Expr[JsonCodec[T]] =
'{
val decoder: JsonDecoder[T] = JsonDecoder[String].mapOrFail(v => util.Try(${ getEnum[T] }(v)).fold(e => Left(e.getMessage), v => Right(v)) )
val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
JsonCodec.apply(encoder, decoder)
}
def getEnum[T: Type](using Quotes): Expr[String => T] =
import quotes.reflect.*
val companion = Ref(TypeTree.of[T].symbol.companionModule)
Select.unique(companion, "valueOf").asExprOf[String => T]
Now the error is
enum Color:
case Red, Green, Blue
gen[Color].encodeJson(Color.Red, None)
// Exception occurred while executing macro expansion.
// java.lang.Exception: Expected an expression.
// This is a partially applied Term. Try eta-expanding the term first.
You can wrap Select.unique
with Apply
(and replace functions String => T
with methods). So either
inline def gen[T <: reflect.Enum]: JsonCodec[T] =
val decoder: JsonDecoder[T] = JsonDecoder[String].mapOrFail(v => util.Try(getEnum[T](v)).fold(e => Left(e.getMessage), v => Right(v)))
val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
JsonCodec.apply(encoder, decoder)
inline def getEnum[T](v: String): T = ${ getEnumImpl[T]('v) }
def getEnumImpl[T: Type](v: Expr[String])(using Quotes): Expr[T] =
import quotes.reflect.*
val companion = Ref(TypeTree.of[T].symbol.companionModule)
Apply(Select.unique(companion, "valueOf"), List(v.asTerm)).asExprOf[T]
or
inline def gen[T <: reflect.Enum]: JsonCodec[T] = ${genImpl[T]}
def genImpl[T: Type](using Quotes): Expr[JsonCodec[T]] =
'{
val decoder: JsonDecoder[T] = JsonDecoder[String].mapOrFail(v => util.Try(${ getEnum[T]('v) }).fold(e => Left(e.getMessage), v => Right(v)) )
val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
JsonCodec.apply(encoder, decoder)
}
def getEnum[T: Type](v: Expr[String])(using Quotes): Expr[T] =
import quotes.reflect.*
val companion = Ref(TypeTree.of[T].symbol.companionModule)
Apply(Select.unique(companion, "valueOf"), List(v.asTerm)).asExprOf[T]
Also in order to derive JsonCodec
for enums you can use Mirror
import scala.compiletime.{constValue, erasedValue, summonInline}
import scala.deriving.Mirror
inline given [T <: reflect.Enum with Singleton](using
m: Mirror.ProductOf[T]
): JsonCodec[T] =
val decoder: JsonDecoder[T] =
val label = constValue[m.MirroredLabel]
JsonDecoder[String].mapOrFail(v => Either.cond(label == v, m.fromProduct(EmptyTuple), s"$label != $v"))
val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
JsonCodec.apply(encoder, decoder)
inline def mkSumDecoder[T, Tup <: Tuple]: JsonDecoder[T] = inline erasedValue[Tup] match
case _: (h *: EmptyTuple) => summonInline[JsonDecoder[h & T]].widen[T]
case _: (h *: t) => summonInline[JsonDecoder[h & T]].widen[T] <> mkSumDecoder[T, t]
inline given [T <: reflect.Enum](using
m: Mirror.SumOf[T]
): JsonCodec[T] =
val decoder: JsonDecoder[T] = mkSumDecoder[T, m.MirroredElemTypes]
val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
JsonCodec.apply(encoder, decoder)
Testing:
import zio.json.given
(Color.Blue: Color.Blue.type).toJson // "Blue"
(Color.Blue: Color).toJson // "Blue"
""" "Blue" """.fromJson[Color.Blue.type] // Right(Blue)
""" "Blue" """.fromJson[Color] // Right(Blue)
There can be also cases depending on a parameter like in
enum Color:
case Red(i: Int)
case Green, Blue
We can also use https://github.com/typelevel/shapeless-3 for derivation or derive in Shapeless 2 style
sealed trait Coproduct extends Product with Serializable
sealed trait +:[+H, +T <: Coproduct] extends Coproduct
final case class Inl[+H, +T <: Coproduct](head: H) extends (H +: T)
final case class Inr[+H, +T <: Coproduct](tail: T) extends (H +: T)
sealed trait CNil extends Coproduct
object Coproduct:
def unsafeToCoproduct(length: Int, value: Any): Coproduct =
(0 until length).foldLeft[Coproduct](Inl(value))((c, _) => Inr(c))
@scala.annotation.tailrec
def unsafeFromCoproduct(c: Coproduct): Any = c match
case Inl(h) => h
case Inr(c) => unsafeFromCoproduct(c)
case _: CNil => sys.error("impossible")
type ToCoproduct[T <: Tuple] <: Coproduct = T match
case EmptyTuple => CNil
case h *: t => h +: ToCoproduct[t]
type ToTuple[C <: Coproduct] <: Tuple = C match
case CNil => EmptyTuple
case h +: t => h *: ToTuple[t]
trait Generic[T]:
type Repr
def to(t: T): Repr
def from(r: Repr): T
object Generic:
type Aux[T, Repr0] = Generic[T] {type Repr = Repr0}
def instance[T, Repr0](f: T => Repr0, g: Repr0 => T): Aux[T, Repr0] =
new Generic[T]:
override type Repr = Repr0
override def to(t: T): Repr0 = f(t)
override def from(r: Repr0): T = g(r)
object ops:
extension[A] (a: A)
def toRepr(using g: Generic[A]): g.Repr = g.to(a)
extension[Repr] (a: Repr)
def to[A](using g: Generic.Aux[A, Repr]): A = g.from(a)
given[T <: Product](using
// ev: NotGiven[T <:< Tuple],
// ev1: NotGiven[T <:< Coproduct],
m: Mirror.ProductOf[T],
m1: Mirror.ProductOf[m.MirroredElemTypes]
): Aux[T, m.MirroredElemTypes] = instance(
m1.fromProduct,
m.fromProduct
)
given[T, C <: Coproduct](using
// ev: NotGiven[T <:< Tuple],
// ev1: NotGiven[T <:< Coproduct],
m: Mirror.SumOf[T],
ev2: Coproduct.ToCoproduct[m.MirroredElemTypes] =:= C
): Generic.Aux[T, C/*Coproduct.ToCoproduct[m.MirroredElemTypes]*/] = instance(
t => Coproduct.unsafeToCoproduct(m.ordinal(t), t).asInstanceOf[C],
Coproduct.unsafeFromCoproduct(_).asInstanceOf[T]
)
inline given singleton[T <: reflect.Enum with Singleton](using
m: Mirror.ProductOf[T]
): JsonCodec[T] =
val decoder: JsonDecoder[T] =
val label = constValue[m.MirroredLabel]
JsonDecoder[String].mapOrFail(v => Either.cond(label == v, m.fromProduct(EmptyTuple), s"$label != $v"))
val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
JsonCodec(encoder, decoder)
inline given [T <: reflect.Enum](using
m: Mirror.ProductOf[T]
): JsonCodec[T] = DeriveJsonCodec.gen[T]
given [T <: reflect.Enum](using
gen: Generic.Aux[T, _ <: Coproduct],
codec: JsonCodec[gen.Repr]
): JsonCodec[T] = codec.transform(gen.from, gen.to)
given [H, T <: Coproduct](using
hCodec: JsonCodec[H],
tCodec: JsonCodec[T]
): JsonCodec[H +: T] = (hCodec <+> tCodec).transform({
case Left(h) => Inl(h)
case Right(t) => Inr(t)
}, {
case Inl(h) => Left(h)
case Inr(t) => Right(t)
})
given JsonCodec[CNil] = JsonCodec[String].transform(_ => sys.error("impossible"), _ => sys.error("impossible"))
import zio.json.given
(Color.Blue: Color.Blue.type).toJson // "Blue"
(Color.Blue: Color).toJson // "Blue"
(Color.Red(1): Color.Red).toJson // {"i":1}
(Color.Red(1): Color).toJson // {"i":1}
""" "Blue" """.fromJson[Color.Blue.type] // Right(Blue)
""" "Blue" """.fromJson[Color] // Right(Blue)
"""{"i":1}""".fromJson[Color.Red] // Right(Red(1))
"""{"i":1}""".fromJson[Color] // Right(Red(1))