Skip to content

Commit a13741f

Browse files
committed
Added annotation to ignore None values in Case Classes
SCALA-300
1 parent f3f146a commit a13741f

File tree

5 files changed

+127
-15
lines changed

5 files changed

+127
-15
lines changed

bson/src/main/scala/org/mongodb/scala/bson/codecs/Macros.scala

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ object Macros {
5252
* @return the CodecProvider for the case class
5353
*/
5454
@compileTimeOnly("Creating a CodecProvider utilises Macros and must be run at compile time.")
55-
def createCodecProvider[T](): CodecProvider = macro CaseClassProvider.createCaseClassProvider[T]
55+
def createCodecProvider[T](): CodecProvider = macro CaseClassProvider.createCodecProviderEncodeNone[T]
5656

5757
/**
5858
* Creates a CodecProvider for a case class using the given class to represent the case class
@@ -62,25 +62,67 @@ object Macros {
6262
* @return the CodecProvider for the case class
6363
*/
6464
@compileTimeOnly("Creating a CodecProvider utilises Macros and must be run at compile time.")
65-
implicit def createCodecProvider[T](clazz: Class[T]): CodecProvider = macro CaseClassProvider.createCaseClassProviderWithClass[T]
65+
implicit def createCodecProvider[T](clazz: Class[T]): CodecProvider = macro CaseClassProvider.createCodecProviderWithClassEncodeNone[T]
66+
67+
/**
68+
* Creates a CodecProvider for a case class that ignores any `None` values
69+
*
70+
* @tparam T the case class to create a Codec from
71+
* @return the CodecProvider for the case class
72+
* @since 2.1
73+
*/
74+
@compileTimeOnly("Creating a CodecProvider utilises Macros and must be run at compile time.")
75+
def createCodecProviderIgnoreNone[T](): CodecProvider = macro CaseClassProvider.createCodecProviderIgnoreNone[T]
76+
77+
/**
78+
* Creates a CodecProvider for a case class that ignores any `None` values, using the given class to represent the case class
79+
*
80+
* @param clazz the clazz that is the case class
81+
* @tparam T the case class to create a Codec from
82+
* @return the CodecProvider for the case class
83+
* @since 2.1
84+
*/
85+
@compileTimeOnly("Creating a CodecProvider utilises Macros and must be run at compile time.")
86+
def createCodecProviderIgnoreNone[T](clazz: Class[T]): CodecProvider = macro CaseClassProvider.createCodecProviderWithClassIgnoreNone[T]
87+
88+
/**
89+
* Creates a Codec for a case class
90+
*
91+
* @tparam T the case class to create a Codec from
92+
* @return the Codec for the case class
93+
*/
94+
@compileTimeOnly("Creating a Codec utilises Macros and must be run at compile time.")
95+
def createCodec[T](): Codec[T] = macro CaseClassCodec.createCodecDefaultCodecRegistryEncodeNone[T]
96+
97+
/**
98+
* Creates a Codec for a case class
99+
*
100+
* @param codecRegistry the Codec Registry to use
101+
* @tparam T the case class to create a codec from
102+
* @return the Codec for the case class
103+
*/
104+
@compileTimeOnly("Creating a Codec utilises Macros and must be run at compile time.")
105+
def createCodec[T](codecRegistry: CodecRegistry): Codec[T] = macro CaseClassCodec.createCodecEncodeNone[T]
66106

67107
/**
68108
* Creates a Codec for a case class
69109
*
70110
* @tparam T the case class to create a Codec from
71111
* @return the Codec for the case class
112+
* @since 2.1
72113
*/
73114
@compileTimeOnly("Creating a Codec utilises Macros and must be run at compile time.")
74-
def createCodec[T](): Codec[T] = macro CaseClassCodec.createCodecNoArgs[T]
115+
def createCodecIgnoreNone[T](): Codec[T] = macro CaseClassCodec.createCodecDefaultCodecRegistryIgnoreNone[T]
75116

76117
/**
77118
* Creates a Codec for a case class
78119
*
79120
* @param codecRegistry the Codec Registry to use
80121
* @tparam T the case class to create a codec from
81122
* @return the Codec for the case class
123+
* @since 2.1
82124
*/
83125
@compileTimeOnly("Creating a Codec utilises Macros and must be run at compile time.")
84-
def createCodec[T](codecRegistry: CodecRegistry): Codec[T] = macro CaseClassCodec.createCodec[T]
126+
def createCodecIgnoreNone[T](codecRegistry: CodecRegistry): Codec[T] = macro CaseClassCodec.createCodecIgnoreNone[T]
85127

86128
}

bson/src/main/scala/org/mongodb/scala/bson/codecs/macrocodecs/CaseClassCodec.scala

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,38 @@ import org.bson.codecs.configuration.CodecRegistry
2424

