-
-
Notifications
You must be signed in to change notification settings - Fork 134
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
Fix #728 - Support case class with more 22 fields in Scala 3 #729
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
@@ -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) | ||
|
@@ -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) | ||
|
@@ -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) -> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Directly use the given product type |
||
(in: Expr[T]) => | ||
{ (f: (Expr[U] => Expr[R])) => | ||
'{ | ||
|
@@ -175,7 +178,6 @@ private[json] trait QuotesHelper { | |
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Returns the elements type for `product`. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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]) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor formatting |
||
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 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test failing before |
||
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" | ||
) | ||
) | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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))" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Case class type is directly usable (and more specific) |
||
) | ||
|
||
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) | ||
|
@@ -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 { | ||
|
@@ -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) | ||
} | ||
} | ||
} | ||
|
||
|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Case class with 26 fields |
||
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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either resolve as field or nullary method