From aa942887f731fd7972b412d913cb08b2942cea88 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 12 Jul 2024 22:44:15 +0800 Subject: [PATCH] Allow field-level and class-level application of `serializeDefaults` as an annotation (#608) Fixes https://github.com/com-lihaoyi/upickle/issues/605 --- upickle/core/src/upickle/core/Config.scala | 7 +++-- .../upickle/implicits/internal/Macros.scala | 24 +++++++++++++--- .../src-3/upickle/implicits/macros.scala | 28 +++++++++++++++++-- .../implicits/CaseClassReadWriters.scala | 1 - .../implicits/src/upickle/implicits/key.scala | 27 +++++++++++++++++- upickle/src/upickle/Api.scala | 7 ----- 6 files changed, 75 insertions(+), 19 deletions(-) diff --git a/upickle/core/src/upickle/core/Config.scala b/upickle/core/src/upickle/core/Config.scala index afd9ceda0..e1a66f607 100644 --- a/upickle/core/src/upickle/core/Config.scala +++ b/upickle/core/src/upickle/core/Config.scala @@ -69,9 +69,10 @@ trait Config { def optionsAsNulls: Boolean = true /** - * Whether or not unknown keys when de-serializing case classes should be allowed. - * Defaults to `true`, but can be set to `false` to make the presence unknown keys - * raise an error + * Configure whether you want upickle to skip unknown keys during de-serialization + * of `case class`es. Can be overriden for the entire serializer via `override def`, and + * further overriden for individual `case class`es via the annotation + * `@upickle.implicits.allowUnknownKeys(b: Boolean)` */ def allowUnknownKeys: Boolean = true } diff --git a/upickle/implicits/src-2/upickle/implicits/internal/Macros.scala b/upickle/implicits/src-2/upickle/implicits/internal/Macros.scala index 821ce936a..67d3942c1 100644 --- a/upickle/implicits/src-2/upickle/implicits/internal/Macros.scala +++ b/upickle/implicits/src-2/upickle/implicits/internal/Macros.scala @@ -192,6 +192,7 @@ object Macros { wrapCaseN( companion, rawArgs, + argSyms, mappedArgs, argSyms.map(_.typeSignature).map(func), hasDefaults, @@ -255,11 +256,18 @@ object Macros { .flatMap(_.scalaArgs.headOption) .map{case Literal(Constant(s)) => s.toString} } + def serializeDefaults(sym: c.Symbol): Option[Boolean] = { + sym.annotations + .find(_.tpe == typeOf[upickle.implicits.serializeDefaults]) + .flatMap(_.scalaArgs.headOption) + .map{case Literal(Constant(s)) => s.asInstanceOf[Boolean]} + } def wrapObject(obj: Tree): Tree def wrapCaseN(companion: Tree, rawArgs: Seq[String], + argSyms: Seq[Symbol], mappedArgs: Seq[String], argTypes: Seq[Type], hasDefaults: Seq[Boolean], @@ -274,6 +282,7 @@ object Macros { def wrapCaseN(companion: c.Tree, rawArgs: Seq[String], + argSyms: Seq[Symbol], mappedArgs: Seq[String], argTypes: Seq[Type], hasDefaults: Seq[Boolean], @@ -395,13 +404,20 @@ object Macros { def internal = q"${c.prefix}.Internal" def wrapCaseN(companion: c.Tree, rawArgs: Seq[String], + argSyms: Seq[Symbol], mappedArgs: Seq[String], argTypes: Seq[Type], hasDefaults: Seq[Boolean], targetType: c.Type, varargs: Boolean) = { val defaults = deriveDefaults(companion, hasDefaults) - val serDfltVals = q"${c.prefix}.serializeDefaults" + def serDfltVals(i: Int) = { + val b: Option[Boolean] = serializeDefaults(argSyms(i)).orElse(serializeDefaults(targetType.typeSymbol)) + b match { + case Some(b) => q"${b}" + case None => q"${c.prefix}.serializeDefaults" + } + } def write(i: Int) = { val snippet = q""" @@ -412,9 +428,9 @@ object Macros { v.${TermName(rawArgs(i))} ) """ - + if (!hasDefaults(i)) snippet - else q"""if ($serDfltVals || v.${TermName(rawArgs(i))} != ${defaults(i)}) $snippet""" + else q"""if (${serDfltVals(i)} || v.${TermName(rawArgs(i))} != ${defaults(i)}) $snippet""" } q""" new ${c.prefix}.CaseClassWriter[$targetType]{ @@ -423,7 +439,7 @@ object Macros { Range(0, rawArgs.length) .map(i => if (!hasDefaults(i)) q"1" - else q"""if ($serDfltVals || v.${TermName(rawArgs(i))} != ${defaults(i)}) 1 else 0""" + else q"""if (${serDfltVals(i)} || v.${TermName(rawArgs(i))} != ${defaults(i)}) 1 else 0""" ) .foldLeft[Tree](q"0"){case (prev, next) => q"$prev + $next"} } diff --git a/upickle/implicits/src-3/upickle/implicits/macros.scala b/upickle/implicits/src-3/upickle/implicits/macros.scala index a68e28ea3..699bfb9b6 100644 --- a/upickle/implicits/src-3/upickle/implicits/macros.scala +++ b/upickle/implicits/src-3/upickle/implicits/macros.scala @@ -41,6 +41,13 @@ def extractKey[A](using Quotes)(sym: quotes.reflect.Symbol): Option[String] = .find(_.tpe =:= TypeRepr.of[upickle.implicits.key]) .map{case Apply(_, Literal(StringConstant(s)) :: Nil) => s} +def extractSerializeDefaults[A](using quotes: Quotes)(sym: quotes.reflect.Symbol): Option[Boolean] = + import quotes.reflect._ + sym + .annotations + .find(_.tpe =:= TypeRepr.of[upickle.implicits.serializeDefaults]) + .map{case Apply(_, Literal(BooleanConstant(s)) :: Nil) => s} + inline def extractIgnoreUnknownKeys[T](): List[Boolean] = ${extractIgnoreUnknownKeysImpl[T]} def extractIgnoreUnknownKeysImpl[T](using Quotes, Type[T]): Expr[List[Boolean]] = import quotes.reflect._ @@ -111,16 +118,28 @@ inline def writeLength[T](inline thisOuter: upickle.core.Types with upickle.impl inline v: T): Int = ${writeLengthImpl[T]('thisOuter, 'v)} +def serDfltVals(using quotes: Quotes)(thisOuter: Expr[upickle.core.Types with upickle.implicits.MacrosCommon], + argSym: quotes.reflect.Symbol, + targetType: quotes.reflect.Symbol): Expr[Boolean] = { + extractSerializeDefaults(argSym).orElse(extractSerializeDefaults(targetType)) match { + case Some(b) => Expr(b) + case None => '{ ${ thisOuter }.serializeDefaults } + } +} def writeLengthImpl[T](thisOuter: Expr[upickle.core.Types with upickle.implicits.MacrosCommon], v: Expr[T]) - (using Quotes, Type[T]): Expr[Int] = + (using quotes: Quotes, t: Type[T]): Expr[Int] = import quotes.reflect.* fieldLabelsImpl0[T] .map{(rawLabel, label) => val defaults = getDefaultParamsImpl0[T] val select = Select.unique(v.asTerm, rawLabel.name).asExprOf[Any] + if (!defaults.contains(label)) '{1} - else '{if (${thisOuter}.serializeDefaults || ${select} != ${defaults(label)}) 1 else 0} + else { + val serDflt = serDfltVals(thisOuter, rawLabel, TypeRepr.of[T].typeSymbol) + '{if (${serDflt} || ${select} != ${defaults(label)}) 1 else 0} + } } .foldLeft('{0}) { case (prev, next) => '{$prev + $next} } @@ -166,7 +185,10 @@ def writeSnippetsImpl[R, T, WS <: Tuple](thisOuter: Expr[upickle.core.Types with ) } if (!defaults.contains(label)) snippet - else '{if (${thisOuter}.serializeDefaults || ${select} != ${defaults(label)}) $snippet} + else { + val serDflt = serDfltVals(thisOuter, rawLabel, TypeRepr.of[T].typeSymbol) + '{if ($serDflt || ${select} != ${defaults(label)}) $snippet} + } }, '{()} diff --git a/upickle/implicits/src/upickle/implicits/CaseClassReadWriters.scala b/upickle/implicits/src/upickle/implicits/CaseClassReadWriters.scala index cd1480994..a637cd170 100644 --- a/upickle/implicits/src/upickle/implicits/CaseClassReadWriters.scala +++ b/upickle/implicits/src/upickle/implicits/CaseClassReadWriters.scala @@ -11,7 +11,6 @@ import upickle.core.{Abort, AbortException, ArrVisitor, NoOpVisitor, ObjVisitor, * package to form the public API1 */ trait CaseClassReadWriters extends upickle.core.Types{ - abstract class CaseClassReader[V] extends SimpleReader[V] { override def expectedMsg = "expected dictionary" diff --git a/upickle/implicits/src/upickle/implicits/key.scala b/upickle/implicits/src/upickle/implicits/key.scala index dc0efe19a..3708bae11 100644 --- a/upickle/implicits/src/upickle/implicits/key.scala +++ b/upickle/implicits/src/upickle/implicits/key.scala @@ -2,5 +2,30 @@ package upickle.implicits import scala.annotation.StaticAnnotation +/** + * Annotation for control over the strings used to serialize your data structure. Can + * be applied in three ways: + * + * 1. To individual fields, in which case it overrides the JSON object fieldname + * + * 2. To `case class`es which are part of `sealed trait`s, where it overrides + * the value of the `"$type": "foo"` discriminator field + * + * 2. To `sealed trait`s themselves, where it overrides + * the key of the `"$type": "foo"` discriminator field + */ class key(s: String) extends StaticAnnotation -class allowUnknownKeys(b: Boolean) extends StaticAnnotation \ No newline at end of file + +/** + * Annotation for fine-grained control of the `def serializeDefaults` configuration + * on the upickle bundle; can be applied to individual fields or to entire `case class`es, + * with finer-grained application taking precedence + */ +class serializeDefaults(s: Boolean) extends StaticAnnotation + +/** + * Annotation for fine-grained control of the `def allowUnknownKeys` configuration + * on the upickle bundle; can be applied to individual `case class`es, taking precedence + * over upickle pickler-level configuration + */ +class allowUnknownKeys(b: Boolean) extends StaticAnnotation diff --git a/upickle/src/upickle/Api.scala b/upickle/src/upickle/Api.scala index 12d39d461..4b3f066ea 100644 --- a/upickle/src/upickle/Api.scala +++ b/upickle/src/upickle/Api.scala @@ -170,13 +170,6 @@ trait Api def write0[R](out: Visitor[_, R], v: T): R = readwriter.write0(out, v) } - /** - * Configure whether you want upickle to skip unknown keys during de-serialization - * of `case class`es. Can be overriden for the entire serializer via `override def`, and - * further overriden for individual `case class`es via the annotation - * `@upickle.implicits.allowUnknownKeys(b: Boolean)` - */ - override def allowUnknownKeys: Boolean = true // End Api }