2525
private[codecs] object CaseClassCodec {
2626

27-
def createCodecNoArgs[T: c.WeakTypeTag](c: whitebox.Context)(): c.Expr[Codec[T]] = {
27+
def createCodecDefaultCodecRegistryEncodeNone[T: c.WeakTypeTag](c: whitebox.Context)(): c.Expr[Codec[T]] = {
28+
import c.universe._
29+
createCodecDefaultCodecRegistry[T](c)(c.Expr[Boolean](q"true"))
30+
}
31+
32+
def createCodecEncodeNone[T: c.WeakTypeTag](c: whitebox.Context)(codecRegistry: c.Expr[CodecRegistry]): c.Expr[Codec[T]] = {
33+
import c.universe._
34+
createCodec[T](c)(codecRegistry, c.Expr[Boolean](q"true"))
35+
}
36+
37+
def createCodecDefaultCodecRegistryIgnoreNone[T: c.WeakTypeTag](c: whitebox.Context)(): c.Expr[Codec[T]] = {
38+
import c.universe._
39+
createCodecDefaultCodecRegistry[T](c)(c.Expr[Boolean](q"false"))
40+
}
41+
42+
def createCodecIgnoreNone[T: c.WeakTypeTag](c: whitebox.Context)(codecRegistry: c.Expr[CodecRegistry]): c.Expr[Codec[T]] = {
43+
import c.universe._
44+
createCodec[T](c)(codecRegistry, c.Expr[Boolean](q"false"))
45+
}
46+
47+
def createCodecDefaultCodecRegistry[T: c.WeakTypeTag](c: whitebox.Context)(encodeNone: c.Expr[Boolean]): c.Expr[Codec[T]] = {
2848
import c.universe._
2949
createCodec[T](c)(c.Expr[CodecRegistry](
3050
q"""
3151
import org.mongodb.scala.bson.codecs.DEFAULT_CODEC_REGISTRY
3252
DEFAULT_CODEC_REGISTRY
3353
"""
34-
)).asInstanceOf[c.Expr[Codec[T]]]
54+
), encodeNone)
3555
}
3656

