Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,12 @@ object JsMacroImpl { // TODO: debug
TypeRepr.of[String]
)

val termSym = tpr.termSymbol
val typeSym = tpr.typeSymbol
val isCase = termSym.flags.is(Flags.Case)

val tpeCaseName: Expr[String] = '{
${ config }.typeNaming(${ Expr(typeName(tpr.typeSymbol)) })
${ config }.typeNaming(${ Expr(typeName(if isCase then termSym else typeSym)) })
Copy link
Author

Choose a reason for hiding this comment

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

By using the termSymbol we get the fully qualified name of the enum case rather than the name of the enum as our type discriminator.

}

val resolve = resolver[Reads, sub](
Expand Down Expand Up @@ -595,8 +599,12 @@ object JsMacroImpl { // TODO: debug
subTpr
)

val termSym = tpr.termSymbol
val typeSym = tpr.typeSymbol
val isCase = termSym.flags.is(Flags.Case)

val tpeCaseName: Expr[String] = '{
${ config }.typeNaming(${ Expr(typeName(tpr.typeSymbol)) })
${ config }.typeNaming(${ Expr(typeName(if isCase then termSym else typeSym)) })
}

val resolve = resolver[Writes, sub](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@

package play.api.libs.json

import scala.deriving.Mirror
import scala.util.Try as TryResult
import scala.util.Success as TrySuccess
import scala.util.Failure as TryFailure

import scala.deriving.Mirror.ProductOf

import scala.quoted.Expr
import scala.quoted.Quotes
import scala.quoted.Type
Expand Down Expand Up @@ -43,51 +42,27 @@ private[json] trait QuotesHelper {
* Class `Lorem` is listed through `SubFoo`,
* but `SubFoo` itself is not returned.
*/
final def knownSubclasses(tpr: TypeRepr): Option[List[TypeRepr]] =
tpr.classSymbol.flatMap { cls =>
@annotation.tailrec
def subclasses(
children: List[Tree],
out: List[TypeRepr]
): List[TypeRepr] = {
val childTpr = children.headOption.collect {
case tpd: Typed =>
tpd.tpt.tpe

case vd: ValDef =>
vd.tpt.tpe
Copy link
Author

Choose a reason for hiding this comment

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

I don't know what to do here to get the singleton Type that the compiler-generated Mirror gives us


case cd: ClassDef =>
cd.constructor.returnTpt.tpe

}

childTpr match {
case Some(child) => {
val tpeSym = child.typeSymbol

if (tpeSym.flags.is(Flags.Abstract) &&
tpeSym.flags.is(Flags.Sealed) &&
!(child <:< anyValTpe)) ||
(tpeSym.flags.is(Flags.Sealed) &&
tpeSym.flags.is(Flags.Trait))
then {
// Ignore sub-trait itself, but check the sub-sub-classes
subclasses(tpeSym.children.map(_.tree) ::: children.tail, out)
} else {
subclasses(children.tail, child :: out)
}
final def knownSubclasses(tpr: TypeRepr): Option[List[TypeRepr]] = {
def gatherNestedSubtypes[Parent: Type, Elems: Type](using Quotes): List[TypeRepr] =
Type.of[Elems] match {
case '[elem *: elems] =>
Expr.summon[Mirror.Of[elem]] match {
case Some('{ $sum: Mirror.SumOf[elem] { type MirroredElemTypes = elementTypes } }) =>
gatherNestedSubtypes[elem, elementTypes] ++ gatherNestedSubtypes[Parent, elems]
case _ =>
TypeRepr.of[elem] :: gatherNestedSubtypes[Parent, elems]
}

case _ =>
out.reverse
}
case '[EmptyTuple] => Nil
}

val types = subclasses(cls.children.map(_.tree), Nil)

if types.isEmpty then None else Some(types)
tpr.asType match {
case '[t] =>
Expr.summon[Mirror.Of[t]].collect {
Copy link
Author

Choose a reason for hiding this comment

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

I think that a less WIP implementation of this should probably use Implicits.search so that it could surface any implicit search error messages to the user

case '{ $sum: Mirror.SumOf[t] { type MirroredElemTypes = elementTypes } } =>
gatherNestedSubtypes[t, elementTypes]
}
}
}

@annotation.tailrec
private def withElems[U <: Product](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

package play.api.libs.json

import org.scalatest.EitherValues
import org.scalatest.matchers.must.Matchers
import org.scalatest.wordspec.AnyWordSpec

final class MacroScala3Spec
extends AnyWordSpec
with Matchers
with EitherValues
with org.scalatestplus.scalacheck.ScalaCheckPropertyChecks {
"Case class" should {
"not be handled" when {
Expand Down Expand Up @@ -57,6 +59,44 @@ final class MacroScala3Spec
}
}
}

"Scala 3 enum" should {
"be handled" when {
"declared with no-arg cases" in {
given Format[Color.Red.type] = Json.format[Color.Red.type]
given Format[Color.Green.type] = Json.format[Color.Green.type]
given Format[Color.Blue.type] = Json.format[Color.Blue.type]
val format = Json.format[Color]

val redJson = Json.obj("_type" -> "play.api.libs.json.Color.Red")
format.writes(Color.Red).mustEqual(redJson)
format.reads(redJson).asEither.value.mustEqual(Color.Red)

val greenJson = Json.obj("_type" -> "play.api.libs.json.Color.Green")
format.writes(Color.Green).mustEqual(greenJson)
format.reads(greenJson).asEither.value.mustEqual(Color.Green)

val blueJson = Json.obj("_type" -> "play.api.libs.json.Color.Blue")
format.writes(Color.Blue).mustEqual(blueJson)
format.reads(blueJson).asEither.value.mustEqual(Color.Blue)
}
}

"declared with single-arg case" in {
given Format[IntOption.Some] = Json.format[IntOption.Some]
given Format[IntOption.None.type] = Json.format[IntOption.None.type]
given format: Format[IntOption] = Json.format[IntOption]

val someValue = IntOption.Some(1)
val someJson = Json.obj("_type" -> "play.api.libs.json.IntOption.Some", "value" -> 1)
format.writes(someValue).mustEqual(someJson)
format.reads(someJson).asEither.value.mustEqual(someValue)

val noneJson = Json.obj("_type" -> "play.api.libs.json.IntOption.None")
format.writes(IntOption.None).mustEqual(noneJson)
format.reads(noneJson).asEither.value.mustEqual(IntOption.None)
}
}
}

final class CustomNoProductOf(val name: String, val age: Int)
Expand All @@ -66,3 +106,12 @@ object CustomNoProductOf {
given Conversion[CustomNoProductOf, Tuple2[String, Int]] =
(v: CustomNoProductOf) => v.name -> v.age
}

enum Color {
case Red, Green, Blue
}

enum IntOption {
case Some(value: Int)
case None
}
Loading