Skip to content

Commit

Permalink
Allow field-level and class-level application of serializeDefaults
Browse files Browse the repository at this point in the history
…as an annotation (#608)

Fixes #605
  • Loading branch information
lihaoyi authored Jul 12, 2024
1 parent dde18ac commit aa94288
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 19 deletions.
7 changes: 4 additions & 3 deletions upickle/core/src/upickle/core/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
24 changes: 20 additions & 4 deletions upickle/implicits/src-2/upickle/implicits/internal/Macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ object Macros {
wrapCaseN(
companion,
rawArgs,
argSyms,
mappedArgs,
argSyms.map(_.typeSignature).map(func),
hasDefaults,
Expand Down Expand Up @@ -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],
Expand All @@ -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],
Expand Down Expand Up @@ -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"""
Expand All @@ -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]{
Expand All @@ -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"}
}
Expand Down
28 changes: 25 additions & 3 deletions upickle/implicits/src-3/upickle/implicits/macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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} }

Expand Down Expand Up @@ -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}
}

},
'{()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
27 changes: 26 additions & 1 deletion upickle/implicits/src/upickle/implicits/key.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
* 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
7 changes: 0 additions & 7 deletions upickle/src/upickle/Api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down

0 comments on commit aa94288

Please sign in to comment.