Skip to content

Commit f4f7f08

Browse files
authored
Add codec generation for simple opaque types (#1314)
1 parent 1fba6a9 commit f4f7f08

File tree

3 files changed

+63
-7
lines changed

3 files changed

+63
-7
lines changed

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,12 @@ object JsonCodecMaker {
813813

814814
def isOpaque(tpe: TypeRepr): Boolean = tpe.typeSymbol.flags.is(Flags.Opaque)
815815

816+
@tailrec
817+
def opaqueDealias(tpe: TypeRepr): TypeRepr = tpe match {
818+
case trTpe @ TypeRef(_, _) if trTpe.isOpaqueAlias => opaqueDealias(trTpe.translucentSuperType.dealias)
819+
case _ => tpe
820+
}
821+
816822
def isSealedClass(tpe: TypeRepr): Boolean = tpe.typeSymbol.flags.is(Flags.Sealed)
817823

818824
def hasSealedParent(tpe: TypeRepr): Boolean =
@@ -1345,6 +1351,11 @@ object JsonCodecMaker {
13451351
case ConstantType(DoubleConstant(v)) =>
13461352
'{ if ($in.readKeyAsDouble() != ${Expr(v)}) $in.decodeError(${Expr(s"expected key: \"$v\"")}); ${Expr(v)} }
13471353
case _ => cannotFindKeyCodecError(tpe)
1354+
} else if (isOpaque(tpe)) {
1355+
val sTpe = opaqueDealias(tpe)
1356+
sTpe.asType match { case '[s] =>
1357+
'{ ${genReadKey[s](sTpe :: types.tail, in)}.asInstanceOf[T] }
1358+
}
13481359
} else cannotFindKeyCodecError(tpe)
13491360
}.asExprOf[T]
13501361

@@ -1560,6 +1571,12 @@ object JsonCodecMaker {
15601571
case ConstantType(FloatConstant(v)) => '{ $out.writeKey(${Expr(v)}) }
15611572
case ConstantType(DoubleConstant(v)) => '{ $out.writeKey(${Expr(v)}) }
15621573
case _ => cannotFindKeyCodecError(tpe)
1574+
} else if (isOpaque(tpe)) {
1575+
val sTpe = opaqueDealias(tpe)
1576+
sTpe.asType match { case '[s] =>
1577+
val newX = '{ $x.asInstanceOf[s] }
1578+
genWriteKey[s](newX, sTpe :: types.tail, out)
1579+
}
15631580
} else cannotFindKeyCodecError(tpe)
15641581

15651582
def genWriteConstantKey(name: String, out: Expr[JsonWriter])(using Quotes): Expr[Unit] =
@@ -1891,7 +1908,10 @@ object JsonCodecMaker {
18911908
tpe1.asType match
18921909
case '[t1] => getClassInfo(tpe).genNew(List(List(genNullValue[t1](tpe1 :: types).asTerm))).asExprOf[T]
18931910
} else if (TypeRepr.of[Null] <:< tpe) '{ null }.asExprOf[T]
1894-
else '{ null.asInstanceOf[T] }.asExprOf[T]
1911+
else if (isOpaque(tpe) && !isNamedTuple(tpe)) {
1912+
val sTpe = opaqueDealias(tpe)
1913+
sTpe.asType match { case '[st] => '{ ${genNullValue[st](sTpe :: types.tail)}.asInstanceOf[T] }.asExprOf[T] }
1914+
} else '{ null.asInstanceOf[T] }.asExprOf[T]
18951915

18961916
case class ReadDiscriminator(valDefOpt: Option[ValDef]) {
18971917
def skip(in: Expr[JsonReader], l: Expr[Int])(using Quotes): Expr[Unit] = valDefOpt match {
@@ -2743,7 +2763,13 @@ object JsonCodecMaker {
27432763
} else if (isNonAbstractScalaClass(tpe)) withDecoderFor(methodKey, default, in) { (in, default) =>
27442764
genReadNonAbstractScalaClass(getClassInfo(tpe), types, useDiscriminator, in, default)
27452765
} else if (isConstType(tpe)) genReadConstType(tpe, isStringified, in)
2746-
else cannotFindValueCodecError(tpe)
2766+
else if (isOpaque(tpe)) {
2767+
val sTpe = opaqueDealias(tpe)
2768+
sTpe.asType match { case '[s] =>
2769+
val newDefault = '{ $default.asInstanceOf[s] }.asExprOf[s]
2770+
'{ ${genReadVal[s](sTpe :: types.tail, newDefault, isStringified, useDiscriminator, in)}.asInstanceOf[T] }
2771+
}
2772+
} else cannotFindValueCodecError(tpe)
27472773

27482774
def genWriteNonAbstractScalaClass[T: Type](x: Expr[T], typeInfo: TypeInfo, types: List[TypeRepr],
27492775
optDiscriminator: Option[WriteDiscriminator],
@@ -3249,7 +3275,12 @@ object JsonCodecMaker {
32493275
} else if (isNonAbstractScalaClass(tpe)) withEncoderFor(methodKey, m, out) { (out, x) =>
32503276
genWriteNonAbstractScalaClass(x, getClassInfo(tpe), types, optWriteDiscriminator, out)
32513277
} else if (isConstType(tpe)) getWriteConstType(tpe, isStringified, out)
3252-
else cannotFindValueCodecError(tpe)
3278+
else if (isOpaque(tpe)) {
3279+
val sTpe = opaqueDealias(tpe)
3280+
sTpe.asType match { case '[s] =>
3281+
genWriteVal[s]('{ $m.asInstanceOf[s] }, sTpe :: types.tail, isStringified, optWriteDiscriminator, out)
3282+
}
3283+
} else cannotFindValueCodecError(tpe)
32533284

32543285
val codecDef = '{ // FIXME: generate a type class instance using `ClassDef.apply` and `Symbol.newClass` calls after graduating from experimental API: https://www.scala-lang.org/blog/2022/06/21/scala-3.1.3-released.html
32553286
new JsonValueCodec[A] {

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,27 @@ import scala.jdk.CollectionConverters._
1313
import scala.language.implicitConversions
1414
import scala.util.hashing.MurmurHash3
1515

16+
opaque type Gram = Double
17+
18+
object Gram {
19+
inline def apply(x: Double): Gram = x
20+
21+
extension (x: Gram)
22+
inline def toDouble: Double = x
23+
}
24+
25+
opaque type Meter <: Double = Double
26+
27+
object Meter {
28+
inline def apply(x: Double): Meter = x
29+
}
30+
1631
opaque type Year = Int
1732

1833
object Year {
1934
def apply(x: Int): Option[Year] = if (x > 1900) Some(x) else None
2035

21-
inline def from(inline x: Int): Year =
36+
inline def of(inline x: Int): Year =
2237
requireConst(x)
2338
inline if x > 1900 then x else error("expected year > 1900")
2439

@@ -123,6 +138,16 @@ class JsonCodecMakerNewTypeSpec extends VerifyingSpec {
123138
"""No implicit 'com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec[_ >: scala.Nothing <: scala.Any]' defined for '"A" | "B" | "C"'."""
124139
})
125140
}
141+
"serialize and deserialize Scala3 opaque types" in {
142+
case class Planet(@stringified radius: Meter, mass: Gram)
143+
144+
verifySerDeser(make[Meter], Meter(6.37814e6), "6378140.0")
145+
verifySerDeser(make[Gram](CodecMakerConfig.withIsStringified(true)), Gram(5.976e+27), """"5.976E27"""")
146+
verifySerDeser(make[Array[Meter]], Array(Meter(6.37814e6)), "[6378140.0]")
147+
verifySerDeser(make[Array[Gram]], Array(Gram(5.976e+27)), "[5.976E27]")
148+
verifySerDeser(make[Map[Meter, Gram]], Map(Meter(6.37814e6) -> Gram(5.976e+27)), """{"6378140.0":5.976E27}""")
149+
verifySerDeser(make[Planet], Planet(Meter(6.37814e6), Gram(5.976e+27)), """{"radius":"6378140.0","mass":5.976E27}""")
150+
}
126151
"serialize and deserialize Scala3 opaque types using custom value codecs" in {
127152
case class Period(start: Year, end: Year)
128153

@@ -136,8 +161,8 @@ class JsonCodecMakerNewTypeSpec extends VerifyingSpec {
136161

137162
val nullValue: Year = null.asInstanceOf[Year]
138163
}
139-
verifySerDeser(make[Period], Period(Year.from(1976), Year.from(2022)), """{"start":1976,"end":2022}""")
140-
verifySerDeser(make[Array[Year]], Array(Year(1976).get), "[1976]")
164+
verifySerDeser(make[Period], Period(Year.of(1976), Year.of(2022)), """{"start":1976,"end":2022}""")
165+
verifySerDeser(make[Array[Year]], Array(Year.of(1976)), "[1976]")
141166
}
142167
"serialize and deserialize a Scala3 union type using a custom codec" in {
143168
type Value = String | Boolean | Double

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -943,7 +943,7 @@ class JsonCodecMakerSpec extends VerifyingSpec {
943943
verifySerDeser(codecOfApp, App("Skype", Version.`Current`), """{"name":"Skype","version":"8.10"}""")
944944
verifyDeserError(codecOfApp, """{"name":"Skype","version":"9.0"}""", "illegal version, offset: 0x0000001e")
945945
}
946-
"serialize and deserialize outer types using custom value codecs for opaque types" in {
946+
"serialize and deserialize outer types using custom value codecs for newtypes" in {
947947
abstract class Foo {
948948
type Bar
949949
}

0 commit comments

Comments
 (0)