Skip to content

Commit aa94288

Browse files
authored
Allow field-level and class-level application of serializeDefaults as an annotation (#608)
Fixes #605
1 parent dde18ac commit aa94288

File tree

6 files changed

+75
-19
lines changed

6 files changed

+75
-19
lines changed

upickle/core/src/upickle/core/Config.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,10 @@ trait Config {
6969
def optionsAsNulls: Boolean = true
7070

7171
/**
72-
* Whether or not unknown keys when de-serializing case classes should be allowed.
73-
* Defaults to `true`, but can be set to `false` to make the presence unknown keys
74-
* raise an error
72+
* Configure whether you want upickle to skip unknown keys during de-serialization
73+
* of `case class`es. Can be overriden for the entire serializer via `override def`, and
74+
* further overriden for individual `case class`es via the annotation
75+
* `@upickle.implicits.allowUnknownKeys(b: Boolean)`
7576
*/
7677
def allowUnknownKeys: Boolean = true
7778
}

upickle/implicits/src-2/upickle/implicits/internal/Macros.scala

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ object Macros {
192192
wrapCaseN(
193193
companion,
194194
rawArgs,
195+
argSyms,
195196
mappedArgs,
196197
argSyms.map(_.typeSignature).map(func),
197198
hasDefaults,
@@ -255,11 +256,18 @@ object Macros {
255256
.flatMap(_.scalaArgs.headOption)
256257
.map{case Literal(Constant(s)) => s.toString}
257258
}
259+
def serializeDefaults(sym: c.Symbol): Option[Boolean] = {
260+
sym.annotations
261+
.find(_.tpe == typeOf[upickle.implicits.serializeDefaults])
262+
.flatMap(_.scalaArgs.headOption)
263+
.map{case Literal(Constant(s)) => s.asInstanceOf[Boolean]}
264+
}
258265

259266
def wrapObject(obj: Tree): Tree
260267

261268
def wrapCaseN(companion: Tree,
262269
rawArgs: Seq[String],
270+
argSyms: Seq[Symbol],
263271
mappedArgs: Seq[String],
264272
argTypes: Seq[Type],
265273
hasDefaults: Seq[Boolean],
@@ -274,6 +282,7 @@ object Macros {
274282

275283
def wrapCaseN(companion: c.Tree,
276284
rawArgs: Seq[String],
285+
argSyms: Seq[Symbol],
277286
mappedArgs: Seq[String],
278287
argTypes: Seq[Type],
279288
hasDefaults: Seq[Boolean],
@@ -395,13 +404,20 @@ object Macros {
395404
def internal = q"${c.prefix}.Internal"
396405
def wrapCaseN(companion: c.Tree,
397406
rawArgs: Seq[String],
407+
argSyms: Seq[Symbol],
398408
mappedArgs: Seq[String],
399409
argTypes: Seq[Type],
400410
hasDefaults: Seq[Boolean],
401411
targetType: c.Type,
402412
varargs: Boolean) = {
403413
val defaults = deriveDefaults(companion, hasDefaults)
404-
val serDfltVals = q"${c.prefix}.serializeDefaults"
414+
def serDfltVals(i: Int) = {
415+
val b: Option[Boolean] = serializeDefaults(argSyms(i)).orElse(serializeDefaults(targetType.typeSymbol))
416+
b match {
417+
case Some(b) => q"${b}"
418+
case None => q"${c.prefix}.serializeDefaults"
419+
}
420+
}
405421

406422
def write(i: Int) = {
407423
val snippet = q"""
@@ -412,9 +428,9 @@ object Macros {
412428
v.${TermName(rawArgs(i))}
413429
)
414430
"""
415-
431+
416432
if (!hasDefaults(i)) snippet
417-
else q"""if ($serDfltVals || v.${TermName(rawArgs(i))} != ${defaults(i)}) $snippet"""
433+
else q"""if (${serDfltVals(i)} || v.${TermName(rawArgs(i))} != ${defaults(i)}) $snippet"""
418434
}
419435
q"""
420436
new ${c.prefix}.CaseClassWriter[$targetType]{
@@ -423,7 +439,7 @@ object Macros {
423439
Range(0, rawArgs.length)
424440
.map(i =>
425441
if (!hasDefaults(i)) q"1"
426-
else q"""if ($serDfltVals || v.${TermName(rawArgs(i))} != ${defaults(i)}) 1 else 0"""
442+
else q"""if (${serDfltVals(i)} || v.${TermName(rawArgs(i))} != ${defaults(i)}) 1 else 0"""
427443
)
428444
.foldLeft[Tree](q"0"){case (prev, next) => q"$prev + $next"}
429445
}

upickle/implicits/src-3/upickle/implicits/macros.scala

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ def extractKey[A](using Quotes)(sym: quotes.reflect.Symbol): Option[String] =
4141
.find(_.tpe =:= TypeRepr.of[upickle.implicits.key])
4242
.map{case Apply(_, Literal(StringConstant(s)) :: Nil) => s}
4343

44+
def extractSerializeDefaults[A](using quotes: Quotes)(sym: quotes.reflect.Symbol): Option[Boolean] =
45+
import quotes.reflect._
46+
sym
47+
.annotations
48+
.find(_.tpe =:= TypeRepr.of[upickle.implicits.serializeDefaults])
49+
.map{case Apply(_, Literal(BooleanConstant(s)) :: Nil) => s}
50+
4451
inline def extractIgnoreUnknownKeys[T](): List[Boolean] = ${extractIgnoreUnknownKeysImpl[T]}
4552
def extractIgnoreUnknownKeysImpl[T](using Quotes, Type[T]): Expr[List[Boolean]] =
4653
import quotes.reflect._
@@ -111,16 +118,28 @@ inline def writeLength[T](inline thisOuter: upickle.core.Types with upickle.impl
111118
inline v: T): Int =
112119
${writeLengthImpl[T]('thisOuter, 'v)}
113120

121+
def serDfltVals(using quotes: Quotes)(thisOuter: Expr[upickle.core.Types with upickle.implicits.MacrosCommon],
122+
argSym: quotes.reflect.Symbol,
123+
targetType: quotes.reflect.Symbol): Expr[Boolean] = {
124+
extractSerializeDefaults(argSym).orElse(extractSerializeDefaults(targetType)) match {
125+
case Some(b) => Expr(b)
126+
case None => '{ ${ thisOuter }.serializeDefaults }
127+
}
128+
}
114129
def writeLengthImpl[T](thisOuter: Expr[upickle.core.Types with upickle.implicits.MacrosCommon],
115130
v: Expr[T])
116-
(using Quotes, Type[T]): Expr[Int] =
131+
(using quotes: Quotes, t: Type[T]): Expr[Int] =
117132
import quotes.reflect.*
118133
fieldLabelsImpl0[T]
119134
.map{(rawLabel, label) =>
120135
val defaults = getDefaultParamsImpl0[T]
121136
val select = Select.unique(v.asTerm, rawLabel.name).asExprOf[Any]
137+
122138
if (!defaults.contains(label)) '{1}
123-
else '{if (${thisOuter}.serializeDefaults || ${select} != ${defaults(label)}) 1 else 0}
139+
else {
140+
val serDflt = serDfltVals(thisOuter, rawLabel, TypeRepr.of[T].typeSymbol)
141+
'{if (${serDflt} || ${select} != ${defaults(label)}) 1 else 0}
142+
}
124143
}
125144
.foldLeft('{0}) { case (prev, next) => '{$prev + $next} }
126145

@@ -166,7 +185,10 @@ def writeSnippetsImpl[R, T, WS <: Tuple](thisOuter: Expr[upickle.core.Types with
166185
)
167186
}
168187
if (!defaults.contains(label)) snippet
169-
else '{if (${thisOuter}.serializeDefaults || ${select} != ${defaults(label)}) $snippet}
188+
else {
189+
val serDflt = serDfltVals(thisOuter, rawLabel, TypeRepr.of[T].typeSymbol)
190+
'{if ($serDflt || ${select} != ${defaults(label)}) $snippet}
191+
}
170192

171193
},
172194
'{()}

upickle/implicits/src/upickle/implicits/CaseClassReadWriters.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import upickle.core.{Abort, AbortException, ArrVisitor, NoOpVisitor, ObjVisitor,
1111
* package to form the public API1
1212
*/
1313
trait CaseClassReadWriters extends upickle.core.Types{
14-
1514
abstract class CaseClassReader[V] extends SimpleReader[V] {
1615
override def expectedMsg = "expected dictionary"
1716

upickle/implicits/src/upickle/implicits/key.scala

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,30 @@ package upickle.implicits
22

33
import scala.annotation.StaticAnnotation
44

5+
/**
6+
* Annotation for control over the strings used to serialize your data structure. Can
7+
* be applied in three ways:
8+
*
9+
* 1. To individual fields, in which case it overrides the JSON object fieldname
10+
*
11+
* 2. To `case class`es which are part of `sealed trait`s, where it overrides
12+
* the value of the `"$type": "foo"` discriminator field
13+
*
14+
* 2. To `sealed trait`s themselves, where it overrides
15+
* the key of the `"$type": "foo"` discriminator field
16+
*/
517
class key(s: String) extends StaticAnnotation
6-
class allowUnknownKeys(b: Boolean) extends StaticAnnotation
18+
19+
/**
20+
* Annotation for fine-grained control of the `def serializeDefaults` configuration
21+
* on the upickle bundle; can be applied to individual fields or to entire `case class`es,
22+
* with finer-grained application taking precedence
23+
*/
24+
class serializeDefaults(s: Boolean) extends StaticAnnotation
25+
26+
/**
27+
* Annotation for fine-grained control of the `def allowUnknownKeys` configuration
28+
* on the upickle bundle; can be applied to individual `case class`es, taking precedence
29+
* over upickle pickler-level configuration
30+
*/
31+
class allowUnknownKeys(b: Boolean) extends StaticAnnotation

upickle/src/upickle/Api.scala

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,6 @@ trait Api
170170
def write0[R](out: Visitor[_, R], v: T): R = readwriter.write0(out, v)
171171
}
172172

173-
/**
174-
* Configure whether you want upickle to skip unknown keys during de-serialization
175-
* of `case class`es. Can be overriden for the entire serializer via `override def`, and
176-
* further overriden for individual `case class`es via the annotation
177-
* `@upickle.implicits.allowUnknownKeys(b: Boolean)`
178-
*/
179-
override def allowUnknownKeys: Boolean = true
180173
// End Api
181174
}
182175

0 commit comments

Comments
 (0)