Skip to content

Commit aad6b09

Browse files
committed
Use fix-point type instead of pattern matching on types for defining recursive union type + code and test clean up
1 parent bc38e93 commit aad6b09

File tree

4 files changed

+52
-53
lines changed

4 files changed

+52
-53
lines changed

jsoniter-scala-macros/shared/src/main/scala-2/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMaker.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,7 @@ object JsonCodecMaker {
965965
else {
966966
val typeParamsAndArgs = tpeClass.typeParams.map(_.toString).zip(typeArgs(tpe)).toMap
967967
classSymbol.toType.substituteTypes(typeParams, typeParams.map(tp => typeParamsAndArgs.getOrElse(tp.toString, fail {
968-
s"Cannot resolve generic type(s) for `${classSymbol.toType}`. Please provide a custom implicitly accessible codec for it."
968+
s"Cannot resolve generic type(s) for '${classSymbol.toType}'. Please provide a custom implicitly accessible codec for it."
969969
})))
970970
}
971971
if (isSealedClass(subTpe)) collectRecursively(subTpe)

jsoniter-scala-macros/shared/src/main/scala-3/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMaker.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,8 +1215,8 @@ object JsonCodecMaker {
12151215
case PolyType(names, _, resPolyTp) =>
12161216
val tpBinding = resolveParentTypeArgs(sym, tpeArgsFromChild, typeArgs(tpe), Map.empty)
12171217
val ctArgs = names.map { name =>
1218-
tpBinding.getOrElse(name, fail(s"Type parameter $name of $sym can't be deduced from " +
1219-
s"type arguments of ${tpe.show}. Please provide a custom implicitly accessible codec for it."))
1218+
tpBinding.getOrElse(name, fail(s"Type parameter '$name' of '$sym' can't be deduced from " +
1219+
s"type arguments of '${tpe.show}'. Please provide a custom implicitly accessible codec for it."))
12201220
}
12211221
val polyRes = resPolyTp match
12221222
case MethodType(_, _, resTp) => resTp
@@ -1226,7 +1226,7 @@ object JsonCodecMaker {
12261226
case AppliedType(base, _) => base.appliedTo(ctArgs)
12271227
case AnnotatedType(AppliedType(base, _), annot) => AnnotatedType(base.appliedTo(ctArgs), annot)
12281228
case _ => polyRes.appliedTo(ctArgs)
1229-
case other => fail(s"Primary constructor for ${tpe.show} is not MethodType or PolyType but $other")
1229+
case other => fail(s"Primary constructor for '${tpe.show}' is not 'MethodType' or 'PolyType' but '$other''")
12301230
} else if (sym.isTerm) Ref(sym).tpe
12311231
else fail("Only concrete (no free type parameters) Scala classes & objects are supported for ADT leaf classes. " +
12321232
s"Please consider using of them for ADT with base '${tpe.show}' or provide a custom implicitly accessible codec for the ADT base.")

jsoniter-scala-macros/shared/src/test/scala-3/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMakerNewTypeSpec.scala

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,42 @@ given nullableValueCodec: JsonValueCodec[Int | Null | String] = new JsonValueCod
6868
def nullValue: Int | Null | String = null
6969
}
7070

71+
// Borrowed from an amazing work of Matthias Berndt: https://scastie.scala-lang.org/rCmIrOrnRdydyDvWfJRAzw
72+
object FixTypes:
73+
opaque type Fix[F[_]] = F[Any]
74+
75+
object Fix:
76+
def EqPoly[F[_]]: F[Fix[F]] =:= Fix[F] = <:<.refl.asInstanceOf
77+
78+
extension[F[_]] (fix: Fix[F]) def unfix: F[Fix[F]] = EqPoly[F].flip(fix)
79+
80+
trait FixCompanion[F[_]]:
81+
val Eq: F[Fix[F]] =:= Fix[F] = Fix.EqPoly[F]
82+
83+
def apply(unfix: F[Fix[F]]): Fix[F] = Eq(unfix)
84+
85+
import FixTypes.*
86+
87+
type JsonF[A] = mutable.Buffer[A] | mutable.Map[String, A] | String | Double | Boolean | None.type
88+
89+
type Json = Fix[JsonF]
90+
91+
object Json extends FixCompanion[JsonF]
92+
93+
def obj(values: (String, JsonF[Json] | Json)*): Json =
94+
val len = values.length
95+
val map = new LinkedHashMap[String, JsonF[Json] | Json](len << 1, 0.5f)
96+
var i = 0
97+
while (i < len) {
98+
val kv = values(i)
99+
map.put(kv._1, kv._2)
100+
i += 1
101+
}
102+
Json(Json.Eq.substituteCo[[A] =>> mutable.Map[String, A | Json]](map.asScala))
103+
104+
def arr(values: JsonF[Json] | Json*): Json =
105+
Json(Json.Eq.substituteCo[[A] =>> mutable.Buffer[A | Json]](mutable.ArrayBuffer[JsonF[Json] | Json](values*)))
106+
71107
class JsonCodecMakerNewTypeSpec extends VerifyingSpec {
72108
val codecOfIArrays = make[IArrays]
73109

@@ -227,27 +263,14 @@ class JsonCodecMakerNewTypeSpec extends VerifyingSpec {
227263
verifySerDeser(summon[JsonValueCodec[Int | BigDecimal]], BigDecimal("1" * 33), "1" * 33)
228264
}
229265
"serialize and deserialize recursive Scala3 union types using a custom value codec" in {
230-
type JsonPrimitive = String | Int | Double | Boolean | None.type
231-
232-
type Rec[JA[_], JO[_], A] = A match { // FIXME: remove this workaround after adding support of recursive types
233-
case JsonPrimitive => JsonPrimitive | JA[Rec[JA, JO, JsonPrimitive]] | JO[Rec[JA, JO, JsonPrimitive]]
234-
case _ => A | JA[Rec[JA, JO, A]] | JO[Rec[JA, JO, A]]
235-
}
236-
237-
type Json = Rec[[A] =>> mutable.Buffer[A], [A] =>> mutable.Map[String, A], JsonPrimitive]
238-
239-
type JsonObject = mutable.Map[String, Json]
240-
241-
type JsonArray = mutable.Buffer[Json]
242-
243266
val jsonCodec: JsonValueCodec[Json] = new JsonValueCodec[Json] {
244267
def decodeValue(in: JsonReader, default: Json): Json = decode(in, 128)
245268

246269
def encodeValue(x: Json, out: JsonWriter): Unit = encode(x, out, 128)
247270

248-
val nullValue: Json = None
271+
val nullValue: Json = Json(None)
249272

250-
private[this] def decode(in: JsonReader, depth: Int): Json =
273+
private def decode(in: JsonReader, depth: Int): Json = Json {
251274
val b = in.nextToken()
252275
if (b == '"') {
253276
in.rollbackToken()
@@ -257,13 +280,10 @@ class JsonCodecMakerNewTypeSpec extends VerifyingSpec {
257280
in.readBoolean()
258281
} else if ((b >= '0' && b <= '9') || b == '-') {
259282
in.rollbackToken()
260-
val d = in.readDouble()
261-
val i = d.toInt
262-
if (i.toDouble == d) i
263-
else d
283+
in.readDouble()
264284
} else if (b == '[') {
265285
if (depth <= 0) in.decodeError("depth limit exceeded")
266-
val arr = new mutable.ArrayBuffer[Json](8)
286+
val arr = new mutable.ArrayBuffer[Json](4)
267287
if (!in.isNextToken(']')) {
268288
in.rollbackToken()
269289
val dp = depth - 1
@@ -288,13 +308,13 @@ class JsonCodecMakerNewTypeSpec extends VerifyingSpec {
288308
}
289309
obj.asScala
290310
} else in.readNullOrError(None, "expected JSON value")
311+
}
291312

292-
private[this] def encode(x: Json, out: JsonWriter, depth: Int): Unit = x match
313+
private def encode(x: Json, out: JsonWriter, depth: Int): Unit = x match
293314
case str: String => out.writeVal(str)
294315
case b: Boolean => out.writeVal(b)
295-
case i: Int => out.writeVal(i)
296316
case d: Double => out.writeVal(d)
297-
case arr: JsonArray =>
317+
case arr: mutable.Buffer[Json] =>
298318
if (depth <= 0) out.encodeError("depth limit exceeded")
299319
out.writeArrayStart()
300320
val dp = depth - 1
@@ -305,7 +325,7 @@ class JsonCodecMakerNewTypeSpec extends VerifyingSpec {
305325
i += 1
306326
}
307327
out.writeArrayEnd()
308-
case obj: JsonObject =>
328+
case obj: mutable.Map[String, Json] =>
309329
if (depth <= 0) out.encodeError("depth limit exceeded")
310330
out.writeObjectStart()
311331
val dp = depth - 1
@@ -319,21 +339,8 @@ class JsonCodecMakerNewTypeSpec extends VerifyingSpec {
319339
case _ => out.writeNull()
320340
}
321341

322-
def obj(values: (String, Json)*): Json =
323-
val len = values.length
324-
val map = new LinkedHashMap[String, Json](len << 1, 0.5f)
325-
var i = 0
326-
while (i < len) {
327-
val kv = values(i)
328-
map.put(kv._1, kv._2)
329-
i += 1
330-
}
331-
map.asScala
332-
333-
def arr(values: Json*): Json = mutable.ArrayBuffer[Json](values*)
334-
335342
verifySerDeser(jsonCodec, arr("VVV", 1.2, true, obj("WWW" -> None, "XXX" -> 777)),
336-
"""["VVV",1.2,true,{"WWW":null,"XXX":777}]""")
343+
"""["VVV",1.2,true,{"WWW":null,"XXX":777.0}]""")
337344
}
338345
"serialize and deserialize case class with union types having null value (default behavior)" in {
339346
verifySerDeser(make[List[NullableProperty]],

jsoniter-scala-macros/shared/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMakerSpec.scala

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2129,15 +2129,7 @@ class JsonCodecMakerSpec extends VerifyingSpec {
21292129
"""case class HigherKindedType[F[_]](f: F[Int], fs: F[HigherKindedType[F]])
21302130
|
21312131
|JsonCodecMaker.make[HigherKindedType[Option]]""".stripMargin
2132-
}).getMessage.contains(
2133-
s"""Recursive type(s) detected: ${
2134-
if (ScalaVersionCheck.isScala2) "'HigherKindedType[Option]', 'Option[HigherKindedType[Option]]'"
2135-
else "'HigherKindedType[[A >: scala.Nothing <: scala.Any] => scala.Option[A]]', 'scala.Option[HigherKindedType[[A >: scala.Nothing <: scala.Any] => scala.Option[A]]]'"
2136-
}. Please consider
2137-
|using a custom implicitly accessible codec for this type to control the level of recursion or turn on the
2138-
|'com.github.plokhotnyuk.jsoniter_scala.macros.CodecMakerConfig.allowRecursiveTypes' for the trusted input
2139-
|that will not exceed the thread stack size.""".stripMargin.replace('\n', ' ')
2140-
))
2132+
}).getMessage.contains("Recursive type(s) detected: 'HigherKindedType["))
21412133
}
21422134
"serialize and deserialize indented by spaces and new lines if it was configured for writer" in {
21432135
verifySerDeser(codecOfRecursive,
@@ -3217,8 +3209,8 @@ class JsonCodecMakerSpec extends VerifyingSpec {
32173209
|val v = FooImpl[Bar, String](Qux, Vector.empty[String])
32183210
|val c = JsonCodecMaker.make[Foo[Bar]]""".stripMargin
32193211
}).getMessage.contains {
3220-
if (ScalaVersionCheck.isScala2) "Cannot resolve generic type(s) for `FooImpl[F,A]`. Please provide a custom implicitly accessible codec for it."
3221-
else "Type parameter A of class FooImpl can't be deduced from type arguments of Foo[[A >: scala.Nothing <: scala.Any] => Bar[A]]. Please provide a custom implicitly accessible codec for it."
3212+
if (ScalaVersionCheck.isScala2) "Cannot resolve generic type(s) for 'FooImpl[F,A]'. Please provide a custom implicitly accessible codec for it."
3213+
else "Type parameter 'A' of 'class FooImpl' can't be deduced from type arguments of 'Foo[[A >: scala.Nothing <: scala.Any]"
32223214
})
32233215
}
32243216
"don't generate codecs when 'AnyVal' or one value classes with 'CodecMakerConfig.withInlineOneValueClasses(true)' are leaf types of the ADT base" in {

0 commit comments

Comments
 (0)