Skip to content

Commit

Permalink
Merge pull request #729 from playframework/fix/728_macros
Browse files Browse the repository at this point in the history
Fix #728 - Support case class with more 22 fields in Scala 3
  • Loading branch information
mergify[bot] authored Jun 22, 2022
2 parents 0eed41c + cc89874 commit eb6afe7
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ rewrite.neverInfix.excludeFilters = [
# better for play-json dsl
and, andKeep, andThen,
# For scalatest
in, should, when, must mustEqual, mustBe, "must_==="
in, should, when, must mustEqual, mustNot, mustBe, "must_==="
]
rewrite.sortModifiers.order = [
"private",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -673,8 +673,7 @@ object JsMacroImpl { // TODO: debug
${ f('{ ok }) }
}

val (tupleTpe, withTupled) =
withTuple[T, P, JsObject](tpr, toProduct, types)
val (tupleTpe, withTupled) = withTuple[T, P, JsObject](tpr, toProduct)

def writeFields(input: Expr[T]): Expr[JsObject] =
withTupled(input) { tupled =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import scala.quoted.Expr
import scala.quoted.Quotes
import scala.quoted.Type

// TODO: Unit tests
private[json] trait QuotesHelper {
protected type Q <: Quotes

Expand Down Expand Up @@ -94,14 +93,14 @@ private[json] trait QuotesHelper {
@annotation.tailrec
private def withElems[U <: Product](
tupled: Expr[U],
fields: List[(Symbol, TypeRepr, Symbol)],
fields: List[(Symbol, TypeRepr, Term => Term)],
prepared: List[Tuple2[String, (Ref => Term) => Term]]
): Map[String, (Ref => Term) => Term] = fields match {
case (sym, t, f) :: tail => {
val elem = ValDef.let(
Symbol.spliceOwner,
s"tuple${f.name}",
Typed(tupled.asTerm.select(f), Inferred(t))
s"tuple${sym.name}",
Typed(f(tupled.asTerm), Inferred(t))
)

withElems(tupled, tail, (sym.name -> elem) :: prepared)
Expand All @@ -121,16 +120,32 @@ private[json] trait QuotesHelper {
decls: List[(Symbol, TypeRepr)],
debug: String => Unit
): Map[String, (Term => Term) => Term] = {
val fields = decls.zipWithIndex.flatMap { case ((sym, t), i) =>
val field = tupleTpe.typeSymbol.declaredMethod(s"_${i + 1}")
val tupleTpeSym = tupleTpe.typeSymbol

field.map { meth =>
debug(
s"// Field: ${sym.owner.owner.fullName}.${sym.name}, type = ${t.typeSymbol.fullName}, annotations = [${sym.annotations.map(_.show).mkString(", ")}]"
)
val fields = decls.zipWithIndex.map { case ((sym, t), i) =>
debug(
s"// Field: ${sym.owner.owner.fullName}.${sym.name}, type = ${t.typeSymbol.fullName}, annotations = [${sym.annotations.map(_.show).mkString(", ")}]"
)

val fieldNme = s"_${i + 1}"

val resolve: Term => Term = {
val field = tupleTpeSym.declaredField(fieldNme)

Tuple3(sym, t, meth)
if (field == Symbol.noSymbol) {
tupleTpeSym.declaredMethod(fieldNme) match {
case meth :: Nil =>
(_: Term).select(meth)

case _ =>
report.errorAndAbort(s"Fails to resolve field: '${tupleTpeSym.fullName}.${fieldNme}'")
}
} else {
(_: Term).select(field)
}
}

Tuple3(sym, t, resolve)
}

withElems[U](tupled, fields, List.empty)
Expand All @@ -143,30 +158,18 @@ private[json] trait QuotesHelper {
*
* @param tpr the type for which a `ProductOf` is provided
* @param toProduct the function to convert the input value as product `U`
* @param types the types of the elements (fields)
*
* @return The tuple type + `{ v: Term => { tuple: Ref => ... } }`
* with `v` a term of type `tpe`, and `tuple` the product created from.
*/
def withTuple[T, U <: Product, R: Type](
tpr: TypeRepr,
toProduct: Expr[T => U],
types: List[TypeRepr]
)(using
Type[T],
Type[U]
): Tuple2[TypeRepr, Expr[T] => (Expr[U] => Expr[R]) => Expr[R]] = {
val unappliedTupleTpr: TypeRepr = {
if (types.isEmpty) {
TypeRepr.of[EmptyTuple]
} else {
TypeRepr.typeConstructorOf(Class.forName(s"scala.Tuple${types.size}"))
}
}

val tupleTpr = unappliedTupleTpr.appliedTo(types)

tupleTpr -> {
tt: Type[T],
ut: Type[U]
): Tuple2[TypeRepr, Expr[T] => (Expr[U] => Expr[R]) => Expr[R]] =
TypeRepr.of(using ut) -> {
(in: Expr[T]) =>
{ (f: (Expr[U] => Expr[R])) =>
'{
Expand All @@ -175,7 +178,6 @@ private[json] trait QuotesHelper {
}
}
}
}

/**
* Returns the elements type for `product`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ object Json extends JsonFacade with JsMacros with JsValueMacros {
JsValueWrapperImpl(w.writes(field))

def obj(fields: (String, JsValueWrapper)*): JsObject = JsObject(fields.map(f => (f._1, unwrap(f._2))))
def arr(items: JsValueWrapper*): JsArray = JsArray(items.iterator.map(unwrap).toArray[JsValue])

def arr(items: JsValueWrapper*): JsArray = JsArray(items.iterator.map(unwrap).toArray[JsValue])

// Passed nulls will typecheck without needing the implicit conversion, so they need to checked at runtime
private def unwrap(wrapper: JsValueWrapper) = wrapper match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,47 @@ final class MacroScala3Spec
with org.scalatestplus.scalacheck.ScalaCheckPropertyChecks {
"Case class" should {
"not be handled" when {
"no Product Conversion" in {
import MacroSpec.UsingAlias

"Macros.writer[UsingAlias]".mustNot(typeCheck)
"no custom ProductOf" in {
"Json.writes[CustomNoProductOf]" mustNot typeCheck
}
}

"no custom ProductOf" in {
"Macros.writer[CustomNoProductOf]".mustNot(typeCheck)
"be handled" when {
"is declared with more than 22 fields" in {
val format = Json.format[BigFat]

format
.writes(BigFat.example)
.mustEqual(
Json.obj(
"e" -> Seq(1, 2, 3),
"n" -> "n",
"t" -> Seq(8),
"a" -> 1,
"m" -> 12,
"i" -> "i",
"v" -> "v",
"p" -> 13,
"r" -> 15,
"w" -> Seq(9, 10, 11),
"k" -> 10,
"s" -> "s",
"x" -> 12,
"j" -> Seq(4, 5),
"y" -> Seq(13, 14),
"u" -> 16,
"f" -> 6,
"q" -> 14,
"b" -> 2,
"g" -> 7,
"l" -> 11,
"c" -> 3,
"h" -> 8,
"o" -> Seq(6, 7),
"z" -> 15,
"d" -> "d"
)
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,31 @@ final class QuotesSpec extends AnyWordSpec with Matchers with org.scalatestplus.
"from Foo" in {
testWithTuple(
Foo("1", 2)
).mustEqual("scala.Tuple2[scala.Predef.String, scala.Int]/Foo(1,2)")
).mustEqual("play.api.libs.json.Foo/Foo(1,2)")
}

"from generic Bar" in {
testWithTuple(
Bar[Double]("bar1", None, Seq(1.2D, 34.5D))
).mustEqual(
"scala.Tuple3[scala.Predef.String, scala.Option[scala.Double], scala.collection.immutable.Seq[scala.Double]]/Bar(bar1,None,List(1.2, 34.5))"
"play.api.libs.json.Bar[scala.Double]/Bar(bar1,None,List(1.2, 34.5))"
)

testWithTuple(
Bar[Int]("bar2", Some(2), Seq(3.45D))
).mustEqual(
"scala.Tuple3[scala.Predef.String, scala.Option[scala.Int], scala.collection.immutable.Seq[scala.Double]]/Bar(bar2,Some(2),List(3.45))"
"play.api.libs.json.Bar[scala.Int]/Bar(bar2,Some(2),List(3.45))"
)
}

"from BigFat" in {
List(
"play.api.libs.json.BigFat/BigFat(1,2.0,3.0,d,List(1, 2, 3),6,7.0,8.0,i,List(4, 5),10,11.0,12.0,n,List(6, 7),13,14.0,15.0,s,List(8),16.0,v,List(9, 10, 11),12,List(13, 14),15.0)", // JVM: With .0 for Double & Float
"play.api.libs.json.BigFat/BigFat(1,2,3,d,List(1, 2, 3),6,7,8,i,List(4, 5),10,11,12,n,List(6, 7),13,14,15,s,List(8),16,v,List(9, 10, 11),12,List(13, 14),15)"
).contains(testWithTuple[BigFat](BigFat.example)).mustEqual(true)

}

"from non-case class" when {
"fail when there is no Conversion[T, _ <: Product]" in {
"""testWithTuple(new TestUnion.UC("name", 2))""".mustNot(typeCheck)
Expand All @@ -97,7 +105,7 @@ final class QuotesSpec extends AnyWordSpec with Matchers with org.scalatestplus.

testWithTuple(
new TestUnion.UC("name", 2)
).mustEqual("scala.Tuple$package.EmptyTuple/(name,2)")
).mustEqual("scala.Tuple2[scala.Predef.String, scala.Int]/(name,2)")
}

"be successful when conversion is provided" in {
Expand All @@ -121,6 +129,16 @@ final class QuotesSpec extends AnyWordSpec with Matchers with org.scalatestplus.
Bar("bar3", Some("opt2"), Seq(3.1D, 4.5D))
).mustEqual("name=bar3,opt=Some(opt2),scores=List(3.1, 4.5)")
}

"BigFat" in {
val jvmToStr =
"e=List(1, 2, 3),n=n,t=List(8),a=1,m=12.0,i=i,v=v,p=13,r=15.0,w=List(9, 10, 11),k=10,s=s,x=12,j=List(4, 5),y=List(13, 14),u=16.0,f=6,q=14.0,b=2.0,g=7.0,l=11.0,c=3.0,h=8.0,o=List(6, 7),z=15.0,d=d" // With .0 for decimal

val jsToStr =
"e=List(1, 2, 3),n=n,t=List(8),a=1,m=12,i=i,v=v,p=13,r=15,w=List(9, 10, 11),k=10,s=s,x=12,j=List(4, 5),y=List(13, 14),u=16,f=6,q=14,b=2,g=7,l=11,c=3,h=8,o=List(6, 7),z=15,d=d"

Seq(jvmToStr, jsToStr).contains(testWithFields(BigFat.example)).mustEqual(true)
}
}
}

Expand All @@ -143,6 +161,66 @@ case class Foo(bar: String, lorem: Int)

case class Bar[T](name: String, opt: Option[T], scores: Seq[Double])

case class BigFat(
a: Int,
b: Double,
c: Float,
d: String,
e: Seq[Int],
f: Int,
g: Double,
h: Float,
i: String,
j: Seq[Int],
k: Int,
l: Double,
m: Float,
n: String,
o: Seq[Int],
p: Int,
q: Double,
r: Float,
s: String,
t: Seq[Int],
u: Float,
v: String,
w: Seq[Int],
x: Int,
y: Seq[Int],
z: Double
)

object BigFat {
def example = BigFat(
a = 1,
b = 2D,
c = 3F,
d = "d",
e = Seq(1, 2, 3),
f = 6,
g = 7D,
h = 8F,
i = "i",
j = Seq(4, 5),
k = 10,
l = 11D,
m = 12F,
n = "n",
o = Seq(6, 7),
p = 13,
q = 14D,
r = 15F,
s = "s",
t = Seq(8),
u = 16F,
v = "v",
w = Seq(9, 10, 11),
x = 12,
y = Seq(13, 14),
z = 15D
)
}

object TestUnion:
sealed trait UT
case object UA extends UT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,10 @@ object TestMacros:
}

val tpe = TypeRepr.of[T]
val tpeElements = Expr
.summon[ProductOf[T]]
.map {
helper.productElements(tpe, _).get
}
.getOrElse(List.empty[(Symbol, TypeRepr)])

val types = tpeElements.map(_._2)

val (tupleTpe, withTuple) =
helper.withTuple[T, P, String](tpe, toProduct, types)
val (tupleTpe, withTupled) =
helper.withTuple[T, P, String](tpe, toProduct)

withTuple(pure) { (tupled: Expr[P]) =>
withTupled(pure) { (tupled: Expr[P]) =>
val a = Expr(tupleTpe.show)

'{
Expand Down Expand Up @@ -127,10 +118,9 @@ object TestMacros:
helper.productElements(tpe, _).get
}
.get
val types = tpeElements.map(_._2)

val (tupleTpe, withTuple) =
helper.withTuple[T, T, String](tpe, '{ identity[T] }, types)
helper.withTuple[T, T, String](tpe, '{ identity[T] })

withTuple(pure) { (tupled: Expr[T]) =>
val fieldMap =
Expand Down

0 comments on commit eb6afe7

Please sign in to comment.