3757
// scalastyle:off method.length
38-
def createCodec[T: c.WeakTypeTag](c: whitebox.Context)(codecRegistry: c.Expr[CodecRegistry]): c.Expr[Codec[T]] = {
58+
def createCodec[T: c.WeakTypeTag](c: whitebox.Context)(codecRegistry: c.Expr[CodecRegistry], encodeNone: c.Expr[Boolean]): c.Expr[Codec[T]] = {
3959
import c.universe._
4060

4161
// Declared types
@@ -195,10 +215,11 @@ private[codecs] object CaseClassCodec {
195215
f match {
196216
case optional if isOption(optional) => q"""
197217
val localVal = instanceValue.$name
198-
writer.writeName($key)
199218
if (localVal.isDefined) {
219+
writer.writeName($key)
200220
this.writeFieldValue($key, writer, localVal.get, encoderContext)
201-
} else {
221+
} else if ($encodeNone) {
222+
writer.writeName($key)
202223
this.writeFieldValue($key, writer, this.bsonNull, encoderContext)
203224
}"""
204225
case _ => q"""

bson/src/main/scala/org/mongodb/scala/bson/codecs/macrocodecs/CaseClassProvider.scala

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,27 @@ import org.bson.codecs.configuration.{ CodecProvider, CodecRegistry }
2222

2323
private[codecs] object CaseClassProvider {
2424

25-
def createCaseClassProviderWithClass[T: c.WeakTypeTag](c: whitebox.Context)(clazz: c.Expr[Class[T]]): c.Expr[CodecProvider] =
26-
createCaseClassProvider[T](c)().asInstanceOf[c.Expr[CodecProvider]]
25+
def createCodecProviderEncodeNone[T: c.WeakTypeTag](c: whitebox.Context)(): c.Expr[CodecProvider] = {
26+
import c.universe._
27+
createCodecProvider[T](c)(c.Expr[Boolean](q"true"))
28+
}
29+
30+
def createCodecProviderWithClassEncodeNone[T: c.WeakTypeTag](c: whitebox.Context)(clazz: c.Expr[Class[T]]): c.Expr[CodecProvider] = {
31+
import c.universe._
32+
createCodecProvider[T](c)(c.Expr[Boolean](q"true"))
33+
}
34+
35+
def createCodecProviderWithClassIgnoreNone[T: c.WeakTypeTag](c: whitebox.Context)(clazz: c.Expr[Class[T]]): c.Expr[CodecProvider] = {
36+
import c.universe._
37+
createCodecProvider[T](c)(c.Expr[Boolean](q"false"))
38+
}
39+
40+
def createCodecProviderIgnoreNone[T: c.WeakTypeTag](c: whitebox.Context)(): c.Expr[CodecProvider] = {
41+
import c.universe._
42+
createCodecProvider[T](c)(c.Expr[Boolean](q"false"))
43+
}
2744

28-
def createCaseClassProvider[T: c.WeakTypeTag](c: whitebox.Context)(): c.Expr[CodecProvider] = {
45+
def createCodecProvider[T: c.WeakTypeTag](c: whitebox.Context)(encodeNone: c.Expr[Boolean]): c.Expr[CodecProvider] = {
2946
import c.universe._
3047

3148
// Declared type
@@ -34,7 +51,7 @@ private[codecs] object CaseClassProvider {
3451

3552
// Names
3653
def exprCodecRegistry = c.Expr[CodecRegistry](q"codecRegistry")
37-
def codec = CaseClassCodec.createCodec[T](c)(exprCodecRegistry)
54+
def codec = CaseClassCodec.createCodec[T](c)(exprCodecRegistry, encodeNone)
3855

3956
c.Expr[CodecProvider](
4057
q"""

bson/src/test/scala/org/mongodb/scala/bson/codecs/MacrosSpec.scala

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ import java.util
2121
import java.util.Date
2222

2323
import scala.collection.JavaConverters._
24+
import scala.language.implicitConversions
2425
import scala.reflect.ClassTag
2526

2627
import org.bson._
2728
import org.bson.codecs.configuration.{ CodecConfigurationException, CodecProvider, CodecRegistries }
2829
import org.bson.codecs.{ Codec, DecoderContext, EncoderContext }
2930
import org.bson.io.{ BasicOutputBuffer, ByteBufferBsonInput, OutputBuffer }
3031

31-
import org.mongodb.scala.bson.codecs.Macros.createCodecProvider
32+
import org.mongodb.scala.bson.codecs.Macros.{ createCodecProvider, createCodecProviderIgnoreNone }
3233
import org.mongodb.scala.bson.collection.immutable.Document
3334
import org.scalatest.{ FlatSpec, Matchers }
3435

@@ -141,6 +142,23 @@ class MacrosSpec extends FlatSpec with Matchers {
141142
decode(registry.get(classOf[OptionalValue]), buffer) should equal(OptionalValue("Bob", None))
142143
}
143144

145+
it should "be able to round trip optional values, when None is ignored" in {
146+
roundTrip(OptionalValue("Bob", None), """{name: "Bob"}""", createCodecProviderIgnoreNone[OptionalValue]())
147+
roundTrip(OptionalValue("Bob", Some("value")), """{name: "Bob", value: "value"}""", createCodecProviderIgnoreNone[OptionalValue]())
148+
roundTrip(OptionalCaseClass("Bob", None), """{name: "Bob"}""", createCodecProviderIgnoreNone[OptionalCaseClass]())
149+
roundTrip(
150+
OptionalCaseClass("Bob", Some(Person("Charlie", "Jones"))),
151+
"""{name: "Bob", value: {firstName: "Charlie", lastName: "Jones"}}""",
152+
createCodecProviderIgnoreNone[OptionalCaseClass](), createCodecProviderIgnoreNone[Person]()
153+
)
154+
155+
roundTrip(OptionalRecursive("Bob", None), """{name: "Bob"}""", createCodecProviderIgnoreNone[OptionalRecursive]())
156+
roundTrip(
157+
OptionalRecursive("Bob", Some(OptionalRecursive("Charlie", None))),
158+
"""{name: "Bob", value: {name: "Charlie"}}""", createCodecProviderIgnoreNone[OptionalRecursive]()
159+
)
160+
}
161+
144162
it should "roundtrip all the supported bson types" in {
145163
val value =
146164
roundTrip(
@@ -258,6 +276,6 @@ class MacrosSpec extends FlatSpec with Matchers {
258276
codec.decode(reader, DecoderContext.builder().build())
259277
}
260278

261-
val documentCodec = DEFAULT_CODEC_REGISTRY.get(classOf[Document])
279+
val documentCodec: Codec[Document] = DEFAULT_CODEC_REGISTRY.get(classOf[Document])
262280

263281
}

docs/reference/content/bson/macros.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,17 @@ case class Leaf(value: Int) extends Tree
8484

8585
val codecRegistry = fromRegistries( fromProviders(classOf[Tree]), DEFAULT_CODEC_REGISTRY )
8686
```
87+
88+
89+
## Options and None values.
90+
91+
By default `Option` values are always stored. In 2.1.0 a new macro helpers were added so that `None` values would not be stored in the
92+
database. In the following example only if an address is present will it be stored in the database:
93+
94+
```scala
95+
import org.mongodb.scala.bson.codecs.Macros
96+
97+
case class Person(firstName: String, secondName: String, address: Option[Address])
98+
99+
val personCodecProvider = Macros.createCodecProviderIgnoreNone[Person]()
100+
```

0 commit comments

Comments
 (0)